fix: close library upload flow and harden image storage
This commit is contained in:
@@ -425,7 +425,9 @@ def create_app(settings=None, start_worker=True):
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
with Image.open(BytesIO(data)) as source:
|
||||
with Image.open(
|
||||
BytesIO(data), formats=("JPEG", "PNG", "WEBP")
|
||||
) as source:
|
||||
if source.width * source.height > 25_000_000:
|
||||
raise ValueError()
|
||||
if source.format not in {"JPEG", "PNG", "WEBP"}:
|
||||
@@ -438,13 +440,13 @@ def create_app(settings=None, start_worker=True):
|
||||
thumb = cached.with_name(cached.stem + "-thumb.jpg")
|
||||
from PIL import Image
|
||||
|
||||
with Image.open(cached) as img:
|
||||
with Image.open(cached, formats=("JPEG", "PNG", "WEBP")) as img:
|
||||
# The 430px cards need up to 860px on 2x displays. Replace the
|
||||
# existing cache in place so old 300px previews are upgraded.
|
||||
expected_edge = min(860, max(img.size))
|
||||
regenerate = not thumb.exists()
|
||||
if not regenerate:
|
||||
with Image.open(thumb) as existing:
|
||||
with Image.open(thumb, formats=("JPEG", "PNG", "WEBP")) as existing:
|
||||
regenerate = max(existing.size) != expected_edge
|
||||
if regenerate:
|
||||
img.thumbnail((860, 860), Image.Resampling.LANCZOS)
|
||||
@@ -452,7 +454,7 @@ def create_app(settings=None, start_worker=True):
|
||||
return FileResponse(thumb, media_type="image/jpeg")
|
||||
from PIL import Image
|
||||
|
||||
with Image.open(cached) as original:
|
||||
with Image.open(cached, formats=("JPEG", "PNG", "WEBP")) as original:
|
||||
media_type = Image.MIME.get(
|
||||
original.format or "", "application/octet-stream"
|
||||
)
|
||||
@@ -575,15 +577,15 @@ def create_app(settings=None, start_worker=True):
|
||||
if len(data) > settings.max_bytes:
|
||||
raise FlowError(E.INVALID_REQUEST, "Image exceeds 20 MiB")
|
||||
name = Path(file.filename or "image").name[:120]
|
||||
asset = assets.put(
|
||||
with assets.creating(
|
||||
await run_in_threadpool(convert, data, fit, profile),
|
||||
settings.sanitize(name),
|
||||
kind="cache",
|
||||
)
|
||||
db.execute(
|
||||
"INSERT INTO asset_metadata(asset_id,origin) VALUES(?,?)",
|
||||
(asset["asset_id"], "manual"),
|
||||
)
|
||||
) as asset:
|
||||
db.execute(
|
||||
"INSERT INTO asset_metadata(asset_id,origin) VALUES(?,?)",
|
||||
(asset["asset_id"], "manual"),
|
||||
)
|
||||
return assets.info(asset["asset_id"])
|
||||
|
||||
@app.get(
|
||||
@@ -620,7 +622,7 @@ def create_app(settings=None, start_worker=True):
|
||||
dependencies=[Depends(require("images:delete"))],
|
||||
)
|
||||
def delete_asset(asset_id: str):
|
||||
path = assets.path(asset_id)
|
||||
assets.path(asset_id)
|
||||
if db.one(
|
||||
"SELECT id FROM assets WHERE id=? AND kind IN ('source-images','renditions')",
|
||||
(asset_id,),
|
||||
@@ -645,14 +647,7 @@ def create_app(settings=None, start_worker=True):
|
||||
"Asset is referenced by a task, template or pending operation",
|
||||
409,
|
||||
)
|
||||
path.unlink(missing_ok=True)
|
||||
db.execute(
|
||||
"DELETE FROM photo_preferences WHERE source_id='@assets' AND item_id=?",
|
||||
(asset_id,),
|
||||
)
|
||||
db.execute("DELETE FROM asset_metadata WHERE asset_id=?", (asset_id,))
|
||||
db.execute("DELETE FROM asset_sources WHERE asset_id=?", (asset_id,))
|
||||
db.execute("DELETE FROM assets WHERE id=?", (asset_id,))
|
||||
service.libraries.remove_asset(asset_id)
|
||||
for row in db.all(
|
||||
"SELECT key,payload FROM app_config WHERE key LIKE 'wrist-thumbnail:%'"
|
||||
):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from fastapi import Depends, Query, Request
|
||||
import hashlib
|
||||
|
||||
from fastapi import Depends, File, Query, Request, UploadFile
|
||||
from fastapi.responses import Response
|
||||
from pydantic import Field
|
||||
|
||||
@@ -8,6 +10,7 @@ from pigway_frame_flow.display.libraries import GenerationPlan
|
||||
from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
from pigway_frame_flow.models import ImageExclusion, ImageStar, StrictModel
|
||||
from pigway_frame_flow.providers.photo_sources import PhotoSourceInput
|
||||
from pigway_frame_flow.providers.source_browser import BrowseFilters
|
||||
|
||||
|
||||
@@ -16,6 +19,10 @@ class BindingInput(StrictModel):
|
||||
plan_id: str = Field(pattern=r"^[a-f0-9]{32}$")
|
||||
|
||||
|
||||
class UploadURLInput(StrictModel):
|
||||
url: str = Field(min_length=1, max_length=2048)
|
||||
|
||||
|
||||
class ImportInput(StrictModel):
|
||||
item_id: str = Field(min_length=1, max_length=2048)
|
||||
|
||||
@@ -56,6 +63,46 @@ def install_libraries(app, service):
|
||||
for source in service.photo_sources.list()
|
||||
]
|
||||
|
||||
def save_upload(source_id, data, name):
|
||||
if len(data) > service.settings.max_bytes:
|
||||
raise FlowError(E.INVALID_REQUEST, "Image exceeds size limit")
|
||||
with service.lock:
|
||||
if source_id == "manual-uploads" and not db.one(
|
||||
"SELECT key FROM app_config WHERE key='photo_source:manual-uploads'"
|
||||
):
|
||||
from pigway_frame_flow.renderers.images import convert
|
||||
|
||||
convert(data, size=(1, 1))
|
||||
service.photo_sources.save(
|
||||
source_id,
|
||||
PhotoSourceInput(name="手动上传", kind="uploads", location="."),
|
||||
)
|
||||
if not service.photo_sources.get(source_id).enabled:
|
||||
raise FlowError(E.CONFLICT, "Photo source is disabled", 409)
|
||||
return libraries.ingest(
|
||||
source_id, "upload:" + hashlib.sha256(data).hexdigest(), data, name, {}
|
||||
)
|
||||
|
||||
@app.post(
|
||||
"/api/v1/source-libraries/{source_id}/upload",
|
||||
dependencies=[Depends(require("images:write"))],
|
||||
)
|
||||
def upload(source_id: str, file: UploadFile = File(...)):
|
||||
return save_upload(
|
||||
source_id,
|
||||
file.file.read(service.settings.max_bytes + 1),
|
||||
file.filename or "upload.jpg",
|
||||
)
|
||||
|
||||
@app.post(
|
||||
"/api/v1/source-libraries/{source_id}/upload-url",
|
||||
dependencies=[Depends(require("images:write"))],
|
||||
)
|
||||
def upload_url(source_id: str, payload: UploadURLInput):
|
||||
return save_upload(
|
||||
source_id, service.assets.fetch_url(payload.url), "url-upload.jpg"
|
||||
)
|
||||
|
||||
@app.get(
|
||||
"/api/v1/source-libraries/{source_id}/images",
|
||||
dependencies=[Depends(require("images:read"))],
|
||||
@@ -183,7 +230,7 @@ def install_libraries(app, service):
|
||||
|
||||
@app.put(
|
||||
"/api/v1/source-images/{picture_id}/star",
|
||||
dependencies=[Depends(require("admin"))],
|
||||
dependencies=[Depends(require("images:write"))],
|
||||
)
|
||||
def star(picture_id: str, payload: ImageStar):
|
||||
picture = libraries.picture(picture_id)
|
||||
@@ -193,7 +240,7 @@ def install_libraries(app, service):
|
||||
|
||||
@app.put(
|
||||
"/api/v1/source-images/{picture_id}/exclusion",
|
||||
dependencies=[Depends(require("admin"))],
|
||||
dependencies=[Depends(require("images:write"))],
|
||||
)
|
||||
def exclude(picture_id: str, payload: ImageExclusion):
|
||||
picture = libraries.picture(picture_id)
|
||||
|
||||
Reference in New Issue
Block a user