fs: files, permissions, locks¶
fs holds what differs between Windows and Unix when working with files.
Permissions¶
Mode bits are not enforced on Windows. Code that refuses a world-readable key
file, and tests that assert 0600, are wrong there unless they ask first.
| Function | Behavior |
|---|---|
PermissionsEnforced() |
Whether the platform enforces mode bits; false on Windows |
IsPrivate(info) |
Owner-only. Always true where modes are not enforced |
CheckPrivate(info) |
An error matching ErrNotPrivate that names the mode; nil where modes are not enforced |
ChmodPrivate(f) |
Restricts an open file to its owner (0600); a failure is ignored where modes are not enforced |
WriteFilePrivate, MkdirAllPrivate |
Create a file or directory for the owner only |
PrivateFilePerm, PrivateDirPerm, DefaultFilePerm, DefaultDirPerm |
The usual modes as constants |
Refusing a file other users can read:
info, err := os.Stat(path)
if err != nil {
return err
}
if err := fs.CheckPrivate(info); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
In a test, assert only where modes mean something:
if fs.PermissionsEnforced() {
info, _ := os.Stat(path)
if err := fs.CheckPrivate(info); err != nil {
t.Fatal(err)
}
}
File locks¶
unlock, err := fs.Lock(ctx, "/var/lib/myapp/state.lock")
if err != nil {
return err
}
defer unlock()
Lock takes an exclusive advisory lock on a file, creating it (owner-only) if
needed. It uses flock on Unix and LockFileEx on Windows.
- It waits until the lock is free or
ctxends, and then returns the context's error. Give it a deadline. - The lock is held on an open handle, so the operating system releases it if the process dies, however it dies. A crashed holder never wedges everyone else.
- Two calls in the same process exclude each other, as two processes do.
- It is advisory: it excludes only callers that also take it.
unlockmay be called more than once.- On a platform with no locking it returns
ErrLockUnsupported. A caller that needs mutual exclusion should fail closed and not proceed without it.
Keep the lock file separate from the file it protects. Replacing the protected file by rename would otherwise swap the file the lock is held on.
Durable renames¶
A rename is durable across a crash only once the directory is flushed. SyncDir
does that on Unix. On Windows, and wherever the platform has no way to ask, it
does nothing and returns nil: a rename there is atomic and nothing stronger is
available. You get the strongest guarantee the platform offers without a
platform check.
The complete write-temp, flush, rename, flush-directory sequence is shown in Getting started.
Paths¶
| Function | Behavior |
|---|---|
ValidatePath(p) |
Rejects .. traversal and absolute or rooted paths, with either separator |
ValidateSlashPath(p) |
Like ValidatePath, for a path that uses forward slashes everywhere; also rejects backslashes, colons, and empty or . elements |
IsAbsAny(p) |
Whether a path is absolute or rooted on any platform |
SafeJoin(base, rel) |
Joins and refuses a result that escapes base |
NormalizeLineEndings(s), NormalizeLineEndingsBytes(b) |
Convert CRLF to LF; a lone CR is kept |
NormalizePath(p) |
Converts to forward slashes, for storage |
OSPath(p) |
Converts to the platform's separator |
PathEqual, PathHasPrefix |
Compare with the platform's case rules |
IsExecutable(path) |
Mode bits on Unix; extension on Windows |
Paths from somewhere else¶
A path read from a document, an archive, a configuration file or another
system must stay inside its root whichever platform reads it. filepath.IsAbs
cannot check that: it answers for the platform it runs on. Windows does not
treat /etc/passwd as absolute, because it names a folder on the current
drive, and Unix does not treat \etc\passwd, \\server\share or C:x as
anything but a relative file name.
IsAbsAny gives the same answer everywhere. It is true for a path that starts
with / or \, including UNC paths, and for one that starts with a drive
letter, including the drive-relative C:x.
| Path | filepath.IsAbs on Unix |
on Windows | IsAbsAny |
|---|---|---|---|
/etc/passwd |
true | false | true |
\etc\passwd |
false | false | true |
\\server\share |
false | true | true |
C:\x, C:/x |
false | true | true |
C:x |
false | false | true |
docs/a.md |
false | false | false |
Choose the check by what the path names:
- A file on this machine, such as a command-line argument:
filepath.IsAbsandfilepath.Joinare right, because the platform's rules apply. - A path relative to a root, from anywhere, with either separator:
ValidatePath, thenSafeJointo read it. - A path that uses forward slashes everywhere, such as a path inside a
repository, an archive or a URL:
ValidateSlashPath. It also refuses backslashes, colons, and empty or.elements, so the path names the same file on every platform.
if err := fs.ValidateSlashPath(src.Path); err != nil {
return fmt.Errorf("source path %q: %w", src.Path, err)
}
data, err := os.ReadFile(filepath.Join(checkout, filepath.FromSlash(src.Path)))
The errors are ErrEmptyPath, ErrAbsolutePath, ErrPathTraversal and, from
ValidateSlashPath only, ErrInvalidPath.