mirror of
https://git.ikl.sh/132ikl/liteshort.git
synced 2023-08-10 21:13:04 +03:00
Add PyPi package, add standard config paths
This commit is contained in:
parent
8a782c0843
commit
2d0fb48a1e
3
.gitignore
vendored
3
.gitignore
vendored
@ -108,6 +108,3 @@ venv.bak/
|
||||
|
||||
# Databases
|
||||
*.db
|
||||
|
||||
# liteshort
|
||||
config.yml
|
||||
|
38
CONVENTIONS
38
CONVENTIONS
@ -1,3 +1,5 @@
|
||||
TODO: Move to wiki
|
||||
|
||||
The following is a description of design philosphies and standards for liteshort. This document is mostly for developers, however if you are interested feel free to take a peek.
|
||||
|
||||
DESIGN PHILOSPHIES:
|
||||
@ -7,21 +9,25 @@ DESIGN PHILOSPHIES:
|
||||
- Liteshort should be simple to install and maintain. Installing should be a simple process, whether from a package manager or when installing from source.
|
||||
- Liteshort should follow system design standards. This includes basic Unix design principles and the Filesystem Hierarchy Standard.
|
||||
|
||||
FILE STRUCTURE:
|
||||
- Executable file: /usr/bin/liteshort
|
||||
- Source directory: /usr/lib/python3/dist-packages/liteshort
|
||||
- Config directory: /etc/liteshort/
|
||||
- Socket file: /run/liteshort.sock
|
||||
|
||||
PYPI PACKAGE:
|
||||
The PyPi package should provide no more than the following:
|
||||
- Install source to Python package directory
|
||||
- Install template config files
|
||||
- Install debug executable (liteshort.py) installed
|
||||
- Install python-only requirements (no uwsgi)
|
||||
PROPOSAL FOR A DEBIAN PACKAGE
|
||||
|
||||
DEBIAN PACAKGE:
|
||||
The Debian package should provide no more than the following:
|
||||
- Install uwsgi
|
||||
- Install and enable systemd service
|
||||
- Should be based upon pybuild, as to prevent redundant packaging
|
||||
File Structure:
|
||||
x Debug Executable file: liteshort in $PATH
|
||||
x Hashpw executable file: lshash in $PATH
|
||||
x Source directory: system Python module folder (eg. /usr/lib/python3/dist-packages/liteshort)
|
||||
x Config directory: /etc/liteshort/ OR appdirs.site_config_dir OR appdirs.user_config_dir
|
||||
- Socket file (if applicable): /run/liteshort.sock
|
||||
|
||||
Pypi Package:
|
||||
The PyPi package should provide no more than the following:
|
||||
x Install source to Python package directory
|
||||
x Install template config files
|
||||
x Install debug executables installed
|
||||
x Install minimum requirements (no uwsgi)
|
||||
|
||||
Debian Pacakge:
|
||||
The Debian package should provide no more than the following:
|
||||
- Install uwsgi
|
||||
- Install and enable systemd service
|
||||
- Should be based upon pybuild, as to prevent redundant packaging
|
||||
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright 2020 132ikl
|
||||
Copyright 2020 Steven Spangler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from liteshort.main import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
115
liteshort/config.py
Normal file
115
liteshort/config.py
Normal file
@ -0,0 +1,115 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from shutil import copyfile
|
||||
|
||||
from appdirs import site_config_dir, user_config_dir
|
||||
from pkg_resources import resource_filename
|
||||
from yaml import safe_load
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_config():
|
||||
APP = "liteshort"
|
||||
AUTHOR = "132ikl"
|
||||
|
||||
paths = [
|
||||
Path("/etc/liteshort"),
|
||||
Path(site_config_dir(APP, AUTHOR)),
|
||||
Path(user_config_dir(APP, AUTHOR)),
|
||||
Path(),
|
||||
]
|
||||
|
||||
for path in paths:
|
||||
f = path / "config.yml"
|
||||
if f.exists():
|
||||
LOGGER.debug(f"Selecting config file {f}")
|
||||
return open(f, "r")
|
||||
|
||||
for path in paths:
|
||||
try:
|
||||
path.mkdir(exist_ok=True)
|
||||
template = resource_filename(__name__, "config.template.yml")
|
||||
copyfile(template, (path / "config.template.yml"))
|
||||
copyfile(template, (path / "config.yml"))
|
||||
return open(path / "config.yml", "r")
|
||||
except (PermissionError, OSError) as e:
|
||||
LOGGER.warn(f"Failed to create config in {path}")
|
||||
LOGGER.debug("", exc_info=True)
|
||||
|
||||
raise FileNotFoundError("Cannot find config.yml, and failed to create it")
|
||||
|
||||
|
||||
# TODO: yikes
|
||||
def load_config():
|
||||
with get_config() as config:
|
||||
configYaml = safe_load(config)
|
||||
config = {
|
||||
k.lower(): v for k, v in configYaml.items()
|
||||
} # Make config keys case insensitive
|
||||
|
||||
req_options = {
|
||||
"admin_username": "admin",
|
||||
"database_name": "urls",
|
||||
"random_length": 4,
|
||||
"allowed_chars": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
|
||||
"random_gen_timeout": 5,
|
||||
"site_name": "liteshort",
|
||||
"site_domain": None,
|
||||
"show_github_link": True,
|
||||
"secret_key": None,
|
||||
"disable_api": False,
|
||||
"subdomain": "",
|
||||
"latest": "l",
|
||||
"selflinks": False,
|
||||
"blocklist": [],
|
||||
}
|
||||
|
||||
config_types = {
|
||||
"admin_username": str,
|
||||
"database_name": str,
|
||||
"random_length": int,
|
||||
"allowed_chars": str,
|
||||
"random_gen_timeout": int,
|
||||
"site_name": str,
|
||||
"site_domain": (str, type(None)),
|
||||
"show_github_link": bool,
|
||||
"secret_key": str,
|
||||
"disable_api": bool,
|
||||
"subdomain": (str, type(None)),
|
||||
"latest": (str, type(None)),
|
||||
"selflinks": bool,
|
||||
"blocklist": list,
|
||||
}
|
||||
|
||||
for option in req_options.keys():
|
||||
if (
|
||||
option not in config.keys()
|
||||
): # Make sure everything in req_options is set in config
|
||||
config[option] = req_options[option]
|
||||
|
||||
for option in config.keys():
|
||||
if option in config_types:
|
||||
matches = False
|
||||
if type(config_types[option]) is not tuple:
|
||||
config_types[option] = (
|
||||
config_types[option],
|
||||
) # Automatically creates tuple for non-tuple types
|
||||
for req_type in config_types[
|
||||
option
|
||||
]: # Iterates through tuple to allow multiple types for config options
|
||||
if type(config[option]) is req_type:
|
||||
matches = True
|
||||
if not matches:
|
||||
raise TypeError(option + " is incorrect type")
|
||||
if not config["disable_api"]:
|
||||
if "admin_hashed_password" in config.keys() and config["admin_hashed_password"]:
|
||||
config["password_hashed"] = True
|
||||
elif "admin_password" in config.keys() and config["admin_password"]:
|
||||
config["password_hashed"] = False
|
||||
else:
|
||||
raise TypeError(
|
||||
"admin_password or admin_hashed_password must be set in config.yml"
|
||||
)
|
||||
return config
|
@ -5,13 +5,13 @@ admin_username: 'admin'
|
||||
# String: Plaintext password to make admin API requests
|
||||
# Safe to remove if admin_hashed_password is set
|
||||
# Default: unset
|
||||
admin_password:
|
||||
#admin_password:
|
||||
|
||||
# String: Hashed password (bcrypt) to make admin API requests - Preferred over plaintext, use securepass.sh to generate
|
||||
# String: Hashed password (bcrypt) to make admin API requests - Preferred over plaintext, use lshash to generate
|
||||
# Please note that authentication takes noticeably longer than using plaintext password
|
||||
# Don't include the <username>: segment, just the hash
|
||||
# Default: unset (required to start application)
|
||||
#admin_hashed_password:
|
||||
admin_hashed_password:
|
||||
|
||||
# Boolean: Disables API. If set to true, admin_password/admin_hashed_password do not need to be set.
|
||||
# Default: false
|
||||
@ -56,6 +56,7 @@ subdomain:
|
||||
|
||||
# String: URL which takes you to the most recent short URL's destination
|
||||
# Short URLs cannot be created with this string if set
|
||||
# Unset to disable
|
||||
# Default: l
|
||||
latest: 'l'
|
||||
|
@ -7,84 +7,12 @@ import urllib
|
||||
import flask
|
||||
from bcrypt import checkpw
|
||||
from flask import current_app, g, redirect, render_template, request, url_for
|
||||
from yaml import safe_load
|
||||
|
||||
from .config import load_config
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
|
||||
|
||||
def load_config():
|
||||
with open("config.yml") as config:
|
||||
configYaml = safe_load(config)
|
||||
config = {
|
||||
k.lower(): v for k, v in configYaml.items()
|
||||
} # Make config keys case insensitive
|
||||
|
||||
req_options = {
|
||||
"admin_username": "admin",
|
||||
"database_name": "urls",
|
||||
"random_length": 4,
|
||||
"allowed_chars": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
|
||||
"random_gen_timeout": 5,
|
||||
"site_name": "liteshort",
|
||||
"site_domain": None,
|
||||
"show_github_link": True,
|
||||
"secret_key": None,
|
||||
"disable_api": False,
|
||||
"subdomain": "",
|
||||
"latest": "l",
|
||||
"selflinks": False,
|
||||
"blocklist": [],
|
||||
}
|
||||
|
||||
config_types = {
|
||||
"admin_username": str,
|
||||
"database_name": str,
|
||||
"random_length": int,
|
||||
"allowed_chars": str,
|
||||
"random_gen_timeout": int,
|
||||
"site_name": str,
|
||||
"site_domain": (str, type(None)),
|
||||
"show_github_link": bool,
|
||||
"secret_key": str,
|
||||
"disable_api": bool,
|
||||
"subdomain": (str, type(None)),
|
||||
"latest": (str, type(None)),
|
||||
"selflinks": bool,
|
||||
"blocklist": list,
|
||||
}
|
||||
|
||||
for option in req_options.keys():
|
||||
if (
|
||||
option not in config.keys()
|
||||
): # Make sure everything in req_options is set in config
|
||||
config[option] = req_options[option]
|
||||
|
||||
for option in config.keys():
|
||||
if option in config_types:
|
||||
matches = False
|
||||
if type(config_types[option]) is not tuple:
|
||||
config_types[option] = (
|
||||
config_types[option],
|
||||
) # Automatically creates tuple for non-tuple types
|
||||
for req_type in config_types[
|
||||
option
|
||||
]: # Iterates through tuple to allow multiple types for config options
|
||||
if type(config[option]) is req_type:
|
||||
matches = True
|
||||
if not matches:
|
||||
raise TypeError(option + " is incorrect type")
|
||||
if not config["disable_api"]:
|
||||
if "admin_hashed_password" in config.keys() and config["admin_hashed_password"]:
|
||||
config["password_hashed"] = True
|
||||
elif "admin_password" in config.keys() and config["admin_password"]:
|
||||
config["password_hashed"] = False
|
||||
else:
|
||||
raise TypeError(
|
||||
"admin_password or admin_hashed_password must be set in config.yml"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def authenticate(username, password):
|
||||
return username == current_app.config["admin_username"] and check_password(
|
||||
password, current_app.config
|
||||
|
20
liteshort/util.py
Normal file
20
liteshort/util.py
Normal file
@ -0,0 +1,20 @@
|
||||
from getpass import getpass
|
||||
|
||||
import bcrypt
|
||||
|
||||
|
||||
def hash_passwd():
|
||||
salt = bcrypt.gensalt()
|
||||
try:
|
||||
unhashed = getpass("Type password to hash: ")
|
||||
unhashed2 = getpass("Confirm: ")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
|
||||
if unhashed != unhashed2:
|
||||
print("Passwords don't match.")
|
||||
return None
|
||||
|
||||
hashed = bcrypt.hashpw(unhashed.encode("utf-8"), salt)
|
||||
|
||||
print("Password hash: " + hashed.decode("utf-8"))
|
@ -1,3 +0,0 @@
|
||||
flask
|
||||
bcrypt
|
||||
pyyaml
|
@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
## bcrypt passwd generator ##
|
||||
#############################
|
||||
CMD=$(which htpasswd 2>/dev/null)
|
||||
OPTS="-nBC 12"
|
||||
|
||||
read -p "Username: " USERNAME
|
||||
|
||||
check_config() {
|
||||
if [ -z $CMD ]; then
|
||||
printf "Exiting: htpasswd is missing.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$USERNAME" ]; then
|
||||
usage
|
||||
fi
|
||||
}
|
||||
|
||||
check_config $USERNAME
|
||||
printf "Generating Bcrypt hash for username: $USERNAME\n\n"
|
||||
$CMD $OPTS $USERNAME
|
||||
exit $?
|
30
setup.py
Normal file
30
setup.py
Normal file
@ -0,0 +1,30 @@
|
||||
import setuptools
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="liteshort",
|
||||
version="2.0.0",
|
||||
author="Steven Spangler",
|
||||
author_email="132@ikl.sh",
|
||||
description="User-friendly, actually lightweight, and configurable URL shortener",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/132ikl/liteshort",
|
||||
packages=setuptools.find_packages(),
|
||||
package_data={"liteshort": ["templates/*", "static/*", "config.template.yml"]},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"liteshort = liteshort.main:app.run",
|
||||
"lshash = liteshort.util:hash_passwd",
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
],
|
||||
install_requires=["flask~=1.1.2", "bcrypt~=3.1.7", "pyyaml~=5.3.1"],
|
||||
python_requires=">=3.7",
|
||||
)
|
Loading…
Reference in New Issue
Block a user