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