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 fieldSourceArtifactGridFsId 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.
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-269RejectAsync 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 bothdevTools/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 #853objectKey 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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
.tiftoart-asset-artifactsand 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.SourceArtifactGridFsIdon the artifact ref: marks the edit ("Edited" badge), records which stack it derives from, and is the frontend's completion-poll signal.ArtVariantUploadJobstatus record ({Scanning, Ready, RejectedInfected, Failed}) the UI polls — the asset stays Completed, so the normal progress path never fires.Hard prerequisite (infra)
clamd was capped at 128M; edited 16-bit stack TIFFs run 100–250MB. Raised clamd
StreamMaxLength/MaxFileSizeto 512M + the worker'sClamAV:MaxStreamSizeBytesto match the 500MB endpoint cap, and joined the ArtPipeProcessor photostack stack to theclamavoverlay network.Out of scope
Destructive replace (this is additive only); multi-file/batch variant upload.
Resolved across all three repos — spikersoft-backend PR #463 (
2b62a2b3), spikersoft-infrastructure PR #145 (bb59f3c), spikersoft-angular PR #550 (d8a967a8). Verified againstorigin/master:Backend
ArtVariantUploadJob.cs:21,70— job entity +{Scanning, Ready, RejectedInfected, Failed}; mapped atSpikerDbContext.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.POST {id}/artifacts/{sourceGridFsId}/variant(500 MB cap,.tifonly) +GET variant-jobs/{jobId}.ProcessUploadedArtVariantConsumer.cs:175-181idempotency (skips when the job is no longerScanning);:194-203scans strictly before decode, with:205generating derivatives only after a clean result;:267-269RejectAsyncdeletes the staged object so an infected upload leaves nothing behind.ArtPipeProcessor/Program.cs:79-87wires the consumer when the deployment's stage list containsAlignFuse, not on the orchestrator.appsettings.json:28-34—GracefulDegradation: false,MaxStreamSizeBytes: 536870912.Infrastructure —
clamav/docker-stack.yml:61-62raisesCLAMD_CONF_StreamMaxLength/MaxFileSizeto 512M so a large.tifis actually scannable rather than silently skipped, andspikersoft-artpipe-photostack/docker-stack.yml:61joins theclamavoverlay.Angular — "upload edited" control + "Edited" badge in
art-artifact-panel.component.html, poll-to-terminal inart-studio-shell.component.ts:537-560, i18n in bothdevTools/en.jsonandes.json.One naming note so nobody trips on it later: the ticket specified
SourceArtifactGridFsId; it shipped asSourceArtifactKeybecause of the #853objectKeyrename. It's consistent end-to-end and the JSON-column hazard is handled —ArtAsset.cs:538-540carries both[BsonElement("sourceArtifactKey")]and[JsonPropertyName("sourceArtifactKey")], matchingart-studio.models.ts:138,143.Closing.