fix: close library upload flow and harden image storage
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user