Skip to content

About

Write files atomically and durably from Go, so a crash never leaves a partial file

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

522 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

atomicfile

Go Reference Go version Mutation

atomicfile writes files from Go so that a crash or a power loss leaves the complete old content or the complete new content, never a partial file.

It replaces the temp file, fsync and rename sequence you would otherwise write around os.WriteFile. It runs on Linux, containers included, uses only the standard library at run time, needs Go 1.27 or later and is licensed under Apache-2.0. macOS and BSD may work but are untested.

Why use it

atomicfile is built for Go services that keep state, configuration or credentials on disk and must not lose them to a crash.

  • Every write syncs the file and its parent directory, and Result.Durable reports whether every sync succeeded.
  • A nil error means the new data is at the path. Any other error leaves the old file in place, and a failure inside the write is a *WriteError naming the step that failed.
  • WithMode proves the mode the filesystem stored, so a filesystem that widens a 0600 request fails the write instead of publishing a readable file.
  • The *InRoot functions run through an *os.Root, so a symlink or .. in a name cannot leave the directory tree.
  • Writes refuse a symlink, FIFO, socket or device node at the target path.

Consider google/renameio if you also need to replace a symbolic link atomically.

Install

go get github.com/cplieger/atomicfile/v4@latest

Usage

package main

import (
	"context"
	"log"
	"os"

	"github.com/cplieger/atomicfile/v4"
)

func main() {
	ctx := context.Background()

	// Replace a file. Result.Durable reports whether the file and its
	// directory were both synced to disk.
	res, err := atomicfile.WriteFile(ctx, "/var/lib/myapp/state.json", []byte(`{"ok":true}`))
	if err != nil {
		log.Fatal(err)
	}
	if !res.Durable {
		log.Printf("%s is written but not yet durable", res.Path)
	}

	// A credential: staged owner-only, published at exactly 0600 or not at all.
	_, err = atomicfile.WriteFile(ctx, "/var/lib/myapp/token", []byte("secret"),
		atomicfile.WithMode(0o600))
	if err != nil {
		log.Fatal(err)
	}

	// Confined to one tree: a symlink or ".." in the name cannot leave it.
	root, err := os.OpenRoot("/var/lib/myapp")
	if err != nil {
		log.Fatal(err)
	}
	defer root.Close()
	_, err = atomicfile.WriteFileInRoot(ctx, root, "cache/index.json", []byte("[]"),
		atomicfile.WithMkdirMode(0o750))
	if err != nil {
		log.Fatal(err)
	}

	// Read it back under a 1 MiB cap.
	data, err := atomicfile.ReadBoundedInRoot(ctx, root, "cache/index.json", 1<<20)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("read %d bytes", len(data))
}

To stream content, use WriteReader. To write a file piece by piece, call NewPendingFile, write to it, then Commit to publish it or Cleanup to abort. The 23 runnable examples on pkg.go.dev cover writes, reads, sweeps, probes and private directories, and go test runs them.

API

  • Writes: WriteFile, WriteReader, NewPendingFile and their *InRoot forms.
  • Reads: ReadBounded, ReadBoundedFile, ReadBoundedInRoot, OpenRegular, OpenRegularInRoot, OpenRegularInRootNoFollow.
  • Shared directory trees: WalkDirInRoot, OpenParentInRoot, RemoveFileInRoot.
  • Modes: EnforceMode, EnsurePrivateDir.
  • Temp files and checks: CleanupStaleTemps, CleanupStaleTempsInRoot, TempName, IsPackageTemp, ProbeWritable, ProbeWritableInRoot, ValidatePath, Identify.
  • Options: WithMode, WithMkdirMode, WithMaxBytes, WithLogger, WithRecursive, WithRepairOwnedDir.
  • Errors: sentinel values for errors.Is, and *WriteError and *NotRegularError for errors.As.

The full reference is on pkg.go.dev. Choosing a function says which call fits which job.

What a nil error means

Every write creates a temp file in the target's directory, writes and syncs it, renames it over the target, then syncs the directory. After a crash the path holds the complete old content or the complete new content.

A nil error means the data reached its final path. Result.Durable then says whether that content will survive a power loss. It is false when the directory sync failed after the rename, for example with an EIO from a failing disk. A crash could then still bring back the old file. The package logs a warning in that case, and a caller that needs crash durability retries the write or raises an alert. A non-nil error means the data did not reach the path, and the previous file is untouched.

Two writers on one path each publish a whole file, and the last rename wins. The package takes no lock.

A process killed before the rename leaves its temp file behind. Nothing in the package removes it on its own, so call CleanupStaleTemps at startup or on a schedule, as Orphaned temp files explains.

Durability and errors has the full sequence and every error value.

File modes

Without WithMode, atomicfile sets no mode. The file is created the way os.Create creates one, at 0666, narrowed by the umask and widened or narrowed by any ACL the directory passes on. A write that replaces an existing file gives the new file those defaults, and the old file's mode is not kept. That suits a shared tree whose permissions an operator manages, such as a media library.

Pass WithMode for any file whose readers must be restricted, such as credentials, configuration and backups. The payload is staged in a file proven owner-only, the final file is proven to hold the mode, and a filesystem that stores anything else fails the write with ErrModeNotStored.

WithMkdirMode creates missing parent directories at a mode it also proves. EnsurePrivateDir sets up one directory level that only the effective user may enter. File modes and private directories explains both.

Unsupported by design

These are deliberate non-goals, not missing features.

  • Windows. os.Rename cannot guarantee an atomic replace there (golang/go#22397), and the package does not build for it.
  • fs.FS interop. fs.FS is a read-only interface, and atomic writes are outside its scope.
  • Atomic symlink replacement. Use google/renameio for that.
  • Detecting a cross-mount temp directory. Temp files are always created in the target's own directory, on the same mount, which is the only correct place for an atomic rename.

Documentation

Credits

The incremental PendingFile API, a staged file that is either committed into place or cleaned up, follows google/renameio.

Contributing

See CONTRIBUTING.md.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

Apache-2.0. See LICENSE.

About

Write files atomically and durably from Go, so a crash never leaves a partial file

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages