fs

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: MIT Imports: 37 Imported by: 0

README

go-rotini/fs

A Go filesystem helpers package for CLIs: atomic writes, safe reads, path resolution, cross-platform user-directory lookup, file watching, archive extraction, scaffolding, advisory file locking, directory-backed caching, log rotation, tail-follow with rotation handling, versioned backups, transactional plan/apply, gitignore-aware walks, memory-mapped reads, and the dozens of small operations a CLI repeatedly needs to get right.

This package is used as the default filesystem package for rotini.

Features

  • Atomic writes (temp file + fsync + rename + parent-dir fsync) via WriteFile, WriteFileSecret, WriteFileExclusive, WriteString, Append, with options WithPerm, WithSync, WithBackup, WithMkdirAll
  • Bounded reads (default 100 MiB, overridable with WithMaxSize) via ReadFile, ReadLines, ReadFirstLine, OpenLines, OpenChunked, ReadAt
  • Idempotent removal: Remove, RemoveAll, RemoveContents, plus symlink-safe RemoveAllNoFollow
  • Path safety primitives: IsSubpath, MustBeChildOf, EvalSymlinksWithin, SanitizeFilename, IsReservedName, LongPath
  • TOCTOU-safe open: OpenNoFollow (refuses final-component symlinks) and OpenAt (resolves through a held directory FD on POSIX)
  • Cross-platform user and system directories (XDG on Linux/FreeBSD, Apple guidelines on macOS, %APPDATA% / %LOCALAPPDATA% / %PROGRAMDATA% on Windows): Home, ConfigDir, CacheDir, DataDir, StateDir, RuntimeDir, ExecutableDir, BinaryPath, plus App*Dir(appName) and System*Dir(appName) variants
  • Path expansion: Expand handles ~, ~user, $VAR, ${VAR} with opt-in strict mode
  • Find-up project discovery: FindUp, FindUpAll, ProjectRoot, FirstExisting
  • Directory walking: Walk with WalkSkipHidden, WalkSkipNames, WalkSkipPatterns, WalkMaxDepth, WalkFollowSymlinks, WalkErrorHandler, WithWalkGitignore; concurrent variant WalkParallel(ctx, ...)
  • Glob: Match, Glob, GlobAny with path expansion before matching
  • Atomic-rename-aware file watching: multi-subscriber broadcast, 75 ms trailing-edge debouncing, per-instance pluggable slog.Logger. v0.1 ships the polling backend; native inotify / kqueue / ReadDirectoryChangesW scheduled as a follow-up release
  • Archive extraction and creation: tar / tar.gz / zip with path-confinement via MustBeChildOf (zip-slip / tar-slip defense), bounded via WithArchiveMaxBytes (default 10 GiB), entry-level filters
  • Scaffolding from io/fs.FS (typically embed.FS): ScaffoldApply with text/template-rendered paths and contents, conflict policies, and a ScaffoldExtract first-run resource-extraction variant
  • Advisory file locking: Lock, LockShared, TryLock, LockTimeout, WithLock, IsLocked, PIDLock with stale-lock reclamation and opt-in WithPIDLockFingerprint for recycled-PID defense; syscall.Flock on POSIX, LockFileEx on Windows
  • Directory-backed cache: *Cache with TTL, mtime-based eviction, size-bounded LRU, content-versioned namespaces, Get / GetWithError / Set / Delete / Purge / Stats / Entries
  • Tail-follow with rotation handling: Tail(ctx, path) iter.Seq2[string, error] detects rename-style rotation and in-place truncation via os.SameFile, reopens transparently (or opts into ErrTailRotated notifications)
  • Log rotation: *Rotator (io.WriteCloser) with size and age triggers, retention count, optional gzip compression of rotated files
  • Versioned backups: WriteFileVersioned / ListVersions / RestoreVersion with WithVersionsKeep, WithVersionsMaxAge, WithVersionsPerm, WithVersionsMaxBytes
  • Transactional plan/apply: build a *Plan of Create / Update / Delete / Rename ops, preview with Diff, execute via Apply against an on-disk journal that supports Resume and Rollback
  • Gitignore parser: *Gitignore with negation, anchoring, ** recursive, and per-segment globs; integrates with Walk via WithWalkGitignore
  • Memory-mapped reads: Mmap returns a *Mapping (syscall.Mmap(PROT_READ, MAP_SHARED) on POSIX, CreateFileMapping + MapViewOfFile on Windows)
  • Content search: FindByContent / FindByContentRegex with binary-file skipping, configurable per-file size cap via WithFindByContentMaxSize
  • Disk-info: DiskUsage, DiskUsageOf, MountPoint, FilesystemType, IsNetworkFS, IsCaseInsensitiveFS, PreflightSpace
  • Hashing: streaming Hash, constant-time HashCompare, HashWriter for io.MultiWriter use; SHA-256 default, plus SHA-512 / SHA-1 / MD5 (the last two for non-security uses)
  • Bytes formatting: strict-SI ParseBytes (1KB == 1000, matching kubectl / docker) plus IEC-binary ParseBytesIEC (1KB == 1024); FormatBytes
  • Project-kind detection: ProjectType recognizes Go, Node, Rust, Python, Ruby, Java, .NET, PHP, Make, Docker via marker files; extensible via RegisterProjectKind
  • Multi-root workspace discovery: WorkspaceRoots parses go.work, package.json workspaces (array + object forms), pnpm-workspace.yaml
  • Stdio: ReadStdin, OpenStdinLines, WriteStdout / WriteStderr (translates EPIPE -> ErrBrokenPipe), IsTerminal
  • Test helpers in fs/fstest: NewTestHarness(t), MockFS, WithTempEnv, TempFileT, TempDirT. Importing github.com/go-rotini/fs does not pull stdlib's testing package into your binary
  • Bi-directional sentinel matching: errors.Is(err, fs.ErrNotFound) matches both the package sentinel AND stdlib's io/fs.ErrNotExist; *MultiError with Go 1.20 Unwrap() []error
  • Pluggable slog.Logger for Apply / Cache / Rotator debug output via package-level SetLogger
  • DoS protection: bounded reads by default, archive extraction size cap, plan/apply journal size split, content-search size cap, gitignore + workspace parser size limits
  • Zero non-stdlib runtime dependencies; cross-platform on Linux, macOS, Windows, FreeBSD

Installation

go get github.com/go-rotini/fs

Requires Go 1.26 or later.

Quick Start

package main

import (
	"fmt"
	"log"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, err := fs.TempDir("", "fs-quickstart-*")
	if err != nil {
		log.Fatal(err)
	}
	defer func() { _ = cleanup() }()

	cfg := dir + "/config.toml"

	// Atomic write: temp + fsync + rename + parent-dir fsync.
	if err := fs.WriteFile(cfg, []byte("[server]\nport = 8080\n")); err != nil {
		log.Fatal(err)
	}

	// Bounded read (default cap 100 MiB; override with WithMaxSize).
	data, err := fs.ReadFile(cfg)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data))

	// Coordinate with peer processes via an advisory file lock.
	if err := fs.WithLock(dir+"/work.lock", func() error {
		// Critical section.
		return nil
	}); err != nil {
		log.Fatal(err)
	}

	// Discover the project root from any subdirectory.
	if root, err := fs.ProjectRoot(dir); err == nil {
		fmt.Println("project root:", root)
	}
}

Documentation

Full API reference is available on pkg.go.dev.

Contributing

See CONTRIBUTING.md for guidelines on how to contribute to this project.

Code of Conduct

This project follows a code of conduct to ensure a welcoming community. See CODE_OF_CONDUCT.md.

Security

To report a vulnerability, see SECURITY.md.

License

This project is licensed under the MIT License. See LICENSE for details.

Documentation

Overview

Package fs is a filesystem helpers library for CLI tools. It covers atomic writes, safe reads, path resolution, cross-platform user- directory lookup, directory walking, file watching, archive extraction, scaffolding, disk-info queries, advisory file locking, directory-backed caching, log rotation, tail-follow with rotation handling, versioned backups, transactional plan/apply, gitignore- aware walks, and the dozens of small operations a CLI tool repeatedly needs to get right.

The package sits on top of stdlib's os, io/fs, and path/filepath, turning common CLI operations into one-line calls with safe defaults and useful errors. It is not a virtual filesystem abstraction (see afero / go-billy); every operation touches the real filesystem. For tests against a fixture without disk I/O, use github.com/go-rotini/fs/fstest: fstest.MockFS for read paths or fstest.NewTestHarness for write paths in a testing.T.TempDir.

Zero third-party dependencies

The fs package is a single flat root for production code: watcher, lock, cache, rotator, plan, archive, scaffold, gitignore, mmap, and disk-info helpers all live in the same package. The production surface has zero non-stdlib runtime imports except for one narrowly-scoped exception (see below) and does not import testing. Platform-native syscalls (flock and LockFileEx for the lock helpers; mmap and MapViewOfFile for the memory-map helpers; the native watcher backends are a planned follow-up, see Stability) go through stdlib's syscall package or the documented golang.org/x/sys exception.

The x/sys exception

golang.org/x/sys/windows is imported by exactly one Windows-only file (mmap_windows.go) for typed Handle, CreateFileMapping, MapViewOfFile, and UnmapViewOfFile wrappers around kernel32.dll. The package is Go-team-maintained, MIT-licensed, and has zero non- stdlib transitive deps. The exception is scoped to platform-syscall files; no public API surface leaks x/sys types. The same exception applies to future syscall surfaces where x/sys carries a wrapper materially safer than hand-rolling syscall.Syscall6 against a DLL proc.

The one sub-package is github.com/go-rotini/fs/fstest, which holds the test helpers (TestHarness, MockFS, WithTempEnv, TempFileT, TempDirT). Keeping those out of the main package lets production binaries avoid pulling stdlib's testing package, and its global flag registration, into their import graph.

API conventions

Every operation that touches a path returns a *PathError wrapping the operation name, the path, and the underlying cause. errors.Is matches both this package's sentinels (e.g., ErrNotFound) and the equivalent stdlib sentinels (e.g., io/fs.ErrNotExist) on the same error. Bulk operations aggregate errors into a *MultiError whose Unwrap returns every component, so errors.Is works across the aggregate.

Defaults are safe by default: writes are atomic via temp+rename; reads are bounded (WithMaxSize, default 100 MiB); removal is idempotent (Remove, RemoveAll, RemoveContents) unless WithStrict; archive extraction confines every entry through MustBeChildOf (zip-slip / tar-slip defense).

Pitfalls

Issues the package documents because they are easy to miss:

  • Atomic writes require the temp file to live in the destination's parent directory. WriteFile handles this; hand-rolled temp+rename code must too. Cross-directory rename is sometimes a copy + delete, not atomic.
  • TOCTOU (time-of-check to time-of-use) races are real. An Exists(path) && ReadFile(path) sequence is racy. The package's predicates exist for ergonomics, not security; security- critical code should use OpenNoFollow or OpenAt and read-and-handle-error.
  • Exists returns false on permission errors. Callers who need to distinguish use Stat directly. Do not write `if !Exists { create }` in privileged contexts.
  • The watcher watches a file's parent directory (with basename filtering) so editor atomic-save patterns are observed correctly. Subscribing to a non-existent path requires NewLazyWatcher.
  • WithMaxSize defends against /dev/zero-class reads. Default 100 MiB; callers reading attacker-controlled paths must keep it set.
  • SanitizeFilename inserts `_` before the extension when the stem matches a Windows reserved name: CON.txt becomes CON_.txt. Suffixing the whole filename (CON.txt_) leaves Windows treating the file as the CON device.
  • Archive extraction (zip/tar) must use ExtractArchive; every entry passes through MustBeChildOf before any filesystem write. Hand-rolled extraction code is the classic zip-slip vulnerability.
  • macOS path/filepath.EvalSymlinks resolves /var to /private/var and similar symlinks. Tests that compare paths from testing.T.TempDir against /var/folders/... should resolve via path/filepath.EvalSymlinks on both sides before comparing.
  • On Windows, symlink creation typically requires Administrator or Developer Mode. The package's symlink-using helpers return a clear error when the privilege isn't held.
  • v0.1 ships polling-only watching. The platform-native backends (inotify on Linux, kqueue on macOS/BSD, ReadDirectoryChangesW on Windows) are not yet wired up; every Watcher uses the polling backend regardless of WithPolling. Default interval is 1 second; pass WithPolling with a finer interval when latency matters. The API will not change when native backends land.
  • The watcher's debouncer adds 75 ms of trailing-edge latency by default (WithDebounce). Tests that need immediate event visibility should pass WithDebounce(0).
  • The polling backend reads file mtimes from os.Lstat; on filesystems that round mtime to second resolution (FAT, network-attached SMB), back-to-back writes within one second can be missed. Until native backends land, WithPolling with a finer interval is the only mitigation.
  • Hash, HashCompare, and HashWriter expose MD5 and SHA-1 for non-security uses (legacy compat, content-addressed caches); they are not secure for integrity defense against attackers.
  • ParseBytes uses strict SI semantics: 1KB == 1000, matching kubectl / docker / kafka. IEC binary units (1KiB == 1024) keep their canonical meaning. For the legacy disk-vendor idiom where bare KB means 1024, use ParseBytesIEC. This is the opposite of what some older Go libraries do, so callers must choose deliberately.

Stability

The public API is stable starting at v0.1.0. New features may arrive in minor releases; breaking changes are reserved for major version bumps. Items still on the roadmap for a later minor release: the platform-native watcher backends (inotify, kqueue, ReadDirectoryChangesW; today every Watcher uses the polling fallback) and the Tier-C items recorded in the package's design doc (umask helpers, xattrs, sparse/preallocate, reflink, MoveToTrash, bind-mount introspection). All future additions land in the fs package itself; the only sub-package is fstest for test helpers.

Index

Examples

Constants

View Source
const (
	// Mode0644 is the default for new regular files (owner rw, group/other r).
	Mode0644 os.FileMode = 0o644

	// Mode0640 restricts read to owner + group.
	Mode0640 os.FileMode = 0o640

	// Mode0600 is owner-only read/write; appropriate for secret-bearing files.
	Mode0600 os.FileMode = 0o600

	// Mode0755 is the default for new directories (owner rwx, group/other rx).
	Mode0755 os.FileMode = 0o755

	// Mode0750 restricts directory access to owner + group.
	Mode0750 os.FileMode = 0o750

	// Mode0700 is owner-only directory access; appropriate for state dirs
	// containing secrets.
	Mode0700 os.FileMode = 0o700
)

Common file-mode presets. These are typed os.FileMode values so they pass directly to os.Chmod, os.OpenFile, etc.

View Source
const DefaultMaxReadSize int64 = 100 << 20

DefaultMaxReadSize bounds the default size cap on every read operation in the package. 100 MiB defends against accidental DoS via /dev/zero, runaway log files, and similar pathological inputs without rejecting any plausible CLI config / fixture / corpus input.

Variables

View Source
var (
	// ErrNotFound matches stdlib [io/fs.ErrNotExist] via wrapping.
	ErrNotFound = fmt.Errorf("fs: not found: %w", stdfs.ErrNotExist)

	// ErrAlreadyExists matches stdlib [io/fs.ErrExist] via wrapping.
	ErrAlreadyExists = fmt.Errorf("fs: already exists: %w", stdfs.ErrExist)

	// ErrPermission matches stdlib [io/fs.ErrPermission] via wrapping.
	ErrPermission = fmt.Errorf("fs: permission denied: %w", stdfs.ErrPermission)

	// ErrNotDir is returned when a directory was expected but the
	// path is something else.
	ErrNotDir = errors.New("fs: not a directory")

	// ErrIsDir is returned when a regular file was expected.
	ErrIsDir = errors.New("fs: is a directory")

	// ErrCrossDevice is returned when a Rename across filesystems
	// fails (POSIX EXDEV / Windows ERROR_NOT_SAME_DEVICE). [Move]
	// falls back to Copy + Remove on this; [Rename] surfaces it.
	ErrCrossDevice = errors.New("fs: cross-device link")

	// ErrFileTooLarge is returned when a bounded read exceeds the cap
	// configured via [WithMaxSize].
	ErrFileTooLarge = errors.New("fs: file size exceeds limit")

	// ErrEmptyFile is returned by [ReadFirstLine] when the file is
	// empty.
	ErrEmptyFile = errors.New("fs: empty file")

	// ErrEscapesRoot is returned by [IsSubpath] / [MustBeChildOf] /
	// [EvalSymlinksWithin] when a path tries to escape its
	// constraint, and by archive extraction when an entry resolves
	// outside the destination root.
	ErrEscapesRoot = errors.New("fs: path escapes constraint root")

	// ErrSymlinkLoop is returned by [EvalSymlinks] after too many
	// hops, and by [OpenNoFollow] when the final component is a
	// symlink.
	ErrSymlinkLoop = errors.New("fs: symlink loop detected")

	// ErrInvalidPath is returned for malformed paths (NUL bytes,
	// path-separator characters in app-name args, etc.).
	ErrInvalidPath = errors.New("fs: invalid path")

	// ErrHashMismatch is returned by [HashCompare] on mismatch.
	ErrHashMismatch = errors.New("fs: hash mismatch")

	// ErrNotEmpty is returned when a directory expected to be empty
	// is not.
	ErrNotEmpty = errors.New("fs: directory not empty")

	// ErrShortRead is returned by [ReadAt] when fewer bytes were
	// available than the caller requested. [WithAllowShort] disables
	// it.
	ErrShortRead = errors.New("fs: short read")

	// ErrBrokenPipe is returned by [WriteStdout] / [WriteStderr] when
	// the consumer has closed the pipe (typical pattern: `mytool |
	// head`).
	ErrBrokenPipe = errors.New("fs: broken pipe")

	// ErrNotSupported is returned for queries that the platform
	// doesn't expose (e.g., birth time on Linux without statx, owner
	// on Windows).
	ErrNotSupported = errors.New("fs: not supported on this platform")

	// ErrInvalidByteSize is returned by [ParseBytes] / [ParseBytesStrict]
	// when the input is empty, has no numeric prefix, or has an
	// unrecognized unit suffix.
	ErrInvalidByteSize = errors.New("fs: invalid byte size")
)

Sentinel errors. The struct-pointer sentinels (those constructed as &XError{}) match by type via the corresponding Is method; the errors.New sentinels match by value identity.

Where applicable, sentinels wrap stdlib equivalents so errors.Is(err, fs.ErrNotFound) matches both this package's ErrNotFound AND the stdlib's io/fs.ErrNotExist.

View Source
var (
	// ErrLockTimeout is returned by [LockTimeout] when the requested
	// duration elapses before the lock could be acquired.
	ErrLockTimeout = errors.New("fs: lock: timeout exceeded")

	// ErrStaleLock is returned by [PIDLock] when the on-disk PID file
	// references a dead or recycled process and the lock is reclaimed
	// on behalf of the caller. The returned [*LockHandle] is valid;
	// the error is informational.
	ErrStaleLock = errors.New("fs: lock: stale lock reclaimed")
)
View Source
var (
	// ErrWatcherEmptyPath is returned by [NewWatcher] / [NewLazyWatcher]
	// / [NewDirWatcher] when path is empty.
	ErrWatcherEmptyPath = errors.New("fs: watcher: path must not be empty")

	// ErrWatcherNilContext is returned by [(*Watcher).Subscribe] when
	// ctx is nil.
	ErrWatcherNilContext = errors.New("fs: watcher: nil context")

	// ErrWatcherClosed is returned by [(*Watcher).Subscribe] when the
	// watcher has already been closed.
	ErrWatcherClosed = errors.New("fs: watcher: closed")
)

Sentinel errors for the watcher surface. Wrapped via *PathError at the cross-platform shell entry points.

View Source
var ErrArchiveFormatUnknown = errors.New("fs: archive format unknown")

ErrArchiveFormatUnknown is returned by ExtractArchive when the leading bytes don't match any recognized archive container, and by CreateArchive when an out-of-range ArchiveFormat is requested.

View Source
var ErrArchiveTooLarge = errors.New("fs: archive too large")

ErrArchiveTooLarge is returned by ExtractArchive when the cumulative extracted size exceeds the WithArchiveMaxBytes cap.

View Source
var ErrCacheClosed = errors.New("fs: cache: closed")

ErrCacheClosed is returned by Cache operations after Cache.Close has been called.

View Source
var ErrInsufficientSpace = errors.New("fs: insufficient free space")

ErrInsufficientSpace is returned by PreflightSpace when a filesystem doesn't have enough free bytes for the requested reservation.

View Source
var ErrRotatorClosed = errors.New("fs: rotator: closed")

ErrRotatorClosed is returned by Rotator.Write after Rotator.Close has been called.

View Source
var ErrScaffoldPromptRequired = errors.New("fs: scaffold: prompt function required for PromptInteractive")

ErrScaffoldPromptRequired is returned when ScaffoldPromptInteractive is selected without a prompt function provided via WithScaffoldPromptFunc.

View Source
var ErrScaffoldPromptUnsupported = errors.New("fs: scaffold: prompt returned unsupported action")

ErrScaffoldPromptUnsupported is returned when WithScaffoldPromptFunc returns an action that isn't ScaffoldActionSkip / ScaffoldActionOverwrite / ScaffoldActionCreate.

View Source
var ErrScaffoldUnresolvedConflict = errors.New("fs: scaffold: unresolved conflict")

ErrScaffoldUnresolvedConflict is returned when a conflict makes it through to the apply phase without being resolved (typically only happens when the prompt callback is misconfigured).

View Source
var ErrTailRotated = errors.New("fs: tail: file rotated")

ErrTailRotated is yielded by Tail's iterator each time it detects a rotation (rename-style or in-place truncation) and reopens the underlying file, but only when the caller opts in via WithTailNotifyRotation. By default rotation is transparent.

The yield is ("", ErrTailRotated); the iterator continues afterwards. Callers who pattern-match on this can log a rotation event or flush partial state. errors.Is recognizes it.

Functions

func Abs

func Abs(path string, opts ...PathOption) (string, error)

Abs returns the absolute form of path, resolving any leading ~ and $VAR via Expand first.

func AppCacheDir

func AppCacheDir(appName string) (string, error)

AppCacheDir returns CacheDir/<appName>. See AppConfigDir for appName validation.

func AppConfigDir

func AppConfigDir(appName string) (string, error)

