From aa6a1d23731fff5e759acf97d971f0fcb28f8407 Mon Sep 17 00:00:00 2001 From: Q Date: Sat, 19 Aug 2023 11:35:11 +0300 Subject: [PATCH] new --- README.md | 33 ++++++ code/.dockerignore | 3 + code/Dockerfile | 35 +++++++ code/app.py | 163 +++++++++++++++++++++++++++++ code/docker-builder.sh | 7 ++ code/entrypoint.sh | 22 ++++ code/init_db.sh | 13 +++ code/requirements.txt | 2 + code/revprox.py | 33 ++++++ code/utils/__init__.py | 3 + code/utils/crypt.py | 5 + code/utils/files.py | 230 +++++++++++++++++++++++++++++++++++++++++ code/utils/misc.py | 62 +++++++++++ docker-compose.yaml | 26 +++++ env.example | 10 ++ test/run-tests.sh | 150 +++++++++++++++++++++++++++ 16 files changed, 797 insertions(+) create mode 100644 README.md create mode 100644 code/.dockerignore create mode 100644 code/Dockerfile create mode 100644 code/app.py create mode 100644 code/docker-builder.sh create mode 100644 code/entrypoint.sh create mode 100644 code/init_db.sh create mode 100644 code/requirements.txt create mode 100644 code/revprox.py create mode 100644 code/utils/__init__.py create mode 100644 code/utils/crypt.py create mode 100644 code/utils/files.py create mode 100644 code/utils/misc.py create mode 100644 docker-compose.yaml create mode 100644 env.example create mode 100755 test/run-tests.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5898db --- /dev/null +++ b/README.md @@ -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; + } +``` diff --git a/code/.dockerignore b/code/.dockerignore new file mode 100644 index 0000000..96eac8b --- /dev/null +++ b/code/.dockerignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__ +*.swp diff --git a/code/Dockerfile b/code/Dockerfile new file mode 100644 index 0000000..56f40a8 --- /dev/null +++ b/code/Dockerfile @@ -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 diff --git a/code/app.py b/code/app.py new file mode 100644 index 0000000..04d1793 --- /dev/null +++ b/code/app.py @@ -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//", 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//", 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//", 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) diff --git a/code/docker-builder.sh b/code/docker-builder.sh new file mode 100644 index 0000000..dd54adc --- /dev/null +++ b/code/docker-builder.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -eux + +python3 -m venv /opt/venv +. /opt/venv/bin/activate +pip3 install -r requirements.txt diff --git a/code/entrypoint.sh b/code/entrypoint.sh new file mode 100644 index 0000000..ce54cb7 --- /dev/null +++ b/code/entrypoint.sh @@ -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 diff --git a/code/init_db.sh b/code/init_db.sh new file mode 100644 index 0000000..8b66414 --- /dev/null +++ b/code/init_db.sh @@ -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 diff --git a/code/requirements.txt b/code/requirements.txt new file mode 100644 index 0000000..e4a286c --- /dev/null +++ b/code/requirements.txt @@ -0,0 +1,2 @@ +flask +gunicorn diff --git a/code/revprox.py b/code/revprox.py new file mode 100644 index 0000000..3914560 --- /dev/null +++ b/code/revprox.py @@ -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) diff --git a/code/utils/__init__.py b/code/utils/__init__.py new file mode 100644 index 0000000..e45bb4d --- /dev/null +++ b/code/utils/__init__.py @@ -0,0 +1,3 @@ +from .misc import * +from .files import * +from .crypt import * diff --git a/code/utils/crypt.py b/code/utils/crypt.py new file mode 100644 index 0000000..609715f --- /dev/null +++ b/code/utils/crypt.py @@ -0,0 +1,5 @@ +import secrets + + +def random_token(): + return secrets.token_urlsafe(8) diff --git a/code/utils/files.py b/code/utils/files.py new file mode 100644 index 0000000..b7d5d19 --- /dev/null +++ b/code/utils/files.py @@ -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,)) diff --git a/code/utils/misc.py b/code/utils/misc.py new file mode 100644 index 0000000..0c862e3 --- /dev/null +++ b/code/utils/misc.py @@ -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 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..f617828 --- /dev/null +++ b/docker-compose.yaml @@ -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: + + diff --git a/env.example b/env.example new file mode 100644 index 0000000..823234f --- /dev/null +++ b/env.example @@ -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 diff --git a/test/run-tests.sh b/test/run-tests.sh new file mode 100755 index 0000000..8961dde --- /dev/null +++ b/test/run-tests.sh @@ -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