forked from mirrors/VernissageServer
Restore HDR editing on existing posts with private staging and atomic saves #1
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
HDR controls on existing posts currently reject add/replace/remove to avoid non-atomic binary changes. Restore this later through private staging and atomic Save, without weakening privacy, history or the single-copy recovery work.
Implement backend endpoints/capability/validation, additive staging state and cleanup, atomic reference/history/replay integration and controlled-storage regressions described below. Update both shared contract copies when implementation begins. No frontend controls should be enabled before this capability is real.
Frontend companion: klafbang/VernissageWeb#1
Current code:
Controllers/AttachmentsController.swiftrejects attached HDR mutations;PostAccess/PostAttachmentEdits.swiftcontains metadata-only full snapshots. The separateattachmentHdrEditsfield intentionally preserves those snapshot semantics.Shared contract: atomic HDR edits v1 (proposed, deferred)
This is the shared implementation specification for both forks on
michael. It extends the atomic attachment contract; it is not implemented by filing this issue. Implement later, after the current single-copy storage work. No rollout blocker or priority escalation is intended.Capability and compatibility
Add
atomicHdrEdits: truetoGET /api/v1/post-access/capabilitiesonly when all guarantees below are implemented. Frontend requires supported/enabled contract 1.x,atomicAttachmentEdits === true, andatomicHdrEdits === true. Missing/false keeps attached HDR controls disabled. Existing unattached HDR endpoints continue unchanged; direct attached HDR mutations still return422 attachedMediaRequiresStatusSave. Do not add binary fields to the existing complete metadata snapshots.Private upload staging
POST /api/v1/post-access/statuses/{statusId}/attachments/{attachmentId}/hdr-stagesAuthenticated account plus existing XSRF protection and UUID-v4
Idempotency-Keyare required. Multipart body contains exactly onefile; accept validated AVIF bytes, 1 through 4,194,303 bytes (matching the existing backend's strict less-than-4-MiB limit). Allow at most 5 MiB for the complete multipart body; the file itself remains subject to the stricter limit. Validate actual AVIF content and record decoded dimensions; do not trust extension or caller-supplied storage metadata. Do not transcode the uploaded original. Require current ownership of the post and attachment and membership of that attachment in that post. Recheck eligibility before making the stage ready if upload I/O suspended.Successful response:
201,Cache-Control: private, no-store:Stage IDs are opaque strings; all post/attachment IDs remain strings. Stages are owner/post/attachment-bound, private, and valid for 24 hours from becoming ready. Uploading does not alter the current attachment, history, post revision, delivery or visibility. Stages have no public URL and cannot be read by guest/follower/recipient/status-media routes. The editor previews the local selected file through a releasable blob URL; no new stage-read endpoint is needed.
Scope upload replay by actor/method/path/key, matching the file bytes (digest) and validated media type. While the stage remains ready, an identical retry returns the same stage and original expiry, without extending it or creating another object; different content returns
409 idempotencyConflict. Serialize/claim concurrent same-key uploads without holding database locks during upload/storage I/O. If the ready stage has expired, return410 hdrStageExpired, requiring a fresh upload key. Consumed stages return409 hdrStageAlreadyCommitted; cancelled stages return404 hdrStageNotFound. Neither recreates an upload. Incomplete uploads must remain recoverable or safely cleanable, including failures between object creation and final database recording. Retain sufficient replay/tombstone information to prevent a late retry recreating a cancelled or consumed stage as a fresh upload.Cancellation:
DELETE /api/v1/post-access/statuses/{statusId}/attachments/{attachmentId}/hdr-stages/{stageId}, account/XSRF protected. Returns204for a matching unconsumed stage or its repeated cancellation. Atomically makes it unusable, then schedules safe cleanup. A stage already committed into a post returns409 hdrStageAlreadyCommittedand must never delete its live/history object. Cancellation racing Save has one database winner; it cannot delete a successfully committed HDR. Unknown/not-owned/wrong-target stage IDs return404 hdrStageNotFoundwithout disclosing another owner's records. A target post that was deleted/changed is handled consistently with ordinary ownership/existence checks; expiry handles abandoned stages.Atomic post-save addition
Extend only
PUT /api/v1/statuses/{statusId}with:{ id, stageId: "..." }: add/replace HDR using the ready stage.{ id, stageId: null }: remove HDR; already absent is a no-op.Normal account/XSRF,
If-MatchandIdempotency-Keyrequirements apply. Atomically validate and consume stages, snapshot the old HDR file references into history, swap/remove current references, apply all other post changes, update revision, and record durable location/delivery/replay work. Stage records distinguish ready/consumed/cancelled/expired states. Consumed bytes become the referenced media object: no permanent extra staging copy. Validation/conflict failures leave the post/history/revision unchanged and stages available until their original expiry. Only one successful operation may consume a stage; new-key reuse returns409 hdrStageAlreadyCommitted.Use the existing
StatusDtoand quoted ETag for200success;originalHdrFilereflects the committed result. Full-body idempotency matching includesattachmentHdrEdits. A successful unchanged retry replays BEFORE stage expiry/consumption or stale-revision validation, with current owner/existence checks, and creates no extra history, consumption, location moves or delivery. Original key/body/If-Match must be retained for lost-response and committed-storage-error retries. Existing503 mediaNotReadyand503 mediaRevocationPendingmeanings and priority remain unchanged.Storage, history and cleanup
Integrate with the single-copy location/recovery model. Promote the stage's existing private object through durable state, then move it as current audience policy requires. Replaced/removed HDR stays referenced by history and becomes protected when history-only. Current/snapshot authorization must locate it correctly. Old public origin bytes must be removed/moved safely; never serve a protected history image through its old public origin. Existing Followers/external-Mentioned public-media exceptions and CDN cache-expiry exception remain unchanged.
Expiry/cancellation/orphan cleanup must atomically establish that no committed current/history reference, consumption, active upload or unfinished move needs the object. A referenced object in the wrong location is reconciled, not deleted. Recheck authoritative bucket/key and generation; stale stage cleanup must not remove promoted media. Account/post/attachment deletion, crashes, partial moves and concurrent cleanup must converge safely. Keep one retained object per variant in steady state; temporary recovery copies are permitted. Perform external I/O outside shared database locks.
Errors
Use existing post-access JSON envelope, no-store responses and failures paths. Keep
401 authenticationRequired, ordinary post/ownership errors,412 revisionConflict,428 revisionRequired/idempotencyKeyRequired, and409 idempotencyConflict. Add422 invalidHdrStagefor invalid upload,422 invalidAttachmentHdrEditsfor malformed/invalid edit structure or attachment selection,404 hdrStageNotFoundfor unknown/not-owned/misbound stages,410 hdrStageExpiredfor expired unconsumed stages, and409 hdrStageAlreadyCommittedfor consumed-stage reuse/cancellation. Field errors identifyfileorattachmentHdrEdits[i].stageId/.id. Replays after an already successful Save must not fail because the referenced stage is consumed/expired.Shared acceptance
Verify HDR add/replace/remove, omitted-preserve and explicit-null removal; combined metadata/visibility/order edits; invalid media and size boundaries; cross-owner/target attempts; concurrent stage use/cancel/expiry/cleanup; stale revision with untouched draft/stage; lost upload/save responses; original-request replay after consumption/expiry; old HDR accessible only under history authorization; public-to-private origin cleanup and failure recovery; single-copy steady state; crashes and stale orphan/deletion work. Run browser integration with service workers enabled against the real API and disposable storage, including upgrade/refresh, restricted previews, post-save HDR/fallback display and cleanup/revocation warnings. Record actual frontend/backend versions and limitations.