{"id":50553431,"url":"https://github.com/cplieger/atomicfile","last_synced_at":"2026-06-04T05:01:08.683Z","repository":{"id":361641270,"uuid":"1255210253","full_name":"cplieger/atomicfile","owner":"cplieger","description":"Atomic, durable file writes for Go: temp -\u003e fsync -\u003e rename with path validation","archived":false,"fork":false,"pushed_at":"2026-05-31T15:58:02.000Z","size":23,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-31T17:13:42.892Z","etag":null,"topics":["atomic","durability","filesystem","go"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cplieger.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T14:48:03.000Z","updated_at":"2026-05-31T15:24:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cplieger/atomicfile","commit_stats":null,"previous_names":["cplieger/atomicfile"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/cplieger/atomicfile","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fatomicfile","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fatomicfile/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fatomicfile/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fatomicfile/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cplieger","download_url":"https://codeload.github.com/cplieger/atomicfile/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fatomicfile/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33890052,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["atomic","durability","filesystem","go"],"created_at":"2026-06-04T05:01:07.982Z","updated_at":"2026-06-04T05:01:08.667Z","avatar_url":"https://github.com/cplieger.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# atomicfile\n\n[![CI](https://github.com/cplieger/atomicfile/actions/workflows/ci.yaml/badge.svg)](https://github.com/cplieger/atomicfile/actions/workflows/ci.yaml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/cplieger/atomicfile.svg)](https://pkg.go.dev/github.com/cplieger/atomicfile)\n[![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](LICENSE)\n\n\u003e Crash-safe atomic file writes for Go\n\nA standalone Go library providing atomic file writes (temp→fsync→rename→dir-fsync), path-traversal validation, bounded reads, streaming writes, and JSON helpers. Standard-library only, no external dependencies.\n\n## Platform Support\n\n**Linux only (including Docker/containers).** Windows is unsupported by design — `os.Rename` cannot guarantee atomicity on Windows ([golang/go#22397](https://github.com/golang/go/issues/22397#issuecomment-498856679)). macOS/BSD may work but is untested.\n\n## Install\n\n`go get github.com/cplieger/atomicfile@latest`\n\n## Usage\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/cplieger/atomicfile\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\n\t// Atomic write with default mode (0644)\n\tatomicfile.WriteFile(ctx, \"/tmp/data.txt\", []byte(\"hello\"))\n\n\t// Atomic write with custom mode\n\tatomicfile.WriteFile(ctx, \"/tmp/secret.txt\", []byte(\"s3cr3t\"),\n\t\tatomicfile.WithMode(0o600))\n\n\t// Streaming write from io.Reader\n\tatomicfile.WriteReader(ctx, \"/tmp/stream.txt\", strings.NewReader(\"streamed\"), 0o644)\n\n\t// PendingFile for incremental writes (mirrors google/renameio)\n\tpf, _ := atomicfile.NewPendingFile(\"/tmp/pending.txt\", 0o644)\n\tdefer pf.Cleanup()\n\tpf.Write([]byte(\"incremental\"))\n\tpf.CommitFile()\n\n\t// Preserve existing file permissions across replace\n\tatomicfile.WriteFile(ctx, \"/tmp/data.txt\", []byte(\"updated\"),\n\t\tatomicfile.WithPreserveMode())\n\n\t// Auto-create parent directories\n\tatomicfile.WriteFile(ctx, \"/tmp/nested/dir/file.txt\", []byte(\"deep\"),\n\t\tatomicfile.WithMkdirMode(0o755))\n\n\t// Skip fsync for speed (atomicity without durability)\n\tatomicfile.WriteFile(ctx, \"/tmp/cache.txt\", []byte(\"fast\"),\n\t\tatomicfile.WithNoSync())\n\n\t// JSON round-trip\n\tvar mu sync.Mutex\n\tatomicfile.SaveJSON(\"/tmp/config.json\", \u0026mu, map[string]int{\"x\": 1}, \"app\", 0o644)\n\tvar cfg map[string]int\n\tatomicfile.LoadJSON(ctx, \"/tmp/config.json\", 1\u003c\u003c20, \u0026cfg)\n}\n```\n\n## API\n\n### Write Functions\n\n- `WriteFile(ctx, path, data, opts ...Option)` — atomic write (default mode 0644)\n- `WriteReader(ctx, path, r, mode, opts ...Option)` — atomic write from `io.Reader` (uses `io.WriterTo` optimization when available)\n- `Prepare(ctx, path, data, opts ...Option)` — create temp file ready for commit\n- `Commit(tmpPath, finalPath, opts ...Option)` — rename temp to final + dir fsync\n- `SaveBytes(path, data, perm, opts ...Option)` — atomic write with auto-mkdir\n- `SaveJSON(path, mu, v, label, perm, opts ...Option)` — marshal + atomic write\n\n### Streaming Writer\n\n- `NewPendingFile(path, mode, opts ...Option)` — open a temp file for incremental writing\n- `(*PendingFile).CommitFile()` — fsync + close + rename (finalize)\n- `(*PendingFile).Cleanup()` — close + remove (abort; no-op after commit)\n\n`PendingFile` embeds `*os.File`, providing full `io.Writer`/`io.ReaderFrom`/`fmt.Fprintf` support.\n\n### Read Functions\n\n- `ReadBounded(ctx, path, maxBytes)` — size-checked read\n- `LoadJSON(ctx, path, maxBytes, v)` — bounded read + JSON unmarshal\n\n### Utilities\n\n- `CleanupStaleTemps(dir, maxAge, opts ...Option)` — remove old temp files\n\n## Functional Options\n\nAll write functions accept variadic `Option` values to customize behavior. Omit options for defaults.\n\n| Option | Description |\n|--------|-------------|\n| `WithLogger(l)` | Custom `*slog.Logger` for diagnostic output (default: `slog.Default()`) |\n| `WithTempPattern(p)` | `os.CreateTemp` pattern for temp files (default: `\".atomicfile-*.tmp\"`) |\n| `WithMode(mode)` | File permission (default: `0o644`). Used by `WriteFile` and `Prepare`. |\n| `WithMkdirMode(mode)` | When set, create parent directories with this mode before writing |\n| `WithPreserveMode()` | Stat target and reuse its mode (like `renameio.WithExistingPermissions`) |\n| `WithPreserveOwnership()` | Stat target and chown temp to match uid/gid (requires CAP_CHOWN) |\n| `WithNoSync()` | Skip fsync for speed (atomicity without durability, like google/renameio) |\n| `WithAllowSymlinkTarget()` | Permit writing to a symlink path (default: refuse with `ErrSymlinkTarget`) |\n\n## Durability Guarantees\n\nEvery atomic write follows the sequence: create temp file → write data → fsync temp → close → rename to final path → fsync parent directory. This ensures that after a crash, the file contains either the complete old content or the complete new content — never a partial write. The directory fsync is best-effort and ensures the rename is durable even if the system loses power immediately after the call returns.\n\n## Symlink Safety\n\nBy default, all write functions refuse to write to a path that is currently a symlink, returning `ErrSymlinkTarget`. This is because `os.Rename` replaces the symlink itself rather than the file it points to — which is rarely the caller's intent and can lead to data loss or security issues.\n\nTo opt in to writing through symlinks (replacing the symlink with a regular file), use `WithAllowSymlinkTarget()`.\n\n## Durability vs Speed\n\nBy default, all writes are durable: temp files are fsynced before rename, and the parent directory is fsynced after rename. This ensures data survives power loss.\n\nUse `WithNoSync()` for atomicity without durability (like google/renameio's design). The file will never be partially written, but may be lost entirely on power failure. Useful for caches, metrics, and other reconstructible data.\n\n## Unsupported by Design\n\nThe following features are intentionally not implemented:\n\n| Feature | Rationale |\n|---------|-----------|\n| **Windows rename-over semantics** | Target platform is Linux. `os.Rename` is atomic on Linux. Windows cannot guarantee atomicity ([golang/go#22397](https://github.com/golang/go/issues/22397#issuecomment-498856679)). google/renameio also refuses Windows. |\n| **`fs.FS` interop** | `fs.FS` is a read-only interface. Atomic writes are inherently outside its scope. |\n| **Atomic symlink replacement** | Out of scope. Use google/renameio if needed. |\n| **Umask-aware permissions** | The library uses `Chmod` for exact permissions (ignoring umask). This is the correct secure default for server/CLI tools. Equivalent to renameio's `WithStaticPermissions`. |\n| **`TempDir` cross-mount detection** | Temp files are always created in the target directory (same mount point), the only correct approach for atomic rename. |\n| **`os.Root` scoped operations** | Niche security feature not needed by target consumers. |\n| **Context on `SaveBytes`/`SaveJSON`** | Convenience wrappers for small payloads. Use `WriteFile`/`WriteReader` for cancellable operations. |\n\n## License\n\nGPL-3.0 — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcplieger%2Fatomicfile","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcplieger%2Fatomicfile","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcplieger%2Fatomicfile/lists"}