iox

package
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package iox provides atomic file IO and file-locking primitives used by agentsync's apply pipeline.

Index

Constants

View Source
const AllowSymlinkDestEnv = "AGENTSYNC_ALLOW_SYMLINK_DEST"

AllowSymlinkDestEnv lets callers opt in to AtomicWrite following an existing symlink rather than refusing. Default behaviour refuses so a chezmoi-style dotfile symlink at the destination is not silently replaced with a regular file.

Variables

View Source
var ErrSymlinkDest = errors.New("destination is a symlink")

ErrSymlinkDest is returned when dest is an existing symlink and the caller has not set AGENTSYNC_ALLOW_SYMLINK_DEST=1.

Functions

func AtomicWrite

func AtomicWrite(dest string, data []byte, mode os.FileMode) error

AtomicWrite writes data to dest using a three-phase approach: write to a sibling .agentsync.tmp file (always created mode 0o600 so cleartext payloads — secrets, age TOML — never sit world-readable in the destination directory between create and rename), fsync it, rename(2) into place, chmod to the caller-requested mode, then fsync the parent directory. If the process crashes between phases, the destination is either the old content (rename did not run) or the new content (rename ran). Never partial.

ATOMIC AGAINST A CRASH, NOT AGAINST A CONCURRENT WRITER. The temp file's name is FIXED (dest + ".agentsync.tmp"), so two processes writing the same dest share one temp inode: B's O_TRUNC can land inside A's write window, and A then renames a short or empty file into place. Callers get atomicity from the global lock (internal/cli/lock.go), which is why almost every writer in the tool holds it. The exception is state.SaveLastRun on the deliberately lock-free upgrade-notice path, where a torn record is a self-repairing UX marker rather than data loss — see internal/cli/upgrade_notice.go. Anything else that needs to write dest without the lock must NOT reach for this: append (cf. ensureStateGitignore, whose read-modify-write lost user data under exactly this race) or take the lock.

If dest is an existing symlink, AtomicWrite refuses unless AGENTSYNC_ALLOW_SYMLINK_DEST=1. Replacing the symlink with a regular file via rename would silently break a chezmoi/Stow setup where the user's real source lives behind the link. With the env set, dest is resolved through the link first so the underlying file is updated in place and the symlink itself is preserved.

The parent-directory fsync ensures that on filesystems where the directory entry update is asynchronous (ext4 without data=journal, btrfs, and historically some NFS configurations), a power loss immediately after the rename does not revert the entry to point at the old inode. On platforms where opening a directory for fsync is not supported (notably Windows) the directory fsync is best-effort and silently skipped.

Parent directory is created if missing (with mode 0o755).

Types

type Lock

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

Lock represents an acquired exclusive file lock. Release() drops it.

func AcquireLock

func AcquireLock(path string) (*Lock, error)

AcquireLock takes an exclusive lock on path, blocking forever until it succeeds. The parent directory is created if missing.

func AcquireLockTimeout

func AcquireLockTimeout(path string, timeout time.Duration) (*Lock, error)

AcquireLockTimeout takes an exclusive lock on path, returning an error if the lock cannot be acquired within timeout.

func (*Lock) Release

func (l *Lock) Release() error

Release drops the lock. Idempotent.

Jump to

Keyboard shortcuts

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