AppConfigDir returns ConfigDir/<appName>. appName must not contain a path separator (`/` or `\`), be empty, or be `.` / `..`. Invalid appName errors with ErrInvalidPath.

func AppDataDir

func AppDataDir(appName string) (string, error)

AppDataDir returns DataDir/<appName>.

func AppRuntimeDir

func AppRuntimeDir(appName string) (string, error)

AppRuntimeDir returns RuntimeDir/<appName>.

func AppStateDir

func AppStateDir(appName string) (string, error)

AppStateDir returns StateDir/<appName>.

func Append

func Append(path string, data []byte, opts ...WriteOption) error

Append appends data to path. NOT atomic with respect to other writers. Pass WithLocked for advisory-flock-protected append on POSIX; on Windows, O_APPEND is already serialized by the OS.

Creates the file with WithPerm (default 0o644) if missing.

func AppendString

func AppendString(path, s string, opts ...WriteOption) error

AppendString is shorthand for Append(path, []byte(s), opts...).

func Apply

func Apply(p *Plan, journalDir string, opts ...ApplyOption) error

Apply runs the plan, writing a journal under journalDir so an interrupted apply can be resumed. journalDir must be an empty or nonexistent directory; Apply refuses to overwrite an existing journal.

On any per-op failure, Apply returns the error without rolling back. The journal records progress so the caller can decide whether to Rollback or Resume.

Apply, Resume, and Rollback acquire an advisory Lock on <journalDir>/.lock for the duration of their work, so concurrent same-journal callers serialize automatically. Independent journals are independent.

Example

A Plan records intended filesystem mutations. Diff renders a preview for `--dry-run`; Apply executes the plan against the filesystem and writes a journal so an interrupted run can be resumed or rolled back.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "plan-example-*")
	defer func() { _ = cleanup() }()
	target := filepath.Join(dir, "out", "hello.txt")
	jdir := filepath.Join(dir, "journal")

	p := fs.NewPlan().Create(target, []byte("hi"), 0o644)
	_ = p.Diff() // render preview for --dry-run (output omitted; path is temp)

	if err := fs.Apply(p, jdir); err != nil {
		return
	}
	fmt.Println("applied")
}
Output:
applied

func ApplyTransient

func ApplyTransient(p *Plan, opts ...ApplyOption) error

ApplyTransient runs the plan in-memory, with no on-disk journal, no backup snapshots, and no resume/rollback support. The first op's failure surfaces as the return; subsequent ops are skipped.

Use this when sequenced execution is wanted without the journal overhead. For executions that must be resumable or reversible, use Apply against a journal directory.

ApplyTransient has no internal locking. Concurrent calls touching the same on-disk files race exactly as the underlying WriteFile / os.Remove / os.Rename would.

func Atime

func Atime(path string) (time.Time, error)

Atime returns path's access time.

func BTime

func BTime(path string) (time.Time, error)

BTime returns ErrNotSupported on Linux. Birth time is reported by the statx(2) syscall introduced in kernel 4.11+, but stdlib's syscall package does not expose statx. Callers needing birth time on Linux can fall back to a third-party syscall wrapper.

func Base

func Base(path string) string

Base wraps filepath.Base.

func BinaryPath

func BinaryPath() (string, error)

BinaryPath returns the absolute, symlink-resolved path of the running executable. Wraps os.Executable + filepath.EvalSymlinks.

func CacheDir

func CacheDir() (string, error)

CacheDir returns the user's per-user cache directory.

Linux/BSD: $XDG_CACHE_HOME or ~/.cache
macOS:     ~/Library/Caches
Windows:   %LOCALAPPDATA%

func Chdir

func Chdir(path string) error

Chdir changes the process's current working directory.

Process-global state: cwd is shared across every goroutine, not goroutine-local. Concurrent callers race; this function is NOT safe to use from tests that call `t.Parallel()`; a parallel sibling test calling any cwd-relative API (including filepath.Abs on a relative path) will see the wrong directory for the duration of this call. Prefer WithDir for scoped changes, and confine all cwd-mutating code to serial tests.

For code under test that needs a working directory, prefer taking an explicit `dir string` argument over reading process cwd.

func Chmod

func Chmod(path string, mode os.FileMode) error

Chmod wraps os.Chmod with the package's error envelope. The mode argument is interpreted by the OS; only the permission bits (`os.ModePerm`) are honored on POSIX, and on Windows only the read-only bit changes meaningfully.

func ChownRecursive

func ChownRecursive(root string, uid, gid int) error

ChownRecursive walks root and applies uid/gid to every entry, including root itself. Passing -1 for uid or gid leaves that value unchanged on POSIX (matching os.Chown semantics).

On Windows returns ErrNotSupported; POSIX uid/gid does not map cleanly onto NTFS ACLs.

Symlinks are not followed; the link itself has its ownership changed via os.Lchown so cross-mount targets are not affected.

func Clean

func Clean(path string) string

Clean wraps filepath.Clean.

func ConfigDir

func ConfigDir() (string, error)

ConfigDir returns the user's per-user config directory.

Linux/BSD: $XDG_CONFIG_HOME or ~/.config
macOS:     ~/Library/Application Support
Windows:   %APPDATA%

func CopyDir

func CopyDir(src, dst string, opts ...CopyOption) error

CopyDir recursively copies src to dst. dst is created if missing (mode mirrors src). Symlinks are recreated as symlinks unless WithFollowSymlinks. Per-entry errors aggregate into a *MultiError; the walk continues so a partial copy surfaces every problem entry.

WithFilter skips entries (and their subtrees, for directories) when the predicate returns false.

Example

CopyDir recursively copies a tree. Per-entry errors aggregate into a *fs.MultiError; the walk continues so a partial copy surfaces every problem entry rather than the first.

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sort"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	for _, p := range []string{"a.txt", "sub/b.txt", "sub/deep/c.txt"} {
		_ = os.MkdirAll(filepath.Dir(filepath.Join(src, p)), 0o755)
		_ = fs.WriteFile(filepath.Join(src, p), []byte(p))
	}
	if err := fs.CopyDir(src, dst); err != nil {
		log.Fatal(err)
	}

	var got []string
	_ = filepath.WalkDir(dst, func(path string, d os.DirEntry, _ error) error {
		if d.IsDir() {
			return nil
		}
		rel, _ := filepath.Rel(dst, path)
		got = append(got, filepath.ToSlash(rel))
		return nil
	})
	sort.Strings(got)
	for _, p := range got {
		fmt.Println(p)
	}
}
Output:
a.txt
sub/b.txt
sub/deep/c.txt

func CopyFile

func CopyFile(src, dst string, opts ...CopyOption) error

CopyFile copies src to dst. The copy is atomic: bytes go to a temp file in dst's parent, then the temp is renamed over dst. Source mode and (by default) mtime are preserved on dst.

If src is a symlink and WithFollowSymlinks is false (default), the symlink itself is recreated at dst; the link target is not dereferenced.

Non-regular, non-symlink sources (devices, FIFOs, sockets) error with ErrNotSupported.

Example

CopyFile copies one file atomically: bytes go to a temp file in the destination's parent, then the temp is renamed over the destination. Source mode (and, by default, mtime) are preserved.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	src := filepath.Join(tmp, "src")
	dst := filepath.Join(tmp, "dst")
	_ = fs.WriteFile(src, []byte("payload"))

	if err := fs.CopyFile(src, dst); err != nil {
		log.Fatal(err)
	}
	got, _ := fs.ReadFile(dst)
	fmt.Println(string(got))
}
Output:
payload

func CreateArchive

func CreateArchive(w io.Writer, root string, opts ...ArchiveCreateOption) error

CreateArchive walks root and writes every regular file and directory entry to w as an archive in the format selected by WithArchiveFormat (default ArchiveFormatTar).

WithArchiveCreateFilter skips entries when its predicate returns false.

func CreateArchiveFile

func CreateArchiveFile(path, root string, opts ...ArchiveCreateOption) error

CreateArchiveFile is shorthand for creating a file at path and calling CreateArchive. The file is closed on return.

Example

CreateArchiveFile writes a tar / tar.gz / zip archive from a directory tree. Pass fs.WithArchiveFormat to choose the container; fs.WithArchiveCreateFilter to skip entries.

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	src := filepath.Join(tmp, "tree")
	_ = os.MkdirAll(src, 0o755)
	_ = fs.WriteFile(filepath.Join(src, "keep.txt"), []byte("ok"))
	_ = fs.WriteFile(filepath.Join(src, "skip.tmp"), []byte("scratch"))

	out := filepath.Join(tmp, "out.zip")
	err := fs.CreateArchiveFile(out, src,
		fs.WithArchiveFormat(fs.ArchiveFormatZip),
		fs.WithArchiveCreateFilter(func(p string, _ os.FileInfo) bool {
			return filepath.Ext(p) != ".tmp"
		}))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(fs.Exists(out))
}
Output:
true

func Ctime

func Ctime(path string) (time.Time, error)

Ctime returns path's status-change time.

func Cwd

func Cwd() (string, error)

Cwd returns the process's current working directory. Wraps os.Getwd with the package's error envelope.

func DataDir

func DataDir() (string, error)

DataDir returns the user's per-user data directory.

Linux/BSD: $XDG_DATA_HOME or ~/.local/share
macOS:     ~/Library/Application Support
Windows:   %APPDATA%

func Dir

func Dir(path string) string

Dir wraps filepath.Dir.

func DirSize

func DirSize(ctx context.Context, path string) (int64, error)

DirSize sums the sizes of all regular files under path. Symlinks are not followed; non-regular entries (devices, FIFOs, sockets) are counted as zero. ctx is checked between entries; cancellation returns the partial total and ctx.Err().

func EnsureDir

func EnsureDir(path string, perm os.FileMode, opts ...MkdirOption) (created bool, err error)

EnsureDir ensures path exists as a directory, creating it (and any missing parents) when absent. perm is applied to newly-created components; pass WithEnforcePerm to chmod the chain. Returns created=true when the directory did not exist at call time.

Under concurrent calls on a missing path, multiple callers may each see created=true since the existence check and creation aren't atomic; os.MkdirAll itself is idempotent so no caller errors, but the created flag's accuracy degrades under races. For strict "exactly one creator wins" semantics, use Mkdir + detect ErrAlreadyExists.

func EnsureFile

func EnsureFile(path string, defaults []byte, opts ...WriteOption) (created bool, err error)

EnsureFile ensures path exists, writing defaults if it doesn't. Returns created=true when the file was created by this call, false when it already existed.

Concurrent callers race-safely: the underlying WriteFileExclusive uses `O_EXCL`, so exactly one caller writes the defaults; the rest observe `created=false`. The first caller's content wins; later callers do not overwrite.

All [WriteOption]s are honored; set WithMkdirAll to create parents, WithPerm to override the file mode, etc.

func EnsurePerm

func EnsurePerm(path string, mode os.FileMode) error

EnsurePerm chmods path to mode if the current permission bits don't already match. A path that already has the desired mode is a no-op (no syscall, no error). Useful in idempotent setup routines.

Only the permission bits (`os.ModePerm`) are compared; type bits (directory, symlink, etc.) on the existing inode are ignored.

func EqualPath

func EqualPath(a, b string) bool

EqualPath reports whether a and b refer to the same path after Clean. Comparison is case-insensitive on Windows (mirroring NTFS default behavior).

func EvalSymlinks(path string) (string, error)

EvalSymlinks resolves all symlinks in path and returns the canonical absolute path. Wraps filepath.EvalSymlinks. A symlink loop or excessive hop count returns ErrSymlinkLoop.

For constrained resolution that won't escape a parent directory, use EvalSymlinksWithin.

func EvalSymlinksWithin

func EvalSymlinksWithin(parent, path string) (string, error)

EvalSymlinksWithin resolves all symlinks in path while ensuring the resolved path stays inside parent. Returns ErrEscapesRoot when the resolution leaves the parent.

Both parent and path are themselves passed through filepath.EvalSymlinks before the containment check, so symlinked roots (e.g., macOS's /var to /private/var) compare correctly.

func ExecutableDir

func ExecutableDir() (string, error)

ExecutableDir returns the directory containing the running executable. Symlinks in the path are NOT resolved; use BinaryPath for the symlink-resolved binary path.

func Exists

func Exists(path string) bool

Exists reports whether path is statable. Returns false on permission errors as well as missing paths; callers who need the distinction should use Stat directly.

func Expand

func Expand(path string, opts ...PathOption) (string, error)

Expand resolves ~, ~user, $VAR, and ${VAR} in path. Resolution is purely lexical; the filesystem is consulted only when ~user is supplied and the user must be looked up via os/user.

Expand("~/.config/myapp")        -> "/Users/alice/.config/myapp"
Expand("$HOME/notes")            -> "/Users/alice/notes"
Expand("${XDG_CACHE_HOME}/data") -> "/Users/alice/.cache/data"
Expand("~bob/notes")             -> "/Users/bob/notes"  (or error)

An unset env var expands to "" by default; pass WithStrictExpansion to error instead.

Example

Expand resolves leading ~ and embedded $VAR / ${VAR} references. Resolution is purely lexical; the filesystem is consulted only when a ~user is supplied. Unset variables expand to the empty string by default; pass fs.WithStrictExpansion to error instead. The example uses a literal path so the Output block is deterministic across environments.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	// A path with no expansion markers passes through unchanged.
	got, _ := fs.Expand("/etc/myapp/config.yaml")
	fmt.Println(got)

	// An unset variable, in strict mode, errors instead of
	// silently expanding to "".
	_, err := fs.Expand("$ROTINI_FS_NEVER_SET_VAR/x", fs.WithStrictExpansion())
	fmt.Println(err != nil)
}
Output:
/etc/myapp/config.yaml
true

func Ext

func Ext(path string) string

Ext wraps filepath.Ext.

func ExtFormat

func ExtFormat(path string) string

ExtFormat returns a lowercase format identifier inferred from the extension of path. Returns "" for unrecognized or absent extensions. Multi-extension filenames (e.g., `foo.tar.gz`) are resolved by the LAST extension only, mirroring filepath.Ext; callers that need compound-extension awareness compose with filepath.Ext themselves.

func ExtractArchive

func ExtractArchive(r io.Reader, dst string, opts ...ArchiveExtractOption) error

ExtractArchive auto-detects the format of r and extracts entries under dst. Every entry path is resolved through MustBeChildOf(dst, ...) before any filesystem write; this defends against zip-slip / tar-slip attacks where a crafted entry named `../../etc/passwd` would otherwise escape the extraction root.

Modes are masked to safe defaults (0o644 files, 0o755 dirs) unless WithPreserveMode. Cumulative bytes written are capped by WithArchiveMaxBytes (default 10 GiB); over-cap aborts with ErrArchiveTooLarge.

WithArchiveFilter skips entries when its predicate returns false.

func ExtractArchiveFile

func ExtractArchiveFile(path, dst string, opts ...ArchiveExtractOption) error

ExtractArchiveFile is shorthand for opening path and calling ExtractArchive.

Example

ExtractArchiveFile auto-detects the format (tar / tar.gz / zip) from the file's leading bytes. Every entry passes through fs.MustBeChildOf before any filesystem write; extraction outside dst is impossible by construction (zip-slip / tar-slip defense).

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sort"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	// Build a tiny tar.gz to extract.
	src := filepath.Join(tmp, "src")
	_ = os.MkdirAll(src, 0o755)
	_ = fs.WriteFile(filepath.Join(src, "README.md"), []byte("# example\n"))
	_ = fs.WriteFile(filepath.Join(src, "main.go"), []byte("package main\n"))
	archive := filepath.Join(tmp, "release.tar.gz")
	if err := fs.CreateArchiveFile(archive, src, fs.WithArchiveFormat(fs.ArchiveFormatTarGz)); err != nil {
		log.Fatal(err)
	}

	// Extract elsewhere.
	dst := filepath.Join(tmp, "out")
	if err := fs.ExtractArchiveFile(archive, dst); err != nil {
		log.Fatal(err)
	}

	// List what landed.
	var got []string
	_ = filepath.WalkDir(dst, func(path string, d os.DirEntry, _ error) error {
		if d.IsDir() {
			return nil
		}
		rel, _ := filepath.Rel(dst, path)
		got = append(got, filepath.ToSlash(rel))
		return nil
	})
	sort.Strings(got)
	for _, p := range got {
		fmt.Println(p)
	}
}
Output:
README.md
main.go

func FilesystemType

func FilesystemType(path string) (string, error)

FilesystemType returns a best-effort string identification of the filesystem at path: "ext4", "apfs", "ntfs", "nfs4", etc. Returns "unknown" when the syscall succeeds but the type cannot be classified.

func Find

func Find(root, pattern string, opts ...WalkOption) ([]string, error)

Find returns paths under root whose basename matches pattern (a filepath.Match glob). Paths are returned as absolute paths when root is absolute; relative-to-cwd otherwise. The root entry itself is included if its basename matches.

Honors all [WalkOption]s (WalkSkipHidden, WalkSkipNames, WalkSkipPatterns, WalkMaxDepth, WalkFollowSymlinks, WalkErrorHandler).

func FindByRegex

func FindByRegex(root string, re *regexp.Regexp, opts ...WalkOption) ([]string, error)

FindByRegex is Find using a regexp.Regexp matched against the basename instead of a glob.

func FindFunc

func FindFunc(root string, pred func(path string, info os.FileInfo) bool, opts ...WalkOption) ([]string, error)

FindFunc returns paths under root for which pred returns true. pred receives the walk path (absolute when root is absolute) and the entry's os.FileInfo.

func FindUp

func FindUp(name, startDir string, opts ...FindOption) (string, bool, error)

FindUp walks parent-by-parent from startDir looking for a basename matching name. name is matched as a glob pattern via filepath.Match. Returns the absolute path and ok=true on the first match; ok=false (and no error) when the walk exhausts without finding a match.

The walk is bounded by WithMaxAncestors (default 32) and optionally by WithStopAt.

Example

FindUp walks parent directories looking for a file with the given glob pattern. Useful for tools that need a project-relative config file no matter where they're invoked from.

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	leaf := filepath.Join(tmp, "src", "pkg", "internal")
	_ = os.MkdirAll(leaf, 0o755)
	_ = fs.WriteFile(filepath.Join(tmp, ".env"), nil)

	got, ok, err := fs.FindUp(".env", leaf)
	if err != nil {
		log.Fatal(err)
	}
	rel, _ := filepath.Rel(tmp, got)
	fmt.Printf("found=%v rel=%s\n", ok, filepath.ToSlash(rel))
}
Output:
found=true rel=.env

func FindUpAll

func FindUpAll(name, startDir string, opts ...FindOption) ([]string, error)

FindUpAll returns every match from startDir up to filesystem root. Order is leaf-to-root: the closest match comes first.

func FirstExisting

func FirstExisting(paths []string) (string, bool)

FirstExisting returns the first path in paths that exists. Useful for fallback chains like `["./config.yaml", "~/.myapp/config.yaml", "/etc/myapp/config.yaml"]`. Permission errors on a candidate are folded into "doesn't exist" (see Exists), so the walk continues to the next candidate.

func FormatBytes

func FormatBytes(n int64) string

FormatBytes formats n as a human-readable IEC string ("1.5 GiB"). Negative values are formatted with a leading minus. Values below 1024 are returned as raw byte counts ("999 B"). Output uses one decimal place for non-integer mantissas.

Example

FormatBytes renders an integer byte count as an IEC string.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	for _, n := range []int64{999, 1024, 1500, 5 << 20, 3 << 30} {
		fmt.Printf("%d -> %s\n", n, fs.FormatBytes(n))
	}
}
Output:
999 -> 999 B
1024 -> 1 KiB
1500 -> 1.5 KiB
5242880 -> 5 MiB
3221225472 -> 3 GiB

func FormatError

func FormatError(err error, color ...bool) string

FormatError renders err into a human-readable string. *MultiError renders each branch on its own line; *PathError renders as "fs: <op> <path>: <cause>". Pass color=true to wrap each message in ANSI bold-red. Returns "" when err is nil.

func FromSlash

func FromSlash(path string) string

FromSlash wraps filepath.FromSlash.

func Glob

func Glob(pattern string, opts ...PathOption) ([]string, error)

Glob returns paths matching pattern after applying Expand (`~`/`$VAR` resolution) to the pattern. Pattern syntax is filepath.Match. A pattern with no matches returns (nil, nil); no-match is not an error, mirroring stdlib's filepath.Glob.

Honors WithStrictExpansion via PathOption.

func GlobAny

func GlobAny(patterns []string, opts ...PathOption) ([]string, error)

GlobAny returns the deduplicated union of Glob over each pattern. Order: first occurrence of each match is preserved across patterns. On error from any pattern, the partial union accumulated so far is returned alongside the error.

func Hardlink(target, linkPath string) error

Hardlink creates a hard link at linkPath pointing to target. Idempotent: if linkPath already refers to the same inode as target, returns nil. If linkPath exists but is a different file, returns ErrAlreadyExists. Cross-device links surface as ErrCrossDevice.

target must exist and be a regular file (most filesystems forbid hard-linking directories).

func Hash

func Hash(path string, algo HashAlgo) (string, error)

Hash streams path through algo and returns the hex-encoded digest. Empty files hash to algo's zero-length digest (e.g., SHA-256: `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`).

Example

Hash streams a file through the chosen algorithm and returns the hex-encoded digest. The zero value of fs.HashAlgo is fs.HashSHA256, which is the package default.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "in")
	_ = fs.WriteFile(path, []byte("abc"))

	digest, err := fs.Hash(path, fs.HashSHA256)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(digest)
}
Output:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad

func HashCompare

func HashCompare(path, expected string, algo HashAlgo) error

HashCompare returns nil if path's hash matches expected (hex). Comparison is constant-time via crypto/subtle.ConstantTimeCompare ; protects integrity-check call sites against timing oracles. Returns ErrHashMismatch on mismatch.

expected may be upper or lower case; case is normalized by the hex-decoding step. A malformed expected string returns a wrapped hex-decode error.

Example

HashCompare returns fs.ErrHashMismatch when a file's digest differs from the expected hex string. The comparison is constant-time via crypto/subtle.ConstantTimeCompare.

package main

import (
	"errors"
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "in")
	_ = fs.WriteFile(path, []byte("abc"))

	// Correct digest.
	if err := fs.HashCompare(path, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", fs.HashSHA256); err != nil {
		log.Fatal(err)
	}
	fmt.Println("first: match")

	// Tampered digest.
	err := fs.HashCompare(path, "0000000000000000000000000000000000000000000000000000000000000000", fs.HashSHA256)
	if errors.Is(err, fs.ErrHashMismatch) {
		fmt.Println("second: mismatch")
	}
}
Output:
first: match
second: mismatch

func Home

func Home() (string, error)

Home returns the current user's home directory. Wraps os.UserHomeDir.

func IsAbs

func IsAbs(path string) bool

IsAbs wraps filepath.IsAbs.

func IsBlockDevice

func IsBlockDevice(path string) bool

IsBlockDevice reports whether path exists and is a block-special file. Returns false on Windows where the file type doesn't apply.

func IsCaseInsensitiveFS

func IsCaseInsensitiveFS(path string) (bool, error)

IsCaseInsensitiveFS reports whether path lives on a case-insensitive volume. Probe-based: writes a temp file with a known case in the parent directory and checks whether the inverse case resolves to the same inode. Best-effort; a probe error returns (false, error).

func IsCharDevice

func IsCharDevice(path string) bool

IsCharDevice reports whether path exists and is a character-special file. Returns false on Windows where the file type doesn't apply.

func IsDir

func IsDir(path string) bool

IsDir reports whether path exists and is a directory. Symlinks to directories are followed.

func IsEmpty

func IsEmpty(path string) (bool, error)

IsEmpty reports whether path is an empty directory. A non-directory returns (ErrNotDir); a missing path returns the wrapped ErrNotFound.

func IsExecutable

func IsExecutable(path string) bool

IsExecutable reports whether path exists and is executable for the calling process. Uses access(2) with X_OK so the answer reflects effective uid/gid and ACLs, not just the mode bits.

func IsFIFO

func IsFIFO(path string) bool

IsFIFO reports whether path exists and is a named pipe (FIFO). Returns false on Windows where the file type doesn't apply.

func IsFile

func IsFile(path string) bool

IsFile reports whether path exists and is a regular file. Symlinks to regular files are followed.

func IsLocked

func IsLocked(path string) bool

IsLocked reports whether path currently has an exclusive lock held by another holder. Returns false on any error (path missing, permission denied, etc.) so the predicate is safe to call from log statements without error handling.

func IsNetworkFS

func IsNetworkFS(path string) bool

IsNetworkFS reports whether path lives on a network filesystem (NFS, SMB, FUSE-network, AFP). Used to recommend WithPolling-based watching where kernel notifications are unreliable. A type-detection error is folded to false.

func IsReadable

func IsReadable(path string) bool

IsReadable reports whether the calling process can read path.

func IsReservedName

func IsReservedName(name string) bool

IsReservedName reports whether name (or its base before the last extension) matches a Windows reserved device name. Comparison is case-insensitive. Reserved names: CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9.

On any platform that lacks these reservations, the result is still returned; the predicate is portability-safe rather than platform-conditional, so callers writing files for cross-platform consumption (archive extraction, scaffolding) catch reserved names regardless of the host OS.

func IsSocket

func IsSocket(path string) bool

IsSocket reports whether path exists and is a Unix-domain socket. Returns false on Windows where the file type doesn't apply.

func IsSubpath

func IsSubpath(parent, child string) (bool, error)

IsSubpath reports whether child resolves to a path inside parent after Abs + Clean. Returns (false, nil) for paths outside (never returning a permissive answer on resolution error). Returns (false, err) only on resolution failure.

IsSubpath does not follow symlinks. Use EvalSymlinksWithin when the constraint must hold even after symlink resolution.

func IsSymlink(path string) bool

IsSymlink reports whether path exists and is itself a symlink (not a target of one). Uses os.Lstat.

func IsTerminal

func IsTerminal(f *os.File) bool

IsTerminal reports whether f is connected to a terminal. Useful for callers deciding between human-readable and machine-readable output. The fs package exposes only this primitive; pager invocation, color decisions, and width queries belong in the future rotini/cli framework.

Returns false for nil f.

func IsWritable

func IsWritable(path string) bool

IsWritable reports whether the calling process can write path. For directories this means new entries can be created inside.

func Join

func Join(elem ...string) string

Join wraps filepath.Join.

func JoinSlash

func JoinSlash(elem ...string) string

JoinSlash returns elem joined with `/` regardless of platform. Useful for portable URI-like paths.

func ListDir

func ListDir(path string, opts ...ListOption) ([]stdfs.DirEntry, error)

ListDir reads path and returns its entries. Default order is filesystem order; pass WithSorted for name-sorted output. WithSkipHidden drops dot-prefixed names; WithListFilter applies a custom predicate.

func LongPath

func LongPath(path string) (string, error)

LongPath is a no-op pass-through on POSIX systems; the kernel imposes per-component but no MAX_PATH-style aggregate limit, so no prefix is needed.

func Lstat

func Lstat(path string) (os.FileInfo, error)

Lstat is the symlink-not-following variant of Stat.

func Magic

func Magic(path string, n int) ([]byte, error)

Magic returns the first n bytes of path. Useful for callers that want to inspect a file's leading bytes against known signatures (the package does not enumerate signatures itself; that's thousands of formats; compose with your own table or a third-party magic-bytes library).

If path is shorter than n bytes, the returned slice is shorter than n (no error). n <= 0 returns an empty slice without opening the file.

func Match

func Match(pattern, name string) (bool, error)

Match reports whether name matches the filepath.Match glob pattern. Wraps the stdlib function with the package's error envelope for consistency.

func Mkdir

func Mkdir(path string, perm os.FileMode) error

Mkdir creates a single directory. Returns ErrAlreadyExists if path already exists. Use MkdirAll to silently create-if-missing.

func MkdirAll

func MkdirAll(path string, perm os.FileMode, opts ...MkdirOption) error

MkdirAll creates path and any missing parents. A path that already exists as a directory is not an error. Pass WithEnforcePerm to chmod components this call creates so umask doesn't strip bits.

func MountPoint

func MountPoint(path string) (string, error)

MountPoint returns the absolute mount point of the filesystem containing path. POSIX walks the kernel-supplied mount table (Linux: `/proc/self/mountinfo`; BSD/macOS: `getmntinfo` via `syscall.Statfs`). Windows uses `GetVolumePathName`.

func Move

func Move(src, dst string, opts ...CopyOption) error

Move renames src to dst, falling back to copy+remove when the rename fails because src and dst live on different filesystems (EXDEV on POSIX, ERROR_NOT_SAME_DEVICE on Windows). The fallback is NOT atomic from the caller's perspective: the destination appears after the copy succeeds, the source is removed last.

Honors WithOverwrite. The atomic-rename path overwrites unconditionally on POSIX; a Lstat pre-check enforces [WithOverwrite(false)] before the rename attempt.

func Mtime

func Mtime(path string) (time.Time, error)

Mtime returns path's modification time.

func MustBeChildOf

func MustBeChildOf(parent, child string) error

MustBeChildOf returns nil when child resolves inside parent, or ErrEscapesRoot otherwise. Use to constrain user-supplied paths to a sandbox.

Example

MustBeChildOf returns nil when child resolves inside parent, or fs.ErrEscapesRoot otherwise. Use to constrain user-supplied paths to a sandbox before any filesystem operation.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	fmt.Println(fs.MustBeChildOf("/var/lib/myapp", "/var/lib/myapp/data/cfg") == nil)
	fmt.Println(fs.MustBeChildOf("/var/lib/myapp", "/var/lib/myapp/../etc/passwd") == nil)
}
Output:
true
false

func NormalizeLineEndings

func NormalizeLineEndings(b []byte, target LineEnding) []byte

NormalizeLineEndings rewrites every CR / LF / CRLF in b as target. A final line without a terminator is preserved as-is.

target = LineNone is a no-op (returns b unchanged).

func OpenAt

func OpenAt(dir *os.File, name string, flag int, perm os.FileMode) (*os.File, error)

OpenAt opens name relative to dir. On POSIX it uses the `openat(2)` syscall, which resolves name through dir's underlying inode rather than re-walking the path; this defends against directory-replace races where an attacker swaps a directory for a symlink between calls.

On Windows, where there is no native `openat` equivalent in the stable API surface, the implementation falls back to filepath.Join + os.OpenFile. The fallback is NOT race-safe; callers that need TOCTOU resistance on Windows must use other hardening (e.g., transactional NTFS, locked parent directories).

dir must be non-nil. name is interpreted as a relative path component; absolute paths are ignored by the underlying syscall.

func OpenAutoArchive

func OpenAutoArchive(path string) (io.ReadCloser, error)

OpenAutoArchive opens path, sniffs its leading bytes, and returns a transparent decompressor. Currently supports gzip (`.gz`); a plain (uncompressed) input is returned as-is. Other codecs (xz, zstd, lz4) are deferred to post-v0.1 because none ship in stdlib.

func OpenChunked

func OpenChunked(path string, size int) (iter.Seq2[[]byte, error], func() error, error)

OpenChunked returns an iterator that streams path's contents in chunks of size bytes. The iterator stops at EOF; callers can break early. The returned cleanup function closes the underlying file and is idempotent; safe to call multiple times (e.g., once via `defer` and again explicitly).

If size <= 0, a 64 KiB default is used.

func OpenLines

func OpenLines(path string, opts ...ReadOption) (iter.Seq2[string, error], func() error, error)

OpenLines returns an iterator over path's lines. Trailing line endings are stripped per line. The caller MUST invoke the returned close function to release the file handle; the close function is idempotent and safe to call from a `defer`.

The iterator yields (line, nil) for each successfully read line, and (zero-value, err) once at the end if the underlying scanner failed (line too long, mid-stream I/O error, etc.). Callers should always check the error in the loop body:

seq, closeFn, err := fs.OpenLines("/tmp/log")
if err != nil { return err }
defer closeFn()
for line, err := range seq {
    if err != nil {
        return err
    }
    // process line
}
Example

OpenLines returns an iterator. The second value is non-nil only if the underlying scanner errors mid-stream (line too long, I/O failure, etc.); always check it inside the loop.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "log")
	_ = fs.WriteFile(path, []byte("one\ntwo\nthree\n"))

	seq, closeFn, err := fs.OpenLines(path)
	if err != nil {
		log.Fatal(err)
	}
	defer closeFn()

	for line, lerr := range seq {
		if lerr != nil {
			log.Fatal(lerr)
		}
		fmt.Println(line)
	}
}
Output:
one
two
three

func OpenNoFollow

func OpenNoFollow(path string, flag int, perm os.FileMode) (*os.File, error)

OpenNoFollow opens path without following a symlink at the final component. If the final component IS a symlink, the call returns ErrSymlinkLoop. Defends against link-replace attacks where an attacker swaps the target between a stat and an open.

On POSIX this is implemented via `O_NOFOLLOW`, which atomically fails the open with `ELOOP` when the final component is a symbolic link.

On Windows the implementation opens the path with FILE_FLAG_OPEN_REPARSE_POINT and then inspects the resulting handle's attributes; if FILE_ATTRIBUTE_REPARSE_POINT is set (covers symbolic links, junctions, and mount points; every link-like reparse Windows surfaces) the handle is closed and ErrSymlinkLoop is returned. This is not strictly atomic the way POSIX `O_NOFOLLOW` is, but the handle pins the inode that was resolved at open time, so the attribute query reflects that inode, not whatever sat at the path afterward.

Intermediate components are still resolved normally; if `/a/b` is a symlink and you open `/a/b/c`, the symlink at `b` is followed but the final component `c` is not.

The flag and perm arguments mirror os.OpenFile.

func OpenStdinLines

func OpenStdinLines() iter.Seq[string]

OpenStdinLines returns an iterator over os.Stdin lines. Trailing line endings (LF / CRLF / CR) are stripped per line, per bufio.Scanner.Text. The iterator continues until EOF.

Stdin can't be reopened, so the returned iterator can only be consumed once per process. There is no cleanup function; the caller's lifecycle owns os.Stdin.

func OpenWrite

func OpenWrite(path string, opts ...WriteOption) (*os.File, func() error, error)

OpenWrite opens path for streaming writes. The first return is the open file the caller writes through; the second is a finalize function that closes the file and atomically renames the underlying temp file over path. Until finalize is called the destination is unchanged.

If finalize is not called (or returns an error), the temp file is removed on the second call so the destination is never affected.

Finalize is idempotent: calling it twice returns the same result the first call did (success returns nil; failure returns the original error).

func Owner

func Owner(path string) (uid, gid int, err error)

Owner returns path's POSIX uid and gid.

func ParseBytes

func ParseBytes(s string) (int64, error)

ParseBytes parses a human-readable size string with strict SI semantics: `KB` = 1000, `MB` = 1000000, etc. IEC binary units (`KiB`, `MiB`, ...) keep their canonical 1024-based meaning.

This is the same convention `kubectl`, `docker`, `kafka`, and most other modern CLI tools use. For the legacy "disk-vendor" idiom where bare `KB` means 1024, see ParseBytesIEC.

Accepted units (case-insensitive):

bare / B               -> 1
K / KB                 -> 1000          KiB -> 1024
M / MB                 -> 1000²         MiB -> 1024²
G / GB                 -> 1000³         GiB -> 1024³
T / TB                 -> 1000⁴         TiB -> 1024⁴
P / PB                 -> 1000⁵         PiB -> 1024⁵
E / EB                 -> 1000⁶         EiB -> 1024⁶

Whitespace between the number and the unit is optional. A bare number (no unit) is treated as bytes. Decimal mantissas are supported ("1.5GB" to 1500000000).

Example

ParseBytes uses strict SI semantics by default: bare KB / MB / GB / etc. are 1000-based (matching kubectl, docker, kafka). IEC binary units (KiB, MiB, ...) keep their 1024-based meaning.

Use fs.ParseBytesIEC when interoperating with the legacy disk-vendor idiom where bare KB means 1024.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	for _, in := range []string{"1024", "1KiB", "1.5MiB", "10GB"} {
		n, _ := fs.ParseBytes(in)
		fmt.Printf("%s -> %d\n", in, n)
	}
}
Output:
1024 -> 1024
1KiB -> 1024
1.5MiB -> 1572864
10GB -> 10000000000

func ParseBytesIEC

func ParseBytesIEC(s string) (int64, error)

ParseBytesIEC is ParseBytes but with the legacy "disk-vendor" idiom where bare `KB` / `MB` / `GB` mean powers of 1024 (so `1KB == 1024`). IEC-suffixed units (`KiB`, `MiB`, ...) still mean their canonical 1024-based values; they're 1024-based either way.

Use this when interoperating with tools that quote disk sizes in the old `KB == 1024` convention. New code should prefer ParseBytes.

Example

ParseBytesIEC keeps the legacy "bare KB / MB / GB are 1024-based" idiom for callers that need it. IEC-suffixed units (KiB, MiB, ...) are 1024-based in both variants.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	for _, in := range []string{"1KB", "1MB", "1GiB"} {
		n, _ := fs.ParseBytesIEC(in)
		fmt.Printf("%s -> %d\n", in, n)
	}
}
Output:
1KB -> 1024
1MB -> 1048576
1GiB -> 1073741824

func PreflightSpace

func PreflightSpace(path string, requiredBytes int64) error

PreflightSpace returns nil if the filesystem containing path has at least requiredBytes available; otherwise ErrInsufficientSpace. Use before large copies, archive extractions, downloads, etc.

requiredBytes <= 0 is a no-op (returns nil).

func ProcessStartTime

func ProcessStartTime(pid int) (string, error)

ProcessStartTime returns a stable, opaque string identifying the start instant of the OS process with the given PID. Two processes that reuse the same PID produce different start-time strings, so comparing fingerprints against the same PID at two different moments reliably distinguishes the original process from a recycled one.

Intended as the canonical argument to WithPIDLockFingerprint:

h, err := fs.PIDLock(path, fs.WithPIDLockFingerprint(fs.ProcessStartTime))

The returned string is opaque; callers should not parse it.

Platform implementations:

  • Linux: parses `starttime` (field 22) from /proc/<pid>/stat. The value is jiffies-since-boot.
  • Darwin / FreeBSD: shells out to `ps -o lstart=`.
  • Windows: GetProcessTimes via kernel32.dll for the creation FILETIME (100-nanosecond ticks since 1601 UTC), rendered in decimal.

Returns an empty string and a non-nil error if the OS-specific probe fails. Callers using this with WithPIDLockFingerprint can treat an error or empty result as "no fingerprint"; the bare PID-alive probe still defends against the dead-PID case.

func ProjectRoot

func ProjectRoot(startDir string, opts ...FindOption) (string, error)

ProjectRoot returns the first ancestor of startDir that contains any of the project markers (default `.git`, `go.mod`, `package.json`, `Cargo.toml`; override with WithProjectMarkers). Errors with ErrNotFound when the walk exhausts without a match.

Every call walks the filesystem; no caching is performed. The per-call cost is bounded by WithMaxAncestors (default 32 stat calls in the worst case). Callers that hit this hot in a loop; typically long-running daemons that resolve thousands of paths; should cache the result themselves with whatever invalidation policy fits the embedding application; a process-global cache here cannot know when a project layout changes underneath it.

Example

ProjectRoot returns the first ancestor of startDir that contains one of the project markers (default: .git, go.mod, package.json, Cargo.toml). Errors with fs.ErrNotFound when nothing matches.

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	_ = os.MkdirAll(filepath.Join(tmp, ".git"), 0o755)
	deep := filepath.Join(tmp, "src", "pkg", "internal")
	_ = os.MkdirAll(deep, 0o755)

	root, err := fs.ProjectRoot(deep)
	if err != nil {
		log.Fatal(err)
	}
	rel, _ := filepath.Rel(tmp, root)
	fmt.Println("rel:", filepath.ToSlash(rel))
}
Output:
rel: .

func ReadAt

func ReadAt(path string, offset int64, n int, opts ...ReadOption) ([]byte, error)

ReadAt reads n bytes from path starting at offset. By default returns ErrShortRead if fewer than n bytes are available; pass WithAllowShort to return whatever was readable.

func ReadFile

func ReadFile(path string, opts ...ReadOption) ([]byte, error)

ReadFile reads path and returns its contents. Bounded by WithMaxSize (default DefaultMaxReadSize); a file exceeding the cap returns ErrFileTooLarge.

For sources whose size cannot be determined via Stat (FIFOs, character devices), the cap is enforced through an io.LimitReader at cap+1, so over-size streams still error rather than read forever.

Example

ReadFile reads a whole file with a default 100 MiB cap. A file larger than the cap errors with fs.ErrFileTooLarge rather than allocating unbounded memory.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "in")
	_ = fs.WriteFile(path, []byte("hello world"))

	data, err := fs.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data))
}
Output:
hello world

func ReadFileMax

func ReadFileMax(path string, maxSize int64) ([]byte, error)

ReadFileMax is shorthand for ReadFile(path, WithMaxSize(maxSize)).

func ReadFirstLine

func ReadFirstLine(path string, opts ...ReadOption) (string, error)

ReadFirstLine returns the first line of path. Returns ErrEmptyFile when the file is empty.

Example

ReadFirstLine returns just the first line; useful for shebangs, version stamps, single-value config files.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "VERSION")
	_ = fs.WriteFile(path, []byte("v1.2.3\nuntracked details\n"))

	v, err := fs.ReadFirstLine(path)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(v)
}
Output:
v1.2.3

func ReadLines

func ReadLines(path string, opts ...ReadOption) ([]string, error)

ReadLines reads path as a sequence of lines. The trailing line ending (LF, CRLF, or bare CR) is stripped from each line; a leading UTF-8 BOM is stripped from the file. Bounded by WithMaxSize.

Example

ReadLines reads a file into a string slice, one element per line. Trailing line endings (LF, CRLF, or bare CR) are stripped; a leading UTF-8 BOM is removed.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "log")
	_ = fs.WriteFile(path, []byte("first\nsecond\nthird\n"))

	lines, err := fs.ReadLines(path)
	if err != nil {
		log.Fatal(err)
	}
	for _, line := range lines {
		fmt.Println(line)
	}
}
Output:
first
second
third
func ReadLink(linkPath string) (string, error)

ReadLink returns the target stored in the symlink at linkPath. Wraps os.Readlink.

func ReadStdin

func ReadStdin(opts ...ReadOption) ([]byte, error)

ReadStdin reads os.Stdin to EOF, capped by WithMaxSize (default DefaultMaxReadSize). Useful for `cat | mytool` patterns where the input has no path.

A stdin stream exceeding the cap returns ErrFileTooLarge.

func RegisterProjectKind

func RegisterProjectKind(kind ProjectKind, markers ...string)

RegisterProjectKind adds custom marker filenames for a ProjectKind so ProjectType recognizes them in addition to the built-in set.

Safe to call from any goroutine. Registrations are additive (built-in markers are not replaced) and persist for the lifetime of the process. Duplicate (kind, marker) pairs are deduplicated.

func Rel

func Rel(base, target string) (string, error)

Rel wraps filepath.Rel.

func Remove

func Remove(path string, opts ...RemoveOption) error

Remove removes path. Idempotent: a missing path returns nil. Pass WithStrict to error on missing paths (mirroring stdlib's os.Remove).

For directories, only empty directories succeed; use RemoveAll for recursive removal.

func RemoveAll

func RemoveAll(path string, opts ...RemoveOption) error

RemoveAll is the recursive variant of Remove. Idempotent by default. With WithStrict enabled, a missing target errors with ErrNotFound.

func RemoveAllNoFollow

func RemoveAllNoFollow(path string) error

RemoveAllNoFollow recursively removes path, refusing to traverse symlinks. Unlike os.RemoveAll, which follows symlinks during descent, this variant uses os.Lstat at every step so a symlinked directory under path is unlinked (the link itself) but its target subtree is left untouched.

Use this for security-sensitive cleanup where an attacker may have planted a symlink under the target expecting RemoveAll to wipe out a victim directory elsewhere on the filesystem.

Missing paths are not errors (idempotent removal). Permission errors and other syscall failures abort with the first error.

func RemoveContents

func RemoveContents(path string, opts ...RemoveOption) error

RemoveContents removes every entry under path but leaves path itself in place. Errors aggregate into a *MultiError so a partial failure surfaces every problem entry rather than the first.

Idempotent on a missing path by default (returns nil); pass WithStrict to error instead. A path that exists but is not a directory always errors with ErrNotDir, regardless of the strict flag.

func Rename

func Rename(src, dst string, opts ...CopyOption) error

Rename renames src to dst via os.Rename. Strict: a cross-device rename returns the underlying error (typically ErrCrossDevice / EXDEV) without falling back. Use Move when you want the copy+remove fallback. opts is accepted for signature symmetry but unused; rename is atomic only within a single filesystem.

func RestoreVersion

func RestoreVersion(path, versionPath string, opts ...VersionedOption) error

RestoreVersion writes the contents of versionPath to path. The current contents of path (if any) are first saved as a new versioned backup so the restore itself is reversible.

versionPath does not have to be a file produced by WriteFileVersioned; any readable file works. The version file is read into memory bounded by WithVersionsMaxBytes (default 100 MiB).

func Resume

func Resume(journalDir string, opts ...ApplyOption) error

Resume continues a previously-interrupted apply from its journal. If the apply already completed, Resume is a no-op and returns nil. See Apply for the concurrency contract.

func Rollback

func Rollback(journalDir string, opts ...ApplyOption) error

Rollback reverts the operations recorded in the journal in reverse order. Each op's stored backup is used to restore the original state. After successful rollback, the journal directory is left in place. See Apply for the concurrency contract.

func RuntimeDir

func RuntimeDir() (string, error)

RuntimeDir returns the user's runtime directory.

Linux/BSD: $XDG_RUNTIME_DIR (no fallback per XDG spec; errors with
           [ErrNotSupported] when unset).
macOS:     ~/Library/Caches/TemporaryItems
Windows:   %LOCALAPPDATA%\Temp

func SameDevice

func SameDevice(a, b string) (bool, error)

SameDevice reports whether a and b live on the same filesystem.

func SameFile

func SameFile(a, b string) (bool, error)

SameFile reports whether a and b refer to the same file (same dev+inode on POSIX, same volume serial + file index on Windows). Wraps os.SameFile with path-string ergonomics.

func SanitizeFilename

func SanitizeFilename(name string) string

SanitizeFilename returns name with characters illegal on Windows or POSIX stripped, suitable for use as a basename anywhere. The result is the input minus:

  • ASCII control bytes (NUL through 0x1F)
  • The Windows-illegal characters: < > : " | ? * / \
  • Trailing dots and spaces (Windows trims them silently, leading to surprising path resolution)

If the cleaned stem matches a Windows reserved device name (CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9), regardless of case or extension, an underscore is inserted IMMEDIATELY AFTER the stem (before any extension): "CON" to "CON_", "CON.txt" to "CON_.txt". This ensures the result no longer parses as a reserved device name on Windows; Windows recognizes the device by stem, so suffixing the whole filename ("CON.txt_") leaves the file still reserved.

An empty cleaned result falls back to `_`. SanitizeFilename does NOT validate length; Windows MAX_PATH (260) and component-length limits are filesystem-dependent; callers constrain those separately if they matter.

Example

SanitizeFilename strips characters that would be illegal in a filename on Windows or POSIX. When the cleaned stem matches a Windows reserved device name, an underscore is inserted before the extension; "CON.txt" becomes "CON_.txt", not "CON.txt_".

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	fmt.Println(fs.SanitizeFilename("report: q4 / 2024.pdf"))
	fmt.Println(fs.SanitizeFilename("CON.txt"))
	fmt.Println(fs.SanitizeFilename("trailing-space . "))
}
Output:
report q4  2024.pdf
CON_.txt
trailing-space

func ScaffoldApply

func ScaffoldApply(src stdfs.FS, dst string, vars any, opts ...ScaffoldOption) error

ScaffoldApply walks src, renders templates with vars, and writes the rendered tree under dst. Conflict policy is configured via WithScaffoldOnConflict. By default existing destinations are kept (ScaffoldSkipExisting); a re-run on a previously-applied scaffold is therefore a no-op.

Example

ScaffoldApply walks an embedded template tree, renders every path and file contents through text/template with vars, and writes the rendered tree under dst. Default conflict policy is SkipExisting; a re-run on a previously-applied scaffold is a no-op (idempotent).

In real code the source io/fs.FS is typically an embed.FS; the example uses testing/fstest.MapFS to keep it self-contained.

package main

import (
	"fmt"
	"log"
	"path/filepath"
	"sort"
	"testing/fstest"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	src := fstest.MapFS{
		"README.md":             {Data: []byte("# {{.Name}}\n\nA project for {{.Owner}}.\n")},
		"src/{{.Name}}/main.go": {Data: []byte("package {{.Name}}\n")},
	}
	vars := struct{ Name, Owner string }{"myapp", "alice"}

	if err := fs.ScaffoldApply(src, tmp, vars); err != nil {
		log.Fatal(err)
	}

	// Print the rendered tree.
	var got []string
	for _, p := range []string{"README.md", "src/myapp/main.go"} {
		data, err := fs.ReadFile(filepath.Join(tmp, p))
		if err != nil {
			log.Fatal(err)
		}
		got = append(got, p+": "+string(data))
	}
	sort.Strings(got)
	for _, line := range got {
		fmt.Print(line)
	}
}
Output:
README.md: # myapp

A project for alice.
src/myapp/main.go: package myapp

func ScaffoldExtract

func ScaffoldExtract(src stdfs.FS, dst string, opts ...ScaffoldOption) error

ScaffoldExtract is a non-templated copy of src into dst. Used for "extract default resources on first run" workflows where templates aren't needed; the source files are written verbatim.

On every call, the package computes a content-hash of src (a stable SHA-256 over sorted-name + content concatenation) and compares it to the marker file at `<dst>/<versionMarker>` (default `.scaffold-version`; override via WithScaffoldVersionMarker):

  • Same hash to no-op.
  • Different hash to re-extract and update the marker.
  • Marker missing to first extract, write marker.

Conflict policy still applies: by default existing destinations are kept. Use ScaffoldOverwriteAll to force re-extracts to replace user edits when the source version changes.

func SetAtime

func SetAtime(path string, t time.Time) error

SetAtime sets path's access time. Mtime is preserved.

func SetLogger

func SetLogger(l *slog.Logger)

SetLogger swaps the package-level slog.Logger used by Apply, Cache, Rotator, and other subsystems. Pass nil to restore the default discard logger. Safe to call from any goroutine.

Note: package debug records include caller-supplied paths (log filenames, plan op paths including ones that may name secret files). Install a logger only after deciding whether the handler is allowed to see those paths; redact via a custom slog.Handler when callers may be untrusted.

The watcher takes a per-instance logger via its WithLogger option and is not affected by this hook.

func SetMtime

func SetMtime(path string, t time.Time) error

SetMtime sets path's modification time. Atime is preserved.

func SetTimes

func SetTimes(path string, atime, mtime time.Time) error

SetTimes sets both atime and mtime in one syscall.

func Split

func Split(path string) (dir, file string)

Split wraps filepath.Split.

func Stat

func Stat(path string) (os.FileInfo, error)

Stat wraps os.Stat with the package's error chain. The returned FileInfo is the stdlib type.

func StateDir

func StateDir() (string, error)

StateDir returns the user's per-user state directory.

Linux/BSD: $XDG_STATE_HOME or ~/.local/state
macOS:     ~/Library/Application Support
Windows:   %LOCALAPPDATA%

func Stem

func Stem(path string) string

Stem returns the basename of path without its final extension.

Stem("foo/bar.tar.gz") == "bar.tar"
Stem("README")         == "README"
Stem(".env")           == ".env"   (no extension)
Example

Stem returns the basename of a path without its final extension. Leading-dot names (like .env or .gitignore) have no stem-vs-ext split.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	fmt.Println(fs.Stem("foo/bar.tar.gz"))
	fmt.Println(fs.Stem("README"))
	fmt.Println(fs.Stem(".env"))
}
Output:
bar.tar
README
.env

func StripUTF8BOM

func StripUTF8BOM(b []byte) []byte

StripUTF8BOM returns b with a leading UTF-8 BOM removed, or b unchanged when no BOM is present. A BOM that does NOT appear at offset 0 is not stripped; it's a real character anywhere else.

func Symlink(target, linkPath string) error

Symlink creates a symbolic link at linkPath pointing to target. Idempotent: if linkPath already exists as a symlink with the same target, returns nil. If linkPath exists but points elsewhere or is not a symlink, returns ErrAlreadyExists.

Concurrent callers: the idempotency check (Readlink to Symlink) is NOT atomic. Between the two syscalls another process can create the link with a different target; the loser of that race sees ErrAlreadyExists. POSIX `symlink(2)` is atomic for the create-if-not-exists case but Go's stdlib does not expose the flag needed to thread that through. For strict "exactly one creator wins" semantics, accept ErrAlreadyExists from one of the concurrent callers as a successful outcome.

The target is stored verbatim in the link; it is not validated, resolved, or required to exist (a "dangling" symlink is allowed, matching os.Symlink).

func SystemConfigDir

func SystemConfigDir(appName string) (string, error)

SystemConfigDir returns the system-wide config directory for app:

Linux/BSD: /etc/<appName>
macOS:     /Library/Application Support/<appName>
Windows:   %PROGRAMDATA%\<appName>

The calling tool is responsible for creating the directory with appropriate permissions and for handling the access-denied case when run unprivileged.

func SystemDataDir

func SystemDataDir(appName string) (string, error)

SystemDataDir is the system-wide analog of DataDir.

Linux/BSD: /var/lib/<appName>
macOS:     /Library/Application Support/<appName>
Windows:   %PROGRAMDATA%\<appName>

func SystemStateDir

func SystemStateDir(appName string) (string, error)

SystemStateDir is the system-wide analog of StateDir.

Linux/BSD: /var/lib/<appName>
macOS:     /Library/Application Support/<appName>
Windows:   %PROGRAMDATA%\<appName>

func SystemTempDir

func SystemTempDir() string

SystemTempDir returns the system temp directory (os.TempDir).

func Tail

func Tail(ctx context.Context, path string, opts ...TailOption) iter.Seq2[string, error]

Tail returns an iterator over lines appended to path. The iterator starts at EOF (or at offset 0 if WithTailFromStart is set) and blocks until new content is written, the file is rotated, or ctx is canceled.

ctx must not be nil. Pass context.Background for an indefinite tail. Following stdlib convention, a nil ctx panics on first poll.

Rotation handling: between reads, Tail stats path and compares against the held file descriptor via os.SameFile. A different inode (logrotate-style rename-and-create) or a held file truncated below the read offset causes a reopen from offset 0.

On ctx.Done(), the iterator terminates without yielding an error. On an unrecoverable IO error (path becomes permanently unreadable after rotation, etc.), the iterator yields (zero-value, err) once and terminates.

Lines are yielded without their trailing newline; both LF and CRLF are stripped. Line length is capped at 1 MiB; longer lines are truncated and yielded as separate chunks.

Usage:

for line, err := range fs.Tail(ctx, "/var/log/foo.log") {
    if err != nil {
        log.Printf("tail: %v", err)
        break
    }
    handle(line)
}
Example

Tail follows path indefinitely, yielding each appended line. Use a context to bound the lifetime; when ctx is cancelled the iterator returns cleanly. Rotation is handled automatically; when the file is renamed and a fresh one appears at path, the iterator picks up the new file from offset 0.

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "tail-example-*")
	defer func() { _ = cleanup() }()
	logfile := filepath.Join(dir, "app.log")
	_ = os.WriteFile(logfile, nil, 0o644)

	// Append a line on a short delay so the example demonstrates the
	// follow-as-it-grows pattern. Bound the whole example with a
	// short timeout so it terminates deterministically.
	go func() {
		time.Sleep(50 * time.Millisecond)
		f, err := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY, 0)
		if err != nil {
			return
		}
		defer func() { _ = f.Close() }()
		_, _ = f.WriteString("hello\n")
	}()

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	for line, err := range fs.Tail(ctx, logfile, fs.WithTailPollInterval(10*time.Millisecond)) {
		if err != nil {
			return
		}
		fmt.Println(line)
		break
	}
}
Output:
hello

func TempDir

func TempDir(dir, pattern string) (string, func() error, error)

TempDir creates a temp directory in dir using pattern (see os.MkdirTemp). dir defaults to os.TempDir when empty. Returns the directory path, a cleanup function, and any error.

The cleanup is os.RemoveAll. Idempotent; repeated calls run the work exactly once.

func TempFile

func TempFile(dir, pattern string) (*os.File, func() error, error)

TempFile creates a temp file in dir using pattern (see os.CreateTemp). dir defaults to os.TempDir when empty. Returns the open file, a cleanup function, and any error.

The cleanup closes the file (best-effort; may already be closed by the caller) and removes it. Cleanup is idempotent; repeated calls run the work exactly once and return the same error from the first invocation; subsequent calls return nil.

func ToSlash

func ToSlash(path string) string

ToSlash wraps filepath.ToSlash.

func Touch

func Touch(path string, opts ...TouchOption) error

Touch creates path with the given mode (default 0o644) if missing, or updates its mtime to "now" if it exists. WithTimes overrides the default "now" behavior.

func Walk

func Walk(root string, fn WalkFunc, opts ...WalkOption) error

Walk visits every entry under root, invoking fn for each. It wraps filepath.WalkDir when WalkFollowSymlinks is false (default) and uses a custom recursive walker that tracks resolved real paths when symlinks are followed.

Filter options (WalkSkipHidden, WalkSkipNames, WalkSkipPatterns) prune subtrees: when a directory matches a skip rule, the entire subtree is omitted.

WalkMaxDepth bounds recursion. WalkErrorHandler intercepts per-entry errors so a single unreadable directory doesn't abort the walk.

Example

Walk traverses a tree; the callback receives every entry. Use fs.WalkSkipNames to prune well-known directories (`.git`, `node_modules`, `.terraform`, etc.); the skipped directory's entire subtree is omitted.

package main

import (
	"fmt"
	stdfs "io/fs"
	"log"
	"os"
	"path/filepath"
	"sort"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	for _, p := range []string{
		"src/main.go",
		".git/HEAD",
		"node_modules/foo/index.js",
		"README.md",
	} {
		_ = os.MkdirAll(filepath.Dir(filepath.Join(tmp, p)), 0o755)
		_ = fs.WriteFile(filepath.Join(tmp, p), []byte{})
	}

	var visited []string
	err := fs.Walk(tmp, func(path string, _ stdfs.DirEntry, werr error) error {
		if werr != nil {
			return werr
		}
		rel, _ := filepath.Rel(tmp, path)
		visited = append(visited, filepath.ToSlash(rel))
		return nil
	}, fs.WalkSkipNames([]string{".git", "node_modules"}))
	if err != nil {
		log.Fatal(err)
	}
	sort.Strings(visited)
	for _, v := range visited {
		fmt.Println(v)
	}
}
Output:
.
README.md
src
src/main.go

func WalkParallel

func WalkParallel(ctx context.Context, root string, fn WalkParallelFunc, workers int) error

WalkParallel walks root concurrently using workers goroutines. Returns the first non-nil error returned by fn; other workers observe the shutdown signal and exit promptly.

workers <= 0 defaults to runtime.NumCPU. The walk is breadth- first within each worker, but global ordering across workers is unspecified; callers needing deterministic order should use Walk.

Cancel ctx to stop the walk early; already-dispatched fn calls complete, but new fn calls and new directory reads are skipped. ctx must not be nil. Pass context.Background for an unbounded walk. Following stdlib convention, a nil ctx panics.

Symlinks are not followed. Callers needing symlink-follow, max-depth, or gitignore filtering should compose Walk with their own work-pool.

Internally the queue is an unbounded slice guarded by a sync.Cond; workers pop the front, directory reads push the back. This avoids the bounded-channel deadlock a fixed-size buffer suffers when any directory has more children than the buffer can hold.

func WarnInsecurePerm

func WarnInsecurePerm(path string, expected os.FileMode) (insecure bool, actual os.FileMode, err error)

WarnInsecurePerm reports whether path's mode permits more access than expected. Returns insecure=true when actual has any permission bit set that isn't in expected; e.g., expected=0o600, actual=0o644 surfaces the surplus group/other read bits as insecure=true. Permission bits LESS permissive than expected are not flagged (a 0o400 file is fine when expected=0o600).

Callers decide what to do; warn the user, refuse to load the file, or repair via Chmod / EnsurePerm. Pairs with `WriteFile(..., WithPerm(fs.Mode0600))` for end-to-end secret-handling discipline.

On Windows where POSIX permission bits don't apply, the result reflects the limited bits Go's os.FileMode surfaces; callers should treat it as advisory.

func WithDir

func WithDir(path string, fn func() error) (err error)

WithDir runs fn with the process's current working directory set to path, then restores the original cwd. Restoration runs even if fn panics (the panic is re-raised after restore).

If fn returns an error, that error is returned. If fn returns nil but the post-fn restore fails, the restore error is returned.

Process-global state: WithDir is NOT safe to use from tests that call `t.Parallel()`. cwd is shared across every goroutine, so a parallel sibling test calling os.Getwd or any cwd-relative API will observe the temporarily-changed directory mid-flight. Confine WithDir usage to tests that run serially, or refactor the code under test to take an explicit working-directory argument.

func WithLock

func WithLock(path string, fn func() error) (err error)

WithLock acquires an exclusive lock on path, runs fn, and releases the lock (including on panic from fn). The lock-acquire error is returned without invoking fn; any Release error joins fn's error via errors.Join.

Example

WithLock is the idiomatic call: acquire, run, release. Release runs even if fn panics. Use it for any operation that needs to coordinate with peer processes against the same lockfile.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "lock-example-*")
	defer func() { _ = cleanup() }()

	lock := filepath.Join(dir, "app.lock")
	err := fs.WithLock(lock, func() error {
		// Critical section: do work that requires mutual exclusion
		// against other processes holding the same lockfile.
		return nil
	})
	fmt.Println("err:", err == nil)
}
Output:
err: true

func WriteAt

func WriteAt(path string, offset int64, data []byte, opts ...WriteOption) error

WriteAt writes data to path starting at offset. Path must exist; the underlying file is opened with O_RDWR. NOT atomic by definition.

func WriteFile

func WriteFile(path string, data []byte, opts ...WriteOption) error

WriteFile writes data to path atomically (write-temp + rename). Default mode 0o644 for new files; existing files preserve their mode unless WithPerm is set. Errors if the parent directory doesn't exist; pass WithMkdirAll to create it.

For files containing secrets (tokens, keys), pass `WithPerm(fs.Mode0600)` so the result is owner-only.

Example

WriteFile writes atomically: bytes go to a temp file in the destination's parent directory, then the temp is renamed over the destination. A reader concurrently observing the path sees either the old contents or the new contents, never a half-written file.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "config.yaml")
	if err := fs.WriteFile(path, []byte("port: 8080\n")); err != nil {
		log.Fatal(err)
	}
	data, _ := fs.ReadFile(path)
	fmt.Print(string(data))
}
Output:
port: 8080
Example (Backup)

WriteFile honors WithBackup: the existing destination is renamed to "<path><suffix>" before the new content is committed.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "config.yaml")
	_ = fs.WriteFile(path, []byte("v1\n"))
	_ = fs.WriteFile(path, []byte("v2\n"), fs.WithBackup(".bak"))

	current, _ := fs.ReadFile(path)
	backup, _ := fs.ReadFile(path + ".bak")
	fmt.Printf("current=%q backup=%q", string(current), string(backup))
}
Output:
current="v2\n" backup="v1\n"
Example (Ensure)

WriteFile + WithMkdirAll creates missing parent directories before the write.

package main

import (
	"fmt"
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "deep", "nested", "tree", "out.txt")
	if err := fs.WriteFile(path, []byte("hello"), fs.WithMkdirAll(true)); err != nil {
		log.Fatal(err)
	}
	fmt.Println(fs.Exists(path))
}
Output:
true
Example (Secret)

WriteFile honoring WithPerm gives the secrets-file idiom in one call: owner-only read/write, atomic on disk.

package main

import (
	"log"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "token")
	if err := fs.WriteFile(path, []byte("hunter2"), fs.WithPerm(fs.Mode0600)); err != nil {
		log.Fatal(err)
	}
	// On a POSIX system, this file is now mode 0o600.
}

func WriteFileExclusive

func WriteFileExclusive(path string, data []byte, opts ...WriteOption) error

WriteFileExclusive writes data to path with O_CREATE|O_EXCL semantics: errors with ErrAlreadyExists if path already exists. Useful for "create only if not present" idempotent setups.

Atomic-write internals are disabled (the temp+rename strategy would race against a concurrent creator); the file is opened directly with O_EXCL.

func WriteFileVersioned

func WriteFileVersioned(path string, data []byte, opts ...VersionedOption) (backup string, err error)

WriteFileVersioned writes data to path atomically. If path already exists, its current contents are first renamed to "<path>.bak.<timestamp>" so the prior version is preserved. Returns the backup path (empty if there was no prior file) alongside any write error.

After the write, pruning runs per WithVersionsKeep and WithVersionsMaxAge. Pruning errors are returned but do not invalidate the just-completed write.

The rename-to-backup step is atomic. On POSIX the window between rename and new file write is sub-microsecond; readers using read-and-handle-NotFound see at most a transient miss.

Example

WriteFileVersioned writes a new value atomically and saves the prior content as a timestamped backup. WithVersionsKeep bounds the number of retained backups; WithVersionsMaxAge bounds their age. ListVersions returns the surviving backups newest first.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "versioned-example-*")
	defer func() { _ = cleanup() }()
	cfg := filepath.Join(dir, "config.yaml")

	_, _ = fs.WriteFileVersioned(cfg, []byte("v1"))
	_, _ = fs.WriteFileVersioned(cfg, []byte("v2"), fs.WithVersionsKeep(3))
	_, _ = fs.WriteFileVersioned(cfg, []byte("v3"), fs.WithVersionsKeep(3))

	versions, _ := fs.ListVersions(cfg)
	fmt.Println("backups:", len(versions))
}
Output:
backups: 2

func WriteStderr

func WriteStderr(data []byte) error

WriteStderr is the os.Stderr analog of WriteStdout.

func WriteStdout

func WriteStdout(data []byte) error

WriteStdout writes data to os.Stdout. A write that fails because the consumer closed the pipe early (EPIPE on POSIX, ERROR_NO_DATA or ERROR_BROKEN_PIPE on Windows; typical when piping to `head`) is reported as ErrBrokenPipe, which callers conventionally translate to a silent process exit (matching `head`-friendly Unix idiom).

func WriteString

func WriteString(path, s string, opts ...WriteOption) error

WriteString is shorthand for WriteFile(path, []byte(s), opts...).

Types

type ApplyOption

type ApplyOption func(*applyConfig)

ApplyOption configures Apply, Resume, and Rollback.

func WithApplyNoMkdir

func WithApplyNoMkdir() ApplyOption

WithApplyNoMkdir disables automatic creation of missing parent directories for both the journal and target paths. Default behavior creates them with mode 0o755.

type ArchiveCreateOption

type ArchiveCreateOption func(*archiveCreateOptions)

ArchiveCreateOption configures CreateArchive.

func WithArchiveCreateFilter

func WithArchiveCreateFilter(fn func(path string, info os.FileInfo) bool) ArchiveCreateOption

WithArchiveCreateFilter installs a per-walk-entry predicate. Entries (and subtrees, if directories) for which fn returns false are skipped.

func WithArchiveFormat

func WithArchiveFormat(f ArchiveFormat) ArchiveCreateOption

WithArchiveFormat sets the output container format. Default ArchiveFormatTar. ArchiveFormatTarGz wraps the tar stream in gzip; ArchiveFormatZip writes a zip archive.

type ArchiveExtractOption

type ArchiveExtractOption func(*archiveExtractOptions)

ArchiveExtractOption configures ExtractArchive.

func WithArchiveFilter

func WithArchiveFilter(fn func(ArchiveHeader) bool) ArchiveExtractOption

WithArchiveFilter installs a per-entry predicate. Entries for which fn returns false are skipped (no filesystem write).

Named WithArchiveFilter rather than WithFilter because WithFilter is already a CopyOption.

func WithArchiveMaxBytes

func WithArchiveMaxBytes(n int64) ArchiveExtractOption

WithArchiveMaxBytes caps the cumulative extracted bytes; archives that exceed return ErrArchiveTooLarge. Default 10 GiB. Set to zero or negative to disable.

func WithPreserveMode

func WithPreserveMode(b bool) ArchiveExtractOption

WithPreserveMode preserves entry permission bits from the archive. Default false (modes are masked to 0o644 for files, 0o755 for dirs). Enable with care; archives from untrusted sources can contain setuid bits or other surprising mode flags.

type ArchiveFormat

type ArchiveFormat int

ArchiveFormat identifies an archive container.

const (
	// ArchiveFormatUnknown is the zero value; sniffers return it
	// when no recognizable magic is present.
	ArchiveFormatUnknown ArchiveFormat = iota
	// ArchiveFormatTar is a plain (uncompressed) ustar archive.
	ArchiveFormatTar
	// ArchiveFormatTarGz is a gzip-compressed tar.
	ArchiveFormatTarGz
	// ArchiveFormatZip is a zip archive (zlib-compressed inside the
	// container; archive/zip handles per-entry decompression).
	ArchiveFormatZip
)

func (ArchiveFormat) String

func (f ArchiveFormat) String() string

String renders the canonical lowercase name (e.g., "tar", "tar.gz", "zip", "unknown").

type ArchiveHeader

type ArchiveHeader struct {
	// Name is the archive-internal path (no leading slash, no `..`).
	Name string
	// Size is the entry's byte size.
	Size int64
	// Mode is the entry's mode bits.
	Mode os.FileMode
	// ModTime is the entry's last-modification time.
	ModTime time.Time
	// IsDir is true for directory entries.
	IsDir bool
	// LinkTarget is populated for symlink entries (tar); zip doesn't
	// surface symlinks natively.
	LinkTarget string
}

ArchiveHeader is the package's normalized archive-entry header. Both ExtractArchive and the create-side filters operate on this shape so callers don't have to handle tar.Header / zip.FileHeader separately.

type Cache

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

Cache is a directory-backed key/value store with TTL and total-size eviction. Keys are arbitrary strings; values are byte slices.

Cache is safe for concurrent use by multiple goroutines. Multiple processes can share the same cache directory: writes are atomic via temp+rename; eviction sweeps may briefly over- or under-evict if two processes evict simultaneously.

Storage layout (relative to dir, with WithCacheVersion(v) applied):

<dir>/<version-or-empty>/<hash[0:2]>/<hash[2:]>.bin

where hash = sha256(key). Each entry file holds only the value bytes; the file's modification time encodes the entry's creation time and drives TTL accounting.

Eviction is mtime-based LRU when WithCacheMaxBytes is set: after every Set whose cumulative size pushes total bytes above the cap, the oldest entries are removed until total bytes is below the cap.

func NewCache

func NewCache(dir string, opts ...CacheOption) (*Cache, error)

NewCache opens (or creates) a Cache rooted at dir. The directory is created with mode 0o755 if missing. Returns a non-nil error if dir is empty, malformed, or cannot be created.

Example

Cache is the idiomatic compute-on-miss pattern: Get; on miss, compute the expensive result and Set it. Errors from Set surface to the caller; Get treats every failure as a miss.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "cache-example-*")
	defer func() { _ = cleanup() }()

	c, err := fs.NewCache(dir)
	if err != nil {
		return
	}
	defer func() { _ = c.Close() }()

	key := "expensive-result-of-X"
	if v, ok := c.Get(key); ok {
		fmt.Println("hit:", string(v))
		return
	}

	// Miss path: compute the value and remember it for next time.
	computed := []byte("computed-once")
	if err := c.Set(key, computed); err != nil {
		return
	}
	fmt.Println("miss to computed and stored")
}
Output:
miss to computed and stored

func (*Cache) Close

func (c *Cache) Close() error

Close marks the cache as closed. Subsequent operations return ErrCacheClosed. Close does not remove files on disk; call Cache.Purge before Close or os.RemoveAll after to remove the directory. Idempotent.

func (*Cache) Delete

func (c *Cache) Delete(key string) error

Delete removes the entry for key. Returns nil if the entry was missing.

func (*Cache) Entries

func (c *Cache) Entries() iter.Seq[CacheEntry]

Entries returns an iterator over every stored cache entry. Order is unspecified.

Iteration walks the cache directory once up-front to build a snapshot. Entries added after iteration begins are not visible; entries deleted during iteration are still yielded with their pre-deletion metadata. Returns nil after Close.

func (*Cache) Get

func (c *Cache) Get(key string) (value []byte, ok bool)

Get returns the cached value for key. ok is true on a hit; false when the entry is missing, expired, or unreadable.

Get never returns an error; read failures are treated as misses so callers can write the compute-on-miss form without error plumbing. Expired entries are deleted from disk before the miss is reported.

func (*Cache) GetWithError

func (c *Cache) GetWithError(key string) (value []byte, ok bool, err error)

GetWithError is the error-surfacing variant of Cache.Get. ok is true on a hit; ok is false either because the entry was missing or expired (err nil) or because reading it failed (err non-nil).

func (*Cache) Purge

func (c *Cache) Purge() error

Purge removes every entry under the cache's effective directory. The directory itself is recreated empty so the Cache remains usable. Idempotent.

func (*Cache) Set

func (c *Cache) Set(key string, value []byte) error

Set writes value to the cache under key, replacing any existing entry. The write is atomic (temp+rename) so concurrent readers never see a partial value. After the write, eviction runs when WithCacheMaxBytes is configured.

func (*Cache) Stats

func (c *Cache) Stats() (CacheStats, error)

Stats walks the cache directory and returns the entry count and total bytes. O(n) in the number of entries.

type CacheEntry

type CacheEntry struct {
	// HashedKey is the hex-encoded SHA-256 of the original key.
	HashedKey string

	// Size is the entry's value size in bytes.
	Size int64

	// ModTime is the entry's last-write time.
	ModTime time.Time
}

CacheEntry describes one stored entry by its on-disk metadata. The original key is not surfaced: keys are SHA-256-hashed before storage and the package does not maintain a reverse index.

type CacheOption

type CacheOption func(*cacheConfig)

CacheOption configures NewCache.

func WithCacheClock

func WithCacheClock(now func() time.Time) CacheOption

WithCacheClock overrides the wall clock used for TTL accounting. Useful only in tests.

func WithCacheMaxBytes

func WithCacheMaxBytes(n int64) CacheOption

WithCacheMaxBytes sets a soft cap on total bytes across all entries. After each Set, the cache evicts oldest-by-mtime entries until the total is at or below n. Zero disables the cap.

func WithCacheTTL

func WithCacheTTL(d time.Duration) CacheOption

WithCacheTTL sets the per-entry time-to-live. Entries older than d at Get time are deleted and reported as misses. Zero disables TTL.

func WithCacheVersion

func WithCacheVersion(v string) CacheOption

WithCacheVersion namespaces all entries under a version segment in the cache directory. Bumping the version is equivalent to a full purge. On Cache open, sibling version directories under dir are removed.

Allowed characters: [A-Za-z0-9._-]. Other inputs are rejected by NewCache with ErrInvalidPath.

Example

Versioned caches scope every entry under a per-app version string; bumping the version after an upgrade implicitly invalidates the entire previous-version cache. Old version subdirs are deleted on open so the cache directory does not grow unbounded.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "cache-example-*")
	defer func() { _ = cleanup() }()

	c, err := fs.NewCache(dir, fs.WithCacheVersion("schema-v3"))
	if err != nil {
		return
	}
	defer func() { _ = c.Close() }()

	_ = c.Set("k", []byte("v"))
	stats, _ := c.Stats()
	fmt.Println("entries:", stats.Entries)
}
Output:
entries: 1

type CacheStats

type CacheStats struct {
	Entries int
	Bytes   int64
}

CacheStats is a point-in-time snapshot of the cache state.

type ContentMatch

type ContentMatch struct {
	// Path is the absolute path of the file that contained the match.
	Path string

	// Line is the 1-indexed line number where the match was found.
	Line int

	// Text is the matching line's contents with trailing CR/LF
	// stripped.
	Text string
}

ContentMatch is a single line that matched a FindByContent search.

func FindByContent

func FindByContent(root, substr string, opts ...WalkOption) ([]ContentMatch, error)

FindByContent walks root looking for files whose contents contain substr. Returns one ContentMatch per matching line.

Pass any WalkOption to scope the walk or tune content-search behavior (e.g., WalkSkipPatterns, WithWalkGitignore, WithFindByContentMaxSize).

Lines containing a NUL byte are skipped (matching grep's --binary-files=without-match behavior). Files larger than the configured cap (default 100 MiB) are skipped.

Example

FindByContent walks a directory tree returning every line that contains the search string. Use FindByContentRegex for richer patterns.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "fbc-example-*")
	defer func() { _ = cleanup() }()
	_ = fs.WriteFile(filepath.Join(dir, "a.txt"), []byte("hello\nworld\n"))

	matches, _ := fs.FindByContent(dir, "world")
	fmt.Println("matches:", len(matches))
}
Output:
matches: 1

func FindByContentRegex

func FindByContentRegex(root string, re *regexp.Regexp, opts ...WalkOption) ([]ContentMatch, error)

FindByContentRegex is the regex variant of FindByContent. re.MatchString is used per line.

type CopyOption

type CopyOption func(*copyOptions)

CopyOption configures CopyFile, CopyDir, Move, and Rename.

func WithFilter

func WithFilter(fn func(path string, e stdfs.DirEntry) bool) CopyOption

WithFilter installs a per-entry predicate for CopyDir. Returning false skips the entry; for directories, the entire subtree is skipped. The path passed to fn is the absolute walk path under the source root.

func WithFollowSymlinks(b bool) CopyOption

WithFollowSymlinks dereferences symlinks at the source instead of recreating them at the destination. Default false (symlinks are copied as symlinks).

func WithOverwrite

func WithOverwrite(b bool) CopyOption

WithOverwrite controls whether an existing destination is replaced. Default true.

func WithPreserveMtime

func WithPreserveMtime(b bool) CopyOption

WithPreserveMtime preserves the source modification time on the destination. Default true. Symlinks themselves do not have their mtime preserved (the stdlib doesn't expose lutimes).

type DiskUsage

type DiskUsage struct {
	TotalBytes     uint64
	FreeBytes      uint64 // free for any user (root may use more)
	AvailableBytes uint64 // free for the calling user (after reserved blocks)
	UsedBytes      uint64
	InodesTotal    uint64
	InodesFree     uint64
}

DiskUsage describes the filesystem capacity of the volume containing a path. InodesTotal / InodesFree are zero on Windows (and on other filesystems that don't surface inode counts).

func DiskUsageOf

func DiskUsageOf(path string) (DiskUsage, error)

DiskUsageOf returns the DiskUsage for the filesystem containing path. POSIX uses `syscall.Statfs`; Windows uses `GetDiskFreeSpaceExW` via `syscall.Syscall6`. Inode fields are zero on Windows where the concept doesn't apply.

type FindOption

type FindOption func(*findOptions)

FindOption configures the find-up family (FindUp, FindUpAll, ProjectRoot).

func WithMaxAncestors

func WithMaxAncestors(n int) FindOption

WithMaxAncestors bounds how many directories the find-up walk traverses before giving up. Default 32; enough for any realistic project tree, while defending against pathological symlink loops or deeply-nested mounts.

func WithProjectMarkers

func WithProjectMarkers(markers []string) FindOption

WithProjectMarkers replaces the default ProjectRoot marker list (`.git`, `go.mod`, `package.json`, `Cargo.toml`). An empty slice is ignored; the default list is preserved.

func WithStopAt

func WithStopAt(path string) FindOption

WithStopAt sets an absolute boundary the walk will not cross. The boundary is checked AFTER inspecting the directory, so a marker living directly inside stopAt is still found.

type Gitignore

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

Gitignore is a compiled set of .gitignore-style ignore rules. Match reports whether a path is ignored. The matcher implements the standard gitignore syntax: leading `!` negation, trailing `/` directory-only, leading or embedded `/` anchoring, `**` recursive wildcard, and the `*`/`?`/`[...]` per-segment globs. Edge cases outside the standard syntax (interaction with git's index, non-POSIX character-class collation) are out of scope.

A Gitignore is immutable after construction and safe for concurrent use.

func LoadGitignore

func LoadGitignore(filePath string) (*Gitignore, error)

LoadGitignore reads patterns from filePath and compiles them into a *Gitignore.

func NewGitignore

func NewGitignore(patterns []string) *Gitignore

NewGitignore compiles patterns into a *Gitignore. The input has the same format as a .gitignore file body: one pattern per line, `#` comments, blank lines ignored.

Example

Gitignore parses .gitignore-style patterns and matches paths against them. It handles negation, anchoring, directory-only patterns, `**` recursive wildcards, and the standard glob metacharacters. Pair it with WithWalkGitignore for source-tree walks that respect the project's ignore rules.

package main

import (
	"fmt"

	"github.com/go-rotini/fs"
)

func main() {
	g := fs.NewGitignore([]string{
		"*.log",
		"!important.log",
		"node_modules/",
	})

	fmt.Println("foo.log:", g.Match("foo.log", false))
	fmt.Println("important.log:", g.Match("important.log", false))
	fmt.Println("node_modules:", g.Match("node_modules", true))
	fmt.Println("src/main.go:", g.Match("src/main.go", false))
}
Output:
foo.log: true
important.log: false
node_modules: true
src/main.go: false

func (*Gitignore) Match

func (g *Gitignore) Match(relPath string, isDir bool) bool

Match reports whether relPath is ignored. relPath must be relative to the directory the .gitignore was placed in, using POSIX separators. isDir indicates whether the entry is a directory; dir-only patterns (trailing `/`) match only when this is true.

Match implements the gitignore precedence rule: later patterns override earlier ones. A negation (leading `!`) re-includes a previously-ignored path; see WithWalkGitignore for ancestor- directory enforcement during walks.

type HashAlgo

type HashAlgo int

HashAlgo selects the hash function used by Hash, HashCompare, and HashWriter. The zero value is HashSHA256, the package default.

const (
	// HashSHA256 selects SHA-256 (default).
	HashSHA256 HashAlgo = iota
	// HashSHA512 selects SHA-512.
	HashSHA512
	// HashSHA1 selects SHA-1. Cryptographically broken; use only for
	// non-security-sensitive applications (legacy compatibility,
	// content-addressed caches, etc.).
	HashSHA1
	// HashMD5 selects MD5. Cryptographically broken; use only for
	// non-security-sensitive applications.
	HashMD5
)

func (HashAlgo) String

func (a HashAlgo) String() string

String returns a lowercase canonical name for algo, e.g., "sha256".

type HashingWriter

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

HashingWriter is an io.Writer that computes a running hash of every byte written through it. HashingWriter.Hex returns the hex digest of what's been written so far; HashingWriter.Reset clears the running state.

The type is concrete rather than an interface because there is exactly one implementation; callers who want abstraction can declare a one-method interface at their use site (the Go-idiomatic "accept interfaces, return concrete types" pattern).

func HashWriter

func HashWriter(algo HashAlgo) *HashingWriter

HashWriter returns a fresh *HashingWriter for algo. The typical use is to fold a hash computation into a copy operation:

h := fs.HashWriter(fs.HashSHA256)
if _, err := io.Copy(io.MultiWriter(dst, h), src); err != nil { ... }
digest := h.Hex()
Example

HashWriter folds a hash computation into a copy operation. Pair it with io.MultiWriter to hash bytes you're also writing elsewhere; handy for "download + integrity-check" flows.

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"

	"github.com/go-rotini/fs"
)

