make package

This commit is contained in:
Denis Lehmann 2022-04-24 17:48:32 +02:00
parent 4146cb5b8a
commit 1d4ac859d7
15 changed files with 552 additions and 0 deletions

116
.gitignore vendored Normal file
View file

@ -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/

3
README.org Normal file
View file

@ -0,0 +1,3 @@
* raincloud
/Self-hosted file sharing cloud for you and your firends./

8
images/logo.svg Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="1e3" height="1e3" version="1.1" viewBox="0 0 264.58 264.58" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(8.6354 0 0 8.6354 -354.85 -792.76)" fill="#28bcff" stroke-width=".1158">
<path transform="scale(.26458)" d="m210.73 357.03a31.693 31.693 0 0 0-28.924 18.787 22.48 22.48 0 0 0-4.0215-0.36133 22.48 22.48 0 0 0-22.48 22.479 22.48 22.48 0 0 0 22.48 22.48 22.48 22.48 0 0 0 0.12305-4e-3v4e-3h65.643v-4e-3a27.68 27.68 0 0 0 27.557-27.676 27.68 27.68 0 0 0-27.68-27.68 27.68 27.68 0 0 0-9.7676 1.791 31.693 31.693 0 0 0-22.93-9.8164z"/>
<path transform="scale(.26458)" d="m191.91 424.19-16.477 28.537h8.7285l16.477-28.537zm19.332 0-16.477 28.537h8.7285l16.477-28.537zm19.332 0-16.477 28.537h8.7266l16.477-28.537z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 853 B

121
raincloud/__init__.py Executable file
View file

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

View file

@ -0,0 +1,3 @@
CLOUD_NAME = "raincloud"
SECRET_KEY = "dev"
BASE_PATH = "public"

View file

@ -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}"
)
)

View file

@ -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_]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

15
raincloud/static/logo.svg Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="210mm" height="297mm" version="1.1" viewBox="0 0 210 297" xmlns="http://www.w3.org/2000/svg">
<g fill="#1a1a1a">
<circle cx="64.541" cy="98.952" r="12.752"/>
<circle cx="82.328" cy="93.285" r="18.419"/>
<circle cx="99.921" cy="96.465" r="15.238"/>
<rect x="64.541" y="107.76" width="35.38" height="3.9457"/>
<g transform="translate(1.3938 -8.511)">
<rect transform="rotate(30)" x="119.65" y="72.299" width="4.5931" height="22.966" rx="1.2522" ry="1.2522"/>
<rect transform="rotate(30)" x="129.01" y="66.893" width="4.5931" height="22.966" rx="1.2522" ry="1.2522"/>
<rect transform="rotate(30)" x="138.38" y="61.487" width="4.5931" height="22.966" rx="1.2522" ry="1.2522"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 822 B

108
raincloud/static/style.css Normal file
View file

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

View file

@ -0,0 +1,7 @@
{% 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

@ -0,0 +1,18 @@
<!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

@ -0,0 +1,27 @@
{% 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 }}</p>
{% if config["download"] %}
<a href="{{ config["directory"] }}/{{ f }}" class="button">Download</a>
{% endif %}
</div>
{% endfor %}
{% endblock %}

7
run.py Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env python
import raincloud
if __name__ == "__main__":
app = raincloud.app()
app.run()

10
setup.py Normal file
View file

@ -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"],
)