clean repo
This commit is contained in:
parent
1d4ac859d7
commit
311a65b465
8 changed files with 147 additions and 355 deletions
12
flake.lock
generated
12
flake.lock
generated
|
|
@ -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
153
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
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
|
|
|
|||
177
raincloud.py
177
raincloud.py
|
|
@ -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 |
108
static/style.css
108
static/style.css
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue