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 ¶
- Constants
- Variables
- func Abs(path string, opts ...PathOption) (string, error)
- func AppCacheDir(appName string) (string, error)
- func AppConfigDir(appName string) (string, error)
- func AppDataDir(appName string) (string, error)
- func AppRuntimeDir(appName string) (string, error)
- func AppStateDir(appName string) (string, error)
- func Append(path string, data []byte, opts ...WriteOption) error
- func AppendString(path, s string, opts ...WriteOption) error
- func Apply(p *Plan, journalDir string, opts ...ApplyOption) error
- func ApplyTransient(p *Plan, opts ...ApplyOption) error
- func Atime(path string) (time.Time, error)
- func BTime(path string) (time.Time, error)
- func Base(path string) string
- func BinaryPath() (string, error)
- func CacheDir() (string, error)
- func Chdir(path string) error
- func Chmod(path string, mode os.FileMode) error
- func ChownRecursive(root string, uid, gid int) error
- func Clean(path string) string
- func ConfigDir() (string, error)
- func CopyDir(src, dst string, opts ...CopyOption) error
- func CopyFile(src, dst string, opts ...CopyOption) error
- func CreateArchive(w io.Writer, root string, opts ...ArchiveCreateOption) error
- func CreateArchiveFile(path, root string, opts ...ArchiveCreateOption) error
- func Ctime(path string) (time.Time, error)
- func Cwd() (string, error)
- func DataDir() (string, error)
- func Dir(path string) string
- func DirSize(ctx context.Context, path string) (int64, error)
- func EnsureDir(path string, perm os.FileMode, opts ...MkdirOption) (created bool, err error)
- func EnsureFile(path string, defaults []byte, opts ...WriteOption) (created bool, err error)
- func EnsurePerm(path string, mode os.FileMode) error
- func EqualPath(a, b string) bool
- func EvalSymlinks(path string) (string, error)
- func EvalSymlinksWithin(parent, path string) (string, error)
- func ExecutableDir() (string, error)
- func Exists(path string) bool
- func Expand(path string, opts ...PathOption) (string, error)
- func Ext(path string) string
- func ExtFormat(path string) string
- func ExtractArchive(r io.Reader, dst string, opts ...ArchiveExtractOption) error
- func ExtractArchiveFile(path, dst string, opts ...ArchiveExtractOption) error
- func FilesystemType(path string) (string, error)
- func Find(root, pattern string, opts ...WalkOption) ([]string, error)
- func FindByRegex(root string, re *regexp.Regexp, opts ...WalkOption) ([]string, error)
- func FindFunc(root string, pred func(path string, info os.FileInfo) bool, opts ...WalkOption) ([]string, error)
- func FindUp(name, startDir string, opts ...FindOption) (string, bool, error)
- func FindUpAll(name, startDir string, opts ...FindOption) ([]string, error)
- func FirstExisting(paths []string) (string, bool)
- func FormatBytes(n int64) string
- func FormatError(err error, color ...bool) string
- func FromSlash(path string) string
- func Glob(pattern string, opts ...PathOption) ([]string, error)
- func GlobAny(patterns []string, opts ...PathOption) ([]string, error)
- func Hardlink(target, linkPath string) error
- func Hash(path string, algo HashAlgo) (string, error)
- func HashCompare(path, expected string, algo HashAlgo) error
- func Home() (string, error)
- func IsAbs(path string) bool
- func IsBlockDevice(path string) bool
- func IsCaseInsensitiveFS(path string) (bool, error)
- func IsCharDevice(path string) bool
- func IsDir(path string) bool
- func IsEmpty(path string) (bool, error)
- func IsExecutable(path string) bool
- func IsFIFO(path string) bool
- func IsFile(path string) bool
- func IsLocked(path string) bool
- func IsNetworkFS(path string) bool
- func IsReadable(path string) bool
- func IsReservedName(name string) bool
- func IsSocket(path string) bool
- func IsSubpath(parent, child string) (bool, error)
- func IsSymlink(path string) bool
- func IsTerminal(f *os.File) bool
- func IsWritable(path string) bool
- func Join(elem ...string) string
- func JoinSlash(elem ...string) string
- func ListDir(path string, opts ...ListOption) ([]stdfs.DirEntry, error)
- func LongPath(path string) (string, error)
- func Lstat(path string) (os.FileInfo, error)
- func Magic(path string, n int) ([]byte, error)
- func Match(pattern, name string) (bool, error)
- func Mkdir(path string, perm os.FileMode) error
- func MkdirAll(path string, perm os.FileMode, opts ...MkdirOption) error
- func MountPoint(path string) (string, error)
- func Move(src, dst string, opts ...CopyOption) error
- func Mtime(path string) (time.Time, error)
- func MustBeChildOf(parent, child string) error
- func NormalizeLineEndings(b []byte, target LineEnding) []byte
- func OpenAt(dir *os.File, name string, flag int, perm os.FileMode) (*os.File, error)
- func OpenAutoArchive(path string) (io.ReadCloser, error)
- func OpenChunked(path string, size int) (iter.Seq2[[]byte, error], func() error, error)
- func OpenLines(path string, opts ...ReadOption) (iter.Seq2[string, error], func() error, error)
- func OpenNoFollow(path string, flag int, perm os.FileMode) (*os.File, error)
- func OpenStdinLines() iter.Seq[string]
- func OpenWrite(path string, opts ...WriteOption) (*os.File, func() error, error)
- func Owner(path string) (uid, gid int, err error)
- func ParseBytes(s string) (int64, error)
- func ParseBytesIEC(s string) (int64, error)
- func PreflightSpace(path string, requiredBytes int64) error
- func ProcessStartTime(pid int) (string, error)
- func ProjectRoot(startDir string, opts ...FindOption) (string, error)
- func ReadAt(path string, offset int64, n int, opts ...ReadOption) ([]byte, error)
- func ReadFile(path string, opts ...ReadOption) ([]byte, error)
- func ReadFileMax(path string, maxSize int64) ([]byte, error)
- func ReadFirstLine(path string, opts ...ReadOption) (string, error)
- func ReadLines(path string, opts ...ReadOption) ([]string, error)
- func ReadLink(linkPath string) (string, error)
- func ReadStdin(opts ...ReadOption) ([]byte, error)
- func RegisterProjectKind(kind ProjectKind, markers ...string)
- func Rel(base, target string) (string, error)
- func Remove(path string, opts ...RemoveOption) error
- func RemoveAll(path string, opts ...RemoveOption) error
- func RemoveAllNoFollow(path string) error
- func RemoveContents(path string, opts ...RemoveOption) error
- func Rename(src, dst string, opts ...CopyOption) error
- func RestoreVersion(path, versionPath string, opts ...VersionedOption) error
- func Resume(journalDir string, opts ...ApplyOption) error
- func Rollback(journalDir string, opts ...ApplyOption) error
- func RuntimeDir() (string, error)
- func SameDevice(a, b string) (bool, error)
- func SameFile(a, b string) (bool, error)
- func SanitizeFilename(name string) string
- func ScaffoldApply(src stdfs.FS, dst string, vars any, opts ...ScaffoldOption) error
- func ScaffoldExtract(src stdfs.FS, dst string, opts ...ScaffoldOption) error
- func SetAtime(path string, t time.Time) error
- func SetLogger(l *slog.Logger)
- func SetMtime(path string, t time.Time) error
- func SetTimes(path string, atime, mtime time.Time) error
- func Split(path string) (dir, file string)
- func Stat(path string) (os.FileInfo, error)
- func StateDir() (string, error)
- func Stem(path string) string
- func StripUTF8BOM(b []byte) []byte
- func Symlink(target, linkPath string) error
- func SystemConfigDir(appName string) (string, error)
- func SystemDataDir(appName string) (string, error)
- func SystemStateDir(appName string) (string, error)
- func SystemTempDir() string
- func Tail(ctx context.Context, path string, opts ...TailOption) iter.Seq2[string, error]
- func TempDir(dir, pattern string) (string, func() error, error)
- func TempFile(dir, pattern string) (*os.File, func() error, error)
- func ToSlash(path string) string
- func Touch(path string, opts ...TouchOption) error
- func Walk(root string, fn WalkFunc, opts ...WalkOption) error
- func WalkParallel(ctx context.Context, root string, fn WalkParallelFunc, workers int) error
- func WarnInsecurePerm(path string, expected os.FileMode) (insecure bool, actual os.FileMode, err error)
- func WithDir(path string, fn func() error) (err error)
- func WithLock(path string, fn func() error) (err error)
- func WriteAt(path string, offset int64, data []byte, opts ...WriteOption) error
- func WriteFile(path string, data []byte, opts ...WriteOption) error
- func WriteFileExclusive(path string, data []byte, opts ...WriteOption) error
- func WriteFileVersioned(path string, data []byte, opts ...VersionedOption) (backup string, err error)
- func WriteStderr(data []byte) error
- func WriteStdout(data []byte) error
- func WriteString(path, s string, opts ...WriteOption) error
- type ApplyOption
- type ArchiveCreateOption
- type ArchiveExtractOption
- type ArchiveFormat
- type ArchiveHeader
- type Cache
- func (c *Cache) Close() error
- func (c *Cache) Delete(key string) error
- func (c *Cache) Entries() iter.Seq[CacheEntry]
- func (c *Cache) Get(key string) (value []byte, ok bool)
- func (c *Cache) GetWithError(key string) (value []byte, ok bool, err error)
- func (c *Cache) Purge() error
- func (c *Cache) Set(key string, value []byte) error
- func (c *Cache) Stats() (CacheStats, error)
- type CacheEntry
- type CacheOption
- type CacheStats
- type ContentMatch
- type CopyOption
- type DiskUsage
- type FindOption
- type Gitignore
- type HashAlgo
- type HashingWriter
- type LineEnding
- type ListOption
- type LockHandle
- type Mapping
- type MkdirOption
- type MultiError
- type PIDLockOption
- type PathError
- type PathOption
- type Plan
- type PlanAction
- type PlanOp
- type ProjectKind
- type ReadOption
- type RemoveOption
- type Rotator
- type RotatorOption
- type ScaffoldAction
- type ScaffoldActionOp
- type ScaffoldOnConflict
- type ScaffoldOption
- type TailOption
- type TouchOption
- type VersionInfo
- type VersionedOption
- type WalkFunc
- type WalkOption
- func WalkErrorHandler(fn func(path string, err error) error) WalkOption
- func WalkFollowSymlinks(b bool) WalkOption
- func WalkMaxDepth(n int) WalkOption
- func WalkSkipHidden(b bool) WalkOption
- func WalkSkipNames(names []string) WalkOption
- func WalkSkipPatterns(patterns []string) WalkOption
- func WithFindByContentMaxSize(n int64) WalkOption
- func WithWalkGitignore(g *Gitignore) WalkOption
- type WalkParallelFunc
- type WatchEvent
- type WatchOp
- type Watcher
- type WatcherOption
- type WorkspaceRoot
- type WriteOption
- func WithAtomic(b bool) WriteOption
- func WithBackup(suffix string) WriteOption
- func WithDirPerm(mode os.FileMode) WriteOption
- func WithLocked(b bool) WriteOption
- func WithMkdirAll(b bool) WriteOption
- func WithPerm(mode os.FileMode) WriteOption
- func WithSync(b bool) WriteOption
- func WithTempPattern(pattern string) WriteOption
Examples ¶
- Apply
- CopyDir
- CopyFile
- CreateArchiveFile
- Expand
- ExtractArchiveFile
- FindByContent
- FindUp
- FormatBytes
- Hash
- HashCompare
- HashWriter
- Mmap
- MustBeChildOf
- NewCache
- NewGitignore
- NewRotator
- NewWatcher
- OpenLines
- PIDLock
- ParseBytes
- ParseBytesIEC
- ProjectRoot
- ProjectType
- ReadFile
- ReadFirstLine
- ReadLines
- SanitizeFilename
- ScaffoldApply
- Stem
- Tail
- TryLock
- Walk
- WithCacheVersion
- WithLock
- WriteFile
- WriteFile (Backup)
- WriteFile (Ensure)
- WriteFile (Secret)
- WriteFileVersioned
Constants ¶
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.
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 ¶
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.
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") )
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.
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.
var ErrArchiveTooLarge = errors.New("fs: archive too large")
ErrArchiveTooLarge is returned by ExtractArchive when the cumulative extracted size exceeds the WithArchiveMaxBytes cap.
var ErrCacheClosed = errors.New("fs: cache: closed")
ErrCacheClosed is returned by Cache operations after Cache.Close has been called.
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.
var ErrRotatorClosed = errors.New("fs: rotator: closed")
ErrRotatorClosed is returned by Rotator.Write after Rotator.Close has been called.
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.
var ErrScaffoldPromptUnsupported = errors.New("fs: scaffold: prompt returned unsupported action")
ErrScaffoldPromptUnsupported is returned when WithScaffoldPromptFunc returns an action that isn't ScaffoldActionSkip / ScaffoldActionOverwrite / ScaffoldActionCreate.
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).
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 ¶
AppCacheDir returns CacheDir/<appName>. See AppConfigDir for appName validation.
func AppConfigDir ¶
AppConfigDir returns ConfigDir/<appName>. appName must not contain a path separator (`/` or `\`), be empty, or be `.` / `..`. Invalid appName errors with ErrInvalidPath.
func AppDataDir ¶
AppDataDir returns DataDir/<appName>.
func AppRuntimeDir ¶
AppRuntimeDir returns RuntimeDir/<appName>.
func AppStateDir ¶
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 BTime ¶
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 BinaryPath ¶
BinaryPath returns the absolute, symlink-resolved path of the running executable. Wraps os.Executable + filepath.EvalSymlinks.
func CacheDir ¶
CacheDir returns the user's per-user cache directory.
Linux/BSD: $XDG_CACHE_HOME or ~/.cache macOS: ~/Library/Caches Windows: %LOCALAPPDATA%
func Chdir ¶
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 ¶
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 ¶
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 ConfigDir ¶
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 Cwd ¶
Cwd returns the process's current working directory. Wraps os.Getwd with the package's error envelope.
func DataDir ¶
DataDir returns the user's per-user data directory.
Linux/BSD: $XDG_DATA_HOME or ~/.local/share macOS: ~/Library/Application Support Windows: %APPDATA%
func DirSize ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ExtFormat ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
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 ¶
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 ¶
Home returns the current user's home directory. Wraps os.UserHomeDir.
func IsBlockDevice ¶
IsBlockDevice reports whether path exists and is a block-special file. Returns false on Windows where the file type doesn't apply.
func IsCaseInsensitiveFS ¶
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 ¶
IsCharDevice reports whether path exists and is a character-special file. Returns false on Windows where the file type doesn't apply.
func IsDir ¶
IsDir reports whether path exists and is a directory. Symlinks to directories are followed.
func IsEmpty ¶
IsEmpty reports whether path is an empty directory. A non-directory returns (ErrNotDir); a missing path returns the wrapped ErrNotFound.
func IsExecutable ¶
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 ¶
IsFIFO reports whether path exists and is a named pipe (FIFO). Returns false on Windows where the file type doesn't apply.
func IsFile ¶
IsFile reports whether path exists and is a regular file. Symlinks to regular files are followed.
func IsLocked ¶
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 ¶
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 ¶
IsReadable reports whether the calling process can read path.
func IsReservedName ¶
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 ¶
IsSocket reports whether path exists and is a Unix-domain socket. Returns false on Windows where the file type doesn't apply.
func IsSubpath ¶
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 ¶
IsSymlink reports whether path exists and is itself a symlink (not a target of one). Uses os.Lstat.
func IsTerminal ¶
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 ¶
IsWritable reports whether the calling process can write path. For directories this means new entries can be created inside.
func JoinSlash ¶
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 ¶
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 Magic ¶
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 ¶
Match reports whether name matches the filepath.Match glob pattern. Wraps the stdlib function with the package's error envelope for consistency.
func Mkdir ¶
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 ¶
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 MustBeChildOf ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ParseBytes ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
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 ¶
SameDevice reports whether a and b live on the same filesystem.
func SameFile ¶
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 ¶
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 ¶
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 SetLogger ¶
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 Stat ¶
Stat wraps os.Stat with the package's error chain. The returned FileInfo is the stdlib type.
func StateDir ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
SystemDataDir is the system-wide analog of DataDir.
Linux/BSD: /var/lib/<appName> macOS: /Library/Application Support/<appName> Windows: %PROGRAMDATA%\<appName>
func SystemStateDir ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
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 ¶
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.
}
Output:
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 ¶
WriteStderr is the os.Stderr analog of WriteStdout.
func WriteStdout ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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).
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 ¶
LoadGitignore reads patterns from filePath and compiles them into a *Gitignore.
func NewGitignore ¶
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 ¶
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 )
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) 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 ¶
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
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.
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 ¶
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 ¶
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 ¶
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.
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 (*Plan) Create ¶
Create appends a PlanActionCreate op. Returns p for chaining.
func (*Plan) Delete ¶
Delete appends a PlanActionDelete op. Returns p for chaining.
func (*Plan) Diff ¶
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 ¶
Rename appends a PlanActionRename op moving src to dst. 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 ¶
Close releases the underlying file handle. Subsequent Write calls return ErrRotatorClosed. Idempotent.
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 ¶
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 ¶
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 ¶
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 ¶
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 )
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
}
}
}
Output:
func (*Watcher) Close ¶
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.*`.
Source Files
¶
- archive.go
- archive_create_tar.go
- archive_create_zip.go
- archive_extract_tar.go
- archive_extract_zip.go
- archive_options.go
- archive_sniff.go
- bytes.go
- cache.go
- chown.go
- copy.go
- cwd.go
- dir.go
- diskinfo.go
- diskinfo_linux.go
- doc.go
- encoding.go
- ensure.go
- errors.go
- fault_hooks.go
- find.go
- find_content.go
- format.go
- gitignore.go
- glob.go
- hash.go
- links.go
- lock.go
- lock_unix.go
- logger.go
- longpath_unix.go
- mmap.go
- mmap_unix.go
- mode.go
- path.go
- plan.go
- predicates.go
- predicates_unix.go
- proc_starttime.go
- proc_starttime_linux.go
- project_type.go
- read.go
- remove.go
- remove_nofollow.go
- rotate.go
- safeopen.go
- safeopen_linux.go
- safeopen_unix.go
- sanitize.go
- scaffold.go
- scaffold_options.go
- scaffold_template.go
- scaffold_version.go
- stat.go
- stat_linux.go
- stdio.go
- stdio_linux.go
- stdio_unix.go
- tail.go
- tail_unix.go
- temp.go
- userdirs.go
- userdirs_xdg.go
- versioned.go
- walk.go
- walk_parallel.go
- walk_unix.go
- watcher.go
- watcher_backend.go
- watcher_backend_inotify_linux.go
- watcher_backend_polling.go
- watcher_debounce.go
- watcher_event.go
- watcher_options.go
- workspace.go
- write.go
- write_unix.go
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. |