Repository navigation
Description
Problem
POST /api/upload stores every image under its multipart filename as a blob tag, with no collision handling — so a second upload that arrives under the same name silently re-points that name to the new blob, and the first image disappears from every name-based surface. Uploaded names collide in ordinary use, because they are not unique:
- Paste/drop
Filenames are caller/browser-assigned, not unique per image (a clipboard bitmap isimage.png). The web upload path sends theFileas-is (uploadImageFile,web/src/image-upload.ts:76:formData.append("file", file)— no filename argument), so the multipart filename the server reads is exactlyfile.name. - The server's own fallback for a nameless upload is
paste-{unix_seconds}.{ext}(src/web/routes.rs:1491-1499) — one-second granularity, so two nameless uploads in the same second also collide. - The client's dedicated pasted-image name,
generatePasteFilename(web/src/image-upload.ts:57-60,`paste-${Date.now()}.${ext}`), is never sent to the server at all — it is only the alt-text fallback atimage-upload.ts:217. It has the same shape of problem anyway: millisecond-only entropy.
Evidence (verified this run, main a120b9a9)
Laptop Bun 1.4.2 probe (real image-upload.ts, sha256 d0ab2a43 identical both sides; probe + log in the goal's hidden_files/probe-2026-10-27-laneA-uploadname/):
P1 fixed-clock: call1=paste-1730000000000.png call2=paste-1730000000000.png identical=trueP1 tight-loop: 200 calls -> 1 distinct name(s)P2 paste#1: bytes=3 -> server sees filename="image.png"andP2 paste#2: bytes=4 -> server sees filename="image.png"— two different images present the same filename on the wire, which is the nameupload_handlerkeys on.
Server chain, by code read (src/web/routes.rs):
:1491-1499—filename=field.file_name()when non-empty (reconciled with the content type at:621), elsepaste-{secs}.{ext}.:1553—state.store.tags().set(tag, hash)withTag::from(filename): a bare overwrite of any existing tag of that name. No existence check, no uniquifying, no archiving.:1562/:1575/:1588— thecreated(set_if_absent),modifiedandcontent-typemetadata tags are also keyed by that same filename, so the second upload's metadata merges into the first image's name record.images_list_handler(:1613+) lists images by tag, andget_file_name(:1015+) resolves a hash's name by tag — once the tag is re-pointed, the first image is gone from the image browser/list and from name lookup, even though its blob and any/blob/{hash}embeds keep working (which is why this stays silent).
Contrast with the house pattern: FileOps::rename/copy archive whatever they replace (src/fileops.rs:15-16, archive_existing at :153 — <name>.archive.<unix-ts>). Upload is the outlier: it is the one name-writing path that destroys the previous occupant without a trace. A document save overwriting its own name is intended semantics; an image upload is the creation of a new object, so the replace semantics do not transfer.
Why it matters
Pasting two screenshots into documents is a core editor flow. Today the second paste quietly un-names the first image: it vanishes from the image browser, name-based lookups return the newer image, and the shared metadata record mixes both uploads. Nothing errors anywhere, so the loss is only noticed when someone goes looking for the earlier image by name.
Acceptance criteria
- Two uploads that arrive under the same filename both remain listed by
images_list_handlerand resolvable by name afterwards — e.g. the later upload is uniquified (suffix/counter/hash-derived) or the replaced name is archived first, per thefileopspattern; triage picks and pins one behaviour. - The nameless-upload fallback name cannot repeat for two uploads in the same second (and, if the client paste name is ever sent,
generatePasteFilenamegets the same guarantee). - Regression coverage in the existing upload router tests (
upload_router_tests,src/web/routes.rs:2333+): two same-name uploads with different bytes → both images still listed/resolvable, first upload's name record not silently merged with the second's. - No change to document save semantics (a save still updates its own name's tag) and no change to the
/blob/{hash}URL shape.
Notes
- Not a duplicate of id(web): image upload completing after the document shrinks is silently lost — stale insert position throws, success logged as failed #1379 (upload completing after the document shrinks — insert-position, client side) or id(web): /blob serves any blob as text/html via the ?filename= parameter — no CSP/nosniff/disposition headers (same-origin XSS by link) #908 (upload filename/type reconciliation — the
reconcile_upload_filenamestep above is untouched by this). - Deliberately not self-solved by the filing lane: the fix is a naming-semantics decision on the upload path (uniquify vs archive-first) that interacts with the image browser and metadata tags — queue-lane triage should pin it.
Activity
developing-today commentedon Oct 28, 2026
CODE-599
dezren39 commentedon Oct 28, 2026
🤖 Agent note · filed
new-issue-creator-code-a)46 remaining items
dezren39 commentedon Oct 28, 2026
Parked: fix implemented and PR opened — #1696 (branch
queue/1824-1549-upload-uniquify, commit 4d423fb). Stored-name uniquifier per the triage ruling: intended name kept when free or already naming the same blob (idempotent re-upload); stem-N.ext with smallest free N when a different blob owns the name; the paste-{secs} fallback passes through the same uniquifier. Laptop tests:cargo test --features web --lib web::routes::upload_router_tests— 12 passed, 0 failed, including 2 new #1549 regression tests. CI at park time: 13 pass / 18 pending / 0 fail. Resume condition: PR #1696 checks settle — merge if green per standing criteria, then complete queue task 1824.dezren39 commentedon Oct 28, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 28, 2026
🤖 Agent note · done
fix(id/web): make upload-name claiming atomic (#2363)