func main() {
	hw := fs.HashWriter(fs.HashSHA256)

	// In real code dst is a file; io.Discard keeps the example
	// hermetic.
	if _, err := io.Copy(io.MultiWriter(io.Discard, hw), bytes.NewReader([]byte("abc"))); err != nil {
		log.Fatal(err)
	}
	fmt.Println(hw.Hex())
}
Output:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad

func (*HashingWriter) Hex

func (w *HashingWriter) Hex() string

Hex returns the hex-encoded digest of every byte written since the last HashingWriter.Reset (or since construction). Named Hex rather than Sum so it doesn't shadow hash.Hash.Sum, which has a different signature and returns bytes rather than a hex string.

func (*HashingWriter) Reset

func (w *HashingWriter) Reset()

Reset clears the running state.

func (*HashingWriter) Write

func (w *HashingWriter) Write(p []byte) (int, error)

Write writes p into the underlying hash. The error return is for io.Writer conformance only; hash.Hash.Write never errors.

type LineEnding

type LineEnding int

LineEnding identifies a line-ending convention.

const (
	// LineNone means no line endings were detected.
	LineNone LineEnding = iota

	// LineLF is Unix-style "\n".
	LineLF

	// LineCRLF is Windows-style "\r\n".
	LineCRLF

	// LineCR is classic Mac-style "\r".
	LineCR

	// LineMixed indicates more than one line-ending family is
	// present in the input.
	LineMixed
)

