Art Studio: let owners upload an edited .tif as a non-destructive stack variant #807

Closed
opened 2026-07-23 04:04:48 +00:00 by spikerj · 1 comment
Owner

Follow-up to #806. Now that owners can download the fused stack .tif, they want to edit it offline and upload it back so the Art Studio shows their edited version (as small AVIF thumbnail/preview, like any stack) alongside the original — non-destructive.

Because it's a user-uploaded file (unlike the pipeline's fusion output, whose inputs were pre-scanned photographs), it must be ClamAV-scanned fail-closed before anything decodes or serves it, owner-only authorized, format/size validated, and its AVIF derivatives generated in the worker (cavif is worker-only).

Design

  • Fail-closed, scan-before-decode: the API uploads the .tif to art-asset-artifacts and dispatches a fire-and-forget job, but does NOT attach the artifact ref. Presign is per-ref, so an unreferenced object is unreachable; a hostile TIFF never reaches ImageSharp/cavif. The worker scans first; only a clean file is decoded, derived, and referenced. Infected → the staged object is deleted.
  • Lightweight worker consumer co-hosted on the align_fuse deployment (has cavif; produces the stacks), NOT the stage orchestrator.
  • Provenance field SourceArtifactGridFsId on the artifact ref: marks the edit ("Edited" badge), records which stack it derives from, and is the frontend's completion-poll signal.
  • Explicit rejection via an ArtVariantUploadJob status record ({Scanning, Ready, RejectedInfected, Failed}) the UI polls — the asset stays Completed, so the normal progress path never fires.
  • Idempotent under at-least-once redelivery (skip when the job is no longer Scanning).

Hard prerequisite (infra)

clamd was capped at 128M; edited 16-bit stack TIFFs run 100–250MB. Raised clamd StreamMaxLength/MaxFileSize to 512M + the worker's ClamAV:MaxStreamSizeBytes to match the 500MB endpoint cap, and joined the ArtPipeProcessor photostack stack to the clamav overlay network.

Out of scope

Destructive replace (this is additive only); multi-file/batch variant upload.

Follow-up to #806. Now that owners can download the fused stack `.tif`, they want to edit it offline and upload it back so the Art Studio shows their edited version (as small AVIF thumbnail/preview, like any stack) **alongside** the original — non-destructive. Because it's a user-uploaded file (unlike the pipeline's fusion output, whose inputs were pre-scanned photographs), it must be **ClamAV-scanned fail-closed** before anything decodes or serves it, owner-only authorized, format/size validated, and its AVIF derivatives generated in the worker (cavif is worker-only). ## Design - **Fail-closed, scan-before-decode**: the API uploads the `.tif` to `art-asset-artifacts` and dispatches a fire-and-forget job, but does NOT attach the artifact ref. Presign is per-ref, so an unreferenced object is unreachable; a hostile TIFF never reaches ImageSharp/cavif. The worker scans first; only a clean file is decoded, derived, and referenced. Infected → the staged object is deleted. - **Lightweight worker consumer** co-hosted on the align_fuse deployment (has cavif; produces the stacks), NOT the stage orchestrator. - **Provenance field** `SourceArtifactGridFsId` on the artifact ref: marks the edit ("Edited" badge), records which stack it derives from, and is the frontend's completion-poll signal. - **Explicit rejection** via an `ArtVariantUploadJob` status record ({Scanning, Ready, RejectedInfected, Failed}) the UI polls — the asset stays Completed, so the normal progress path never fires. - **Idempotent** under at-least-once redelivery (skip when the job is no longer Scanning). ## Hard prerequisite (infra) clamd was capped at 128M; edited 16-bit stack TIFFs run 100–250MB. Raised clamd `StreamMaxLength`/`MaxFileSize` to 512M + the worker's `ClamAV:MaxStreamSizeBytes` to match the 500MB endpoint cap, and joined the ArtPipeProcessor photostack stack to the `clamav` overlay network. ## Out of scope Destructive replace (this is additive only); multi-file/batch variant upload.
Author
Owner

Resolved across all three repos — spikersoft-backend PR #463 (2b62a2b3), spikersoft-infrastructure PR #145 (bb59f3c), spikersoft-angular PR #550 (d8a967a8). Verified against origin/master:

Backend

  • ArtVariantUploadJob.cs:21,70 — job entity + {Scanning, Ready, RejectedInfected, Failed}; mapped at SpikerDbContext.cs:106,927.
  • UploadArtVariantCommandHandler.cs:51-95 — owner + source-artifact authorization, stages the object, and deliberately does not attach the ref until the scan clears.
  • Controller: POST {id}/artifacts/{sourceGridFsId}/variant (500 MB cap, .tif only) + GET variant-jobs/{jobId}.
  • ProcessUploadedArtVariantConsumer.cs:175-181 idempotency (skips when the job is no longer Scanning); :194-203 scans strictly before decode, with :205 generating derivatives only after a clean result; :267-269 RejectAsync deletes the staged object so an infected upload leaves nothing behind.
  • Registered only where it belongs: ArtPipeProcessor/Program.cs:79-87 wires the consumer when the deployment's stage list contains AlignFuse, not on the orchestrator.
  • Fail-closed by config: appsettings.json:28-34GracefulDegradation: false, MaxStreamSizeBytes: 536870912.

Infrastructureclamav/docker-stack.yml:61-62 raises CLAMD_CONF_StreamMaxLength / MaxFileSize to 512M so a large .tif is actually scannable rather than silently skipped, and spikersoft-artpipe-photostack/docker-stack.yml:61 joins the clamav overlay.

Angular — "upload edited" control + "Edited" badge in art-artifact-panel.component.html, poll-to-terminal in art-studio-shell.component.ts:537-560, i18n in both devTools/en.json and es.json.

One naming note so nobody trips on it later: the ticket specified SourceArtifactGridFsId; it shipped as SourceArtifactKey because of the #853 objectKey rename. It's consistent end-to-end and the JSON-column hazard is handled — ArtAsset.cs:538-540 carries both [BsonElement("sourceArtifactKey")] and [JsonPropertyName("sourceArtifactKey")], matching art-studio.models.ts:138,143.

Closing.

Resolved across all three repos — spikersoft-backend PR #463 (`2b62a2b3`), spikersoft-infrastructure PR #145 (`bb59f3c`), spikersoft-angular PR #550 (`d8a967a8`). Verified against `origin/master`: **Backend** - `ArtVariantUploadJob.cs:21,70` — job entity + `{Scanning, Ready, RejectedInfected, Failed}`; mapped at `SpikerDbContext.cs:106,927`. - `UploadArtVariantCommandHandler.cs:51-95` — owner + source-artifact authorization, stages the object, and deliberately does **not** attach the ref until the scan clears. - Controller: `POST {id}/artifacts/{sourceGridFsId}/variant` (500 MB cap, `.tif` only) + `GET variant-jobs/{jobId}`. - `ProcessUploadedArtVariantConsumer.cs:175-181` idempotency (skips when the job is no longer `Scanning`); **`:194-203` scans strictly before decode**, with `:205` generating derivatives only after a clean result; `:267-269` `RejectAsync` deletes the staged object so an infected upload leaves nothing behind. - Registered **only** where it belongs: `ArtPipeProcessor/Program.cs:79-87` wires the consumer when the deployment's stage list contains `AlignFuse`, not on the orchestrator. - Fail-closed by config: `appsettings.json:28-34` — `GracefulDegradation: false`, `MaxStreamSizeBytes: 536870912`. **Infrastructure** — `clamav/docker-stack.yml:61-62` raises `CLAMD_CONF_StreamMaxLength` / `MaxFileSize` to 512M so a large `.tif` is actually scannable rather than silently skipped, and `spikersoft-artpipe-photostack/docker-stack.yml:61` joins the `clamav` overlay. **Angular** — "upload edited" control + "Edited" badge in `art-artifact-panel.component.html`, poll-to-terminal in `art-studio-shell.component.ts:537-560`, i18n in **both** `devTools/en.json` and `es.json`. One naming note so nobody trips on it later: the ticket specified `SourceArtifactGridFsId`; it shipped as **`SourceArtifactKey`** because of the #853 `objectKey` rename. It's consistent end-to-end and the JSON-column hazard is handled — `ArtAsset.cs:538-540` carries both `[BsonElement("sourceArtifactKey")]` and `[JsonPropertyName("sourceArtifactKey")]`, matching `art-studio.models.ts:138,143`. Closing.
Sign in to join this conversation.