getting there
This commit is contained in:
40
README.md
40
README.md
@@ -3,31 +3,39 @@
|
|||||||
A mini file sharing website.
|
A mini file sharing website.
|
||||||
|
|
||||||
|
|
||||||
# installation
|
|
||||||
|
|
||||||
- `bash create_config.sh`
|
|
||||||
- `docker-compose up --build`
|
|
||||||
|
|
||||||
# configuration
|
# configuration
|
||||||
|
|
||||||
- configure service with data/config.json
|
Configure service with .env:
|
||||||
- Change your `app_secret_key` !!
|
|
||||||
- Change your `access_token` !!
|
```
|
||||||
- Change your `public_url`
|
EXPOSE=0.0.0.0:8136 # IP/Port to bind
|
||||||
- Change your `timezone`
|
UID=1000 # files will be written as user
|
||||||
- `uid` = user id for new files
|
GID=1000 # files will be writter as group
|
||||||
- `workers` = parallel processes (i.e. one upload reserves a process)
|
TZ=Europe/Helsinki # your timezone
|
||||||
- `timeout` = timeout for processes, single upload might take a long time!
|
WORKERS=4 # number of concurrent processes
|
||||||
- configure bind host and port in .env
|
FLASK_APP_SECRET_KEY=8a36bfea77d842386a2a0c7c3e044228363d # Key that encrypts cookies
|
||||||
|
FLASK_ACCESS_TOKEN=dff789f0bbe8183d3254258b33a147d5 # Key used in request headers to allow upload etc.
|
||||||
|
FLASK_PUBLIC_URL=http://localhost:8136 # Public url
|
||||||
|
FLASK_DEFAULT_EXPIRE=2592000 # Default seconds to expire files
|
||||||
|
FLASK_DEFAULT_MAX_DL=9999 # Default maximum download times
|
||||||
|
```
|
||||||
|
|
||||||
- proxy with nginx, match bind port, body size and timeout to your needs:
|
- proxy with nginx, match bind port, body size and timeout to your needs:
|
||||||
```
|
```
|
||||||
location /flees/ {
|
location /mfl/ {
|
||||||
proxy_pass http://localhost:8136/;
|
proxy_pass http://localhost:8136/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Scheme $scheme;
|
proxy_set_header X-Scheme $scheme;
|
||||||
proxy_set_header X-Script-Name /flees;
|
proxy_set_header X-Script-Name /mfl;
|
||||||
client_max_body_size 8G;
|
client_max_body_size 8G;
|
||||||
client_body_timeout 3600s;
|
client_body_timeout 3600s;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Running
|
||||||
|
|
||||||
|
- `docker-compose up --build`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ RUN apt-get update -yqq \
|
|||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ARG UID
|
ARG UUID
|
||||||
ARG GID
|
ARG UGID
|
||||||
ARG TZ
|
ARG TZ
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
RUN groupadd -g $GID user && \
|
RUN groupadd -g $UGID user && \
|
||||||
useradd -u $UID -g $GID -ms /bin/bash user && \
|
useradd -u $UUID -g $UGID -ms /bin/bash user && \
|
||||||
mkdir -p /opt/venv && chown $UID:$GID /opt/venv
|
mkdir -p /opt/venv && chown $UUID:$UGID /opt/venv
|
||||||
COPY ./requirements.txt /requirements.txt
|
COPY ./requirements.txt /requirements.txt
|
||||||
COPY docker-builder.sh /
|
COPY docker-builder.sh /
|
||||||
USER user
|
USER user
|
||||||
@@ -28,7 +28,7 @@ USER user
|
|||||||
RUN bash /docker-builder.sh
|
RUN bash /docker-builder.sh
|
||||||
COPY ./ /app
|
COPY ./ /app
|
||||||
USER root
|
USER root
|
||||||
RUN chown -R $UID:$GID /app
|
RUN chown -R $UUID:$UGID /app
|
||||||
USER user
|
USER user
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
104
code/app.py
104
code/app.py
@@ -1,37 +1,33 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import os, sys, time
|
import os
|
||||||
import json
|
import sys
|
||||||
from datetime import datetime
|
import time
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask,
|
Flask,
|
||||||
render_template,
|
render_template,
|
||||||
jsonify,
|
jsonify,
|
||||||
current_app,
|
|
||||||
Response,
|
|
||||||
redirect,
|
|
||||||
url_for,
|
|
||||||
request,
|
request,
|
||||||
g,
|
|
||||||
session,
|
|
||||||
send_file,
|
|
||||||
send_from_directory,
|
send_from_directory,
|
||||||
abort,
|
|
||||||
)
|
)
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from revprox import ReverseProxied
|
from revprox import ReverseProxied
|
||||||
from utils import (
|
from utils.misc import (
|
||||||
random_token,
|
random_token,
|
||||||
|
)
|
||||||
|
from utils.files import (
|
||||||
db_store_file,
|
db_store_file,
|
||||||
file_details,
|
file_details,
|
||||||
file_list,
|
file_list,
|
||||||
|
file_full_path,
|
||||||
|
file_full_url,
|
||||||
db_add_download,
|
db_add_download,
|
||||||
db_get_file,
|
db_get_file,
|
||||||
db_delete_file,
|
db_delete_file,
|
||||||
db_maintenance,
|
db_maintenance,
|
||||||
)
|
)
|
||||||
|
|
||||||
__MINI_FLEES_VERSION__ = "20230818.0"
|
__VERSION__ = "20230818.0"
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(__name__)
|
app.config.from_object(__name__)
|
||||||
app.config.from_prefixed_env()
|
app.config.from_prefixed_env()
|
||||||
@@ -42,11 +38,30 @@ app.wsgi_app = ReverseProxied(app.wsgi_app)
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
|
"""Returns Nothing"""
|
||||||
return "", 200
|
return "", 200
|
||||||
|
|
||||||
|
|
||||||
@app.route("/upload", methods=["POST"])
|
@app.route("/upload", methods=["POST"])
|
||||||
def upload():
|
def upload():
|
||||||
|
"""
|
||||||
|
Upload a file, example CURL:
|
||||||
|
|
||||||
|
cat file | \
|
||||||
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
|
-H "Name: my.file.ext" \
|
||||||
|
-H "Max-Downloads: 4000" \
|
||||||
|
-H "Expires-Days: 14" \
|
||||||
|
-H "Secret: dff789f0bbe8183d32542" \
|
||||||
|
"$FLASK_PUBLIC_URL"/upload
|
||||||
|
|
||||||
|
- Additionally, "Expires-Hours" can be used.
|
||||||
|
- Max-Dowloads: -1 means no upper limit
|
||||||
|
|
||||||
|
|
||||||
|
Returns the file download URL
|
||||||
|
"""
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
file = request.files.get("file")
|
file = request.files.get("file")
|
||||||
name = request.headers.get("Name", None)
|
name = request.headers.get("Name", None)
|
||||||
@@ -68,13 +83,16 @@ def upload():
|
|||||||
|
|
||||||
if file:
|
if file:
|
||||||
safe_filename = secure_filename(name)
|
safe_filename = secure_filename(name)
|
||||||
|
while True:
|
||||||
token = random_token()
|
token = random_token()
|
||||||
folder = os.path.join(app.config["DATAFOLDER"], token)
|
folder = os.path.join(app.config["DATAFOLDER"], token)
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
break
|
||||||
os.mkdir(folder)
|
os.mkdir(folder)
|
||||||
filename = os.path.join(folder, safe_filename)
|
filename = file_full_path(token, safe_filename)
|
||||||
file.save(filename)
|
file.save(filename)
|
||||||
db_store_file(token, safe_filename, expires, max_dl)
|
db_store_file(token, safe_filename, expires, max_dl)
|
||||||
download_url = f"{app.config['PUBLIC_URL']}/dl/{token}/{safe_filename}"
|
download_url = file_full_url(token, safe_filename)
|
||||||
return "File uploaded\n%s\n" % (download_url,), 200
|
return "File uploaded\n%s\n" % (download_url,), 200
|
||||||
else:
|
else:
|
||||||
return "Use the 'file' variable to upload\n", 400
|
return "Use the 'file' variable to upload\n", 400
|
||||||
@@ -82,6 +100,14 @@ def upload():
|
|||||||
|
|
||||||
@app.route("/details/<token>/<name>", methods=["GET"])
|
@app.route("/details/<token>/<name>", methods=["GET"])
|
||||||
def details(token, name):
|
def details(token, name):
|
||||||
|
"""
|
||||||
|
Get JSON of file details. Size, added date, download times, etc.
|
||||||
|
|
||||||
|
curl -fL -w "\n" \
|
||||||
|
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
||||||
|
"$ROOTURL"/details/OdD7X0aKOGM/big_file1.ext
|
||||||
|
|
||||||
|
"""
|
||||||
secret = request.headers.get("Secret", "")
|
secret = request.headers.get("Secret", "")
|
||||||
if secret != app.config["ACCESS_TOKEN"]:
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
return "Error", 401
|
return "Error", 401
|
||||||
@@ -91,11 +117,14 @@ def details(token, name):
|
|||||||
|
|
||||||
@app.route("/delete/<token>/<name>", methods=["GET"])
|
@app.route("/delete/<token>/<name>", methods=["GET"])
|
||||||
def delete_file(name, token):
|
def delete_file(name, token):
|
||||||
|
"""
|
||||||
|
Delete a file from the system
|
||||||
|
"""
|
||||||
secret = request.headers.get("Secret", "")
|
secret = request.headers.get("Secret", "")
|
||||||
if secret != app.config["ACCESS_TOKEN"]:
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
return "Error", 401
|
return "Error", 401
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(os.getenv("DATAFOLDER"), token, name))
|
os.remove(os.path.join(app.config["DATAFOLDER"], token, name))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
db_delete_file(token, name)
|
db_delete_file(token, name)
|
||||||
@@ -104,6 +133,9 @@ def delete_file(name, token):
|
|||||||
|
|
||||||
@app.route("/ls", methods=["GET"])
|
@app.route("/ls", methods=["GET"])
|
||||||
def ls():
|
def ls():
|
||||||
|
"""
|
||||||
|
Lists all uploaded files
|
||||||
|
"""
|
||||||
secret = request.headers.get("Secret", "")
|
secret = request.headers.get("Secret", "")
|
||||||
if secret != app.config["ACCESS_TOKEN"]:
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
return "Error", 401
|
return "Error", 401
|
||||||
@@ -112,6 +144,11 @@ def ls():
|
|||||||
|
|
||||||
@app.route("/maintenance", methods=["GET"])
|
@app.route("/maintenance", methods=["GET"])
|
||||||
def maintenance():
|
def maintenance():
|
||||||
|
"""
|
||||||
|
Clears DB of expired entries.
|
||||||
|
Deletes folders without DB entry
|
||||||
|
"""
|
||||||
|
|
||||||
secret = request.headers.get("Secret", "")
|
secret = request.headers.get("Secret", "")
|
||||||
if secret != app.config["ACCESS_TOKEN"]:
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
return "Error", 401
|
return "Error", 401
|
||||||
@@ -120,29 +157,30 @@ def maintenance():
|
|||||||
|
|
||||||
@app.route("/dl/<token>/<name>", methods=["GET"])
|
@app.route("/dl/<token>/<name>", methods=["GET"])
|
||||||
def download(name, token):
|
def download(name, token):
|
||||||
|
"""
|
||||||
|
Download a file
|
||||||
|
"""
|
||||||
return download_file(token, name)
|
return download_file(token, name)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/script/client", methods=["GET"])
|
@app.route("/script/mfl", methods=["GET"])
|
||||||
def script_client():
|
def script_mfl():
|
||||||
|
secret = request.headers.get("Secret", "")
|
||||||
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
|
return "Error", 401
|
||||||
return render_template(
|
return render_template(
|
||||||
"client.py", name=name, token=token, rooturl=request.url_root
|
"mfl",
|
||||||
)
|
token=app.config["ACCESS_TOKEN"],
|
||||||
|
rooturl=app.config["PUBLIC_URL"],
|
||||||
|
version=__VERSION__,
|
||||||
@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):
|
def download_file(token, name):
|
||||||
full_path = os.path.join(os.getenv("FLASK_DATAFOLDER"), token, name)
|
"""
|
||||||
|
check for file expiry, and send file if allowed
|
||||||
|
"""
|
||||||
|
full_path = os.path.join(app.config["DATAFOLDER"], token, name)
|
||||||
if not os.path.exists(full_path):
|
if not os.path.exists(full_path):
|
||||||
return "Error", 404
|
return "Error", 404
|
||||||
db_stat = db_get_file(token, name)
|
db_stat = db_get_file(token, name)
|
||||||
@@ -150,13 +188,13 @@ def download_file(token, name):
|
|||||||
added, expires, downloads, max_dl = db_stat
|
added, expires, downloads, max_dl = db_stat
|
||||||
else:
|
else:
|
||||||
return "Error", 404
|
return "Error", 404
|
||||||
if downloads >= max_dl:
|
if downloads >= max_dl and max_dl > -1:
|
||||||
return "Expired", 401
|
return "Expired", 401
|
||||||
if expires < time.time():
|
if expires < time.time():
|
||||||
return "Expired", 401
|
return "Expired", 401
|
||||||
db_add_download(token, name)
|
db_add_download(token, name)
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
directory=os.path.join(os.getenv("FLASK_DATAFOLDER"), token), path=name
|
directory=os.path.join(app.config["DATAFOLDER"], token), path=name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
from .misc import *
|
|
||||||
from .files import *
|
|
||||||
from .crypt import *
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import secrets
|
|
||||||
|
|
||||||
|
|
||||||
def random_token():
|
|
||||||
return secrets.token_urlsafe(8)
|
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import stat
|
|
||||||
import time
|
import time
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import shutil
|
import shutil
|
||||||
from .misc import *
|
from .misc import file_size_MB, file_size_human, file_date_human
|
||||||
from .crypt import *
|
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
db = sqlite3.connect(os.getenv("FLASK_DB"), timeout=5)
|
db = sqlite3.connect(app.config["DB"], timeout=5)
|
||||||
c = db.cursor()
|
c = db.cursor()
|
||||||
return db, c
|
return db, c
|
||||||
|
|
||||||
@@ -49,7 +44,7 @@ def db_get_files():
|
|||||||
"""
|
"""
|
||||||
SELECT token,name,added,expires,downloads,max_downloads
|
SELECT token,name,added,expires,downloads,max_downloads
|
||||||
FROM files
|
FROM files
|
||||||
WHERE expires > ? and downloads < max_downloads
|
WHERE expires > ? and (downloads < max_downloads or max_downloads = -1)
|
||||||
""",
|
""",
|
||||||
(time.time(),),
|
(time.time(),),
|
||||||
)
|
)
|
||||||
@@ -82,13 +77,14 @@ def db_add_download(token, name):
|
|||||||
|
|
||||||
def db_maintenance():
|
def db_maintenance():
|
||||||
messages = []
|
messages = []
|
||||||
|
# === Delete DB entries where expiry or max DL is used up ===
|
||||||
db, c = get_db()
|
db, c = get_db()
|
||||||
rows = db.execute(
|
rows = db.execute(
|
||||||
"""
|
"""
|
||||||
select
|
select
|
||||||
token, name
|
token, name
|
||||||
from files
|
from files
|
||||||
where expires < ? or downloads >= max_downloads
|
where expires < ? or (downloads >= max_downloads and max_downloads != -1)
|
||||||
""",
|
""",
|
||||||
(int(time.time()),),
|
(int(time.time()),),
|
||||||
)
|
)
|
||||||
@@ -101,7 +97,7 @@ def db_maintenance():
|
|||||||
c.executemany("DELETE FROM files WHERE token = ?", deleted_tokens)
|
c.executemany("DELETE FROM files WHERE token = ?", deleted_tokens)
|
||||||
if c.rowcount > 0:
|
if c.rowcount > 0:
|
||||||
db.commit()
|
db.commit()
|
||||||
|
# Delete files, if DB entry is gone
|
||||||
subdirs = next(os.walk(app.config["DATAFOLDER"]))[1]
|
subdirs = next(os.walk(app.config["DATAFOLDER"]))[1]
|
||||||
db, c = get_db()
|
db, c = get_db()
|
||||||
for d in subdirs:
|
for d in subdirs:
|
||||||
@@ -111,13 +107,35 @@ def db_maintenance():
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
if not keep:
|
if not keep:
|
||||||
try:
|
try:
|
||||||
for fname in os.listdir(os.path.join(app.config["DATAFOLDER"],d)):
|
for fname in os.listdir(os.path.join(app.config["DATAFOLDER"], d)):
|
||||||
os.remove(os.path.join(app.config["DATAFOLDER"],d,fname))
|
os.remove(os.path.join(app.config["DATAFOLDER"], d, fname))
|
||||||
messages.append(f"Deleting file {d}/{fname}")
|
messages.append(f"Deleting file {d}/{fname}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
shutil.rmtree(os.path.join(app.config["DATAFOLDER"],d), ignore_errors=True)
|
shutil.rmtree(os.path.join(app.config["DATAFOLDER"], d), ignore_errors=True)
|
||||||
messages.append(f"Deleting folder {d}")
|
messages.append(f"Deleting folder {d}")
|
||||||
|
|
||||||
|
# Delete DB entries, if files have been deleted (probably manually)
|
||||||
|
db, c = get_db()
|
||||||
|
rows = db.execute(
|
||||||
|
"""
|
||||||
|
select
|
||||||
|
token, name
|
||||||
|
from files
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
deleted_tokens = []
|
||||||
|
for row in rows:
|
||||||
|
full_path = file_full_path(row[0], row[1])
|
||||||
|
if not os.path.exists(full_path):
|
||||||
|
deleted_tokens.append((row[0],))
|
||||||
|
messages.append(f"Deleting DB {row[0]}/{row[1]} - files missing")
|
||||||
|
if len(deleted_tokens) > 0:
|
||||||
|
db, c = get_db()
|
||||||
|
c.executemany("DELETE FROM files WHERE token = ?", deleted_tokens)
|
||||||
|
if c.rowcount > 0:
|
||||||
|
db.commit()
|
||||||
|
messages.append("Maintenance done.")
|
||||||
return "\n".join(messages)
|
return "\n".join(messages)
|
||||||
|
|
||||||
|
|
||||||
@@ -133,7 +151,7 @@ def file_age(path):
|
|||||||
|
|
||||||
|
|
||||||
def file_details(token, name):
|
def file_details(token, name):
|
||||||
full_path = os.path.join(os.getenv("FLASK_DATAFOLDER"), token, name)
|
full_path = file_full_path(token, name)
|
||||||
try:
|
try:
|
||||||
s = os.stat(full_path)
|
s = os.stat(full_path)
|
||||||
db_stat = db_get_file(token, name)
|
db_stat = db_get_file(token, name)
|
||||||
@@ -146,7 +164,7 @@ def file_details(token, name):
|
|||||||
"hsize": file_size_human(s.st_size, HTML=False),
|
"hsize": file_size_human(s.st_size, HTML=False),
|
||||||
"added": file_date_human(added),
|
"added": file_date_human(added),
|
||||||
"name": name,
|
"name": name,
|
||||||
"url": f"{app.config['PUBLIC_URL']}/dl/{token}/{name}",
|
"url": file_full_url(token, name),
|
||||||
"expires": file_date_human(expires),
|
"expires": file_date_human(expires),
|
||||||
"downloaded": downloads,
|
"downloaded": downloads,
|
||||||
"max-dl": max_dl,
|
"max-dl": max_dl,
|
||||||
@@ -155,76 +173,23 @@ def file_details(token, name):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def file_full_path(token, name):
|
||||||
|
return os.path.join(app.config["DATAFOLDER"], token, name)
|
||||||
|
|
||||||
|
|
||||||
|
def file_full_url(token, name):
|
||||||
|
return f"{app.config['PUBLIC_URL']}/dl/{token}/{name}"
|
||||||
|
|
||||||
|
|
||||||
def file_list():
|
def file_list():
|
||||||
files = list(db_get_files())
|
files = list(db_get_files())
|
||||||
details = []
|
details = []
|
||||||
details.append(" Added/Expiry DL/MaxDL URL")
|
details.append(" Added/Expiry DL/MaxDL URL")
|
||||||
details.append("=" * 75)
|
details.append("=" * 75)
|
||||||
for file in files:
|
for file in files:
|
||||||
url = f"{app.config['PUBLIC_URL']}/dl/{file[0]}/{file[1]}"
|
url = file_full_url(file[0], file[1])
|
||||||
details.append(
|
added = file_date_human(file[2])
|
||||||
f"{file_date_human(file[2])}/{file_date_human(file[3])} {file[4]:4d}/{file[5]:4d} {url}"
|
expiry = file_date_human(file[3])
|
||||||
)
|
details.append(f"{added}/{expiry} {file[4]:4d}/{file[5]:4d} {url}")
|
||||||
|
|
||||||
return details
|
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,))
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import current_app as app
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
|
VALID_TOKEN_CHARS = string.digits + string.ascii_letters
|
||||||
|
|
||||||
|
|
||||||
|
def random_token():
|
||||||
|
return "".join(secrets.choice(VALID_TOKEN_CHARS) for i in range(8))
|
||||||
|
|
||||||
|
|
||||||
def file_date_human(num):
|
def file_date_human(num):
|
||||||
@@ -22,45 +29,3 @@ def file_size_human(num, HTML=True):
|
|||||||
|
|
||||||
def file_size_MB(num):
|
def file_size_MB(num):
|
||||||
return "{:,.2f}".format(num / (1024 * 1024))
|
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
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: code
|
context: code
|
||||||
args:
|
args:
|
||||||
UID: ${UID}
|
UUID: ${UUID}
|
||||||
GID: ${GID}
|
UGID: ${UGID}
|
||||||
TZ: ${TZ}
|
TZ: ${TZ}
|
||||||
ports:
|
ports:
|
||||||
- "${EXPOSE}:5000"
|
- "${EXPOSE}:5000"
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ _qCol() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
cont() {
|
_cont() {
|
||||||
set +x
|
set +x
|
||||||
_qCol G
|
_qCol G
|
||||||
echo Continue
|
echo Continue
|
||||||
@@ -76,6 +76,13 @@ cont() {
|
|||||||
_qCol z
|
_qCol z
|
||||||
set -x
|
set -x
|
||||||
}
|
}
|
||||||
|
_title() {
|
||||||
|
_qCol G
|
||||||
|
echo "$1"
|
||||||
|
_qCol Y
|
||||||
|
echo =========================================
|
||||||
|
_qCol z
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -86,82 +93,184 @@ SMALL="small file"
|
|||||||
SMALLS="small_file"
|
SMALLS="small_file"
|
||||||
IMAGE="image.jpg"
|
IMAGE="image.jpg"
|
||||||
|
|
||||||
|
|
||||||
test -f "$BIG" || dd if=/dev/zero of="$BIG" bs=8192 count=40000
|
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 "$SMALL" ||dd if=/dev/urandom of="$SMALL" bs=4096 count=400
|
||||||
test -f "$IMAGE" || convert -size 640x480 xc:gray $IMAGE
|
test -f "$IMAGE" || convert -size 640x480 xc:gray $IMAGE
|
||||||
|
|
||||||
set -x
|
. ../.env
|
||||||
#~ cont
|
|
||||||
ROOTURL="http://localhost:8136"
|
function t00-rebuild-docker() {
|
||||||
|
CWD=$PWD
|
||||||
|
cd ..
|
||||||
|
docker-compose up --build -d --force-recreate
|
||||||
|
cd "$CWD"
|
||||||
|
}
|
||||||
|
|
||||||
|
function t01-maintenance-begin() {
|
||||||
|
curl -fL -w "\n" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/maintenance
|
||||||
|
}
|
||||||
|
|
||||||
|
function t02-upload-small-image() {
|
||||||
|
|
||||||
if false; then
|
|
||||||
pv "$IMAGE" | \
|
pv "$IMAGE" | \
|
||||||
curl -fL -w "\n" -F file="@-" -X POST \
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
-H "Name: $IMAGE" \
|
-H "Name: $IMAGE" \
|
||||||
-H "Max-Downloads: 4" \
|
-H "Max-Downloads: 4" \
|
||||||
-H "Expires-Days: 14" \
|
-H "Expires-Days: 1" \
|
||||||
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
"$ROOTURL"/upload
|
"$FLASK_PUBLIC_URL"/upload
|
||||||
fi
|
}
|
||||||
|
|
||||||
if false; then
|
function t03-upload-small-file() {
|
||||||
pv "$SMALL" | \
|
pv "$SMALL" | \
|
||||||
curl -fL -w "\n" -F file="@-" -X POST \
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
-H "Name: $SMALL" \
|
-H "Name: $SMALL" \
|
||||||
-H "Max-Downloads: 4000" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
-H "Expires-Days: 14" \
|
"$FLASK_PUBLIC_URL"/upload
|
||||||
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
|
||||||
"$ROOTURL"/upload
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function t04-upload-large-file() {
|
||||||
pv "$BIG" | \
|
pv "$BIG" | \
|
||||||
curl -fL -w "\n" -F file="@-" -X POST \
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
-H "Name: $BIG" \
|
-H "Name: $BIG" \
|
||||||
-H "Max-Downloads: 4000" \
|
-H "Max-Downloads: 4" \
|
||||||
-H "Expires-Days: 14" \
|
-H "Expires-Days: 1" \
|
||||||
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
"$ROOTURL"/upload
|
"$FLASK_PUBLIC_URL"/upload
|
||||||
fi
|
}
|
||||||
|
|
||||||
sqlite3 ../data/flees.db "select * FROM files"
|
function t05-check-db-manually() {
|
||||||
|
sqlite3 ../data/flees.db "select * FROM files"
|
||||||
|
}
|
||||||
|
|
||||||
|
function t06-list-files() {
|
||||||
|
|
||||||
if false; then
|
|
||||||
curl -fL -w "\n" \
|
curl -fL -w "\n" \
|
||||||
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
"$ROOTURL"/details/OdD7X0aKOGM/big_file1.ext
|
"$FLASK_PUBLIC_URL"/ls
|
||||||
|
}
|
||||||
|
|
||||||
fi
|
function t07-file-download() {
|
||||||
|
download_url=$( curl -fL -w "\n" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/ls | grep "$BIGS" | tail -n 1 | sed s,.*http,http, )
|
||||||
|
|
||||||
if false; then
|
rm -f "$BIGS"
|
||||||
rm -f big_file1.ext
|
wget "$download_url"
|
||||||
wget \
|
}
|
||||||
"$ROOTURL"/dl/OdD7X0aKOGM/big_file1.ext
|
|
||||||
fi
|
|
||||||
|
|
||||||
if false; then
|
function t08-file-details() {
|
||||||
|
download_url=$( curl -fL -w "\n" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/ls | grep "$BIGS" | tail -n 1 | sed s,.*http,http, )
|
||||||
|
token_name=$( echo $download_url | sed s,.*/dl/,, )
|
||||||
curl -fL -w "\n" \
|
curl -fL -w "\n" \
|
||||||
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
"$ROOTURL"/delete/SKDMsQ3ifx8/image.jpg
|
"$FLASK_PUBLIC_URL"/details/"$token_name"
|
||||||
fi
|
}
|
||||||
|
function t09-file-delete() {
|
||||||
|
|
||||||
if true; then
|
|
||||||
|
download_url=$( curl -fL -w "\n" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/ls | grep "$BIGS" | tail -n 1 | sed s,.*http,http, )
|
||||||
|
token_name=$( echo $download_url | sed s,.*/dl/,, )
|
||||||
curl -fL -w "\n" \
|
curl -fL -w "\n" \
|
||||||
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
"$ROOTURL"/ls
|
"$FLASK_PUBLIC_URL"/delete/"$token_name"
|
||||||
fi
|
}
|
||||||
|
|
||||||
if true; then
|
function t09-unlimited-downloads() {
|
||||||
pv "$SMALL" | \
|
pv "$SMALL" | \
|
||||||
curl -fL -w "\n" -F file="@-" -X POST \
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
-H "Name: $SMALL" \
|
-H "Name: $SMALL" \
|
||||||
-H "Max-Downloads: 4000" \
|
-H "Max-Downloads: -1" \
|
||||||
-H "Expires-Days: -5" \
|
-H "Expires-Days: 1" \
|
||||||
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
"$ROOTURL"/upload
|
"$FLASK_PUBLIC_URL"/upload
|
||||||
|
download_url=$( curl -fL -w "\n" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/ls | grep "$SMALLS" | tail -n 1 | sed s,.*http,http, )
|
||||||
|
token_name=$( echo $download_url | sed s,.*/dl/,, )
|
||||||
|
for ((i=0;i<10;i++)); do
|
||||||
|
curl -fL -w "\n" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/dl/"$token_name" > /dev/null
|
||||||
|
curl -fL -w "\n" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/details/"$token_name"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function t10-maintenance-post() {
|
||||||
|
|
||||||
|
pv "$SMALL" | \
|
||||||
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
|
-H "Name: $SMALL" \
|
||||||
|
-H "Max-Downloads: 4" \
|
||||||
|
-H "Expires-Days: -5" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/upload
|
||||||
|
pv "$SMALL" | \
|
||||||
|
curl -fL -w "\n" -F file="@-" -X POST \
|
||||||
|
-H "Name: $SMALL" \
|
||||||
|
-H "Max-Downloads: 0" \
|
||||||
|
-H "Expires-Days: 3" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/upload
|
||||||
|
|
||||||
curl -fL -w "\n" \
|
curl -fL -w "\n" \
|
||||||
-H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
"$ROOTURL"/maintenance
|
"$FLASK_PUBLIC_URL"/maintenance
|
||||||
fi
|
|
||||||
|
curl -fL -w "\n" \
|
||||||
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
|
"$FLASK_PUBLIC_URL"/ls
|
||||||
|
}
|
||||||
|
|
||||||
|
_getlist() {
|
||||||
|
declare -F | awk '{ print $3 }' | grep -v ^_
|
||||||
|
echo exit
|
||||||
|
}
|
||||||
|
|
||||||
|
_getnext() {
|
||||||
|
hitfound=0
|
||||||
|
for line in $( _getlist ); do
|
||||||
|
if [[ $hitfound -eq 1 ]]; then
|
||||||
|
echo "$line"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$line" = "$1" ]]; then
|
||||||
|
hitfound=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_getchoice() {
|
||||||
|
_getlist | \
|
||||||
|
smenu -m "Pick task" -n 20 -t 1 -a c:2,b m:3,b -s '/'$1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
next_task=0
|
||||||
|
while true; do
|
||||||
|
choice=$( _getchoice $next_task )
|
||||||
|
if [[ -z "$choice" ]]; then break; fi
|
||||||
|
_title "$choice"
|
||||||
|
set -x
|
||||||
|
"$choice"
|
||||||
|
set +x
|
||||||
|
_title ""
|
||||||
|
#~ next_task=$(( next_task + 1 ))
|
||||||
|
next_task=$( _getnext "$choice" )
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user