273 lines
7.5 KiB
Python
Executable File
273 lines
7.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from urllib.parse import urljoin
|
|
|
|
import requests
|
|
from flask import (
|
|
Flask,
|
|
request,
|
|
redirect,
|
|
url_for,
|
|
jsonify,
|
|
send_from_directory,
|
|
render_template,
|
|
)
|
|
|
|
BASE = Path("/srv/www")
|
|
STATIC_DIR = BASE / "static"
|
|
STATIC_DIR.mkdir(exist_ok=True)
|
|
|
|
# Where the Mirage API lives
|
|
MIRAGE_API_BASE = os.environ.get(
|
|
"MIRAGE_API_BASE", "http://127.0.0.1:5151/api/v1"
|
|
)
|
|
# How mirrors are exposed over HTTP (for links)
|
|
MIRROR_HTTP_BASE = os.environ.get("MIRROR_HTTP_BASE", "/mirrors/")
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
def _api_url(path: str) -> str:
|
|
return urljoin(MIRAGE_API_BASE.rstrip("/") + "/", path.lstrip("/"))
|
|
|
|
|
|
def api_get(path: str, **kwargs):
|
|
resp = requests.get(_api_url(path), timeout=5, **kwargs)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
def api_post(path: str, json=None, **kwargs):
|
|
resp = requests.post(_api_url(path), json=json, timeout=10, **kwargs)
|
|
return resp
|
|
|
|
|
|
# -------------------- ROUTES --------------------
|
|
@app.route("/static/<path:filename>")
|
|
def static_file(filename):
|
|
return send_from_directory(STATIC_DIR, filename)
|
|
|
|
|
|
@app.route("/", methods=["GET"])
|
|
def index():
|
|
# Ask Mirage for mirrors
|
|
data = api_get("mirrors")
|
|
mirrors = data.get("mirrors", [])
|
|
|
|
cats = set()
|
|
rows = []
|
|
for m in mirrors:
|
|
categories = m.get("categories") or []
|
|
for c in categories:
|
|
cats.add(c)
|
|
raw = m.get("last_updated")
|
|
disp = raw.replace("T", " ").replace("Z", " UTC") if raw else None
|
|
rows.append({
|
|
"slug": m["slug"],
|
|
"categories": categories,
|
|
"categories_joined": ", ".join(categories),
|
|
"url": m["url"],
|
|
"status": m.get("status") or "idle",
|
|
"last_updated_raw": raw,
|
|
"last_updated": disp,
|
|
})
|
|
|
|
return render_template(
|
|
"index.html",
|
|
mirrors=rows,
|
|
categories=sorted(cats),
|
|
error=None,
|
|
)
|
|
|
|
|
|
@app.route("/add", methods=["POST"])
|
|
def add_mirror_route():
|
|
slug = (request.form.get("slug") or "").strip()
|
|
categories_raw = (request.form.get("categories") or "").strip()
|
|
url = (request.form.get("url") or "").strip()
|
|
ignore_robots = bool(request.form.get("ignore_robots"))
|
|
|
|
error = None
|
|
if not slug or not categories_raw or not url:
|
|
error = "Slug, categories, and URL are required."
|
|
elif " " in slug:
|
|
error = "Slug cannot contain spaces."
|
|
|
|
categories = [c.strip() for c in categories_raw.split(",") if c.strip()]
|
|
|
|
if error:
|
|
# Same re-render pattern as before, but using Mirage API now
|
|
data = api_get("mirrors")
|
|
mirrors = data.get("mirrors", [])
|
|
cats = set()
|
|
rows = []
|
|
for m in mirrors:
|
|
cs = m.get("categories") or []
|
|
for c in cs:
|
|
cats.add(c)
|
|
raw = m.get("last_updated")
|
|
disp = raw.replace("T", " ").replace("Z", " UTC") if raw else None
|
|
rows.append({
|
|
"slug": m["slug"],
|
|
"categories": cs,
|
|
"categories_joined": ", ".join(cs),
|
|
"url": m["url"],
|
|
"status": m.get("status") or "idle",
|
|
"last_updated_raw": raw,
|
|
"last_updated": disp,
|
|
})
|
|
return (
|
|
render_template("index.html",
|
|
mirrors=rows,
|
|
categories=sorted(cats),
|
|
error=error),
|
|
400,
|
|
)
|
|
|
|
payload = {
|
|
"slug": slug,
|
|
"url": url,
|
|
"categories": categories,
|
|
"ignore_robots": ignore_robots,
|
|
"enqueue": True,
|
|
}
|
|
|
|
resp = api_post("mirrors", json=payload)
|
|
if resp.status_code >= 400:
|
|
try:
|
|
msg = resp.json().get("error", resp.text)
|
|
except Exception:
|
|
msg = resp.text or "Failed to create mirror."
|
|
# Same error re-render as above
|
|
data = api_get("mirrors")
|
|
mirrors = data.get("mirrors", [])
|
|
cats = set()
|
|
rows = []
|
|
for m in mirrors:
|
|
cs = m.get("categories") or []
|
|
for c in cs:
|
|
cats.add(c)
|
|
raw = m.get("last_updated")
|
|
disp = raw.replace("T", " ").replace("Z", " UTC") if raw else None
|
|
rows.append({
|
|
"slug": m["slug"],
|
|
"categories": cs,
|
|
"categories_joined": ", ".join(cs),
|
|
"url": m["url"],
|
|
"status": m.get("status") or "idle",
|
|
"last_updated_raw": raw,
|
|
"last_updated": disp,
|
|
})
|
|
return (
|
|
render_template("index.html",
|
|
mirrors=rows,
|
|
categories=sorted(cats),
|
|
error=msg),
|
|
400,
|
|
)
|
|
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
@app.route("/update/<slug>", methods=["POST"])
|
|
def trigger_update(slug):
|
|
api_post(f"mirrors/{slug}/update")
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
@app.route("/status", methods=["GET"])
|
|
def status():
|
|
data = api_get("mirrors")
|
|
mirrors = data.get("mirrors", [])
|
|
out = []
|
|
for m in mirrors:
|
|
raw = m.get("last_updated")
|
|
disp = raw.replace("T", " ").replace("Z", " UTC") if raw else None
|
|
out.append({
|
|
"slug": m["slug"],
|
|
"categories": m.get("categories") or [],
|
|
"url": m["url"],
|
|
"status": m.get("status") or "idle",
|
|
"last_updated": raw,
|
|
"last_updated_display": disp or "",
|
|
})
|
|
return jsonify({"mirrors": out})
|
|
|
|
|
|
@app.route("/logs/<slug>")
|
|
def log_view(slug):
|
|
# Template just polls /logs/<slug>/tail via JS
|
|
return render_template("log.html", slug=slug)
|
|
|
|
|
|
@app.route("/logs/<slug>/tail")
|
|
def log_tail(slug):
|
|
try:
|
|
resp = requests.get(_api_url(f"mirrors/{slug}/log"),
|
|
params={"bytes": 65536},
|
|
timeout=5)
|
|
except requests.RequestException:
|
|
return "", 200
|
|
|
|
if resp.status_code >= 400:
|
|
return "", 200
|
|
|
|
return resp.text, 200, {"Content-Type": "text/plain; charset=utf-8"}
|
|
|
|
|
|
@app.route("/search", methods=["GET"])
|
|
def content_search():
|
|
q = (request.args.get("q") or "").strip()
|
|
if not q:
|
|
return jsonify({"results": []})
|
|
|
|
try:
|
|
resp = requests.get(_api_url("search"),
|
|
params={"q": q},
|
|
timeout=10)
|
|
except requests.RequestException:
|
|
return jsonify({
|
|
"results": [{
|
|
"path": "(error)",
|
|
"line": 0,
|
|
"url": "",
|
|
"snippet": "Search request to Mirage API failed.",
|
|
}]
|
|
})
|
|
|
|
if resp.status_code >= 400:
|
|
return jsonify({
|
|
"results": [{
|
|
"path": "(error)",
|
|
"line": 0,
|
|
"url": "",
|
|
"snippet": f"Mirage API returned {resp.status_code}.",
|
|
}]
|
|
})
|
|
|
|
data = resp.json()
|
|
results = []
|
|
for r in data.get("results", []):
|
|
rel_path = r.get("path", "")
|
|
lineno = int(r.get("line", 0))
|
|
snippet = r.get("snippet") or ""
|
|
|
|
# Web UI decides how mirrors are exposed over HTTP:
|
|
url = MIRROR_HTTP_BASE.rstrip("/") + "/" + rel_path.replace("\\", "/")
|
|
|
|
results.append({
|
|
"path": rel_path,
|
|
"line": lineno,
|
|
"url": url,
|
|
"snippet": snippet,
|
|
})
|
|
|
|
return jsonify({"results": results})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="127.0.0.1", port=5000, debug=False)
|