make package
This commit is contained in:
parent
4146cb5b8a
commit
1d4ac859d7
15 changed files with 552 additions and 0 deletions
116
.gitignore
vendored
Normal file
116
.gitignore
vendored
Normal 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
3
README.org
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
* raincloud
|
||||
|
||||
/Self-hosted file sharing cloud for you and your firends./
|
||||
8
images/logo.svg
Normal file
8
images/logo.svg
Normal 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
121
raincloud/__init__.py
Executable 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
|
||||
3
raincloud/default_settings.py
Normal file
3
raincloud/default_settings.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
CLOUD_NAME = "raincloud"
|
||||
SECRET_KEY = "dev"
|
||||
BASE_PATH = "public"
|
||||
73
raincloud/directory_handler.py
Normal file
73
raincloud/directory_handler.py
Normal 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}"
|
||||
)
|
||||
)
|
||||
36
raincloud/session_handler.py
Normal file
36
raincloud/session_handler.py
Normal 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_]
|
||||
BIN
raincloud/static/favicon.png
Normal file
BIN
raincloud/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
15
raincloud/static/logo.svg
Normal file
15
raincloud/static/logo.svg
Normal 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
108
raincloud/static/style.css
Normal 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;
|
||||
}
|
||||
7
raincloud/templates/authenticate.html
Normal file
7
raincloud/templates/authenticate.html
Normal 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 %}
|
||||
18
raincloud/templates/base.html
Normal file
18
raincloud/templates/base.html
Normal 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>
|
||||
27
raincloud/templates/directory.html
Normal file
27
raincloud/templates/directory.html
Normal 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
7
run.py
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import raincloud
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = raincloud.app()
|
||||
app.run()
|
||||
10
setup.py
Normal file
10
setup.py
Normal 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"],
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue