113 lines
4.7 KiB
Python
113 lines
4.7 KiB
Python
"""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()
|