Make file→CMS upload sync idempotent#1203
Conversation
Every push re-uploaded unchanged images: an already-suffixed upload got another hash suffix and a new site_uploads record each time, orphaning the prior one. One image grew into many byte-identical duplicates and pushes resent everything until they aborted. Root cause was identity keyed on the mutable suffixed filename. When the CLI re-sent a symbolic name it missed the existing record (stored under its suffixed name), so reconcile created a new record and PocketBase re-suffixed unconditionally. The no-op hash-read also keyed the storage path on the sent name rather than the record's stored name, so it silently fell through to the re-suffixing update branch. - reconcileSiteUploads matches incoming files by content hash first (stable), filename second — an unchanged image is a no-op regardless of name sent. - upsertSiteUpload reads existing bytes by the record's stored filename. - bootstrap now returns created_ids so the CLI's first push can write back canonical names (paired with the CLI-side fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe bootstrap ZIP-import response now returns ChangesUpload idempotency
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant handleBootstrap
participant reconcileSiteUploads
participant site_uploads
CLI->>handleBootstrap: ZIP upload with symbolic or canonical filename
handleBootstrap->>reconcileSiteUploads: reconcile uploaded files
reconcileSiteUploads->>site_uploads: hash stored bytes and match existing upload
site_uploads-->>reconcileSiteUploads: existing record or no match
reconcileSiteUploads-->>handleBootstrap: canonical upload metadata
handleBootstrap-->>CLI: success response with created_ids
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/import.go (1)
3500-3500: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace deprecated
fsys.GetFilewithGetReader.Static analysis flags
fsys.GetFileas deprecated (SA1019); the recommended replacement isGetReader(fileKey). Both changed segments use the deprecated method — line 3500 inupsertSiteUploadand line 3629 inreconcileSiteUploads. Verify thatGetReaderreturns anio.ReadCloser(or equivalent) so the existingreader.Close()calls remain valid.♻️ Proposed refactor for both call sites
// Line 3500 in upsertSiteUpload -reader, readErr := fsys.GetFile(sourceKey) +reader, readErr := fsys.GetReader(sourceKey)// Line 3629 in reconcileSiteUploads -reader, rerr := fsys.GetFile(uploadsColl.Id + "/" + rec.Id + "/" + fn) +reader, rerr := fsys.GetReader(uploadsColl.Id + "/" + rec.Id + "/" + fn)Also applies to: 3629-3629
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/import.go` at line 3500, Replace deprecated fsys.GetFile calls with fsys.GetReader in upsertSiteUpload and reconcileSiteUploads, passing the same source key. Confirm the returned reader supports Close so the existing reader.Close calls remain valid, and preserve current error handling.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@internal/import.go`:
- Line 3500: Replace deprecated fsys.GetFile calls with fsys.GetReader in
upsertSiteUpload and reconcileSiteUploads, passing the same source key. Confirm
the returned reader supports Close so the existing reader.Close calls remain
valid, and preserve current error handling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7f9b761d-71de-4a64-ac44-ad097859ef2b
📒 Files selected for processing (3)
internal/bootstrap.gointernal/import.gointernal/upload_idempotent_test.go
Addresses CodeRabbit: fsys.GetFile is deprecated (SA1019) and logs a warning on every call. GetReader has the identical signature (returns *blob.Reader, still Close-able). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catch — replaced both |
Problem
Every
primo devpush re-uploaded unchanged local images instead of no-op'ing. On each push an already-canonicalized upload got another hash suffix appended and a newsite_uploadsrecord created server-side, orphaning the previous one. Over many pushes one image became many byte-identical duplicates (favicon_x_y_..._z.svgwith stacked_xxxxxxxxxxsegments); in a real site 13 images grew into 187 physical files / manifest entries, resending ~18MB per push until it hitThis operation was aborted.Root cause
Upload identity was keyed on the mutable suffixed filename, not content:
reconcileSiteUploadsindexed existing records byfile(the suffixed name). When the CLI re-sent a symbolic name (favicon.svg), it missed the record stored asfavicon_ab12cd34.svg→ fell into the create branch → new record, and PocketBase'sNewFileFromBytesre-suffixed unconditionally.upsertSiteUpload's no-op hash-read built the storage path from the caller-suppliedfilenameinstead of the record's stored name, and swallowed the read error — so a name mismatch silently fell through to the re-suffixing update branch.bootstrapendpoint ran the full import but droppedcreated_idsfrom its response, so the CLI's first push never wrote back canonical names (kept re-sending symbolic names forever).Fix
reconcileSiteUploadsmatches incoming files by content hash first (stable identity), filename second — an unchanged image is a no-op regardless of the name sent.upsertSiteUploadreads existing bytes by the record's stored filename and returns that stored name as canonical.bootstrapresponse now includescreated_ids.Paired CLI fix (makes the first push write back canonical names): primocms/primo-cli.
Tests
internal/upload_idempotent_test.go— repeated pushes of a byte-identical image (both symbolic and canonical names) stay at exactly 1 record with one stable suffix. Previously the symbolic-name case stacked suffixes and created duplicates on every push.Full
internalsuite passes (TestUpdatedTimestampStableOnNoOpReimportfails identically onmain— pre-existing, unrelated to uploads).Follow-up (not in this PR)
The orphaned
site_uploadsrecords this bug already created still need a--prune-uploads --dedupeGC pass (group by content hash, keep the referenced/oldest record, repoint refs, delete the rest).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
created_idsfield, improving how the CLI tracks newly created records and updates upload references.Bug Fixes
site_uploadsrecords and repeated filename suffixing on subsequent pushes.Tests