safeio

package
v1.10.10 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package safeio centralizes the "never delete or overwrite a user artifact without a recovery path" primitives that agent-deck grew piecemeal after a recurring class of data-loss incidents (the 2026-06-04 profile-index wipe and its 2025-12-11 / 2026-04-17 predecessors).

Three bespoke guards previously lived at three sites:

  • #1383/S1+S2 — SaveInstances refused an empty-payload sweep and backed up the DB before a large drop; SaveUserConfig refused dropping populated sections and backed up config.toml before the atomic rename.
  • #1429 — the conductor dir relocation copied + verified meta.json before RemoveAll'ing the source, refusing to delete the only copy.
  • #1449 — worktree teardown refused to remove a worktree still referenced by a sibling session.

safeio distills those into two composable primitives:

SafeOverwrite — backup → atomic durable write, with pluggable refusal guards
               (refuse-empty, refuse-section-drop) checked BEFORE any mutation.
SafeRemove    — refuse to delete a path that is still referenced / is the only
               copy (via a pluggable predicate), so a destructive RemoveAll is
               structurally guarded rather than guarded ad-hoc at each caller.

The package depends only on the standard library and internal/atomicfile, so any internal package can adopt it without an import cycle.

Index

Constants

This section is empty.

Variables

View Source
var ErrRefusingEmptyOverwrite = errors.New("safeio: refusing to overwrite a populated file with empty content")

ErrRefusingEmptyOverwrite is returned by SafeOverwrite when Options.RefuseEmpty is set and the call would replace a NON-empty file with empty bytes. An empty payload over an already-empty (or absent) file is allowed — only the destructive empty-over-populated case is refused.

View Source
var ErrStillReferenced = errors.New("safeio: refusing to remove a still-referenced path")

ErrStillReferenced is returned by SafeRemove when Options.StillReferenced reports the path is in use (e.g. a worktree shared by a sibling session, or a source whose only copy this would delete).

Functions

func Backup

func Backup(path string) (string, error)

Backup copies an existing file to "<path>.bak" via a temp-file + rename so the .bak is never torn. It returns the backup path on success, "" when the source does not exist (a no-op), or an error. The backup is written 0o600 to keep it as private as the originals it protects (config.toml / state.db may carry session metadata).

func SafeOverwrite

func SafeOverwrite(path string, newBytes []byte, opts Options) error

SafeOverwrite replaces path's contents with newBytes, guarded against the destructive-write footguns. Order of operations:

  1. Read the existing content (nil if the file is new).
  2. Run Options.Guard(old, new) — refuse with the guard's error, untouched.
  3. RefuseEmpty: refuse empty-over-populated with ErrRefusingEmptyOverwrite.
  4. Back up the existing file to <path>.bak (unless SkipBackup or new file).
  5. Atomically + durably write newBytes (temp → fsync → rename → dir fsync).

All refusals happen BEFORE any backup or write, so a refused call leaves both the original and any prior .bak exactly as they were.

func SafeRemove

func SafeRemove(path string, opts RemoveOptions) error

SafeRemove removes path (file or directory tree) only when it is safe to do so. It refuses an empty path (guarding against a stray RemoveAll("")), is a no-op on a non-existent path, and consults Options.StillReferenced before deleting — refusing with ErrStillReferenced when the path is in use.

Types

type Options

type Options struct {
	// Perm is the file mode for the written file. Defaults to 0o600 when zero,
	// matching the private treatment of config.toml / state.db backups.
	Perm os.FileMode

	// RefuseEmpty refuses to replace a non-empty file with empty bytes. This is
	// the generalization of the SaveInstances empty-sweep guard for file writes:
	// a stray empty payload over a populated artifact is almost always a bug.
	RefuseEmpty bool

	// Guard, when non-nil, is called with (oldContent, newContent) BEFORE any
	// backup or write. A non-nil error aborts the overwrite with the file
	// untouched and no backup made. oldContent is nil when the target does not
	// exist yet. This is the extension point for caller-specific refusals such
	// as the SaveUserConfig section-drop guard. The error is returned verbatim
	// (wrap with %w upstream for errors.Is matching).
	Guard func(oldContent, newContent []byte) error

	// SkipBackup disables the pre-write .bak copy. Off by default — the backup
	// is the recovery net the whole package exists to provide.
	SkipBackup bool
}

Options configures SafeOverwrite.

type RemoveOptions

type RemoveOptions struct {
	// StillReferenced, when non-nil, is consulted before removal. Returning
	// (true, reason) refuses the removal with ErrStillReferenced (reason is
	// folded into the error message). This generalizes the #1449 sibling-use
	// guard and the #1429 "destination still aliases the source" defense: the
	// caller supplies the reference check; safeio enforces the refusal.
	StillReferenced func(path string) (referenced bool, reason string)
}

RemoveOptions configures SafeRemove.

Jump to

Keyboard shortcuts

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