🔒 Repository is read-only – file editing is disabled.

PaganLinux/pagan-web-v2/blueprints/repo.py main

362 linii Raw ← Powrót
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})