func DetectLineEnding

func DetectLineEnding(b []byte) LineEnding

DetectLineEnding scans b and reports which line-ending family is in use. LineMixed is returned when more than one family appears. A trailing CR followed by LF is counted as a single CRLF.

func (LineEnding) String

func (l LineEnding) String() string

String renders the LineEnding as a short human-readable label.

type ListOption

type ListOption func(*listOptions)

ListOption configures ListDir.

func WithListFilter

func WithListFilter(f func(stdfs.DirEntry) bool) ListOption

WithListFilter installs a predicate. Only entries for which the predicate returns true are included.

func WithSkipHidden

func WithSkipHidden(b bool) ListOption

WithSkipHidden filters out entries whose name begins with ".". On Windows this still uses the dot-prefix convention; the FILE_ATTRIBUTE_HIDDEN bit is not consulted.

func WithSorted

func WithSorted(b bool) ListOption

WithSorted makes ListDir sort entries by name. Default unsorted (filesystem order from *os.File.ReadDir).

type LockHandle

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

LockHandle represents an acquired advisory lock on a path. Release the lock by calling LockHandle.Release; the call is idempotent.

LockHandle is not safe for concurrent use across goroutines beyond the Release and PID accessors.

func Lock

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

Lock acquires an exclusive advisory lock on path, blocking until the lock is available. The lockfile is created if it does not yet exist. Returns a *LockHandle that the caller must Release.

