From 1d4ac859d7ef1d813ea78f92ac76debacb84a0c4 Mon Sep 17 00:00:00 2001 From: Denis Lehmann Date: Sun, 24 Apr 2022 17:48:32 +0200 Subject: [PATCH] make package --- .gitignore | 116 ++++++++++++++++++++++++ README.org | 3 + images/logo.svg | 8 ++ raincloud/__init__.py | 121 ++++++++++++++++++++++++++ raincloud/default_settings.py | 3 + raincloud/directory_handler.py | 73 ++++++++++++++++ raincloud/session_handler.py | 36 ++++++++ raincloud/static/favicon.png | Bin 0 -> 1698 bytes raincloud/static/logo.svg | 15 ++++ raincloud/static/style.css | 108 +++++++++++++++++++++++ raincloud/templates/authenticate.html | 7 ++ raincloud/templates/base.html | 18 ++++ raincloud/templates/directory.html | 27 ++++++ run.py | 7 ++ setup.py | 10 +++ 15 files changed, 552 insertions(+) create mode 100644 .gitignore create mode 100644 README.org create mode 100644 images/logo.svg create mode 100755 raincloud/__init__.py create mode 100644 raincloud/default_settings.py create mode 100644 raincloud/directory_handler.py create mode 100644 raincloud/session_handler.py create mode 100644 raincloud/static/favicon.png create mode 100644 raincloud/static/logo.svg create mode 100644 raincloud/static/style.css create mode 100644 raincloud/templates/authenticate.html create mode 100644 raincloud/templates/base.html create mode 100644 raincloud/templates/directory.html create mode 100755 run.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e61bca2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,116 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + diff --git a/README.org b/README.org new file mode 100644 index 0000000..283829e --- /dev/null +++ b/README.org @@ -0,0 +1,3 @@ +* raincloud + + /Self-hosted file sharing cloud for you and your firends./ diff --git a/images/logo.svg b/images/logo.svg new file mode 100644 index 0000000..8003b24 --- /dev/null +++ b/images/logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/raincloud/__init__.py b/raincloud/__init__.py new file mode 100755 index 0000000..e58bd62 --- /dev/null +++ b/raincloud/__init__.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +from hmac import compare_digest as compare_hash +from flask import ( + Flask, + abort, + redirect, + render_template, + request, + send_from_directory, + session, + url_for, +) +from raincloud.directory_handler import DirectoryHandler, RaincloudIOException +from raincloud.session_handler import SessionHandler +from werkzeug.utils import secure_filename +import crypt +import werkzeug + + +def app(settings_file=None): + + # Create app + app = Flask(__name__) + + # Load config + app.config.from_object("raincloud.default_settings") + if settings_file: + app.config.from_pyfile(settings_file) + else: + app.config.from_envvar("RAINCLOUD_SETTINGS", silent=True) + + if not app.config["BASE_PATH"] or app.config["BASE_PATH"] == "": + print("No BASE_PATH defined") + exit(1) + + # Create handlers + dh = DirectoryHandler(app.config["BASE_PATH"]) + sh = SessionHandler() + + @app.route("/", methods=["GET", "POST"]) + @app.route("//", methods=["GET"]) + def directory(directory, filename=None): + + try: + + # Clean sessions + sh.clean_sessions() + + # Logout + if request.method == "POST" and "logout" in request.form: + sh.delete_session(session[directory]) + return redirect(url_for("directory", directory=directory)) + + config = dh.get_config(directory) + + if config["hashed_password"]: + authenticated = ( + True + if directory in session + and sh.validate_session(directory, session[directory]) + else False + ) + + if not authenticated: + if request.method == "POST": + if compare_hash( + config["hashed_password"], + crypt.crypt( + request.form["password"], config["hashed_password"] + ), + ): + id_ = sh.create_session_id() + session[directory] = id_ + sh.add_session(directory, id_) + return redirect(url_for("directory", directory=directory)) + else: + return render_template( + "authenticate.html", + cloud_name=app.config["CLOUD_NAME"], + config=config, + ) + + if request.method == "GET": + # List + if not filename: + files = dh.get_files(directory) + return render_template( + "directory.html", + cloud_name=app.config["CLOUD_NAME"], + config=config, + files=files, + ) + + # Download + else: + if config["download"] and filename != "rc.toml": + return send_from_directory( + dh.get_absolute_path(directory), filename + ) + else: + abort(404) + + # Upload + elif request.method == "POST": + if config["upload"]: + f = request.files["file"] + filename = secure_filename(f.filename) + if filename != "rc.toml": + dh.save_to_directory(f, directory, filename) + + # Reload + return redirect(url_for("directory", directory=directory)) + else: + abort(403) + + except RaincloudIOException as e: + print(e) + abort(404) + + return app diff --git a/raincloud/default_settings.py b/raincloud/default_settings.py new file mode 100644 index 0000000..a189dae --- /dev/null +++ b/raincloud/default_settings.py @@ -0,0 +1,3 @@ +CLOUD_NAME = "raincloud" +SECRET_KEY = "dev" +BASE_PATH = "public" diff --git a/raincloud/directory_handler.py b/raincloud/directory_handler.py new file mode 100644 index 0000000..ea23f11 --- /dev/null +++ b/raincloud/directory_handler.py @@ -0,0 +1,73 @@ +from datetime import datetime +from pathlib import Path +import toml + + +class RaincloudIOException(Exception): + pass + + +class DirectoryHandler: + def __init__(self, base_path): + self.base_path = Path(base_path) + if not self.base_path.exists(): + raise RaincloudIOException( + f"Base path '{self.base_path.resolve()}' not existent" + ) + + def get_config(self, directory): + """Load a 'rc.toml' file from given directory. + + Parameters: + directory - basepath of 'rc.toml' + + Returns: Dictionary of config parameters + """ + path = self.base_path / directory / "rc.toml" + + if path.exists(): + config = {} + config["directory"] = directory + + parsed_config = toml.load(path) + config["hashed_password"] = ( + parsed_config["hashed_password"] + if "hashed_password" in parsed_config + else None + ) + config["download"] = ( + parsed_config["download"] if "download" in parsed_config else False + ) + config["upload"] = ( + parsed_config["upload"] if "upload" in parsed_config else False + ) + return config + + else: + raise RaincloudIOException("No raincloud directory") + + def get_files(self, directory): + """Get files from directory.""" + path = self.base_path / directory + file_paths = [f for f in path.glob("*") if f.is_file()] + files = [] + for p in file_paths: + if p.name != "rc.toml": + files.append(p.name) + return files + + def get_absolute_path(self, directory): + """Get absolute path of 'directory'.""" + return (self.base_path / directory).resolve() + + def save_to_directory(self, file_, directory, filename): + """Save 'file_' to 'directory' with 'filename'.""" + filepath = self.base_path / directory / filename + if not filepath.exists(): + file_.save(filepath) + else: + file_.save( + filepath.with_suffix( + f".{datetime.now().strftime('%Y%m%d%H%M')}{filepath.suffix}" + ) + ) diff --git a/raincloud/session_handler.py b/raincloud/session_handler.py new file mode 100644 index 0000000..c0e11ba --- /dev/null +++ b/raincloud/session_handler.py @@ -0,0 +1,36 @@ +from datetime import datetime, timedelta +import uuid + + +class SessionHandler: + def __init__(self): + self.sessions = [] + + def create_session_id(self): + """Create a new session ID.""" + ids = [s[2] for s in self.sessions] + id_ = uuid.uuid4() + while id_ in ids: + id_ = uuid.uuid4() + return id_ + + def add_session(self, directory, id_): + """Add session with 'id_' allowing access to 'directory'.""" + self.sessions.append((datetime.now(), directory, id_)) + + def clean_sessions(self): + """Remove all sessions which are older than one day.""" + self.sessions = [ + s for s in self.sessions if s[0] > datetime.now() - timedelta(days=1) + ] + + def validate_session(self, directory, id_): + """check if session with 'id_' is allowed to access 'directory'.""" + valid_dates = [s[0] for s in self.sessions if s[1] == directory and s[2] == id_] + if len(valid_dates) > 0 and valid_dates[0] > datetime.now() - timedelta(days=1): + return True + return False + + def delete_session(self, id_): + """Delete session with 'id_'.""" + self.sessions = [s for s in self.sessions if s[2] != id_] diff --git a/raincloud/static/favicon.png b/raincloud/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8628c8cc3c0e085cdba7f3f7a4b4e1abc7c9de94 GIT binary patch literal 1698 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hEQ{ z5-M`MK7HHM)KDg-iwlmimrS4aLxm%7%Y`6;qx^0n!oF8lbV@iXPg~$o%izWuAGlba zM?p$O%S~8d*HQ`YBoRTLGH!`2c7}VBHg4Hqe4PLGw41x5Z{3}H_u9HUjE={@m@<+r`IW*X5Fzb zjQigYqMrpf2?j?-cWqW>{^p98TzP1Y)T;9BbyEDh9%wRE7 zjLdU)oc(E0!q%{v?7M1Nr_?O`yM;ad z@df{c^~zs@Qk4TvP|8C}mNaRlu*Y?qYQA53w09ns>$++JqcVG01CLm{sdC zg}>2m-;Gm>S|N?Ebw8(PO(n+YGwiH^PwwQtA4)GT)yJ4yjV%k^YkBY z)W35qUh+2cow?tJk9U;owk|rQ;&*m3yTxfC%a!fB*S*NpLwegk^$M3gC)a|l%xXseJ{m6L#+#d%Lx6b5E z$dy-)Zit%C`&1ojb8xv+I#A!vvl3Ej}}-fe|Y(`sK2dOtI`%8wb(8uAXP7=e1C?e_aQ4Ct&i4zIo<2eT31j_F5y?W}eFA!x8tsA2O~8X*iv=b-l{JOZL26ANnMv zd0zJ(sBn%8u=gvQv5}SWnc#n&MbnOcdH3e;OTBp_4+5rGYA`=?c{s6jsZgo6>glcQ zA0~bLdiUaCoBU@oVQZ&z-^o4UJ?XBBvEpMUp4hdHg3F{nb*22x`Tjg{!Ho&p<@Xf+ z&)V4cq3hxM1y-DICm2eKD+qX8I?p#X?T+i}QiW5K+xBP%9j{Ky+2FREVKHxI++mBK z#=-m5Bt;cM)(VTN9qBxD$nRIBo}0b(sgloXo-xf4p~?R40}uw|G9Y-pZW~ zEoBcLMalMjn4x&x?bOrBCADR)QLGu#^Un(Zu=^zHedb0$ME>y?>7vhawWqroZ#(eJ z^?}Ls6m1{>sL-HVtnJ^ zWG}zz%L|=sYw_!^5~fJz`7Y24a&xk8U-vLCVMlcEid%77)j?mUYiWuu*b${J{QLTJ zK9*|Jl{K`*HPEJjM#wu+?; P3=9mOu6{1-oD!M + + + + + + + + + + + + + + diff --git a/raincloud/static/style.css b/raincloud/static/style.css new file mode 100644 index 0000000..aa60628 --- /dev/null +++ b/raincloud/static/style.css @@ -0,0 +1,108 @@ +@keyframes fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +body { + font-family: sans-serif; + margin: 0; +} + +#container { + display: flex; + flex-direction: column; +} + +#header { + padding: 0px 30px; + border-bottom: 1px solid lightgray; + font-size: 20px; + display: flex; + justify-content: space-between; + align-items: center; + min-height: 60px; +} + +#cloud-name { + color: #28bcff; + margin-right: 30px; +} + +#directory-name { + font-weight: bold; +} + +#menu { + animation: fade-in ease-in-out .5s; +} + +#content { + margin: 50px 0px; + text-align: center; + animation: fade-in ease-in-out .5s; +} + +.file { + border-bottom: 1px solid lightgrey; + padding: 0px 30px; + min-height: 60px; + display: flex; + justify-content: space-between; + align-items: center; +} + +form { + display: inline-block; +} + +.button { + background-color: #28bcff; + border: none; + border-radius: 5px; + color: white; + padding: 12px 28px; + text-align: center; + text-decoration: none; + display: inline-block; + margin: 4px 2px; + font-size: 16px; + cursor: pointer; +} + +.button:hover { + background-color: #0cb3ff; +} + +.upload { + background-color: #ff814e; +} + +.upload:hover { + background-color: #ff7740; +} + +.logout { + background-color: #ff4e62; +} + +.logout:hover { + background-color: #ff3e54; +} + +input[type="file"] { + display: none; +} + +input[type="password"] { + border: 1px solid lightgrey; + padding: 11px 11px; + font-size: 16px; + border-radius: 5px; + width: 300px; +} + +input[type="password"]:focus { + outline: none; + border: 2px solid #ff814e; + padding: 10px 10px; +} diff --git a/raincloud/templates/authenticate.html b/raincloud/templates/authenticate.html new file mode 100644 index 0000000..bc89042 --- /dev/null +++ b/raincloud/templates/authenticate.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% block content %} +
+ + +
+{% endblock %} diff --git a/raincloud/templates/base.html b/raincloud/templates/base.html new file mode 100644 index 0000000..b291d51 --- /dev/null +++ b/raincloud/templates/base.html @@ -0,0 +1,18 @@ + + + + +{{ config["directory"] }} | {{ cloud_name }} +
+ +
+ {% block content %}{% endblock %} +
+
diff --git a/raincloud/templates/directory.html b/raincloud/templates/directory.html new file mode 100644 index 0000000..7e2ea14 --- /dev/null +++ b/raincloud/templates/directory.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block menu %} + {% if config["upload"] %} +
+ +
+ {% endif %} + {% if config["hashed_password"] %} +
+ + +
+ {% endif %} +{% endblock %} +{% block content %} + {% for f in files %} +
+

{{ f }}

+ {% if config["download"] %} + Download + {% endif %} +
+ {% endfor %} +{% endblock %} diff --git a/run.py b/run.py new file mode 100755 index 0000000..26ca89f --- /dev/null +++ b/run.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python + +import raincloud + +if __name__ == "__main__": + app = raincloud.app() + app.run() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b1ae010 --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import find_packages, setup + +setup( + name="raincloud", + long_description=open("README.org").read(), + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=["flask", "toml"], +)