Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions scripts/sync_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from typing import Any

DEFAULT_SOURCE = "https://plugins.aiuo.net/catalog.json"
DEFAULT_ARTIFACT_BASE = "https://raw.githubusercontent.com/vastsa/pi-desktop-plugins/main"
UA = "pi-desktop-plugins-mirror/1.0"
MAX_PACKAGE_BYTES = 50 * 1024 * 1024
CATALOG_TIMEOUT = 30
Expand Down Expand Up @@ -89,9 +90,9 @@ def resolve_download_url(catalog: dict[str, Any], rel: str) -> str:
return base + rel.lstrip("/")


def rewrite_for_mirror(src: dict[str, Any]) -> dict[str, Any]:
def rewrite_for_mirror(src: dict[str, Any], artifact_base: str = DEFAULT_ARTIFACT_BASE) -> dict[str, Any]:
out = copy.deepcopy(src)
out.pop("artifactBaseUrl", None)
out["artifactBaseUrl"] = artifact_base
out.pop("artifactBaseURL", None)
for p in out.get("plugins") or []:
pid = p.get("id")
Expand Down Expand Up @@ -139,10 +140,11 @@ def sync(root: Path, source: str, dry_run: bool) -> int:
raise SystemExit(f"catalog is not JSON: {e}") from e
validate_catalog(src)

mirrored = rewrite_for_mirror(src)
existing = load_json(root / "catalog.json")
artifact_base = ((existing or {}).get("artifactBaseUrl") or (existing or {}).get("artifactBaseURL") or "").strip()
mirrored = rewrite_for_mirror(src, artifact_base or DEFAULT_ARTIFACT_BASE)
validate_catalog(mirrored)

existing = load_json(root / "catalog.json")
if existing and canonical(existing) == canonical(mirrored):
print("catalog unchanged (ignoring generatedAt/catalogId/updatedAt)")
return 0
Expand Down
37 changes: 34 additions & 3 deletions tests/test_sync_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from threading import Thread
from unittest.mock import patch

ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
Expand All @@ -26,7 +27,7 @@ def load_sync():


class RewriteTests(unittest.TestCase):
def test_rewrite_drops_artifact_base_and_uses_packages(self):
def test_rewrite_declares_mirror_artifact_base_and_uses_packages(self):
src = {
"schemaVersion": 2,
"providerId": "official",
Expand All @@ -45,10 +46,18 @@ def test_rewrite_drops_artifact_base_and_uses_packages(self):
],
}
out = sync.rewrite_for_mirror(src)
self.assertNotIn("artifactBaseUrl", out)
self.assertEqual(out["artifactBaseUrl"], "https://raw.githubusercontent.com/vastsa/pi-desktop-plugins/main")
self.assertEqual(src["artifactBaseUrl"], "https://plugins.aiuo.net/api/pi/v1/")
self.assertEqual(out["plugins"][0]["versions"][0]["url"], "packages/demo.hello-1.0.0.piplug")
self.assertEqual(src["plugins"][0]["versions"][0]["url"], "plugins/demo.hello/versions/1.0.0/artifact")

def test_rewrite_uses_selected_mirror_base_not_source_alias(self):
src = {"artifactBaseURL": "https://center.example/artifacts/", "plugins": []}
out = sync.rewrite_for_mirror(src, "https://mirror.example/plugins/")
self.assertEqual(out["artifactBaseUrl"], "https://mirror.example/plugins/")
self.assertNotIn("artifactBaseURL", out)
self.assertEqual(src["artifactBaseURL"], "https://center.example/artifacts/")

def test_empty_catalog_rejected(self):
with self.assertRaises(SystemExit):
sync.validate_catalog({"providerId": "official", "plugins": []})
Expand Down Expand Up @@ -121,7 +130,7 @@ def log_message(self, format, *args):
self.assertEqual(rc, 0)
new = json.loads((root / "catalog.json").read_text(encoding="utf-8"))
self.assertEqual(new["plugins"][0]["id"], "demo.hello")
self.assertNotIn("artifactBaseUrl", new)
self.assertEqual(new["artifactBaseUrl"], "https://raw.githubusercontent.com/vastsa/pi-desktop-plugins/main")
pkg = root / "packages" / "demo.hello-1.0.0.piplug"
self.assertTrue(pkg.exists())
self.assertEqual(pkg.read_bytes(), blob)
Expand All @@ -140,6 +149,28 @@ def test_http_error_leaves_catalog(self):
sync.sync(root, "http://127.0.0.1:1/catalog.json", dry_run=False)
self.assertEqual((root / "catalog.json").read_text(encoding="utf-8"), old)

def test_sync_keeps_existing_mirror_base(self):
blob = b"hello-piplug"
src = {
"providerId": "official",
"artifactBaseUrl": "https://center.example/artifacts/",
"plugins": [{"id": "demo.hello", "versions": [{
"version": "1.0.0", "shasum": sync.sha256_bytes(blob), "url": "hello.piplug",
}]}],
}
for key in ("artifactBaseUrl", "artifactBaseURL"):
with self.subTest(key=key), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
old = {"providerId": "official", key: "https://mirror.example/releases/", "plugins": []}
(root / "catalog.json").write_text(json.dumps(old), encoding="utf-8")
with patch.object(sync, "fetch", side_effect=[json.dumps(src).encode(), blob, b"signature"]) as fetch:
self.assertEqual(sync.sync(root, "https://center.example/catalog.json", False), 0)
new = json.loads((root / "catalog.json").read_text(encoding="utf-8"))
self.assertEqual(new["artifactBaseUrl"], "https://mirror.example/releases/")
self.assertNotIn("artifactBaseURL", new)
self.assertEqual(new["plugins"][0]["versions"][0]["url"], "packages/demo.hello-1.0.0.piplug")
self.assertEqual(fetch.call_args_list[1].args[0], "https://center.example/artifacts/hello.piplug")


if __name__ == "__main__":
sys.exit(unittest.main())