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/&lt;group&gt;</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.