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
+3 -1
View File
@@ -155,7 +155,9 @@ def install_ai_images(app, service):
image_data = base64.b64decode(result[0]["b64_json"], validate=True)
else:
image_data = service.assets.fetch_url(result[0]["url"])
with Image.open(BytesIO(image_data)) as image:
with Image.open(
BytesIO(image_data), formats=("JPEG", "PNG", "WEBP")
) as image:
if image.width * image.height > MAX_IMAGE_PIXELS:
raise ValueError()
rgba = ImageOps.exif_transpose(image).convert("RGBA")
+12 -6
View File
@@ -76,7 +76,9 @@ def install_extensions(app, service):
if len(data) > settings.max_bytes:
raise FlowError(E.INVALID_REQUEST, "Image exceeds size limit")
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
@@ -92,11 +94,11 @@ def install_extensions(app, service):
raise FlowError(E.INVALID_REQUEST, "Invalid image") from None
result = []
for data, name in prepared:
asset = service.assets.put(data, name)
db.execute(
"INSERT INTO asset_metadata(asset_id,origin) VALUES(?,'manual')",
(asset["asset_id"],),
)
with service.assets.creating(data, name) as asset:
db.execute(
"INSERT INTO asset_metadata(asset_id,origin) VALUES(?,'manual')",
(asset["asset_id"],),
)
result.append(asset)
return result
@@ -220,6 +222,10 @@ def install_extensions(app, service):
for row in db.all("SELECT * FROM clients")
]
@app.get("/api/v1/client-scopes", dependencies=[Depends(require("admin"))])
def client_scopes():
return ALL_SCOPES
@app.post(
"/api/v1/clients",
status_code=201,
+3 -1
View File
@@ -37,7 +37,9 @@ def prepare(service, content, device_id=None):
if source.avoid_duplicates and quality["fingerprint"] in hashes:
continue
hashes.update([quality["sha256"], quality["fingerprint"]])
with Image.open(BytesIO(data)) as image:
with Image.open(
BytesIO(data), formats=("JPEG", "PNG", "WEBP")
) as image:
photo = ImageOps.exif_transpose(image).convert("RGB")
photo.thumbnail((2400, 2400))
output = BytesIO()
+7 -2
View File
@@ -126,7 +126,9 @@ class LayoutStudio:
plan = local_plan(request)
metadata = []
for asset_id in request.asset_ids:
with Image.open(self.renderer.assets.path(asset_id)) as image:
with Image.open(
self.renderer.assets.path(asset_id), formats=("JPEG", "PNG", "WEBP")
) as image:
metadata.append(
{"asset_id": asset_id, "width": image.width, "height": image.height}
)
@@ -203,7 +205,10 @@ class LayoutStudio:
plan = LayoutPlan.model_validate(data["layout"])
canvas = Image.new("RGB", (plan.width, plan.height), "white")
for region in plan.regions:
with Image.open(self.renderer.assets.path(region.asset_id)) as source:
with Image.open(
self.renderer.assets.path(region.asset_id),
formats=("JPEG", "PNG", "WEBP"),
) as source:
image = ImageOps.exif_transpose(source).convert("RGB")
image = ImageOps.fit(
image,
+1 -1
View File
@@ -7,7 +7,7 @@ from PIL import Image, ImageFilter, ImageOps, ImageStat
def photo_quality(data):
with Image.open(BytesIO(data)) as source:
with Image.open(BytesIO(data), formats=("JPEG", "PNG", "WEBP")) as source:
image = ImageOps.exif_transpose(source).convert("L")
image.thumbnail((256, 256))
brightness = ImageStat.Stat(image).mean[0]
+16 -10
View File
@@ -1,6 +1,7 @@
"""Opt-in local retention, protecting stars, current content and durable references."""
import re
import shutil
import time
from datetime import UTC, datetime, timedelta
@@ -80,7 +81,19 @@ class LocalRetention:
def stats(self):
rows = self.inspect()
protected = self.protected()
disk = shutil.disk_usage(self.service.settings.data_dir)
low_space = disk.free < max(512 * 1024 * 1024, int(disk.total * 0.05))
return {
"disk": {
"total_bytes": disk.total,
"used_bytes": disk.used,
"free_bytes": disk.free,
"low_space": low_space,
},
"pending_file_cleanup": len(
self.service.db.all("SELECT id FROM asset_file_journal")
)
+ len(self.service.db.all("SELECT id FROM asset_file_moves")),
"policy": self.config().model_dump(),
"total_bytes": sum(x[2] for x in rows),
"asset_count": len(rows),
@@ -92,6 +105,7 @@ class LocalRetention:
return {}
if not dry_run:
self.last = time.monotonic()
self.service.assets.recover_files()
config = self.config()
if not config.enabled:
return {"deleted": 0, "bytes_freed": 0, "disabled": True}
@@ -101,7 +115,7 @@ class LocalRetention:
total = sum(x[2] for x in rows)
removed = []
freed = 0
for row, path, size in rows:
for row, _path, size in rows:
# Manual uploaded photos are never removed by automatic retention.
if row["id"] in protected or row["kind"] not in ("generated", "cache"):
continue
@@ -119,15 +133,7 @@ class LocalRetention:
total -= size
freed += size
if not dry_run:
path.unlink(missing_ok=True)
for table, key in [
("asset_metadata", "asset_id"),
("asset_sources", "asset_id"),
("assets", "id"),
]:
self.service.db.execute(
f"DELETE FROM {table} WHERE {key}=?", (row["id"],)
)
self.service.libraries.remove_asset(row["id"])
result = {"deleted": len(removed), "bytes_freed": freed, "dry_run": dry_run}
if not dry_run:
self.service.db.execute(