refactor: extract shared atomic-write/relocation-rollback engine into internal/atomicfs #123
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!123
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/116"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #116
What
Extracts the relocation-rollback discipline that
internal/pageopsandinternal/mediaopseach implemented in parallel into one leaf package,internal/atomicfs, and consolidates the three file-write helpers:atomicfs.Relocate(oldAbs, newAbs, edits, write)— the engine: Lstat target-absence pre-check, one atomicos.Rename(collision-shaped rename errors mapped to theErrTargetExistssentinel), ordered writes through an explicitWriteFuncparameter, and on failure best-effort restoration of original bytes plus the reverse rename, with a typed*WriteErrorwhoseRollbackErrcarries the dual-failure "the site needs manual attention" case.atomicfs.WriteFileKeepMode— the permission-preserving write, previously duplicated byte-identically in both packages; all pageops call sites (applyToFile,Reorder's ladder writes,create'swriteLadderStepdefault) now use it.atomicfs.ReplaceFile— the server's temp-file-then-rename save helper, moved with unchanged semantics (same-directory temp file, mode preserved from the existing target, rename over the target).pageops.relocateandmediaops.relocateare now thin adapters: they keep all their precompute logic and their own conflict-vs-internal error-kind classification (errors.Is(err, atomicfs.ErrTargetExists)→ conflict, byte-identical message; everything else internal), so HTTP status mapping is unchanged.Testing
internal/atomicfs's own suite, with failures injected through the write parameter — no permission tricks, so it runs under CI's root runner.writeRelocateEdit/writeMediaEdit, both deleted) remain as thinner integration coverage using read-only files, following the pre-existingTestMoveRollbackOnAliasFailurepattern (skipped under root; verified running and passing here as uid 992).go build ./...,go test ./... -count=1,go vet ./...all green; gofmt clean on every touched package.Deliberate small behavior notes (kinds/status unchanged)
scaffold/site/AGENTS.mduntouched — purely internal refactoring, nothing agent-facing changes (per the issue's out-of-scope note).🤖 Generated with Claude Code
Landing validation — PASS
Verification signal relied on: Forgejo Actions
ci / build-and-test (pull_request)— success (6m16s, run 113). Not re-run locally; the PR is a single commit (8b622db) with no post-CI changes.Checks performed
refactor:); body carries a workingCloses #116, so the merge will auto-close the issue and release theafk/116claim.main(534add4); no conflict, single commit.atomicfs.Relocate) owns rename + ordered writes + rollback, and neitherpageopsnormediaopsretains a rollback loop; the write seam is aWriteFuncparameter with no package-level seam var in the new package (bothwriteRelocateEditandwriteMediaEditdeleted);WriteFileKeepModeexists exactly once and all non-relocation call sites (applyToFile,Reorder,writeLadderStep) use it; the server's save path usesReplaceFilewith the temp-file-then-rename semantics unchanged; nine crash-safety tests live ininternal/atomicfscovering mid-phase restore + reverse rename and the dual-failure manual-attention error.errors.Is(err, atomicfs.ErrTargetExists)→ conflict, everything else internal, so HTTP status mapping is untouched.pageopsrelocation newly performs theLstattarget-absence pre-check it previously lacked. Verified this cannot turn a former no-op into a conflict:Renamereturns early whenslug == b.slug(title-only write, nothing relocated),uniqueSlugExceptguarantees the new slug is free among siblings, andMoveAt's same-parent case is a pure reposition that never reachesrelocate.relocateis therefore never called witholdAbs == newAbs.Non-blocking nit
atomicfs.WriteError.Error's doc comment claims the two templates are "byte-exact with the messages pageops and mediaops produced before this engine was extracted". That is true forpageopsbut not formediaops, whose wording changed from "rewriting the referencing pages failed (…)" to the canonical "writing the relocation edits failed (…)". The PR body discloses this deliberately and no test asserts the strings — only the code comment overstates it. Worth a one-line fix on a future touch; not worth blocking on.Verdict: PASS — proceeding to the human merge gate.