diff --git a/app.py b/app.py
index f13fc37..a9d7f78 100755
--- a/app.py
+++ b/app.py
@@ -1,16 +1,11 @@
#!/usr/bin/env python3
-from mirror_manager import (
- load_mirrors,
- add_mirror,
- update_mirror,
- MIRROR_ROOT,
- LOG_ROOT,
-)
-import re
-import html
-import subprocess
-import threading
+from __future__ import annotations
+
+import os
from pathlib import Path
+from urllib.parse import urljoin
+
+import requests
from flask import (
Flask,
request,
@@ -18,356 +13,39 @@ from flask import (
url_for,
jsonify,
send_from_directory,
- render_template_string
+ 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 _run_update_in_background(slug: str):
- th = threading.Thread(target=update_mirror, args=(slug,), daemon=True)
- th.start()
+def _api_url(path: str) -> str:
+ return urljoin(MIRAGE_API_BASE.rstrip("/") + "/", path.lstrip("/"))
-# -------------------- TEMPLATES --------------------
-INDEX_TEMPLATE = r"""
-
-
-
-
- Mirror Manager
-
-
-
-
-
-
-
-
-
-
-
- Categories:
-
- {% for cat in categories %}
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
- | Slug |
- Categories |
- URL |
- Last updated |
- Status |
- |
-
-
-
- {% for m in mirrors %}
-
- |
-
- |
-
-
- {% for c in m.categories %}
- {{ c }}
- {% endfor %}
-
- |
-
- {{ m.url }}
- |
-
- {% if m.last_updated %}
- {{ m.last_updated }}
- {% else %}
- never
- {% endif %}
- |
-
- {% set st = m.status or 'idle' %}
-
-
- {{ st }}
-
- |
-
-
- |
-
- {% endfor %}
- {% if mirrors|length == 0 %}
-
- |
- No mirrors yet. Add one below.
- |
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"""
+def api_get(path: str, **kwargs):
+ resp = requests.get(_api_url(path), timeout=5, **kwargs)
+ resp.raise_for_status()
+ return resp.json()
-LOG_TEMPLATE = r"""
-
-
-
-
- Log: {{ slug }}
-
-
-
-
-
-
-
Log for {{ slug }}
-
Live tail of wget output (auto-refreshing).
-
-
Open mirror
-
-
-
-
-
-
-"""
+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/")
def static_file(filename):
return send_from_directory(STATIC_DIR, filename)
@@ -375,7 +53,10 @@ def static_file(filename):
@app.route("/", methods=["GET"])
def index():
- mirrors = load_mirrors()
+ # Ask Mirage for mirrors
+ data = api_get("mirrors")
+ mirrors = data.get("mirrors", [])
+
cats = set()
rows = []
for m in mirrors:
@@ -393,25 +74,34 @@ def index():
"last_updated_raw": raw,
"last_updated": disp,
})
- return render_template_string(INDEX_TEMPLATE, mirrors=rows, categories=sorted(cats), error=None)
+
+ 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 = (request.form.get("categories") 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 or not url:
+ 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:
- # re-render with error
- mirrors = load_mirrors()
+ # 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:
@@ -429,12 +119,31 @@ def add_mirror_route():
"last_updated_raw": raw,
"last_updated": disp,
})
- return render_template_string(INDEX_TEMPLATE, mirrors=rows, categories=sorted(cats), error=error), 400
+ return (
+ render_template("index.html",
+ mirrors=rows,
+ categories=sorted(cats),
+ error=error),
+ 400,
+ )
- try:
- add_mirror(slug, categories, url, ignore_robots=ignore_robots)
- except Exception as e:
- mirrors = load_mirrors()
+ 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:
@@ -452,21 +161,27 @@ def add_mirror_route():
"last_updated_raw": raw,
"last_updated": disp,
})
- return render_template_string(INDEX_TEMPLATE, mirrors=rows, categories=sorted(cats), error=str(e)), 400
+ return (
+ render_template("index.html",
+ mirrors=rows,
+ categories=sorted(cats),
+ error=msg),
+ 400,
+ )
- _run_update_in_background(slug)
return redirect(url_for("index"))
@app.route("/update/", methods=["POST"])
def trigger_update(slug):
- _run_update_in_background(slug)
+ api_post(f"mirrors/{slug}/update")
return redirect(url_for("index"))
@app.route("/status", methods=["GET"])
def status():
- mirrors = load_mirrors()
+ data = api_get("mirrors")
+ mirrors = data.get("mirrors", [])
out = []
for m in mirrors:
raw = m.get("last_updated")
@@ -484,78 +199,23 @@ def status():
@app.route("/logs/")
def log_view(slug):
- log_path = LOG_ROOT / f"{slug}.log"
- if not log_path.exists():
- log_path.touch()
- return render_template_string(LOG_TEMPLATE, slug=slug)
+ # Template just polls /logs//tail via JS
+ return render_template("log.html", slug=slug)
@app.route("/logs//tail")
def log_tail(slug):
- log_path = LOG_ROOT / f"{slug}.log"
- if not log_path.exists():
- return "", 200
try:
- with log_path.open("rb") as f:
- f.seek(0, 2)
- size = f.tell()
- block = 65536
- if size <= block:
- f.seek(0)
- data = f.read()
- else:
- f.seek(-block, 2)
- data = f.read()
- return data.decode("utf-8", errors="replace")
- except OSError:
+ 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
-def strip_html(text: str) -> str:
- # Remove script and style blocks first
- text = re.sub(
- r"",
- " ",
- text,
- flags=re.IGNORECASE,
- )
- text = re.sub(
- r"",
- " ",
- text,
- flags=re.IGNORECASE,
- )
- # Strip all remaining tags
- text = re.sub(r"<[^>]+>", " ", text)
- # Unescape HTML entities (& → &, etc.)
- text = html.unescape(text)
- # Collapse whitespace
- text = re.sub(r"\s+", " ", text).strip()
- return text
-
-
-def make_snippet(text: str,
- query: str,
- radius: int = 80,
- max_len: int = 240) -> str:
- if not text:
- return ""
- lower = text.lower()
- qlower = query.lower()
- idx = lower.find(qlower)
- if idx == -1:
- snippet = text[:max_len]
- if len(text) > max_len:
- snippet += "…"
- return snippet
- start = max(0, idx - radius)
- end = min(len(text), idx + len(query) + radius)
- snippet = text[start:end]
- if start > 0:
- snippet = "…" + snippet
- if end < len(text):
- snippet += "…"
- return snippet
+ return resp.text, 200, {"Content-Type": "text/plain; charset=utf-8"}
@app.route("/search", methods=["GET"])
@@ -565,73 +225,46 @@ def content_search():
return jsonify({"results": []})
try:
- proc = subprocess.run(
- [
- "rg",
- "--line-number",
- "--no-heading",
- "--color", "never",
- "--max-count", "5", # per file
- "--type-add", "page:*.{html,htm,md,markdown,txt}",
- "-tpage",
- q,
- str(MIRROR_ROOT),
- ],
- stdout=subprocess.PIPE,
- stderr=subprocess.DEVNULL,
- text=True,
- timeout=10,
- )
- except FileNotFoundError:
+ resp = requests.get(_api_url("search"),
+ params={"q": q},
+ timeout=10)
+ except requests.RequestException:
return jsonify({
"results": [{
"path": "(error)",
"line": 0,
"url": "",
- "snippet": "ripgrep (rg) is not installed."
- }]
- })
- except subprocess.TimeoutExpired:
- return jsonify({
- "results": [{
- "path": "(error)",
- "line": 0,
- "url": "",
- "snippet": "rg timed out."
+ "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 line in proc.stdout.splitlines():
- parts = line.split(":", 2)
- if len(parts) != 3:
- continue
- path, lineno, raw_content = parts
+ for r in data.get("results", []):
+ rel_path = r.get("path", "")
+ lineno = int(r.get("line", 0))
+ snippet = r.get("snippet") or ""
- # Strip HTML/JS/CSS markup from this line before making a snippet
- text_content = strip_html(raw_content)
- if not text_content:
- continue
-
- snippet = make_snippet(text_content, q)
-
- try:
- rel_path = str(Path(path).relative_to(MIRROR_ROOT))
- except ValueError:
- rel_path = path
-
- url = "/mirrors/" + rel_path.replace("\\", "/")
+ # Web UI decides how mirrors are exposed over HTTP:
+ url = MIRROR_HTTP_BASE.rstrip("/") + "/" + rel_path.replace("\\", "/")
results.append({
"path": rel_path,
- "line": int(lineno),
+ "line": lineno,
"url": url,
"snippet": snippet,
})
- if len(results) >= 50:
- break
-
return jsonify({"results": results})
diff --git a/systemd/mirror-manager.service b/systemd/mirage-web.service
similarity index 53%
rename from systemd/mirror-manager.service
rename to systemd/mirage-web.service
index 427e9d6..5af11bc 100644
--- a/systemd/mirror-manager.service
+++ b/systemd/mirage-web.service
@@ -1,13 +1,14 @@
[Unit]
-Description=Mirror Manager Flask App
+Description=Mirage-Web Interface
+Requires=mirage-api.service
After=network.target
[Service]
-User=aargonian
-Group=aargonian
+User=mirage
+Group=mirage
WorkingDirectory=/srv/www
Environment="FLASK_ENV=production"
-ExecStart=/usr/bin/python3 /srv/www/app.py
+ExecStart=/usr/bin/python3 /srv/www/mirage/mirage-web/app.py
Restart=on-failure
RestartSec=5
diff --git a/templates/index.html b/templates/index.html
new file mode 100755
index 0000000..fe036fc
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,287 @@
+
+
+
+
+ Mirror Manager
+
+
+
+
+
+
+
+
+
+
+
+ Categories:
+
+ {% for cat in categories %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+ | Slug |
+ Categories |
+ URL |
+ Last updated |
+ Status |
+ |
+
+
+
+ {% for m in mirrors %}
+
+ |
+
+ |
+
+
+ {% for c in m.categories %}
+ {{ c }}
+ {% endfor %}
+
+ |
+
+ {{ m.url }}
+ |
+
+ {% if m.last_updated %}
+ {{ m.last_updated }}
+ {% else %}
+ never
+ {% endif %}
+ |
+
+ {% set st = m.status or 'idle' %}
+
+
+ {{ st }}
+
+ |
+
+
+ |
+
+ {% endfor %}
+ {% if mirrors|length == 0 %}
+
+ |
+ No mirrors yet. Add one below.
+ |
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/log.html b/templates/log.html
new file mode 100755
index 0000000..386f053
--- /dev/null
+++ b/templates/log.html
@@ -0,0 +1,36 @@
+
+
+
+
+ Log: {{ slug }}
+
+
+
+
+
+
+
Log for {{ slug }}
+
Live tail of wget output (auto-refreshing).
+
+
Open mirror
+
+
+
+
+
+