On POSIX, this is flock(LOCK_EX) via syscall.Flock. On Windows, it is LockFileEx(LOCKFILE_EXCLUSIVE_LOCK) via kernel32.LockFileEx over a single sentinel byte far past end-of-file, so the lockfile's own content stays readable while the lock is held (Windows byte-range locks are otherwise mandatory). The lock is associated with the open file handle, not the path; closing the file (via Release) releases it.

Advisory only: cooperating processes must agree to call Lock to see each other's locks. Direct file reads and writes ignore the lock.

func LockShared

func LockShared(path string) (*LockHandle, error)

LockShared acquires a shared (read) advisory lock on path. Multiple concurrent shared holders are allowed; shared and exclusive locks are mutually exclusive. Blocks until the lock is available.

func LockTimeout

func LockTimeout(path string, d time.Duration) (*LockHandle, error)

LockTimeout acquires an exclusive lock on path, blocking up to d. Returns ErrLockTimeout if the lock is not acquired before d elapses. A non-positive d behaves like TryLock: a single non-blocking attempt that returns ErrLockTimeout on busy.

Implemented as a poll-and-retry loop atop the non-blocking acquire; the wait granularity is 50 ms.

func PIDLock

func PIDLock(path string, opts ...PIDLockOption) (*LockHandle, error)

