fix: close library upload flow and harden image storage
Community checks / verify (push) Skipped
Verify Frame Flow / verify (push) Successful in 8m9s

This commit is contained in:
渡口浪人
2026-10-24 00:38:00 +08:00
parent 0859871363
commit 1494904b12
39 changed files with 1471 additions and 240 deletions
+92 -72
View File
@@ -164,6 +164,13 @@ class Libraries:
return self.binding(terminal_id)
def ingest(self, source_id, item_id, data, name, caption):
if self.service.photo_sources.get(source_id).kind == "uploads":
existing = self.db.one(
"SELECT id FROM source_images WHERE source_id=? AND item_id=?",
(source_id, item_id),
)
if existing:
return self.picture(existing["id"])
digest = hashlib.sha256(
data + b"\0" + encode(caption or {}).encode()
).hexdigest()
@@ -175,7 +182,7 @@ class Libraries:
return self.picture(row["id"])
# Normalize EXIF and JPEG storage without resizing or adding decorations.
try:
with Image.open(BytesIO(data)) as image:
with Image.open(BytesIO(data), formats=("JPEG", "PNG", "WEBP")) as image:
if (
image.format not in {"JPEG", "PNG", "WEBP"}
or image.width * image.height > MAX_IMAGE_PIXELS
@@ -187,35 +194,37 @@ class Libraries:
normalized = convert(data, "cover", size=size, quality=95)
except (OSError, ValueError, Image.DecompressionBombError):
raise FlowError(E.INVALID_REQUEST, "Invalid source image") from None
asset = self.service.assets.put(normalized, name, "source-images")
self.db.execute(
"INSERT INTO source_images VALUES(?,?,?,?,?,?)",
(
asset["asset_id"],
source_id,
item_id,
digest,
encode(caption or {}),
now_iso(),
),
)
self.db.execute(
"INSERT INTO asset_sources VALUES(?,?)",
(
asset["asset_id"],
encode(
{
"type": "library_source",
"source_id": source_id,
"item_id": item_id,
"source_rendition": "preview"
if self.service.photo_sources.get(source_id).kind
== "synology-photos"
else "source",
}
with self.service.assets.creating(normalized, name, "source-images") as asset:
self.db.execute(
"INSERT INTO source_images VALUES(?,?,?,?,?,?)",
(
asset["asset_id"],
source_id,
item_id,
digest,
encode(caption or {}),
now_iso(),
),
),
)
)
self.db.execute(
"INSERT INTO asset_sources VALUES(?,?)",
(
asset["asset_id"],
encode(
{
"type": "library_source",
"source_id": source_id,
"item_id": item_id,
"source_rendition": "upload"
if item_id.startswith("upload:")
else "preview"
if self.service.photo_sources.get(source_id).kind
== "synology-photos"
else "source",
}
),
),
)
return self.picture(asset["asset_id"])
def import_picture(self, source_id, item_id):
@@ -342,37 +351,37 @@ class Libraries:
):
return version
data = self.service.assets.path(picture_id).read_bytes()
asset = self.service.assets.put(
with self.service.assets.creating(
self.render(data, picture["caption"], plan),
picture["name"].removesuffix(".jpg") + "-" + plan.name,
"renditions",
)
self.db.execute(
"INSERT INTO library_versions VALUES(?,?,?,?,?,?)",
(
asset["asset_id"],
picture_id,
plan_id,
revision,
encode(plan.model_dump()),
now_iso(),
),
)
self.db.execute(
"INSERT INTO asset_sources VALUES(?,?)",
(
asset["asset_id"],
encode(
{
"type": "library_version",
"source_image_id": picture_id,
"source_id": plan.source_id,
"plan_id": plan_id,
"revision": revision,
}
) as asset:
self.db.execute(
"INSERT INTO library_versions VALUES(?,?,?,?,?,?)",
(
asset["asset_id"],
picture_id,
plan_id,
revision,
encode(plan.model_dump()),
now_iso(),
),
),
)
)
self.db.execute(
"INSERT INTO asset_sources VALUES(?,?)",
(
asset["asset_id"],
encode(
{
"type": "library_version",
"source_image_id": picture_id,
"source_id": plan.source_id,
"plan_id": plan_id,
"revision": revision,
}
),
),
)
return next(
v
for v in self.versions(picture_id)
@@ -642,21 +651,33 @@ class Libraries:
409,
)
def remove_assets(self, asset_ids):
with self.db.transaction():
for asset_id in asset_ids:
row = self.db.one("SELECT kind FROM assets WHERE id=?", (asset_id,))
if not row:
continue
self.db.execute(
"INSERT OR REPLACE INTO asset_file_journal VALUES(?,?)",
(asset_id, row["kind"]),
)
for table, key in [
("library_versions", "asset_id"),
("terminal_deliveries", "asset_id"),
("source_images", "id"),
("asset_sources", "asset_id"),
("asset_metadata", "asset_id"),
("assets", "id"),
]:
self.db.execute(f"DELETE FROM {table} WHERE {key}=?", (asset_id,))
self.db.execute(
"DELETE FROM photo_preferences WHERE source_id='@assets' AND item_id=?",
(asset_id,),
)
self.service.assets.recover_files()
def remove_asset(self, asset_id):
self.service.assets.path(asset_id).unlink(missing_ok=True)
self.db.execute("DELETE FROM library_versions WHERE asset_id=?", (asset_id,))
self.db.execute("DELETE FROM terminal_deliveries WHERE asset_id=?", (asset_id,))
self.db.execute("DELETE FROM source_images WHERE id=?", (asset_id,))
for table, key in [
("asset_sources", "asset_id"),
("asset_metadata", "asset_id"),
("assets", "id"),
]:
self.db.execute(f"DELETE FROM {table} WHERE {key}=?", (asset_id,))
self.db.execute(
"DELETE FROM photo_preferences WHERE source_id='@assets' AND item_id=?",
(asset_id,),
)
self.remove_assets([asset_id])
def delete_picture(self, picture_id):
with self.service.lock:
@@ -670,8 +691,7 @@ class Libraries:
] + [picture_id]
for asset_id in ids:
self.check_asset_references(asset_id)
for asset_id in ids:
self.remove_asset(asset_id)
self.remove_assets(ids)
def check_source_delete(self, source_id):
if (
+27 -27
View File
@@ -327,19 +327,19 @@ class DisplayService:
if isinstance(content, TemplateContent):
values = self.renderer.resolve(content.template, content.data)
data = self.renderer.render(content.template, values, content.profile)
asset = self.assets.put(
with self.assets.creating(
data, content.template + ".jpg", "cache" if staging else "generated"
)
source = {
"type": "template",
"template": content.template,
"data": self.settings.sanitize(values),
"profile": content.profile,
}
self.db.execute(
"INSERT OR REPLACE INTO asset_sources VALUES(?,?)",
(asset["asset_id"], encode(source)),
)
) as asset:
source = {
"type": "template",
"template": content.template,
"data": self.settings.sanitize(values),
"profile": content.profile,
}
self.db.execute(
"INSERT OR REPLACE INTO asset_sources VALUES(?,?)",
(asset["asset_id"], encode(source)),
)
return asset
if isinstance(content, PhotoSourceContent):
source = self.photo_sources.get(content.source_id)
@@ -478,23 +478,23 @@ class DisplayService:
"UPDATE operations SET result=? WHERE id=?",
(encode({"photo_attempts": attempts}), operation_id),
)
asset = self.assets.put(
with self.assets.creating(
rendered, name, "cache" if staging else "images"
)
self.db.execute(
"INSERT OR REPLACE INTO asset_sources VALUES(?,?)",
(
asset["asset_id"],
encode(
{
"type": "photo_source",
"source_id": content.source_id,
"item_id": item["id"],
"source_path": item.get("source_path"),
}
) as asset:
self.db.execute(
"INSERT OR REPLACE INTO asset_sources VALUES(?,?)",
(
asset["asset_id"],
encode(
{
"type": "photo_source",
"source_id": content.source_id,
"item_id": item["id"],
"source_path": item.get("source_path"),
}
),
),
),
)
)
return self.assets.info(asset["asset_id"])
raise FlowError(
E.INVALID_REQUEST, "No decodable photo found within 5 candidates"