new
This commit is contained in:
33
README.md
Normal file
33
README.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# MINI-FLEES
|
||||||
|
|
||||||
|
A mini file sharing website.
|
||||||
|
|
||||||
|
|
||||||
|
# installation
|
||||||
|
|
||||||
|
- `bash create_config.sh`
|
||||||
|
- `docker-compose up --build`
|
||||||
|
|
||||||
|
# configuration
|
||||||
|
|
||||||
|
- configure service with data/config.json
|
||||||
|
- Change your `app_secret_key` !!
|
||||||
|
- Change your `access_token` !!
|
||||||
|
- Change your `public_url`
|
||||||
|
- Change your `timezone`
|
||||||
|
- `uid` = user id for new files
|
||||||
|
- `workers` = parallel processes (i.e. one upload reserves a process)
|
||||||
|
- `timeout` = timeout for processes, single upload might take a long time!
|
||||||
|
- configure bind host and port in .env
|
||||||
|
- proxy with nginx, match bind port, body size and timeout to your needs:
|
||||||
|
```
|
||||||
|
location /flees/ {
|
||||||
|
proxy_pass http://localhost:8136/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Scheme $scheme;
|
||||||
|
proxy_set_header X-Script-Name /flees;
|
||||||
|
client_max_body_size 8G;
|
||||||
|
client_body_timeout 3600s;
|
||||||
|
}
|
||||||
|
```
|
||||||
3
code/.dockerignore
Normal file
3
code/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
*.swp
|
||||||
35
code/Dockerfile
Normal file
35
code/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update -yqq \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
sqlite3 \
|
||||||
|
tzdata \
|
||||||
|
git \
|
||||||
|
make \
|
||||||
|
python3-venv \
|
||||||
|
python3-pip \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ARG UID
|
||||||
|
ARG GID
|
||||||
|
ARG TZ
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
RUN groupadd -g $GID user && \
|
||||||
|
useradd -u $UID -g $GID -ms /bin/bash user && \
|
||||||
|
mkdir -p /opt/venv && chown $UID:$GID /opt/venv
|
||||||
|
COPY ./requirements.txt /requirements.txt
|
||||||
|
COPY docker-builder.sh /
|
||||||
|
USER user
|
||||||
|
|
||||||
|
RUN bash /docker-builder.sh
|
||||||
|
COPY ./ /app
|
||||||
|
USER root
|
||||||
|
RUN chown -R $UID:$GID /app
|
||||||
|
USER user
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
CMD bash /app/entrypoint.sh
|
||||||
163
code/app.py
Normal file
163
code/app.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os, sys, time
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import (
|
||||||
|
Flask,
|
||||||
|
render_template,
|
||||||
|
jsonify,
|
||||||
|
current_app,
|
||||||
|
Response,
|
||||||
|
redirect,
|
||||||
|
url_for,
|
||||||
|
request,
|
||||||
|
g,
|
||||||
|
session,
|
||||||
|
send_file,
|
||||||
|
send_from_directory,
|
||||||
|
abort,
|
||||||
|
)
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from revprox import ReverseProxied
|
||||||
|
from utils import (
|
||||||
|
random_token,
|
||||||
|
db_store_file,
|
||||||
|
file_details,
|
||||||
|
file_list,
|
||||||
|
db_add_download,
|
||||||
|
db_get_file,
|
||||||
|
db_delete_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
__MINI_FLEES_VERSION__ = "20230818.0"
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(__name__)
|
||||||
|
app.config.from_prefixed_env()
|
||||||
|
app.debug = True
|
||||||
|
app.secret_key = app.config["APP_SECRET_KEY"]
|
||||||
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return "", 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/upload", methods=["POST"])
|
||||||
|
def upload():
|
||||||
|
if request.method == "POST":
|
||||||
|
file = request.files.get("file")
|
||||||
|
name = request.headers.get("Name", None)
|
||||||
|
if name is None:
|
||||||
|
return "Name required", 500
|
||||||
|
secret = request.headers.get("Secret", "")
|
||||||
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
|
return "Error", 401
|
||||||
|
max_dl = request.headers.get("Max-Downloads", app.config["DEFAULT_MAX_DL"])
|
||||||
|
expires = int(time.time()) + int(app.config["DEFAULT_EXPIRE"])
|
||||||
|
if "Expires-days" in request.headers:
|
||||||
|
expires = int(time.time()) + 24 * 3600 * int(
|
||||||
|
request.headers.get("Expires-days")
|
||||||
|
)
|
||||||
|
if "Expires-hours" in request.headers:
|
||||||
|
expires = int(time.time()) + 3600 * int(
|
||||||
|
request.headers.get("Expires-hours")
|
||||||
|
)
|
||||||
|
|
||||||
|
if file:
|
||||||
|
safe_filename = secure_filename(name)
|
||||||
|
token = random_token()
|
||||||
|
folder = os.path.join(app.config["DATAFOLDER"], token)
|
||||||
|
os.mkdir(folder)
|
||||||
|
filename = os.path.join(folder, safe_filename)
|
||||||
|
file.save(filename)
|
||||||
|
db_store_file(token, safe_filename, expires, max_dl)
|
||||||
|
download_url = f"{app.config['PUBLIC_URL']}/dl/{token}/{safe_filename}"
|
||||||
|
return "File uploaded\n%s\n" % (download_url,), 200
|
||||||
|
else:
|
||||||
|
return "Use the 'file' variable to upload\n", 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/details/<token>/<name>", methods=["GET"])
|
||||||
|
def details(token, name):
|
||||||
|
secret = request.headers.get("Secret", "")
|
||||||
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
|
return "Error", 401
|
||||||
|
details = file_details(token, name)
|
||||||
|
return jsonify(details), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/delete/<token>/<name>", methods=["GET"])
|
||||||
|
def delete_file(name, token):
|
||||||
|
secret = request.headers.get("Secret", "")
|
||||||
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
|
return "Error", 401
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(os.getenv("DATAFOLDER"), token, name))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
db_delete_file(token, name)
|
||||||
|
return "OK", 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/ls", methods=["GET"])
|
||||||
|
def ls():
|
||||||
|
secret = request.headers.get("Secret", "")
|
||||||
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
|
return "Error", 401
|
||||||
|
return "\n".join(file_list()), 200
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/dl/<token>/<name>", methods=["GET"])
|
||||||
|
def download(name, token):
|
||||||
|
return download_file(token, name)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/script/client", methods=["GET"])
|
||||||
|
def script_client():
|
||||||
|
return render_template(
|
||||||
|
"client.py", name=name, token=token, rooturl=request.url_root
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/script/flip", methods=["GET"])
|
||||||
|
def script_flip():
|
||||||
|
return render_template(
|
||||||
|
"flip",
|
||||||
|
name=name,
|
||||||
|
token=token,
|
||||||
|
rooturl=request.url_root,
|
||||||
|
version=__FLEES_VERSION__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(token, name):
|
||||||
|
full_path = os.path.join(os.getenv("FLASK_DATAFOLDER"), token, name)
|
||||||
|
if not os.path.exists(full_path):
|
||||||
|
return "Error", 404
|
||||||
|
db_stat = db_get_file(token, name)
|
||||||
|
if db_stat:
|
||||||
|
added, expires, downloads, max_dl = db_stat
|
||||||
|
else:
|
||||||
|
return "Error", 404
|
||||||
|
if downloads >= max_dl:
|
||||||
|
return "Expired", 401
|
||||||
|
if expires < time.time():
|
||||||
|
return "Expired", 401
|
||||||
|
db_add_download(token, name)
|
||||||
|
return send_from_directory(
|
||||||
|
directory=os.path.join(os.getenv("FLASK_DATAFOLDER"), token), path=name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def print_debug(s):
|
||||||
|
if app.config["DEBUG"]:
|
||||||
|
sys.stderr.write(str(s) + "\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True)
|
||||||
7
code/docker-builder.sh
Normal file
7
code/docker-builder.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
python3 -m venv /opt/venv
|
||||||
|
. /opt/venv/bin/activate
|
||||||
|
pip3 install -r requirements.txt
|
||||||
22
code/entrypoint.sh
Normal file
22
code/entrypoint.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
PYTHON=python3
|
||||||
|
SQLITE=sqlite3
|
||||||
|
|
||||||
|
export FLASK_DATAFOLDER="/data"
|
||||||
|
export FLASK_DB="/data/flees.db"
|
||||||
|
export FLASK_CONF="/data/config.json"
|
||||||
|
export SERVER=gunicorn
|
||||||
|
export PID="flees.pid"
|
||||||
|
export WORKERS
|
||||||
|
|
||||||
|
if [[ $( stat -c %u /data ) -ne $( id -u ) ]]; then
|
||||||
|
echo User id and /data folder owner do not match
|
||||||
|
printf 'UID: %s\nFolder: %s\n' $( id -u ) $( stat -c %u /data )
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
. /opt/venv/bin/activate
|
||||||
|
sh ./init_db.sh "$FLASK_DB"
|
||||||
|
|
||||||
|
exec "$SERVER" -w $WORKERS 'app:app' --pid="$PID" -b 0.0.0.0:5000
|
||||||
13
code/init_db.sh
Normal file
13
code/init_db.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
|
||||||
|
cat <<'EOF' | sqlite3 "$1"
|
||||||
|
CREATE TABLE IF NOT EXISTS files (
|
||||||
|
token text PRIMARY KEY,
|
||||||
|
name text NOT NULL,
|
||||||
|
added integer NOT NULL,
|
||||||
|
expires integer NOT NULL,
|
||||||
|
downloads integer NOT NULL,
|
||||||
|
max_downloads integer NOT NULL
|
||||||
|
);
|
||||||
|
EOF
|
||||||
2
code/requirements.txt
Normal file
2
code/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask
|
||||||
|
gunicorn
|
||||||
33
code/revprox.py
Normal file
33
code/revprox.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
class ReverseProxied(object):
|
||||||
|
"""Wrap the application in this middleware and configure the
|
||||||
|
front-end server to add these headers, to let you quietly bind
|
||||||
|
this to a URL other than / and to an HTTP scheme that is
|
||||||
|
different than what is used locally.
|
||||||
|
|
||||||
|
In nginx:
|
||||||
|
location /myprefix {
|
||||||
|
proxy_pass http://192.168.0.1:5001;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Scheme $scheme;
|
||||||
|
proxy_set_header X-Script-Name /myprefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
:param app: the WSGI application
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
script_name = environ.get("HTTP_X_SCRIPT_NAME", "")
|
||||||
|
if script_name:
|
||||||
|
environ["SCRIPT_NAME"] = script_name
|
||||||
|
path_info = environ["PATH_INFO"]
|
||||||
|
if path_info.startswith(script_name):
|
||||||
|
environ["PATH_INFO"] = path_info[len(script_name) :]
|
||||||
|
|
||||||
|
scheme = environ.get("HTTP_X_SCHEME", "")
|
||||||
|
if scheme:
|
||||||
|
environ["wsgi.url_scheme"] = scheme
|
||||||
|
return self.app(environ, start_response)
|
||||||
3
code/utils/__init__.py
Normal file
3
code/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .misc import *
|
||||||
|
from .files import *
|
||||||
|
from .crypt import *
|
||||||
5
code/utils/crypt.py
Normal file
5
code/utils/crypt.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import secrets
|
||||||
|
|
||||||
|
|
||||||
|
def random_token():
|
||||||
|
return secrets.token_urlsafe(8)
|
||||||
230
code/utils/files.py
Normal file
230
code/utils/files.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import current_app as app
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import stat
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
from .misc import *
|
||||||
|
from .crypt import *
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = sqlite3.connect(os.getenv("FLASK_DB"), timeout=5)
|
||||||
|
c = db.cursor()
|
||||||
|
return db, c
|
||||||
|
|
||||||
|
|
||||||
|
def db_store_file(token, name, expires, max_dl):
|
||||||
|
db, c = get_db()
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
insert into files(token,name,added,expires,downloads,max_downloads)
|
||||||
|
values (?,?,?,?,?,?)
|
||||||
|
""",
|
||||||
|
(token, name, int(time.time()), expires, 0, max_dl),
|
||||||
|
)
|
||||||
|
if c.rowcount > 0:
|
||||||
|
db.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def db_get_file(token, name):
|
||||||
|
db, c = get_db()
|
||||||
|
return db.execute(
|
||||||
|
"""
|
||||||
|
SELECT added,expires,downloads,max_downloads
|
||||||
|
FROM files WHERE token = ? AND name = ?
|
||||||
|
""",
|
||||||
|
(token, name),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
def db_get_files():
|
||||||
|
db, c = get_db()
|
||||||
|
return db.execute(
|
||||||
|
"""
|
||||||
|
SELECT token,name,added,expires,downloads,max_downloads
|
||||||
|
FROM files
|
||||||
|
WHERE expires > ? and downloads < max_downloads
|
||||||
|
""",
|
||||||
|
(time.time(),),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def db_delete_file(token, name):
|
||||||
|
db, c = get_db()
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM files WHERE token = ? and name = ?
|
||||||
|
""",
|
||||||
|
(token, name),
|
||||||
|
)
|
||||||
|
if c.rowcount > 0:
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def db_add_download(token, name):
|
||||||
|
db, c = get_db()
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
UPDATE files SET downloads = downloads + 1 WHERE token = ? AND name = ?
|
||||||
|
""",
|
||||||
|
(token, name),
|
||||||
|
)
|
||||||
|
if c.rowcount > 0:
|
||||||
|
db.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def file_autoremove():
|
||||||
|
db, c = get_db()
|
||||||
|
rows = db.execute(
|
||||||
|
"""
|
||||||
|
select
|
||||||
|
token, name
|
||||||
|
from files
|
||||||
|
where expires > ? or downloads >= max_downloads
|
||||||
|
""",
|
||||||
|
(int(time.time()),),
|
||||||
|
)
|
||||||
|
deleted_tokens = []
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(os.getenv("DATAFOLDER"), row[0], row[1]))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.rmdir(os.path.join(os.getenv("DATAFOLDER"), row[0]))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
deleted_tokens.append(row[0])
|
||||||
|
if len(deleted_tokens) > 0:
|
||||||
|
db, c = get_db()
|
||||||
|
for token in deleted_tokens:
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM files WHERE token = ?
|
||||||
|
""",
|
||||||
|
(token,),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def file_age(path):
|
||||||
|
now = datetime.now()
|
||||||
|
then = datetime.fromtimestamp(os.stat(path).st_mtime)
|
||||||
|
diff = now - then
|
||||||
|
return (
|
||||||
|
diff,
|
||||||
|
"%03d d %s"
|
||||||
|
% (diff.days, datetime.utcfromtimestamp(diff.seconds).strftime("%H:%M:%S")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def file_details(token, name):
|
||||||
|
full_path = os.path.join(os.getenv("FLASK_DATAFOLDER"), token, name)
|
||||||
|
try:
|
||||||
|
s = os.stat(full_path)
|
||||||
|
db_stat = db_get_file(token, name)
|
||||||
|
if db_stat:
|
||||||
|
added, expires, downloads, max_dl = db_stat
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"size": file_size_MB(s.st_size),
|
||||||
|
"hsize": file_size_human(s.st_size, HTML=False),
|
||||||
|
"added": file_date_human(added),
|
||||||
|
"name": name,
|
||||||
|
"url": f"{app.config['PUBLIC_URL']}/dl/{token}/{name}",
|
||||||
|
"expires": file_date_human(expires),
|
||||||
|
"downloaded": downloads,
|
||||||
|
"max-dl": max_dl,
|
||||||
|
}
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def file_list():
|
||||||
|
|
||||||
|
files = list(db_get_files())
|
||||||
|
details = []
|
||||||
|
maxlen = 4
|
||||||
|
for file in files:
|
||||||
|
maxlen = max(maxlen, len(file[1]))
|
||||||
|
details = []
|
||||||
|
details.append(
|
||||||
|
"Added/Expiry DL/MaxDL URL"
|
||||||
|
)
|
||||||
|
details.append("=" * 75)
|
||||||
|
for file in files:
|
||||||
|
details.append(
|
||||||
|
" ".join((
|
||||||
|
str(file[2]),str(file[3]),str(file[4]),str(file[5]),
|
||||||
|
f"{app.config['PUBLIC_URL']}/dl/{file[0]}/{file[1]}"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
def get_expiring_file(ehash):
|
||||||
|
connection = apsw.Connection(app.config["SQLITE_FILE"])
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
for row in cursor.execute(
|
||||||
|
"SELECT file, expires FROM expiring WHERE hash = ?", (ehash,)
|
||||||
|
):
|
||||||
|
return row[0], row[1]
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_script_url(public_url, share, end_point, token="[TOKEN]"):
|
||||||
|
cmd = None
|
||||||
|
doc = None
|
||||||
|
if get_or_none("direct_links", share) and end_point == "download":
|
||||||
|
end_point = "direct"
|
||||||
|
url = "%s/script/%s/%s/%s" % (public_url, end_point, share["name"], token)
|
||||||
|
if end_point in ("download", "direct"):
|
||||||
|
cmd = "curl -s %s | bash /dev/stdin [-f]" % (url,)
|
||||||
|
doc = "Download all files in the share. -f to force overwrite existing files."
|
||||||
|
if end_point == "client":
|
||||||
|
cmd = "python <( curl -s %s )" % (url,)
|
||||||
|
doc = "Console client to download and upload files."
|
||||||
|
if end_point == "upload_split":
|
||||||
|
cmd = (
|
||||||
|
"curl -s %s | python - [-s split_size_in_Mb] file_to_upload.ext [second.file.ext]"
|
||||||
|
% (url,)
|
||||||
|
)
|
||||||
|
doc = "Upload files to the share. -s to set splitting size."
|
||||||
|
if end_point == "flip":
|
||||||
|
cmd = "curl -s %s > flip && ./flip" % (url,)
|
||||||
|
doc = "Use the share as a command line clipboard"
|
||||||
|
return {"cmd": cmd, "doc": doc}
|
||||||
|
|
||||||
|
|
||||||
|
def set_expiring_file(share, filename, expires):
|
||||||
|
connection = apsw.Connection(app.config["SQLITE_FILE"])
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ehash = random_expiring_hash()
|
||||||
|
matches = len(
|
||||||
|
list(cursor.execute("SELECT file FROM expiring WHERE hash = ?", (ehash,)))
|
||||||
|
)
|
||||||
|
if matches == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO expiring (hash, file, expires) VALUES (?,?,?)",
|
||||||
|
(ehash, filename, expires),
|
||||||
|
)
|
||||||
|
return "/".join((app.config["PUBLIC_URL"], "e", ehash, os.path.basename(filename)))
|
||||||
|
|
||||||
|
|
||||||
|
def remove_expiring_file(share, filename):
|
||||||
|
connection = apsw.Connection(app.config["SQLITE_FILE"])
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
cursor.execute("DELETE FROM expiring WHERE file = ?", (filename,))
|
||||||
62
code/utils/misc.py
Normal file
62
code/utils/misc.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from flask import current_app as app
|
||||||
|
|
||||||
|
|
||||||
|
def file_date_human(num):
|
||||||
|
return datetime.fromtimestamp(num).strftime("%y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
def file_size_human(num, HTML=True):
|
||||||
|
space = " " if HTML else " "
|
||||||
|
for x in [space + "B", "KB", "MB", "GB", "TB"]:
|
||||||
|
if num < 1024.0:
|
||||||
|
if x == space + "B":
|
||||||
|
return "%d%s%s" % (num, space, x)
|
||||||
|
return "%3.1f%s%s" % (num, space, x)
|
||||||
|
num /= 1024.0
|
||||||
|
|
||||||
|
|
||||||
|
def file_size_MB(num):
|
||||||
|
return "{:,.2f}".format(num / (1024 * 1024))
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_none(key, d, none=None):
|
||||||
|
if key in d:
|
||||||
|
return d[key]
|
||||||
|
else:
|
||||||
|
return none
|
||||||
|
|
||||||
|
|
||||||
|
def is_path_safe(path):
|
||||||
|
if path.startswith("."):
|
||||||
|
return False
|
||||||
|
if "/." in path:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_url(url, qualifying=None):
|
||||||
|
min_attributes = ("scheme", "netloc")
|
||||||
|
qualifying = min_attributes if qualifying is None else qualifying
|
||||||
|
token = urlparse(url)
|
||||||
|
return all([getattr(token, qualifying_attr) for qualifying_attr in qualifying])
|
||||||
|
|
||||||
|
|
||||||
|
def path2url(path):
|
||||||
|
return pathname2url(path)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_name(s):
|
||||||
|
return safe_string(s, "-_")
|
||||||
|
|
||||||
|
|
||||||
|
def safe_path(s):
|
||||||
|
return safe_string(s, "-_/")
|
||||||
|
|
||||||
|
|
||||||
|
def safe_string(s, valid, no_repeat=False):
|
||||||
|
"""return a safe string, replace non alnum characters with _ . all characters in valid are considered valid."""
|
||||||
|
safe = "".join([c if c.isalnum() or c in valid else "_" for c in s])
|
||||||
|
if no_repeat:
|
||||||
|
safe = re.sub(r"_+", "_", safe)
|
||||||
|
return safe
|
||||||
26
docker-compose.yaml
Normal file
26
docker-compose.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
|
||||||
|
services:
|
||||||
|
miniflees:
|
||||||
|
build:
|
||||||
|
context: code
|
||||||
|
args:
|
||||||
|
UID: ${UID}
|
||||||
|
GID: ${GID}
|
||||||
|
TZ: ${TZ}
|
||||||
|
ports:
|
||||||
|
- "${EXPOSE}:5000"
|
||||||
|
volumes:
|
||||||
|
- ./data/:/data/
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
WORKERS: ${WORKERS}
|
||||||
|
FLASK_APP_SECRET_KEY:
|
||||||
|
FLASK_ACCESS_TOKEN:
|
||||||
|
FLASK_PUBLIC_URL:
|
||||||
|
FLASK_DEFAULT_EXPIRE:
|
||||||
|
FLASK_DEFAULT_MAX_DL:
|
||||||
|
TZ:
|
||||||
|
|
||||||
|
|
||||||
10
env.example
Normal file
10
env.example
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
EXPOSE=0.0.0.0:8136
|
||||||
|
UID=1000
|
||||||
|
GID=1000
|
||||||
|
TZ=Europe/Helsinki
|
||||||
|
WORKERS=4
|
||||||
|
FLASK_APP_SECRET_KEY=8a36bfea77d842386a2a0c7c3e044228363dfddadc01fade4b1b78859ffc615b
|
||||||
|
FLASK_ACCESS_TOKEN=dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714
|
||||||
|
FLASK_PUBLIC_URL=http://localhost:8136
|
||||||
|
FLASK_DEFAULT_EXPIRE=2592000
|
||||||
|
FLASK_DEFAULT_MAX_DL=9999
|
||||||
150
test/run-tests.sh
Executable file
150
test/run-tests.sh
Executable file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
_qCol() {
|
||||||
|
# print "easier" mapping of ANSI colors and controls
|
||||||
|
local K="\033[1;30m"
|
||||||
|
local R="\033[1;31m"
|
||||||
|
local G="\033[1;32m"
|
||||||
|
local B="\033[1;34m"
|
||||||
|
local Y="\033[1;33m"
|
||||||
|
local M="\033[1;35m"
|
||||||
|
local C="\033[1;36m"
|
||||||
|
local W="\033[1;37m"
|
||||||
|
|
||||||
|
local k="\033[2;30m"
|
||||||
|
local r="\033[2;31m"
|
||||||
|
local g="\033[2;32m"
|
||||||
|
local b="\033[2;34m"
|
||||||
|
local y="\033[2;33m"
|
||||||
|
local m="\033[2;35m"
|
||||||
|
local c="\033[2;36m"
|
||||||
|
local w="\033[2;37m"
|
||||||
|
|
||||||
|
local bk="\033[40m"
|
||||||
|
local br="\033[41m"
|
||||||
|
local bg="\033[42m"
|
||||||
|
local by="\033[43m"
|
||||||
|
local bb="\033[44m"
|
||||||
|
local bm="\033[45m"
|
||||||
|
local bc="\033[46m"
|
||||||
|
local bw="\033[47m"
|
||||||
|
|
||||||
|
local S='\033[1m' #strong
|
||||||
|
local s='\033[2m' #strong off
|
||||||
|
local U='\033[4m' #underline
|
||||||
|
local u='\033[24m' #underline off
|
||||||
|
local z='\033[0m' #zero colors
|
||||||
|
local Z='\033[0m' #zero colors
|
||||||
|
local ic='\033[7m' #inverse colors
|
||||||
|
local io='\033[27m' #inverse off
|
||||||
|
local st='\033[9m' #strike on
|
||||||
|
local so='\033[29m' #strike off
|
||||||
|
local CLR='\033[2J' # Clear screen
|
||||||
|
local CLREND='\033[K' # Clear to end of line
|
||||||
|
local CLRBEG='\033[1K' # Clear to beginning of line
|
||||||
|
local CLRSCR="$CLR"'\033[0;0H' # Clear screen, reset cursor
|
||||||
|
|
||||||
|
local color_keys=" K R G B Y M C W k r g b y m c w S s U u z Z ic io st so bk br bg by bb bm bc bw CLR CLREND CLRBEG CLRSCR "
|
||||||
|
|
||||||
|
[[ "$1" = "export" ]] && {
|
||||||
|
local key
|
||||||
|
local prefix="$2"
|
||||||
|
[[ -z "$2" ]] && prefix=_c
|
||||||
|
for key in $color_keys; do
|
||||||
|
eval export ${prefix}${key}=\'${!key}\'
|
||||||
|
done
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
local arg val
|
||||||
|
for ((arg=1;arg<=$#;arg++)) {
|
||||||
|
val=${!arg}
|
||||||
|
[[ ${color_keys} = *" $val "* ]] || { echo "No such color code '${val}'" >&2; return 1; }
|
||||||
|
printf ${!val}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cont() {
|
||||||
|
set +x
|
||||||
|
_qCol G
|
||||||
|
echo Continue
|
||||||
|
_qCol z
|
||||||
|
read continue
|
||||||
|
_qCol Y
|
||||||
|
echo =========================================
|
||||||
|
_qCol z
|
||||||
|
set -x
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BIG="big file(1).ext"
|
||||||
|
BIGS="big_file1.ext"
|
||||||
|
SMALL="small file"
|
||||||
|
SMALLS="small_file"
|
||||||
|
IMAGE="image.jpg"
|
||||||
|
|
||||||
|
test -f "$BIG" || dd if=/dev/zero of="$BIG" bs=8192 count=40000
|
||||||
|
test -f "$SMALL" ||dd if=/dev/urandom of="$SMALL" bs=4096 count=400
|
||||||
|
test -f "$IMAGE" || convert -size 640x480 xc:gray $IMAGE
|
||||||
|
|
||||||
|
set -x
|
||||||
|
#~ cont
|
||||||
|
ROOTURL="http://localhost:8136"
|
||||||
|
|
||||||
|
if false; then
|
||||||
|
pv "$IMAGE" | \
|
||||||
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
|
-H "Name: $IMAGE" \
|
||||||
|
-H "Max-Downloads: 4" \
|
||||||
|
-H "Expires-Days: 14" \
|
||||||
|
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
||||||
|
"$ROOTURL"/upload
|
||||||
|
fi
|
||||||
|
|
||||||
|
if false; then
|
||||||
|
pv "$SMALL" | \
|
||||||
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
|
-H "Name: $SMALL" \
|
||||||
|
-H "Max-Downloads: 4000" \
|
||||||
|
-H "Expires-Days: 14" \
|
||||||
|
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
||||||
|
"$ROOTURL"/upload
|
||||||
|
|
||||||
|
pv "$BIG" | \
|
||||||
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
|
-H "Name: $BIG" \
|
||||||
|
-H "Max-Downloads: 4000" \
|
||||||
|
-H "Expires-Days: 14" \
|
||||||
|
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
||||||
|
"$ROOTURL"/upload
|
||||||
|
fi
|
||||||
|
|
||||||
|
sqlite3 ../data/flees.db "select * FROM files"
|
||||||
|
|
||||||
|
if false; then
|
||||||
|
curl -fL -w "\n" \
|
||||||
|
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
||||||
|
"$ROOTURL"/details/OdD7X0aKOGM/big_file1.ext
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
if false; then
|
||||||
|
rm -f big_file1.ext
|
||||||
|
wget \
|
||||||
|
"$ROOTURL"/dl/OdD7X0aKOGM/big_file1.ext
|
||||||
|
fi
|
||||||
|
|
||||||
|
if false; then
|
||||||
|
curl -fL -w "\n" \
|
||||||
|
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
||||||
|
"$ROOTURL"/delete/SKDMsQ3ifx8/image.jpg
|
||||||
|
fi
|
||||||
|
|
||||||
|
if true; then
|
||||||
|
curl -fL -w "\n" \
|
||||||
|
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
||||||
|
"$ROOTURL"/ls
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user