PIDLock writes the calling process's PID to path and acquires an exclusive lock on it. The lockfile content is the textual PID followed by a newline, plus an optional fingerprint (see WithPIDLockFingerprint).

If path already exists, PIDLock reads the recorded PID. If the recorded PID is alive and its fingerprint (if any) matches, PIDLock blocks waiting for the lock as a normal Lock would. Otherwise (PID dead, or PID alive but fingerprint changed indicating PID recycle), PIDLock overwrites the file with the caller's PID and returns the handle wrapped with ErrStaleLock.

Returns the handle and nil on a clean acquisition. Returns the handle and a wrapped ErrStaleLock on a reclaimed stale lock. Returns nil and an error on any acquire failure.

Example

PIDLock records the calling process's PID inside the lockfile so peers can identify the holder by inspecting the file. Stale locks (where the recorded PID no longer exists) are reclaimed automatically and the wrapped error matches fs.ErrStaleLock.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "lock-example-*")
	defer func() { _ = cleanup() }()
	lock := filepath.Join(dir, "daemon.pid")

	h, err := fs.PIDLock(lock)
	if err != nil {
		// On a clean acquire, err is nil. On a stale-lock reclaim,
		// err is wrapped with fs.ErrStaleLock; the handle is still
		// valid in that case.
		return
	}
	defer func() { _ = h.Release() }()
	fmt.Println("locked with PID:", h.PID() > 0)
}
Output:
locked with PID: true

func TryLock

func TryLock(path string) (*LockHandle, bool, error)

TryLock attempts to acquire an exclusive lock on path without blocking. Returns (handle, true, nil) on success and (nil, false, nil) when the lock is held by another holder. Any other error is returned as-is.

Example

TryLock is the non-blocking acquire; useful for "if no one else is running, do this work; otherwise skip" patterns common in cron-style commands.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "lock-example-*")
	defer func() { _ = cleanup() }()
	lock := filepath.Join(dir, "task.lock")

	h, ok, err := fs.TryLock(lock)
	if err != nil {
		return
	}
	if !ok {
		fmt.Println("another instance is running; exiting")
		return
	}
	defer func() { _ = h.Release() }()
	fmt.Println("acquired")
}
Output:
acquired

func (*LockHandle) PID

func (h *LockHandle) PID() int

PID returns the PID recorded by PIDLock, or 0 for any other acquirer.

func (*LockHandle) Release

func (h *LockHandle) Release() error

Release releases the lock and closes the underlying file. Idempotent; the first call's error is returned, subsequent calls return nil. Release does not remove the lockfile from disk.

type Mapping

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

Mapping is a read-only memory-mapped view of a file. The backing file is held open for the lifetime of the Mapping; Close releases both the mapping and the file.

Mapping is safe for concurrent reads but not for concurrent Close. The byte slice returned by Data must not be retained past Close.

func Mmap

func Mmap(path string) (*Mapping, error)

Mmap opens path and memory-maps its entire contents read-only.

POSIX: mmap(2) via syscall.Mmap with PROT_READ | MAP_SHARED. The file is opened O_RDONLY.

Windows: CreateFileMapping + MapViewOfFile with PAGE_READONLY | FILE_MAP_READ.

Empty files cannot be mapped (POSIX EINVAL, Windows ERROR_FILE_INVALID); Mmap returns an error rather than papering over the platform behavior.

Example

Mmap maps a file read-only into memory. Use Data() to access the bytes and Close() to release both the mapping and the file.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "mmap-example-*")
	defer func() { _ = cleanup() }()
	path := filepath.Join(dir, "data.bin")
	_ = fs.WriteFile(path, []byte("hello"))

	m, err := fs.Mmap(path)
	if err != nil {
		return
	}
	defer func() { _ = m.Close() }()
	fmt.Println(string(m.Data()))
}
Output:
hello

func (*Mapping) Close

func (m *Mapping) Close() error

Close releases the mapping and the underlying file handle. Idempotent.

func (*Mapping) Data

func (m *Mapping) Data() []byte

Data returns the mapped byte slice. Writes to the slice produce undefined behavior; the mapping is opened read-only but Go does not enforce that at the slice level.

func (*Mapping) Len

func (m *Mapping) Len() int

Len returns the length of the mapped region in bytes.

type MkdirOption

type MkdirOption func(*mkdirOptions)

MkdirOption configures Mkdir / MkdirAll.

func WithEnforcePerm

func WithEnforcePerm(b bool) MkdirOption

WithEnforcePerm makes MkdirAll chmod every path component it created so the requested perm is the actual perm on disk, bypassing the process umask. Components that already existed before the call are left untouched.

Default false.

type MultiError

type MultiError struct {
	Errors []error
}

MultiError aggregates per-entry errors from bulk operations (CopyDir, RemoveAll under WithStrict, Walk error handler). Implements the Go 1.20+ Unwrap() []error convention so errors.Is and errors.As walk every branch.

func (*MultiError) Append

func (m *MultiError) Append(err error)

Append appends err to the aggregator. Nil errors are ignored.

func (*MultiError) Error

func (m *MultiError) Error() string

Error renders the aggregate; single-element MultiError defers to its child's Error.

func (*MultiError) Is

func (*MultiError) Is(target error) bool

Is matches any *MultiError by type.

func (*MultiError) Unwrap

func (m *MultiError) Unwrap() []error

Unwrap returns the aggregated errors so errors.Is / errors.As walk every branch.

type PIDLockOption

type PIDLockOption func(*pidLockConfig)

PIDLockOption configures PIDLock.

func WithPIDLockFingerprint

func WithPIDLockFingerprint(fn func(pid int) string) PIDLockOption

WithPIDLockFingerprint adds a stronger stale-lock check on top of the bare PID-alive probe. The callback maps a PID to a string the caller treats as a process identity, typically a process start time.

The package's ProcessStartTime is the canonical argument:

h, err := fs.PIDLock(path, fs.WithPIDLockFingerprint(func(pid int) string {
    s, _ := fs.ProcessStartTime(pid)
    return s
}))

On acquire, fn(os.Getpid()) is written next to the PID in the lockfile. On stale-detection, the recorded fingerprint is compared against fn(recordedPID); a mismatch means the PID is alive but belongs to a different process, and the lock is reclaimed.

fn must be safe for concurrent use. fn returning "" disables the fingerprint check for that PID (defers to the bare alive probe).

type PathError

type PathError struct {
	Op    string
	Path  string
	Cause error
}

PathError wraps an underlying filesystem error with the operation and path that produced it. The package returns *PathError from every op-with-a-path entry point; Cause is typically a *os.PathError or a syscall.Errno.

func (*PathError) Error

func (e *PathError) Error() string

Error renders as "fs: <op> <path>: <cause>".

The render format is part of the public API: callers can rely on the "fs: " prefix and on the order of op / path / cause. Format changes are reserved for major version bumps (v1 to v2). For programmatic inspection use the Op / Path / Cause fields rather than parsing the string.

func (*PathError) Is

func (e *PathError) Is(target error) bool

Is matches another *PathError by type and walks the cause chain so callers can compare against either this package's sentinels or the stdlib's io/fs.ErrNotExist / io/fs.ErrPermission / io/fs.ErrExist.

func (*PathError) Unwrap

func (e *PathError) Unwrap() error

Unwrap returns Cause for errors.Unwrap and errors.Is / errors.As.

type PathOption

type PathOption func(*pathOptions)

PathOption configures path operations.

func WithStrictExpansion

func WithStrictExpansion() PathOption

WithStrictExpansion makes Expand return an error when an environment variable is referenced but unset, rather than expanding to the empty string.

type Plan

type Plan struct {
	Ops []PlanOp `json:"ops"`
}

Plan is a sequence of PlanOp executed in order by Apply. Build it with the fluent helpers (*Plan.Create, *Plan.Update, *Plan.Delete, *Plan.Rename) or by appending to Ops directly. Plans are JSON-serializable.

func NewPlan

func NewPlan() *Plan

NewPlan returns an empty *Plan.

func (*Plan) Create

func (p *Plan) Create(path string, data []byte, perm os.FileMode) *Plan

Create appends a PlanActionCreate op. Returns p for chaining.

func (*Plan) Delete

func (p *Plan) Delete(path string) *Plan

Delete appends a PlanActionDelete op. Returns p for chaining.

func (*Plan) Diff

func (p *Plan) Diff() string

Diff renders the plan as a short human-readable summary, one line per op. Suitable for --dry-run previews; not a structured diff.

func (*Plan) Rename

func (p *Plan) Rename(src, dst string) *Plan

Rename appends a PlanActionRename op moving src to dst. Returns p for chaining.

func (*Plan) Update

func (p *Plan) Update(path string, data []byte, perm os.FileMode) *Plan

Update appends a PlanActionUpdate op. Returns p for chaining.

type PlanAction

type PlanAction int

PlanAction enumerates the operation kinds a Plan can hold.

const (
	// PlanActionCreate writes a new file. Errors if the path already
	// exists at apply time.
	PlanActionCreate PlanAction = iota + 1

	// PlanActionUpdate overwrites an existing file's contents. The
	// prior contents are backed up so the op is reversible.
	PlanActionUpdate

	// PlanActionDelete removes a file. The file is backed up before
	// removal so Rollback can restore it.
	PlanActionDelete

	// PlanActionRename moves Source to Path. Reversible.
	PlanActionRename
)

func (PlanAction) String

func (a PlanAction) String() string

String returns the action's lowercase name.

type PlanOp

type PlanOp struct {
	Action PlanAction  `json:"action"`
	Path   string      `json:"path"`
	Source string      `json:"source,omitempty"` // PlanActionRename only
	Data   []byte      `json:"data,omitempty"`   // PlanActionCreate / PlanActionUpdate
	Perm   os.FileMode `json:"perm,omitempty"`   // PlanActionCreate / PlanActionUpdate
}

PlanOp is a single operation inside a Plan.

type ProjectKind

type ProjectKind string

ProjectKind enumerates the project types ProjectType can detect. Multiple kinds can apply to the same root (monorepos commonly have several).

const (
	ProjectKindGo     ProjectKind = "go"
	ProjectKindNode   ProjectKind = "node"
	ProjectKindRust   ProjectKind = "rust"
	ProjectKindPython ProjectKind = "python"
	ProjectKindRuby   ProjectKind = "ruby"
	ProjectKindJava   ProjectKind = "java"
	ProjectKindDotnet ProjectKind = "dotnet"
	ProjectKindPHP    ProjectKind = "php"
	ProjectKindMake   ProjectKind = "make"
	ProjectKindDocker ProjectKind = "docker"
)

Canonical ProjectKind values. New kinds may be added in minor releases; consumers should treat unknown values as opaque tags rather than relying on enum exhaustiveness.

func ProjectType

func ProjectType(root string) ([]ProjectKind, error)

ProjectType inspects root and returns every recognized project kind whose marker file exists in the directory. Returns an empty slice when none match.

The returned slice is sorted alphabetically. Detection is filename-only (no parsing of go.mod, package.json, etc.), so a directory with a stray Gemfile is reported as Ruby even if it's effectively a Go project.

For .NET, the marker is any *.csproj, *.fsproj, or *.sln file.

Example

ProjectType detects which language ecosystems own a project root based on canonical marker files.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "pt-example-*")
	defer func() { _ = cleanup() }()
	_ = fs.WriteFile(filepath.Join(dir, "go.mod"), []byte("module x\n"))

	kinds, _ := fs.ProjectType(dir)
	fmt.Println(kinds)
}
Output:
[go]

type ReadOption

type ReadOption func(*readOptions)

ReadOption configures read operations.

func WithAllowShort

func WithAllowShort(b bool) ReadOption

WithAllowShort permits ReadAt to return fewer than n bytes without surfacing ErrShortRead. The returned slice is whatever was readable up to EOF.

func WithExpand

func WithExpand() ReadOption

WithExpand expands ~ and $VAR in the path before reading.

func WithMaxSize

func WithMaxSize(n int64) ReadOption

WithMaxSize sets the maximum bytes the read will consume. Zero or negative disables the cap (the read becomes unbounded; only do this when you know the source is a regular file of trustworthy size).

type RemoveOption

type RemoveOption func(*removeOptions)

RemoveOption configures remove operations.

func WithStrict

func WithStrict(b bool) RemoveOption

WithStrict makes the operation error on missing paths instead of returning nil. Mirrors stdlib's os.Remove / os.RemoveAll behavior; the package's default is the opposite (idempotent missing-target removal).

type Rotator

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

Rotator is an io.WriteCloser that rotates its backing file when a size or age threshold is exceeded. Rotation renames the current file with a sortable timestamp suffix (optionally gzipping it) and reopens the original path fresh.

Rotator is safe for concurrent Write calls. Within a process, writes are serialized via an internal mutex. Across processes, the backing file is opened with O_APPEND so writes don't interleave inside a single call, and the size check on each Write consults the actual on-disk file size, so two processes sharing the same path cooperate on the byte threshold.

Age-based rotation remains process-local: each Rotator tracks the time it opened the current file. Coordinate manual Rotator.Rotate calls if cross-process age coordination matters.

func NewRotator

func NewRotator(path string, opts ...RotatorOption) (*Rotator, error)

NewRotator opens path (creating it if absent) and returns a *Rotator. Subsequent Rotator.Write calls append to the file and trigger rotation when size or age thresholds are crossed.

Example

NewRotator returns an io.WriteCloser that transparently rotates the backing file when size or age thresholds are exceeded. The keep policy bounds disk usage; gzip compression is optional.

package main

import (
	"fmt"
	"path/filepath"

	"github.com/go-rotini/fs"
)

func main() {
	dir, cleanup, _ := fs.TempDir("", "rotate-example-*")
	defer func() { _ = cleanup() }()
	logfile := filepath.Join(dir, "app.log")

	r, err := fs.NewRotator(logfile,
		fs.WithRotateMaxBytes(10),
		fs.WithRotateKeep(3),
		fs.WithRotateCompress(true),
	)
	if err != nil {
		return
	}
	defer func() { _ = r.Close() }()

	_, _ = r.Write([]byte("first-line\n"))
	_, _ = r.Write([]byte("second-line\n"))
	fmt.Println("done")
}
Output:
done

func (*Rotator) Close

func (r *Rotator) Close() error

Close releases the underlying file handle. Subsequent Write calls return ErrRotatorClosed. Idempotent.

func (*Rotator) Rotate

func (r *Rotator) Rotate() error

Rotate forces an immediate rotation regardless of size or age. Useful for SIGHUP-style "rotate now" semantics.

func (*Rotator) Write

func (r *Rotator) Write(p []byte) (int, error)

Write appends p to the backing file. Before the append, the current size and age are checked against the configured thresholds; if either is exceeded the file is rotated and a fresh one is opened. Returns len(p) on success.

type RotatorOption

type RotatorOption func(*rotatorConfig)

RotatorOption configures NewRotator.

func WithRotateClock

func WithRotateClock(now func() time.Time) RotatorOption

WithRotateClock overrides the wall clock used for age accounting and rotated-filename timestamps. Useful only in tests.

func WithRotateCompress

func WithRotateCompress(b bool) RotatorOption

WithRotateCompress controls gzip compression of rotated files after the move. Compression runs synchronously after the rename; callers needing non-blocking compression should rotate in a background goroutine.

func WithRotateKeep

func WithRotateKeep(n int) RotatorOption

WithRotateKeep retains the n most recent rotated files; older ones are removed after every rotation. Zero keeps every rotated file. The live file is not counted.

func WithRotateMaxAge

func WithRotateMaxAge(d time.Duration) RotatorOption

WithRotateMaxAge rotates the file when the time since the current file was opened exceeds d. Zero disables age-based rotation.

func WithRotateMaxBytes

func WithRotateMaxBytes(n int64) RotatorOption

WithRotateMaxBytes rotates the file when its size would exceed n. The check runs before each Write; an oversized Write is accepted in full and triggers rotation immediately afterward. Zero disables size-based rotation.

type ScaffoldAction

type ScaffoldAction struct {
	// Op classifies what the apply phase will do for this entry.
	Op ScaffoldActionOp
	// SrcPath is the path inside the source [io/fs.FS].
	SrcPath string
	// DstPath is the resolved on-disk destination path.
	DstPath string
	// Reason is a short human-readable explanation suitable for
	// dry-run output. Examples: "would create", "skipped: exists",
	// "would overwrite".
	Reason string
	// IsDir is true for directory entries.
	IsDir bool
}

ScaffoldAction describes one planned scaffold operation.

func ScaffoldPlan

func ScaffoldPlan(src stdfs.FS, dst string, vars any, opts ...ScaffoldOption) ([]ScaffoldAction, error)

ScaffoldPlan walks src, renders every path through text/template with vars (so a source named `{{.AppName}}.go` becomes `myapp.go`), and returns the actions ScaffoldApply would perform; without any filesystem writes. Useful for `--dry-run` flags.

Template syntax errors in source paths or contents abort the plan.

type ScaffoldActionOp

type ScaffoldActionOp int

