diff --git a/flake.lock b/flake.lock index d8c1ef6..8f8641f 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "flake-utils": { "locked": { - "lastModified": 1648297722, - "narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=", + "lastModified": 1649676176, + "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", "owner": "numtide", "repo": "flake-utils", - "rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade", + "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", "type": "github" }, "original": { @@ -17,11 +17,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1649225869, - "narHash": "sha256-u1zLtPmQzhT9mNXyM8Ey9pk7orDrIKdwooeGDEXm5xM=", + "lastModified": 1650701402, + "narHash": "sha256-XKfstdtqDg+O+gNBx1yGVKWIhLgfEDg/e2lvJSsp9vU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b6966d911da89e5a7301aaef8b4f0a44c77e103c", + "rev": "bc41b01dd7a9fdffd32d9b03806798797532a5fe", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 49d1596..a16e2c6 100644 --- a/flake.nix +++ b/flake.nix @@ -1,27 +1,156 @@ { - description = "Stream media files over SSH"; - nixConfig.bash-prompt = "\[\\e[1mmincloud-dev\\e[0m:\\w\]$ "; + description = "Self-hosted file sharing cloud for you and your friends"; inputs = { nixpkgs.url = github:nixos/nixpkgs/nixos-unstable; flake-utils.url = github:numtide/flake-utils; }; - outputs = { self, nixpkgs, flake-utils }: + outputs = { self, nixpkgs, flake-utils }: { - flake-utils.lib.eachDefaultSystem - (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - in - { - # Development shell + nixosModule = { config, ... }: + with nixpkgs.lib; + let + system = config.nixpkgs.localSystem.system; + + python = nixpkgs.legacyPackages.${system}.python; + flask = nixpkgs.legacyPackages.${system}.python3Packages.flask; + gunicorn = nixpkgs.legacyPackages.${system}.python3Packages.gunicorn; + raincloud = self.packages.${system}.raincloud; + toml = nixpkgs.legacyPackages.${system}.python3Packages.toml; + + cfg = config.services.raincloud; + + raincloud_config = nixpkgs.legacyPackages.${system}.writeText "raincloud_config.py" '' + CLOUD_NAME = "${cfg.cloudName}" + SECRET_KEY = "${cfg.secretKey}" + BASE_PATH = "${cfg.basePath}" + ''; + in + { + options.services.raincloud = { + + enable = mkEnableOption "Enable raincloud WSGI server"; + + address = mkOption { + type = types.str; + default = "127.0.0.1"; + example = "0.0.0.0"; + description = "Bind address of the server"; + }; + + port = mkOption { + type = types.str; + default = "8000"; + example = "4000"; + description = "Port on which the server listens"; + }; + + user = mkOption { + type = types.str; + default = "raincloud"; + description = "User under which the server runs"; + }; + + group = mkOption { + type = types.str; + default = "raincloud"; + description = "Group under which the server runs"; + }; + + cloudName = mkOption { + type = types.str; + default = "raincloud"; + description = "Name of the raincloud"; + }; + + basePath = mkOption { + type = types.str; + description = "Base path of the raincloud"; + }; + + secretKey = mkOption { + type = types.str; + default = "i_am_a_key"; + description = "Flask secret key"; + }; + }; + + config = mkIf cfg.enable { + + systemd.services.raincloud = { + description = "Enable raincloud WSGI server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + restartIfChanged = true; + + environment = + let + penv = python.buildEnv.override { + extraLibs = [ flask raincloud toml ]; + }; + in + { + PYTHONPATH = "${penv}/${python.sitePackages}/"; + }; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + Restart = "always"; + RestartSec = "5s"; + PermissionsStartOnly = true; + + ExecStart = '' + ${gunicorn}/bin/gunicorn "raincloud:app('${raincloud_config}')" \ + --bind=${cfg.address}:${cfg.port} + ''; + }; + }; + + users.users = mkIf (cfg.user == "raincloud") { + raincloud = { + group = cfg.group; + isSystemUser = true; + }; + }; + + users.groups = mkIf (cfg.group == "raincloud") { + raincloud = { }; + }; + + }; + + }; + + } // flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + + # Package + packages.raincloud = + pkgs.python3Packages.buildPythonPackage rec { + name = "raincloud"; + src = self; + propagatedBuildInputs = with pkgs; [ + python3Packages.flask + python3Packages.toml + ]; + }; + defaultPackage = self.packages.${system}.raincloud; + + # Development shell devShell = pkgs.mkShell { buildInputs = with pkgs; [ python3 python3Packages.flask - python3Packages.toml + python3Packages.gunicorn + python3Packages.toml ]; }; } - ); + ); } diff --git a/raincloud.py b/raincloud.py deleted file mode 100755 index 858ff3c..0000000 --- a/raincloud.py +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env python - -import crypt -import toml -import uuid -import werkzeug -from datetime import datetime, timedelta -from hmac import compare_digest as compare_hash -from flask import ( - abort, - Flask, - render_template, - redirect, - request, - send_from_directory, - session, - url_for, -) -from pathlib import Path -from werkzeug.utils import secure_filename - -base_path = Path("public") -cloud_name = "raincloud" -sessions = [] - - -class RaincloudIOException(Exception): - pass - - -def clean_sessions(): - global sessions - sessions = [s for s in sessions if s[0] > datetime.now() - timedelta(days=1)] - - -def validate_session(directory, id_): - global sessions - valid_dates = [s[0] for s in 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(id_): - global sessions - sessions = [s for s in sessions if s[2] != id_] - - -def get_session_id(): - global sessions - ids = [s[2] for s in sessions] - id_ = uuid.uuid4() - while id_ in ids: - id_ = uuid.uuid4() - return id_ - - -def get_config(directory): - """Load a 'rc.toml' file from given directory. - - Parameters: - directory - basepath of 'mincloud.conf' - - Returns: Dictionary of config parameters - """ - path = 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(directory): - path = 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({"name": p.name}) - return files - - -app = Flask(__name__) - - -@app.route("/", methods=["GET", "POST"]) -@app.route("//", methods=["GET"]) -def directory(directory, filename=None): - global sessions - try: - - # Clean sessions - clean_sessions() - - # Logout - if request.method == "POST" and "logout" in request.form: - delete_session(session[directory]) - return redirect(url_for("directory", directory=directory)) - - config = get_config(directory) - if config["hashed_password"]: - authenticated = ( - True - if directory in session - and 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_ = get_session_id() - session[directory] = id_ - sessions.append((datetime.now(), directory, id_)) - return redirect(url_for("directory", directory=directory)) - else: - return render_template( - "authenticate.html", cloud_name=cloud_name, config=config - ) - - if request.method == "GET": - # List - if not filename: - files = get_files(directory) - return render_template( - "directory.html", cloud_name=cloud_name, config=config, files=files - ) - - # Download - else: - if config["download"] and filename != "rc.toml": - return send_from_directory(base_path / directory, filename) - else: - abort(403) - - # Upload - elif request.method == "POST": - if config["upload"]: - f = request.files["file"] - filename = secure_filename(f.filename) - if filename != "rc.toml": - f.save(base_path / directory / filename) - - # Reload - return redirect(url_for("directory", directory=directory)) - else: - abort(403) - - except RaincloudIOException as e: - print(e) - abort(404) - - -app.secret_key = "raincloud" -app.run(host="0.0.0.0", debug=True) diff --git a/static/favicon.png b/static/favicon.png deleted file mode 100644 index e5d3ba1..0000000 Binary files a/static/favicon.png and /dev/null differ diff --git a/static/style.css b/static/style.css deleted file mode 100644 index aa60628..0000000 --- a/static/style.css +++ /dev/null @@ -1,108 +0,0 @@ -@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/templates/authenticate.html b/templates/authenticate.html deleted file mode 100644 index bc89042..0000000 --- a/templates/authenticate.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} -{% block content %} -
- - -
-{% endblock %} diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index b291d51..0000000 --- a/templates/base.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - -{{ config["directory"] }} | {{ cloud_name }} -
- -
- {% block content %}{% endblock %} -
-
diff --git a/templates/directory.html b/templates/directory.html deleted file mode 100644 index 92bef8a..0000000 --- a/templates/directory.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "base.html" %} -{% block menu %} - {% if config["upload"] %} -
- -
- {% endif %} - {% if config["hashed_password"] %} -
- - -
- {% endif %} -{% endblock %} -{% block content %} - {% for f in files %} -
-

{{ f["name"] }}

- {% if config["download"] %} - Download - {% endif %} -
- {% endfor %} -{% endblock %}