"""Read-only live Photos check. Never creates an app, worker, job or frame driver.""" import argparse import io import json import os from pathlib import Path from PIL import Image from pigway_frame_flow.errors import FlowError from pigway_frame_flow.providers.photo_sources import PhotoSourceInput, PhotoSources from pigway_frame_flow.settings import Settings from pigway_frame_flow.storage.database import Database def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--bootstrap", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() if Path("/photos").exists(): raise SystemExit("Verification requires /photos to be absent") bootstrap = json.loads(args.bootstrap.read_text()) os.environ["SYNOLOGY_PHOTOS_URL"] = bootstrap["url"] os.environ["SYNOLOGY_PHOTOS_CERT_SHA256"] = bootstrap["pin"] settings = Settings(mock=True, write_enabled=False, api_key="", api_token="") settings.data_dir.mkdir(parents=True, exist_ok=True) settings.config_dir.mkdir(parents=True, exist_ok=True) session = settings.config_dir / "synology-photos-session.json" session.write_text(json.dumps(bootstrap["session"])) session.chmod(0o600) db = Database(settings.data_dir / "verification.db") photos = PhotoSources(db, settings, None) report = {"no_photos_mount": True, "device_writes": 0, "spaces": {}} try: for space in ("shared", "personal"): people = photos.synology.people(space) candidates = sorted( (p for p in people if p["count"] > 1), key=lambda p: p["count"] ) if len(candidates) < 2: raise RuntimeError("Need two accessible populated person albums") selected = [p["id"] for p in candidates[:2]] one = photos.synology.items(selected[:1], space=space) two = photos.synology.items(selected[1:], space=space) ids_one, ids_two = {r["id"] for r in one}, {r["id"] for r in two} any_rows = photos.synology.items(selected, "any", space=space) all_rows = photos.synology.items(selected, "all", space=space) assert {r["id"] for r in any_rows} == ids_one | ids_two assert {r["id"] for r in all_rows} == ids_one & ids_two source_id = "live-" + space photos.save( source_id, PhotoSourceInput( name="Read-only verification", kind="synology-photos", location=".", people_space=space, people_filter=True, person_ids=selected, ), ) catalog = photos.catalog(source_id) assert catalog data, _, _ = photos.read(source_id, catalog[0]["id"]) with Image.open(io.BytesIO(data)) as image: image.load() decoded = { "width": image.width, "height": image.height, "format": image.format, } cache = photos.synology.image_cache before = cache.downloads assert photos.read(source_id, catalog[0]["id"])[0] == data assert cache.downloads == before report["spaces"][space] = { "people": len(people), "any_count": len(any_rows), "all_count": len(all_rows), "catalog_count": len(catalog), "bytes": len(data), "decoded": decoded, "cache_reused": True, } # A deliberately invalid session is isolated to this verification config; # no logout request or change is made to the original NAS session. photos.synology.path.write_text( json.dumps({"sid": "frameflow-deliberately-invalid-session"}) ) try: photos.synology.people() except FlowError as error: assert error.status == 409 report["expired_session"] = "explicit_reconnect_required" else: raise RuntimeError("Invalid session unexpectedly accepted") report["cache_bytes"] = sum( p.stat().st_size for p in (settings.data_dir / "cache/synology-photos").glob("*.image") ) assert report["cache_bytes"] <= settings.photos_cache_bytes report["original_download"] = "not_supported_by_this_verified_implementation" args.output.write_text(json.dumps(report, indent=2) + "\n") print(json.dumps(report, indent=2)) finally: session.unlink(missing_ok=True) if __name__ == "__main__": main()