ScaffoldActionOp classifies a ScaffoldAction. Apply uses this to decide whether to touch the filesystem.

const (
	// ScaffoldActionCreate creates a new entry at DstPath.
	ScaffoldActionCreate ScaffoldActionOp = iota
	// ScaffoldActionSkip leaves DstPath untouched.
	ScaffoldActionSkip
	// ScaffoldActionOverwrite replaces DstPath with rendered content.
	ScaffoldActionOverwrite
	// ScaffoldActionConflict means the conflict resolution failed
	// (typically a prompt callback returned an unsupported value);
	// apply aborts.
	ScaffoldActionConflict
)

func (ScaffoldActionOp) String

func (o ScaffoldActionOp) String() string

String renders the canonical lowercase op name.

type ScaffoldOnConflict

type ScaffoldOnConflict int

ScaffoldOnConflict configures how ScaffoldApply handles an existing destination path.

const (
	// ScaffoldSkipExisting keeps the destination intact (default).
	ScaffoldSkipExisting ScaffoldOnConflict = iota
	// ScaffoldOverwriteAll replaces every existing destination.
	ScaffoldOverwriteAll
	// ScaffoldPromptInteractive calls the [WithScaffoldPromptFunc]
	// callback per conflict.
	ScaffoldPromptInteractive
)

type ScaffoldOption

type ScaffoldOption func(*scaffoldOptions)

ScaffoldOption configures ScaffoldApply, ScaffoldPlan, and ScaffoldExtract.

func WithScaffoldOnConflict

func WithScaffoldOnConflict(c ScaffoldOnConflict) ScaffoldOption

WithScaffoldOnConflict sets the policy used when an output path already exists. Default ScaffoldSkipExisting (don't overwrite).

func WithScaffoldPromptFunc

func WithScaffoldPromptFunc(fn func(path string, plan ScaffoldAction) ScaffoldActionOp) ScaffoldOption

WithScaffoldPromptFunc registers the per-conflict prompt called under ScaffoldPromptInteractive. The function receives the destination path and the planned action, and returns the action to execute (typically ScaffoldActionOverwrite or ScaffoldActionSkip). Required when onConflict is PromptInteractive; a missing prompt with that policy errors.

func WithScaffoldVersionMarker

func WithScaffoldVersionMarker(name string) ScaffoldOption

WithScaffoldVersionMarker overrides the marker filename ScaffoldExtract writes under dst to record the source version. Default ".scaffold-version".

type TailOption

type TailOption func(*tailConfig)

TailOption configures a Tail call.

func WithTailBufferSize

func WithTailBufferSize(n int) TailOption

WithTailBufferSize sets the size of the read buffer in bytes. Default is 4096 bytes. Values <= 0 use the default.

func WithTailFromStart

func WithTailFromStart() TailOption

WithTailFromStart reads path from offset 0 on the first open instead of seeking to EOF. Subsequent rotation-triggered reopens always start at offset 0 regardless of this option.

func WithTailNotifyRotation

func WithTailNotifyRotation() TailOption

WithTailNotifyRotation makes Tail yield ("", ErrTailRotated) once each time it detects a rotation and reopens the file. Off by default.

func WithTailPollInterval

func WithTailPollInterval(d time.Duration) TailOption

WithTailPollInterval sets the cadence at which Tail checks for new content after hitting EOF. Default is 200ms. Values below 10ms are clamped to 10ms.

type TouchOption

type TouchOption func(*touchOptions)

TouchOption configures Touch.

func WithTimes

func WithTimes(atime, mtime time.Time) TouchOption

WithTimes overrides Touch's "now" with explicit atime/mtime.

func WithTouchPerm

func WithTouchPerm(mode os.FileMode) TouchOption

WithTouchPerm sets the mode for newly-created files. Existing files keep their mode.

type VersionInfo

type VersionInfo struct {
	// Path is the absolute path to the backup file.
	Path string

	// Created is the wall-clock time recorded in the version's
	// filename suffix. Always UTC.
	Created time.Time

	// Size is the backup file's size in bytes at list time.
	Size int64
}

VersionInfo describes a single on-disk backup version.

func ListVersions

func ListVersions(path string) ([]VersionInfo, error)

ListVersions returns the on-disk backup versions of path, newest first. Returns an empty slice if path has no backups.

type VersionedOption

type VersionedOption func(*versionedConfig)

VersionedOption configures versioned backup behavior.

func WithVersionsClock

func WithVersionsClock(now func() time.Time) VersionedOption

WithVersionsClock overrides the wall clock used for timestamp generation and age pruning. Useful only in tests.

func WithVersionsKeep

func WithVersionsKeep(n int) VersionedOption

WithVersionsKeep retains the n most recent versions and removes older ones after every successful write. Zero disables count-based pruning.

func WithVersionsMaxAge

func WithVersionsMaxAge(d time.Duration) VersionedOption

WithVersionsMaxAge removes versions whose recorded creation time is older than d. Zero disables age-based pruning.

func WithVersionsMaxBytes

func WithVersionsMaxBytes(n int64) VersionedOption

WithVersionsMaxBytes caps the byte size RestoreVersion will read from a backup file into memory. Default 100 MiB. Larger files fail with ErrFileTooLarge.

func WithVersionsPerm

func WithVersionsPerm(perm os.FileMode) VersionedOption

WithVersionsPerm overrides the file mode used for newly-written backup files. Default 0o644 (matches WriteFile); use 0o600 for secret files.

type WalkFunc

type WalkFunc func(path string, e stdfs.DirEntry, err error) error

WalkFunc is the per-entry callback invoked by Walk. Return filepath.SkipDir to skip the parent directory; return filepath.SkipAll to terminate the walk early. Any other non-nil error aborts the walk and is returned by Walk.

type WalkOption

type WalkOption func(*walkOptions)

WalkOption configures Walk and the walk-backed search functions (Find, FindByRegex, FindFunc).

func WalkErrorHandler

func WalkErrorHandler(fn func(path string, err error) error) WalkOption

WalkErrorHandler intercepts per-entry errors during the walk. Return nil to continue, filepath.SkipDir to skip the parent, or any non-nil error to abort.

func WalkFollowSymlinks(b bool) WalkOption

WalkFollowSymlinks dereferences symlinks during the walk. Symlink loops are detected by tracking resolved real paths via filepath.EvalSymlinks; an already-visited target is silently skipped.

Named "WalkFollowSymlinks" to disambiguate from WithFollowSymlinks on CopyOption.

func WalkMaxDepth

func WalkMaxDepth(n int) WalkOption

WalkMaxDepth bounds recursion. The root is depth 0; n=1 means root + immediate children. n<=0 (default) is unbounded.

func WalkSkipHidden

func WalkSkipHidden(b bool) WalkOption

WalkSkipHidden skips hidden entries during the walk. Hidden is dot-prefix on POSIX and (additionally) the FILE_ATTRIBUTE_HIDDEN bit on Windows. Skipping a directory prunes its subtree.

Named "WalkSkipHidden" rather than "WithSkipHidden" because the latter is already taken by WithSkipHidden on ListOption; Go doesn't allow function-name overloading across return types.

func WalkSkipNames

func WalkSkipNames(names []string) WalkOption

WalkSkipNames skips entries whose basename exactly matches any element of names. Skipping a directory prunes its subtree.

func WalkSkipPatterns

func WalkSkipPatterns(patterns []string) WalkOption

WalkSkipPatterns skips entries whose basename matches any of the filepath.Match glob patterns. Skipping a directory prunes its subtree.

func WithFindByContentMaxSize

func WithFindByContentMaxSize(n int64) WalkOption

WithFindByContentMaxSize caps the size of files FindByContent will scan. Files larger than n are skipped. Pass 0 or negative to use the default cap (100 MiB).

Returned as a WalkOption so callers pass it alongside other walk filters. Setting it on a plain Walk is a no-op.

func WithWalkGitignore

func WithWalkGitignore(g *Gitignore) WalkOption

WithWalkGitignore adds gitignore-based filtering to a Walk. The matcher's anchor is the walk root; matched paths are computed relative to root using POSIX separators.

A directory matched by the gitignore is pruned from the walk (its contents are not visited). Matched file entries are skipped from fn but the walk continues.

type WalkParallelFunc

type WalkParallelFunc func(path string, e stdfs.DirEntry) error

WalkParallelFunc is the per-entry callback for WalkParallel. Returning an error aborts the walk and surfaces that error from the WalkParallel call. fn is invoked from one of N worker goroutines and must be safe for concurrent use.

Unlike WalkFunc, the parallel variant does not honor filepath.SkipDir: directory traversal is interleaved with fn calls, so a SkipDir return cannot prune work that's already been dispatched.

type WatchEvent

type WatchEvent struct {
	Path string
	Op   WatchOp
	Time time.Time
}

WatchEvent describes a single filesystem change reported by a *Watcher subscription. Path is the absolute path of the affected entry; Op is a bitmask of operations that occurred (a single kernel notification can carry multiple flags); Time is the wall-clock instant the cross-platform shell observed the event.

type WatchOp

type WatchOp uint32

WatchOp is a bitmask of filesystem operations. Multiple bits may be set when a single kernel event reports compound activity (e.g., `WatchCreate|WatchWrite` for a freshly-written file).

The constants are prefixed Watch* because the unprefixed names (Create, Write, Remove, Rename, Chmod) collide with existing fs package functions.

const (
	// WatchCreate fires when a path appears (was missing, now present).
	WatchCreate WatchOp = 1 << iota
	// WatchWrite fires when a path's contents are modified.
	WatchWrite
	// WatchRemove fires when a path is unlinked.
	WatchRemove
	// WatchRename fires when a path is moved (the path that vanished
	// gets WatchRename; the destination, if observed, gets WatchCreate).
	WatchRename
	// WatchChmod fires when a path's mode bits change.
	WatchChmod
)

func (WatchOp) Has

func (o WatchOp) Has(target WatchOp) bool

Has reports whether o includes target.

func (WatchOp) String

func (o WatchOp) String() string

String renders the set bits in canonical order, joined by '+'. Returns "none" when no bits are set.

type Watcher

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

Watcher monitors one path (file or directory) for changes. The watcher watches the parent directory and filters by basename, so editor atomic-save patterns (write-temp + rename) are detected correctly.

In v0.1 every Watcher uses the polling backend regardless of platform; `os.Lstat` in a loop at the configured interval (default 1 second; override via WithPolling). The platform-native backends (inotify on Linux, kqueue on macOS/BSD, ReadDirectoryChangesW on Windows) are scheduled for a follow-up release; the API will not change when they land.

Construct via NewWatcher (existing path), NewLazyWatcher (path may not exist yet), or NewDirWatcher (recursive directory watching). Each [(*Watcher).Subscribe] call returns an independent buffered channel; surplus events on a slow subscriber are dropped.

func NewDirWatcher

func NewDirWatcher(path string, opts ...WatcherOption) (*Watcher, error)

NewDirWatcher watches an entire directory tree. Use [WithRecursive(false)] to limit to a single directory level.

func NewLazyWatcher

func NewLazyWatcher(path string, opts ...WatcherOption) (*Watcher, error)

NewLazyWatcher constructs a *Watcher for a path that may not exist yet. The watcher fires `WatchCreate` on the path when it appears.

func NewWatcher

func NewWatcher(path string, opts ...WatcherOption) (*Watcher, error)

NewWatcher constructs a *Watcher for an existing path. The path is resolved to an absolute path; parent-directory watching plus basename filtering means atomic-rename saves are observed correctly on every backend.

Example

NewWatcher monitors a file for changes via the platform's notification mechanism. The watcher follows the file's parent directory and filters by basename, so editor atomic-save patterns (write-temp + rename) are observed correctly.

This example has no Output block because event delivery is asynchronous; pkg.go.dev still renders the body as runnable reference code.

package main

import (
	"context"
	"fmt"
	"log"
	"path/filepath"
	"time"

	"github.com/go-rotini/fs"
)

func main() {
	tmp, cleanup, _ := fs.TempDir("", "fs-example-*")
	defer cleanup()

	path := filepath.Join(tmp, "config.yaml")
	if err := fs.WriteFile(path, []byte("port: 8080\n")); err != nil {
		log.Fatal(err)
	}

	// In v0.1 the polling backend is used regardless of platform;
	// pass WithPolling(d) to tune the cadence. Disable debouncing
	// for prompt visibility in tests.
	w, err := fs.NewWatcher(path,
		fs.WithPolling(50*time.Millisecond),
		fs.WithDebounce(0))
	if err != nil {
		log.Fatal(err)
	}
	defer w.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	events, err := w.Subscribe(ctx)
	if err != nil {
		log.Fatal(err)
	}

	// Mutate the file from another goroutine; the watcher delivers a
	// write event to the subscription channel.
	go func() {
		time.Sleep(100 * time.Millisecond)
		_ = fs.WriteFile(path, []byte("port: 9090\n"))
	}()

	for ev := range events {
		if ev.Op.Has(fs.WatchWrite) {
			fmt.Println("config changed; reloading")
			return
		}
	}
}

func (*Watcher) Close

func (w *Watcher) Close() error

Close releases all resources. Idempotent; repeated calls are no-ops.

The error return is reserved for forward compatibility. v0.1 always returns nil; once native backends (inotify, kqueue, ReadDirectoryChangesW) are wired up, a Close-time flush or fd-release failure may surface here. Existing callers that already check the return will not need to change.

func (*Watcher) Subscribe

func (w *Watcher) Subscribe(ctx context.Context) (<-chan WatchEvent, error)

Subscribe returns a buffered channel that receives WatchEvent values until ctx is canceled or [(*Watcher).Close] is called. Each Subscribe creates an independent subscription; surplus events on a slow subscriber are dropped rather than blocking the worker.

type WatcherOption

type WatcherOption func(*watcherOptions)

WatcherOption configures a *Watcher.

func WithBufferSize

func WithBufferSize(n int) WatcherOption

WithBufferSize sizes each subscription's buffered channel. Default 1. A larger buffer reduces drop frequency on slow subscribers but increases memory per subscription.

func WithDebounce

func WithDebounce(d time.Duration) WatcherOption

WithDebounce overrides the trailing-edge debounce period applied to events before fan-out to subscribers. Default 75ms; zero disables debouncing entirely (every backend event becomes one subscriber event).

func WithLogger

func WithLogger(l *slog.Logger) WatcherOption

WithLogger directs non-fatal diagnostic messages from a *Watcher to l. Default is a discard logger.

func WithPolling

func WithPolling(interval time.Duration) WatcherOption

WithPolling overrides the polling interval. In v0.1 every watcher uses polling regardless; this option is the only way to tune the stat-loop cadence (default 1 second).

Once the platform-native backends (inotify / kqueue / ReadDirectoryChangesW) land in a follow-up release, WithPolling will force the polling backend in preference to the native one; useful for filesystems where the native APIs are known to be unreliable (NFS, FUSE, exotic mounts) or in tests that want deterministic, syscall-free behavior.

interval <= 0 selects the default 1s.

func WithRecursive

func WithRecursive(b bool) WatcherOption

WithRecursive controls whether NewDirWatcher watches subdirectories. Default true; pass false for a single-directory (non-recursive) watch.

type WorkspaceRoot

type WorkspaceRoot struct {
	// Path is the absolute path of the workspace member.
	Path string

	// Kind identifies the manifest form that produced this root.
	// One of "go.work", "package.json", or "pnpm-workspace.yaml".
	Kind string
}

WorkspaceRoot describes one root inside a multi-root workspace.

func WorkspaceRoots

func WorkspaceRoots(root string) ([]WorkspaceRoot, error)

WorkspaceRoots discovers the member roots of a multi-root workspace anchored at root. The detector inspects a small set of canonical manifest files:

  • go.work: each `use` directive.
  • package.json with a `workspaces` field: array of globs or an object with a `packages` array. Globs are expanded via Glob.
  • pnpm-workspace.yaml: the `packages:` list form. See [parsePnpmWorkspaces] for the supported subset.

Returns an empty slice when no manifest is present. Duplicate member paths across manifests are deduplicated.

type WriteOption

type WriteOption func(*writeOptions)

WriteOption configures write operations.

func WithAtomic

func WithAtomic(b bool) WriteOption

WithAtomic toggles the temp-file + rename strategy. Default true. Disable for very large files where doubled disk usage is prohibitive; at the cost of writes that can leave torn content visible to concurrent readers.

func WithBackup

func WithBackup(suffix string) WriteOption

WithBackup renames the existing destination to `<dest><suffix>` before the rename completes. Empty suffix defaults to ".bak". A pre-existing backup at the same path is overwritten.

func WithDirPerm

func WithDirPerm(mode os.FileMode) WriteOption

WithDirPerm sets the mode for parent directories created via WithMkdirAll. Default 0o755.

func WithLocked

func WithLocked(b bool) WriteOption

WithLocked acquires an advisory flock on the file before writing (POSIX only; no-op on Windows where O_APPEND is already serialized by the OS). Used by Append / AppendString.

func WithMkdirAll

func WithMkdirAll(b bool) WriteOption

WithMkdirAll creates missing parent directories before writing.

func WithPerm

func WithPerm(mode os.FileMode) WriteOption

WithPerm sets the file mode for new files. Existing files preserve their current mode unless WithPerm is also set on the call.

func WithSync

func WithSync(b bool) WriteOption

WithSync forces an fsync on the temp file (and parent directory on POSIX) before/after the rename. Default-on for overwrites of existing files; default-off for new files. Setting it explicitly overrides the default in either direction.

func WithTempPattern

func WithTempPattern(pattern string) WriteOption

WithTempPattern overrides the default temp-file naming pattern used by atomic writes. Pattern is a os.CreateTemp template (`*` is replaced by the random suffix). Default `<basename>.tmp.*`.

Directories

Path Synopsis
Package fstest provides test helpers for code that uses the github.com/go-rotini/fs package: a sandbox harness rooted at testing.T.TempDir, an in-memory read-only io/fs.FS, a process-env snapshot/restore helper, and `t.Cleanup`-registering temp-file wrappers.
Package fstest provides test helpers for code that uses the github.com/go-rotini/fs package: a sandbox harness rooted at testing.T.TempDir, an in-memory read-only io/fs.FS, a process-env snapshot/restore helper, and `t.Cleanup`-registering temp-file wrappers.

Jump to

Keyboard shortcuts

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