fix(files): reuse a stored file by digest only for the same uploader #66

Merged
claude merged 1 commit from MEDECA-630-dedup-same-uploader into main 2026-10-03 22:31:08 +00:00
Member

Summary

For MEDECA-630. MongoFileStore.store(name, Array[Byte], metadata) deduplicated by SHA-256 across all callers. It returned the existing file, which keeps its first uploader's uploadedBy. A Klient who uploads the same bytes as another user then gets a file not recorded as theirs, and MEDECA's "the caller uploaded this file" check refuses their own upload.

New rule: reuse a stored file only when its uploadedBy equals the uploader of this call; otherwise store a new file stamped with this call's uploader.

  • The uploader of the call is the uploadedBy after withUploader: the current user when there is one, else the value in metadata.
  • No uploader on both sides: the existing file is reused (earlier behaviour).
  • Stored file has an uploader, call has none (or the reverse): a new file is stored. This is the conservative choice; an anonymous call never gets a file recorded as someone else's.
  • Among several stored files with the same digest, the one with the matching uploader is reused.
  • The streaming and FileRepr store paths do not deduplicate and are unchanged. Existing deduplicated files keep their first uploader.

Same patch on release/0.1.x: the 0.1.x PR. MongoFileStore and its dependencies are identical on both lines.

0.1.25 is to carry this change together with the MEDECA-655 workflow change.

Tests

New MongoFileStoreIntegrationSpec (files/adapters/mongo/it) runs against a real MongoDB named by MONGO_URI (skipped when unset, as the existing spec in that module). It uses its own database per run and drops it at the end.

  • MONGO_URI=mongodb://localhost:<port> ./mill filesMongo.it.testOnly works.iterative.core.service.impl.MongoFileStoreIntegrationSpec against a disposable mongo:5 container: 7 passed, 0 failed. Before the fix: 3 passed, 4 failed (the 4 new-rule cases).
  • Pre-push ./mill __.test: passed. Note that __.test does not include filesMongo.it (its task is testForked), so CI does not run this spec.

Known pre-existing defect (not changed here): MongoJsonFileRepositoryIntegrationSpec in the same module connects to localhost:27017 even when MONGO_URI is set, because MongoClient.layer reads the config through the test environment, which is empty. It fails when nothing listens on 27017. The new spec reads the live MONGO_URI and avoids this.

🤖 Generated with Claude Code

## Summary For **MEDECA-630**. `MongoFileStore.store(name, Array[Byte], metadata)` deduplicated by SHA-256 across all callers. It returned the existing file, which keeps its first uploader's `uploadedBy`. A Klient who uploads the same bytes as another user then gets a file not recorded as theirs, and MEDECA's "the caller uploaded this file" check refuses their own upload. New rule: reuse a stored file only when its `uploadedBy` equals the uploader of this call; otherwise store a new file stamped with this call's uploader. - The uploader of the call is the `uploadedBy` after `withUploader`: the current user when there is one, else the value in `metadata`. - No uploader on **both** sides: the existing file is reused (earlier behaviour). - Stored file has an uploader, call has none (or the reverse): a new file is stored. This is the conservative choice; an anonymous call never gets a file recorded as someone else's. - Among several stored files with the same digest, the one with the matching uploader is reused. - The streaming and `FileRepr` store paths do not deduplicate and are unchanged. Existing deduplicated files keep their first uploader. Same patch on `release/0.1.x`: the 0.1.x PR. `MongoFileStore` and its dependencies are identical on both lines. **0.1.25 is to carry this change together with the MEDECA-655 workflow change.** ## Tests New `MongoFileStoreIntegrationSpec` (`files/adapters/mongo/it`) runs against a real MongoDB named by `MONGO_URI` (skipped when unset, as the existing spec in that module). It uses its own database per run and drops it at the end. - `MONGO_URI=mongodb://localhost:<port> ./mill filesMongo.it.testOnly works.iterative.core.service.impl.MongoFileStoreIntegrationSpec` against a disposable `mongo:5` container: 7 passed, 0 failed. Before the fix: 3 passed, 4 failed (the 4 new-rule cases). - Pre-push `./mill __.test`: passed. Note that `__.test` does not include `filesMongo.it` (its task is `testForked`), so CI does not run this spec. **Known pre-existing defect (not changed here):** `MongoJsonFileRepositoryIntegrationSpec` in the same module connects to `localhost:27017` even when `MONGO_URI` is set, because `MongoClient.layer` reads the config through the test environment, which is empty. It fails when nothing listens on 27017. The new spec reads the live `MONGO_URI` and avoids this. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(files): reuse a stored file by digest only for the same uploader
All checks were successful
CI / Check Formatting (pull_request) Successful in 1m13s
CI / Compile (pull_request) Successful in 3m10s
CI / Check Linting (pull_request) Successful in 3m12s
CI / Run Tests (pull_request) Successful in 3m29s
1c4a46ed81
MongoFileStore.store(name, Array[Byte], metadata) returned any stored
file with the same SHA-256 digest. That file kept the metadata of its
first uploader, so a second caller who uploaded the same bytes got a
file that was not recorded as theirs. A check of "did this caller
upload this file" then refused the caller's own upload.

The store now reuses a file only when its stored `uploadedBy` equals
the uploader of this call (the current user, else `uploadedBy` in the
given metadata). No uploader on both sides keeps the earlier behaviour.
Otherwise it stores a new file stamped with this call's uploader.

The integration spec runs against a real MongoDB named by MONGO_URI.

For MEDECA-630. To be released in 0.1.25 with the MEDECA-655 change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude merged commit 15ff2e8a8b into main 2026-10-03 22:31:08 +00:00
Sign in to join this conversation.
No description provided.