clean repo

This commit is contained in:
Denis Lehmann 2022-04-24 17:56:59 +02:00
parent 1d4ac859d7
commit 311a65b465
8 changed files with 147 additions and 355 deletions

12
flake.lock generated
View file

@ -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": {

153
flake.nix
View file

@ -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
];
};
}
);
);
}

View file

@ -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("/<directory>", methods=["GET", "POST"])
@app.route("/<directory>/<path:filename>", 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -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;
}

View file

@ -1,7 +0,0 @@
{% extends "base.html" %}
{% block content %}
<form action="/{{ config["directory"] }}" method="post">
<input type="password" name="password" placeholder="Password">
<input type="submit" class="button" value="Authenticate">
</form>
{% endblock %}

View file

@ -1,18 +0,0 @@
<!doctype html>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.png') }}">
<title>{{ config["directory"] }} | {{ cloud_name }}</title>
<div id="container">
<div id="header">
<div>
<span id="cloud-name">{{ cloud_name }}</span><span id="directory-name">{{ config["directory"] }}</span>
</div>
<div id="menu">
{% block menu %}{% endblock %}
</div>
</div>
<div id="content" class="a-fade-in">
{% block content %}{% endblock %}
</div>
</div>

View file

@ -1,27 +0,0 @@
{% extends "base.html" %}
{% block menu %}
{% if config["upload"] %}
<form action="/{{ config["directory"] }}" enctype="multipart/form-data" method="post">
<label class="button upload">
<input type="file" name="file" onchange="this.form.submit()">
Upload
</label>
</form>
{% endif %}
{% if config["hashed_password"] %}
<form action="/{{ config["directory"] }}" method="post">
<input hidden type="text" name="logout">
<input type="submit" class="button logout" value="Logout">
</form>
{% endif %}
{% endblock %}
{% block content %}
{% for f in files %}
<div class="file">
<p>{{ f["name"] }}</p>
{% if config["download"] %}
<a href="{{ config["directory"] }}/{{ f["name"] }}" class="button">Download</a>
{% endif %}
</div>
{% endfor %}
{% endblock %}