🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
import json, os, glob as g, re
from datetime import datetime, timezone
from flask import Blueprint, render_template, request, send_from_directory, jsonify, abort, session, flash
from config.settings import REPO_BASE, CATEGORIES, STABLE_CATS, RECIPES_DIR
from database import get_db
repo_bp = Blueprint("repo", __name__)
CAT_ICONS = {"core":"","desktop":"","tools":"","network":"","multimedia":"","gaming":"","office":"","other":""}
def _get_packages(category="", limit=5000):
pkgs = []
cats = [category] if category and category != "stable" else CATEGORIES
if category == "stable":
cats = STABLE_CATS
for cat in cats:
d = os.path.join(REPO_BASE, cat)
if not os.path.isdir(d):
continue
rj = os.path.join(d, "repo.json")
if os.path.exists(rj):
try:
for p in json.load(open(rj)).get("packages", []):
p["size_mb"] = round(p.get("size", 0) / 1048576, 1)
p["category"] = cat
pkgs.append(p)
except Exception:
pass
else:
for fname in sorted(os.listdir(d)):
if not (fname.endswith(".pag") or fname.endswith(".pkg.tar.xz")):
continue
fp = os.path.join(d, fname)
base = fname[:-len(".pag")] if fname.endswith(".pag") else fname.replace(".pkg.tar.xz", "")
# Inteligentne parsowanie: <name>-<ver>-<rel>
import re
m = re.match(r'^(.+)-([\d][\w.-]*?)-(\d+)$', base)
if m:
name, version = m.group(1), m.group(2)
else:
name = base
version = "0"
pkgs.append({
"name": name,
"version": version,
"filename": fname,
"size": os.path.getsize(fp),
"size_mb": round(os.path.getsize(fp) / 1048576, 1),
"arch": "x86_64",
"category": cat,
})
pkgs.sort(key=lambda x: x["name"])
return pkgs[:limit]
def _latest_version(pkg_name):
"""Zwraca najnowszą wersję pakietu (najwyższą znalezioną w repo)."""
best = None
for p in _get_packages("", 5000):
if p.get("name") == pkg_name:
v = p.get("version", "")
if v and (best is None or v > best):
best = v
return best or ""
def _check_updates():
"""Sprawdza obserwowane pakiety i wysyła e-mail o nowych wersjach.
Wywoływane przy starcie / przez admina."""
from mail import send_email
db = get_db()
watches = db.execute("SELECT * FROM package_watches").fetchall()
notified = 0
for w in watches:
latest = _latest_version(w["pkg_name"])
if not latest or not w["last_known_version"]:
continue
if latest != w["last_known_version"]:
# Wyślij powiadomienie
user = db.execute("SELECT email FROM users WHERE username=?", (w["username"],)).fetchone()
if user and user["email"]:
subject = f"PaganOS: nowa wersja {w['pkg_name']} ({latest})"
body = (f"Obserwowany pakiet {w['pkg_name']} ma nową wersję!\n\n"
f"Wersja: {w['last_known_version']} → {latest}\n\n"
f"Zobacz: https://repo.paganlinux.eu/\n")
if send_email(user["email"], subject, body):
notified += 1
db.execute("UPDATE package_watches SET last_known_version=? WHERE id=?",
(latest, w["id"]))
db.commit()
db.close()
return notified
@repo_bp.route("/api/watch", methods=["POST"])
def watch_package():
"""Obserwuj pakiet – dostaniesz e-mail o nowej wersji."""
if not session.get("username"):
return jsonify({"error": "login required"}), 401
name = (request.form.get("name") or "").strip()
if not name:
return jsonify({"error": "missing name"}), 400
latest = _latest_version(name)
db = get_db()
db.execute("INSERT OR IGNORE INTO package_watches (username, pkg_name, last_known_version) VALUES (?,?,?)",
(session["username"], name, latest))
db.commit()
db.close()
return jsonify({"ok": True, "watching": True})
@repo_bp.route("/api/unwatch", methods=["POST"])
def unwatch_package():
if not session.get("username"):
return jsonify({"error": "login required"}), 401
name = (request.form.get("name") or "").strip()
db = get_db()
db.execute("DELETE FROM package_watches WHERE username=? AND pkg_name=?",
(session["username"], name))
db.commit()
db.close()
return jsonify({"ok": True, "watching": False})
@repo_bp.route("/")
def index():
cat = request.args.get("cat", "")
search = request.args.get("q", "").lower()
sort = request.args.get("sort", "name")
page = request.args.get("page", 1, type=int)
per_page = 30
pkgs = _get_packages(cat)
if search:
pkgs = [p for p in pkgs if search in p.get("name", "").lower() or search in p.get("category", "").lower() or search in str(p.get("description", "")).lower()]
if sort == "size":
pkgs.sort(key=lambda x: x.get("size", 0), reverse=True)
elif sort == "version":
pkgs.sort(key=lambda x: x.get("version", ""), reverse=True)
else:
pkgs.sort(key=lambda x: x.get("name", "").lower())
total_pkgs = len(pkgs)
total_pages = max(1, (total_pkgs + per_page - 1) // per_page)
page = min(page, total_pages)
start = (page - 1) * per_page
pkgs = pkgs[start:start + per_page]
# Counts
counts = {}
for c in CATEGORIES:
d = os.path.join(REPO_BASE, c)
rj = os.path.join(d, "repo.json")
if os.path.exists(rj):
try:
counts[c] = len(json.load(open(rj)).get("packages", []))
except Exception:
counts[c] = len(g.glob(os.path.join(d, "*.pkg.tar.xz"))) if os.path.isdir(d) else 0
else:
counts[c] = len(g.glob(os.path.join(d, "*.pkg.tar.xz"))) if os.path.isdir(d) else 0
# Sumaryczny rozmiar paczek (bajty) – z repo.json każdej kategorii
total_size = 0
cat_sizes = {}
for c in CATEGORIES:
d = os.path.join(REPO_BASE, c)
rj = os.path.join(d, "repo.json")
sz = 0
if os.path.exists(rj):
try:
data = json.load(open(rj))
sz = sum(int(p.get("size", 0)) for p in data.get("packages", []))
except Exception:
sz = sum(os.path.getsize(os.path.join(d, f)) for f in g.glob(os.path.join(d, "*.pkg.tar.xz"))) if os.path.isdir(d) else 0
elif os.path.isdir(d):
sz = sum(os.path.getsize(os.path.join(d, f)) for f in g.glob(os.path.join(d, "*.pkg.tar.xz")))
cat_sizes[c] = sz
total_size += sz
total_size_gb = total_size / (1024.0 ** 3)
return render_template("repo/index.html", packages=pkgs, categories=CATEGORIES,
cat=cat, search=search, counts=counts, cat_icons=CAT_ICONS,
total=sum(counts.values()), stable_cats=STABLE_CATS,
page=page, total_pages=total_pages, total_pkgs=total_pkgs,
sort=sort, per_page=per_page,
total_size=total_size, total_size_gb=total_size_gb,
cat_sizes=cat_sizes)
@repo_bp.route("/download/<category>/<filename>")
def download(category, filename):
d = os.path.join(REPO_BASE, category)
if os.path.isfile(os.path.join(d, filename)):
return send_from_directory(d, filename, as_attachment=True)
abort(404)
@repo_bp.route("/sources/<filename>")
def download_source(filename):
sources_dir = "/var/www/repo.paganlinux.eu/sources"
if os.path.isfile(os.path.join(sources_dir, filename)):
return send_from_directory(sources_dir, filename, as_attachment=True)
abort(404)
@repo_bp.route("/package/<category>/<filename>")
def package_detail(category, filename):
"""Szczegóły pakietu."""
d = os.path.join(REPO_BASE, category)
fp = os.path.join(d, filename)
if not os.path.isfile(fp):
abort(404)
# Nazwę i wersję bierzemy z repo.json (authoritative) – parsowanie nazwy
# pliku jest mylące dla wersji z myślnikami (np. aalib-1.4rc5-6.pag dawało
# name="aalib-1.4rc5"). repo.json ma poprawny name/version z metadata.
name = version = None
rj = os.path.join(d, "repo.json")
if os.path.exists(rj):
try:
for p in json.load(open(rj)).get("packages", []):
if p.get("filename") == filename:
name = p.get("name")
version = str(p.get("version", ""))
break
except Exception:
pass
if not name:
# Fallback: parsuj nazwę pliku (format PaganOS: <name>-<ver>-<rel>.pag)
base = filename[:-len(".pag")] if filename.endswith(".pag") else filename.replace(".pkg.tar.xz", "")
m = re.match(r'^(.+)-([\d][\w.-]*?)-(\d+)$', base)
if m:
name, version = m.group(1), m.group(2)
else:
m = re.match(r'^(.+)-([\d][\w.-]*)$', base)
name = m.group(1) if m else base
version = m.group(2) if m else "?"
size = os.path.getsize(fp)
size_mb = round(size / 1048576, 1)
# Receptura PAGBUILD.yaml w drzewie recipes – do bezpośredniego linku git
# (https://git.paganlinux.eu/recipes/file?path=<cat>/<pkg>/PAGBUILD.yaml)
recipe_path = None
if name:
try:
for _cat in sorted(os.listdir(RECIPES_DIR)):
if os.path.isfile(os.path.join(RECIPES_DIR, _cat, str(name), "PAGBUILD.yaml")):
recipe_path = f"{_cat}/{name}/PAGBUILD.yaml"
break
except Exception:
pass
# URL projektu – metadata.json w .pag ma klucz "url" (nie "homepage")
homepage = ""
# Czytaj metadata.json z wnętrza paczki
meta = {}
rundeps = []
builddeps = []
try:
import tarfile
with tarfile.open(fp, "r:xz") as tf:
if "metadata.json" in tf.getnames():
meta = json.load(tf.extractfile("metadata.json"))
rundeps = meta.get("depends", []) or []
builddeps = meta.get("makedepends", []) or []
homepage = meta.get("url") or ""
except Exception:
pass
# Historia wersji tej samej paczki w danej kategorii
history = []
if os.path.isdir(d):
for fname in os.listdir(d):
if not (fname.endswith(".pag") or fname.endswith(".pkg.tar.xz")):
continue
base2 = fname[:-len(".pag")] if fname.endswith(".pag") else fname.replace(".pkg.tar.xz", "")
vm = re.match(r'^' + re.escape(str(name)) + r'-([\d][\w.-]*?)-(\d+)$', base2)
if not vm:
continue
version2 = vm.group(1)
fp2 = os.path.join(d, fname)
history.append({
"filename": fname,
"version": version2,
"size_mb": round(os.path.getsize(fp2) / 1048576, 1),
"current": fname == filename,
})
history.sort(key=lambda x: x["version"], reverse=True)
# Czy użytkownik obserwuje ten pakiet?
watching = False
if session.get("username"):
dbw = get_db()
row = dbw.execute("SELECT 1 FROM package_watches WHERE username=? AND pkg_name=?",
(session["username"], name)).fetchone()
dbw.close()
watching = row is not None
return render_template("repo/package.html",
name=name, version=version, category=category, filename=filename,
size=size, size_mb=size_mb, meta=meta, deps=rundeps, builddeps=builddeps,
history=history, watching=watching, homepage=homepage,
recipe_path=recipe_path)
@repo_bp.route("/api/packages")
def api_packages():
cat = request.args.get("cat", "")
q = request.args.get("q", "").lower()
pkgs = _get_packages(cat, 2000)
if q:
pkgs = [p for p in pkgs if q in p.get("name","").lower() or q in p.get("category","").lower()]
return jsonify({"packages": pkgs, "total": len(pkgs)})
@repo_bp.route("/api/delete-package", methods=["POST"])
def delete_package():
"""Usuwa pakiet (.pag/.asc) i odświeża repo.json danej kategorii."""
if not session.get("is_admin"):
return jsonify({"error": "admin only"}), 403
cat = (request.form.get("category") or "").strip()
filename = (request.form.get("filename") or "").strip()
# Walidacja: żadnych ścieżek / dziwnych nazw
if not cat or not filename or "/" in filename or filename.startswith("."):
return jsonify({"error": "invalid params"}), 400
fp = os.path.join(REPO_BASE, cat, filename)
if not os.path.isfile(fp):
return jsonify({"error": "file not found"}), 404
os.remove(fp)
asc = fp + ".asc"
if os.path.exists(asc):
os.remove(asc)
# Odśwież repo.json kategorii, żeby lista nie pokazywała usuniętego pakietu
rj = os.path.join(REPO_BASE, cat, "repo.json")
if os.path.exists(rj):
try:
data = json.load(open(rj))
pkgs = data.get("packages", [])
data["packages"] = [p for p in pkgs if p.get("filename") != filename]
data["count"] = len(data["packages"])
data["updated"] = datetime.now(timezone.utc).isoformat()
json.dump(data, open(rj, "w"), indent=1, ensure_ascii=False)
except Exception:
pass
flash(f"Usunięto: {filename}", "ok")
return jsonify({"ok": True})
@repo_bp.route("/api/delete-all-packages", methods=["POST"])
def delete_all_packages():
"""Usuwa WSZYSTKIE paczki ze wszystkich kategorii."""
if not session.get("is_admin"):
return jsonify({"error": "admin only"}), 403
import glob as gmod
total = 0
for cat in CATEGORIES:
d = os.path.join(REPO_BASE, cat)
if not os.path.isdir(d):
continue
for f in gmod.glob(os.path.join(d, "*.pkg.tar.xz")):
os.remove(f)
total += 1
# Usuń też repo.json
rj = os.path.join(d, "repo.json")
if os.path.exists(rj):
os.remove(rj)
flash(f"Usunięto {total} pakietów ze wszystkich kategorii", "ok")
return jsonify({"ok": True, "deleted": total})