Chapters:
Always have a backup. With these two files and a build environment you can build TransferDepot as a FlaskRest Application
app.py (main app)
# app.py
import os
from pathlib import Path
from flask import Flask, request, redirect, jsonify, send_from_directory, abort
from werkzeug.utils import secure_filename
app = Flask(__name__)
# ---------------------------
# Zones & storage
# ---------------------------
VALID_GROUPS = ["TTCS", "NDA", "RSU", "ODP", "PAL"] # add more if you need
UPLOAD_FOLDER = os.getenv("TD_UPLOAD_FOLDER", "files")
Path(UPLOAD_FOLDER).mkdir(parents=True, exist_ok=True)
# ---------------------------
# Landing page
# ---------------------------
@app.route("/", methods=["GET"])
def home():
links = "<br>".join(f'<a href="/upload-basic/{g}">{g}</a>' for g in VALID_GROUPS)
return f"""<!doctype html>
<html><head><meta charset="utf-8"><title>TransferDepot</title>
<style>
body{{font-family:system-ui,Segoe UI,Roboto,sans-serif;margin:2rem}}
.btn{{display:inline-block;padding:.6rem 1rem;border:1px solid #bbb;border-radius:.6rem;text-decoration:none;margin-right:.5rem}}
</style>
</head>
<body>
<h1>TransferDepot</h1>
<p>Pick a group / zone:</p>
<p>{links}</p>
<hr>
<p>
<a class="btn" href="/api/v1/admin">Admin</a>
<a class="btn" href="/api/v1/admin_compat">Admin (Compat)</a>
</p>
<p>Power users: POST directly to <code>/upload-basic/<group></code></p>
</body></html>""", 200
# Simple liveness for this app file
@app.route("/ping", methods=["GET", "HEAD"])
def ping():
return jsonify(status="ok", message="pong"), 200
# ---------------------------
# Unified upload (GUI on GET, upload on POST)
# ---------------------------
@app.route("/upload-basic/<group>", methods=["GET", "POST"])
def upload_basic(group):
if group not in VALID_GROUPS:
return "Invalid group specified", 400
group_folder = os.path.join(UPLOAD_FOLDER, group)
os.makedirs(group_folder, exist_ok=True)
if request.method == "POST":
f = request.files.get("file")
if not f or not f.filename:
return "No file selected", 400
safe = secure_filename(f.filename)
f.save(os.path.join(group_folder, safe))
# PRG pattern so refresh shows the updated list
return redirect(f"/upload-basic/{group}", code=303)
# GET: auto‑list files already present (clickable)
try:
names = sorted(
n for n in os.listdir(group_folder)
if os.path.isfile(os.path.join(group_folder, n))
)
except FileNotFoundError:
names = []
if names:
items = "".join(f'<li><a href="/files/{group}/{n}">{n}</a></li>' for n in names)
files_html = f"<ul>{items}</ul>"
else:
files_html = "<p><em>No files yet.</em></p>"
return f"""<!doctype html>
<html><head><meta charset="utf-8"><title>Upload to {group}</title></head>
<body style="font-family: system-ui; margin:2rem">
<h1>Upload to {group}</h1>
<form method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<button type="submit">Upload</button>
</form>
<h2>Files in {group}</h2>
<p>List files: <a href="/files/{group}/">/files/{group}</a></p>
{files_html}
</body></html>"""
# ---------------------------
# File listing & downloads
# ---------------------------
@app.route("/files/<group>/", methods=["GET"])
def list_files(group):
if group not in VALID_GROUPS:
return "Invalid group specified", 400
folder = os.path.join(UPLOAD_FOLDER, group)
try:
names = sorted(
n for n in os.listdir(folder)
if os.path.isfile(os.path.join(folder, n))
)
except FileNotFoundError:
names = []
if names:
items = "".join(f'<li><a href="/files/{group}/{n}">{n}</a></li>' for n in names)
html = f"<ul>{items}</ul>"
else:
html = "<p><em>No files yet.</em></p>"
return f"""<!doctype html>
<html><head><meta charset="utf-8"><title>Files — {group}</title></head>
<body style="font-family: system-ui; margin:2rem">
<h1>Files in {group}</h1>
{html}
<p><a href="/upload-basic/{group}">Back to Upload for {group}</a></p>
</body></html>"""
@app.route("/files/<group>/<path:fname>", methods=["GET", "HEAD"])
def download_file(group, fname):
if group not in VALID_GROUPS:
return "Invalid group specified", 400
safe = os.path.basename(fname)
folder = os.path.join(UPLOAD_FOLDER, group)
full = os.path.join(folder, safe)
if not os.path.isfile(full):
abort(404)
return send_from_directory(folder, safe, as_attachment=True)
# ---------------------------
# Mount the API v1 blueprint
# ---------------------------
from api_v1 import api # noqa: E402
app.register_blueprint(api)
if __name__ == "__main__":
# match your systemd config (host/port)
app.run(host="0.0.0.0", port=8080)
api_v1.py (API v1 blueprint)
# api_v1.py
import os
import time
import hashlib
from pathlib import Path
from flask import Blueprint, jsonify, request, abort, render_template_string
# Blueprint under /api/v1/*
api = Blueprint("api_v1", __name__, url_prefix="/api/v1")
# Reuse the same storage location
UPLOAD_FOLDER = os.getenv("TD_UPLOAD_FOLDER", "files")
API_ZONES = ["TTCS", "NDA", "RSU", "ODP", "PAL"]
def _zone_ok(z: str) -> bool:
return z in API_ZONES
@api.route("/healthz", methods=["GET"], strict_slashes=False)
def healthz():
return jsonify({"ok": True, "time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())})
@api.route("/zones", methods=["GET"])
def zones():
return jsonify(API_ZONES)
@api.route("/zones/<zone>/dropoff", methods=["POST"])
def dropoff(zone):
if not _zone_ok(zone):
abort(400, description="invalid zone")
if "file" not in request.files:
abort(400, description="missing file")
up = request.files["file"]
if not up or not up.filename:
abort(400, description="empty filename")
zdir = Path(UPLOAD_FOLDER) / zone
zdir.mkdir(parents=True, exist_ok=True)
# avoid overwrite
dst = zdir / up.filename
if dst.exists():
ts = time.strftime("%Y%m%d%H%M%S", time.gmtime())
dst = zdir / f"{ts}-{up.filename}"
# stream save + sha256
h = hashlib.sha256()
with dst.open("wb") as out:
for chunk in iter(lambda: up.stream.read(8192), b""):
if not chunk: break
h.update(chunk)
out.write(chunk)
return jsonify({"zone": zone, "filename": dst.name, "sha256": h.hexdigest()})
# -------- Admin (modern) --------
@api.route("/admin", methods=["GET"])
def admin_page():
base = api.url_prefix # "/api/v1"
home = request.url_root.rstrip("/") # site root
return render_template_string("""
<!doctype html>
<meta charset="utf-8">
<title>TransferDepot — Admin</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{font-family:system-ui,sans-serif;margin:2rem;max-width:960px}
.topbar{display:flex;gap:.5rem;margin-bottom:1rem}
.btn{display:inline-block;padding:.6rem 1rem;border:1px solid #bbb;border-radius:.6rem;text-decoration:none}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem}
.card{border:1px solid #e5e5e5;border-radius:.8rem;padding:1rem}
.ok{color:#0a0}.bad{color:#a00}
pre{background:#f7f7f7;padding:.8rem;border-radius:.6rem;overflow:auto}
</style>
<div class="topbar">
<a class="btn" href="{{ home }}">Home</a>
<button class="btn" onclick="history.back()">Back</button>
</div>
<h1>Admin</h1>
<div class="grid">
<div class="card">
<h3>Health</h3>
<p><a class="btn" href="{{ base }}/healthz" target="_blank">Open healthz</a></p>
<button class="btn" id="btn-hz">Check inline</button>
<div id="hz-status" style="margin-top:.6rem"></div>
<pre id="hz-body">{ click "Check inline" }</pre>
</div>
<div class="card">
<h3>Zones</h3>
<p><a class="btn" href="{{ base }}/zones" target="_blank">Open zones</a></p>
<button class="btn" id="btn-z">Show inline</button>
<pre id="z-body">{ click "Show inline" }</pre>
</div>
</div>
<script>
const BASE="{{ base }}";
async function g(p){const r=await fetch(BASE+p,{cache:'no-store'});const t=await r.text();let b=t;try{b=JSON.stringify(JSON.parse(t),null,2);}catch(_){ }return {ok:r.ok,status:r.status,body:b};}
document.getElementById('btn-hz').onclick=async()=>{
const s=document.getElementById('hz-status'), pre=document.getElementById('hz-body');
s.textContent='Checking…';
try{const r=await g('/healthz'); s.innerHTML=r.ok?'<b class="ok">200 OK</b>':'<b class="bad">'+r.status+'</b>'; pre.textContent=r.body;}catch(e){s.innerHTML='<b class="bad">Request failed</b>'; pre.textContent=String(e);}
};
document.getElementById('btn-z').onclick=async()=>{
const pre=document.getElementById('z-body'); pre.textContent='Loading…';
try{const r=await g('/zones'); pre.textContent=r.body;}catch(e){pre.textContent=String(e);}
};
</script>
""", base=base, home=home)
# -------- Admin (legacy/HTTP-only) --------
@api.route("/admin_compat", methods=["GET"])
def admin_compat():
base = api.url_prefix
home = request.url_root.rstrip("/")
return render_template_string("""
<!doctype html>
<meta charset="utf-8">
<title>TransferDepot — Admin (Compat)</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{font-family:Arial,Helvetica,sans-serif;margin:2rem;max-width:960px}
.topbar{margin-bottom:1rem}
.btn{display:inline-block;margin-right:.5rem;padding:.5rem .9rem;border:1px solid #bbb;border-radius:.4rem;text-decoration:none}
.card{border:1px solid #e5e5e5;border-radius:.4rem;padding:1rem;margin:0 0 1rem}
.ok{color:#090}.bad{color:#a00}
pre{background:#f7f7f7;padding:.6rem;border-radius:.4rem;overflow:auto}
</style>
<div class="topbar">
<a class="btn" href="{{ home }}">Home</a>
<a class="btn" href="javascript:history.back()">Back</a>
</div>
<h1>Admin (Compatibility Mode)</h1>
<div class="card">
<h3>Health</h3>
<p>
<a class="btn" href="{{ base }}/healthz" target="_blank">Open healthz</a>
<a class="btn" href="javascript:void(0)" onclick="checkHealth()">Check inline</a>
</p>
<div id="hz-status"></div>
<pre id="hz-body">{ click "Check inline" or use the link }</pre>
</div>
<div class="card">
<h3>Zones</h3>
<p>
<a class="btn" href="{{ base }}/zones" target="_blank">Open zones</a>
<a class="btn" href="javascript:void(0)" onclick="showZones()">Show inline</a>
</p>
<pre id="z-body">{ click "Show inline" or use the link }</pre>
</div>
<script type="text/javascript">
var BASE="{{ base }}";
function xhr(path,cb){try{var x=new XMLHttpRequest();x.onreadystatechange=function(){if(x.readyState===4)cb(null,x.status,x.responseText);};x.open("GET",BASE+path,true);x.setRequestHeader("Cache-Control","no-cache");x.send(null);}catch(e){cb(e,0,"");}}
function checkHealth(){var s=document.getElementById("hz-status");var pre=document.getElementById("hz-body");s.innerHTML="Checking...";xhr("/healthz",function(err,st,body){if(err){s.innerHTML='<b class="bad">Request failed</b>';pre.innerText=String(err);return;}s.innerHTML=(st===200?'<b class="ok">200 OK</b>':'<b class="bad">'+st+'</b>');pre.innerText=body;});}
function showZones(){var pre=document.getElementById("z-body");pre.innerText="Loading...";xhr("/zones",function(err,st,body){if(err){pre.innerText="Request failed: "+String(err);return;}pre.innerText=body;});}
</script>
""", base=base, home=home)
Quick bring‑up checklist
# 1) make sure service uses port 8080 (matches app.py)
sudo systemctl restart transferdepot
# 2) test
curl -i http://127.0.0.1:8080/ping
curl -i http://127.0.0.1:8080/api/v1/healthz
curl -i http://127.0.0.1:8080/upload-basic/RS2
If those return 200s, the proxy endpoint https://transferdepot.sh1re.net/api/v1/healthz should also be good.