safewrite

package
v0.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package safewrite is the single way this tool replaces a file on disk.

Every write -- the target's new content and the .bak holding its prior content alike -- is staged in a temp file beside the target, fsynced, optionally validated, and only then renamed into place, so a crash or an error partway through can never leave a truncated or half-written file. The containing directory is fsynced after the rename where the platform supports it, so the rename itself is durable and not just the bytes behind it. A target that already exists with different bytes is refused unless the caller passes Force -- a tool that authors files into a user's repository must not silently destroy the user's edits. Paths are confined to a caller-supplied root with symlinks resolved, so a symlink planted in a project can never redirect a write outside it.

CANARY: REQ=ENG-4330; FEATURE="SafeWrite"; ASPECT=Storage; STATUS=TESTED; TEST=TestWriteNewFile,TestWriteIdenticalNoop,TestWriteRefusesWithoutForce,TestWriteForceBacksUpAndPreservesMode,TestWriteFailureLeavesOriginal,TestConfineRejectsSymlinkEscape,TestConfineAcceptsPathInsideRoot; UPDATED=2026-08-31

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrWouldReplace reports that the target exists with different content
	// and Force was not set. Callers typically print a notice and skip.
	ErrWouldReplace = errors.New("target exists with different content; pass force")
	// ErrRootEscape reports that the target, with symlinks resolved, lies
	// outside the confinement root.
	ErrRootEscape = errors.New("path resolves outside root")
)

Functions

func Confine

func Confine(root, path string) (string, error)

Confine resolves symlinks in path and its parents and returns the resolved path, or ErrRootEscape when it does not lie within the resolved root. Components that do not exist yet are appended verbatim: a file about to be created cannot be resolved, but every existing directory leading to it can.

Note the ordering: filepath.Abs cleans the path lexically -- collapsing "." and ".." textually -- before any symlink is resolved. So "root/link/../x" is evaluated as "root/x", not as "<link's target>/../x". That is strictly more restrictive than resolving ".." against the link's real parent: it can reject a path that a symlink-first walk would have allowed inside the root, but it can never let one out of it, which is the only direction that matters for confinement.

Types

type Journal added in v0.3.5

type Journal struct {
	// contains filtered or unexported fields
}

Journal records the pre-state of every path a multi-write operation is about to touch, in the order it touches them, so the whole operation can be undone as a unit if a later step fails.

A caller snapshots a file immediately before writing it (Snapshot is a no-op on a path already snapshotted, so repeated writes to the same path only ever remember the very first pre-image) and notes a directory immediately after creating it (NoteDir). Rollback then walks every recorded operation in reverse -- restoring or removing files, then removing directories -- so a directory is never asked to disappear while it still holds a file this journal is about to un-create.

The zero value is ready to use. CANARY: REQ=ENG-4340; FEATURE="InitJournal"; ASPECT=Storage; STATUS=TESTED; TEST=TestJournalRollbackRestoresPreImages,TestJournalSnapshotIsIdempotent,TestJournalNoteDirRemovesCreatedDirsBestEffort,TestJournalRollbackIsNoopWhenEmpty; UPDATED=2026-08-31

func (*Journal) NoteDir added in v0.3.5

func (j *Journal) NoteDir(path string)

NoteDir records that the operation created path as a directory, so Rollback removes it (best-effort, and only if by then it is empty) when undoing the operation.

func (*Journal) Rollback added in v0.3.5

func (j *Journal) Rollback() error

Rollback undoes every recorded operation in reverse order: a file that existed is restored to its pre-image via Write (Force, confined to its own directory); a file that did not exist is removed; a noted directory is removed best-effort, so a directory that Rollback cannot empty (because something outside this journal's view still lives in it) is left in place rather than treated as an error.

Errors restoring or removing files are joined and returned; directory removal failures are never surfaced; they are exactly the "not empty" case this function documents as expected. Rollback does not clean up any <path>.bak file a force-replacement created before this journal ran -- those backups are intentionally left in place as the user's audit trail, even once the restored pre-image makes them redundant.

func (*Journal) Snapshot added in v0.3.5

func (j *Journal) Snapshot(path string) error

Snapshot records path's current state -- its bytes and mode if it exists, or the fact that it does not -- before the caller writes to it. Calling Snapshot again for a path already snapshotted is a no-op: the first pre-image recorded is the one Rollback restores, regardless of how many times the operation rewrites the path afterward.

type Options

type Options struct {
	// Root confines the write. Required: an unset root is an error rather
	// than an implicit "anywhere".
	Root string
	// Force allows replacing an existing file whose bytes differ.
	Force bool
	// Backup writes the prior bytes to <path>.bak before replacing.
	Backup bool
	// Validate, when set, inspects the staged bytes before the rename. A
	// non-nil error aborts the write with the original file untouched.
	Validate func([]byte) error
}

Options configures Write.

type Result

type Result struct {
	// Written is false when the target already held exactly these bytes.
	Written bool
	// Replaced is true when an existing file's content was replaced.
	Replaced bool
	// BackupPath is the .bak file written, when Options.Backup asked for one.
	BackupPath string
	// Diff summarizes old-to-new line changes. It is set whenever an
	// existing file differs -- including on the ErrWouldReplace refusal, so
	// a caller can show what it declined to overwrite.
	Diff string
}

Result describes what Write did.

func Write

func Write(path string, data []byte, mode fs.FileMode, o Options) (Result, error)

Write replaces path's content with data, atomically and confined to Options.Root.

The target is left byte-identical on every failure path. When the target already exists its own permissions are preserved and mode is ignored; mode applies to newly created files only.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL