Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,91 @@ type RemoveFileFS interface {
RemoveFile(name string) error
RemoveAll(name string) error
}

// RenameFS is the interface implemented by a filesystem that supports
// renaming files. On POSIX-backed filesystems Rename is atomic when both
// paths are on the same filesystem, which is the primitive used to commit
// atomic writes.
type RenameFS interface {
fs.FS
Rename(oldpath, newpath string) error
}

// SyncWriterFile is a WriterFile that can flush its contents to stable
// storage. osfs implements Sync via (*os.File).Sync; memfs implements it
// as a no-op so the same caller code works on both backends.
type SyncWriterFile interface {
WriterFile
Sync() error
}
```

## Capability layers

wfs follows the same pattern as `io/fs`'s optional interfaces (`fs.GlobFS`,
`fs.StatFS`, ...): start from `fs.FS` and add capabilities by asserting to
optional interfaces. Each capability has a top-level helper that performs
the assertion and returns `ErrNotImplemented` (wrapped in `*fs.PathError`)
if the underlying filesystem does not support it.

| Capability | Interface | Helper |
| --- | --- | --- |
| Read | `fs.FS` | `fs.Open`, `fs.ReadFile`, ... |
| Write | `wfs.WriteFileFS` | `wfs.MkdirAll`, `wfs.CreateFile`, `wfs.WriteFile` |
| Remove | `wfs.RemoveFileFS` | `wfs.RemoveFile`, `wfs.RemoveAll` |
| Atomic rename | `wfs.RenameFS` | `wfs.Rename` |
| File-level fsync | `wfs.SyncWriterFile` | type-assert the `WriterFile` returned by `CreateFile` |

## Atomic writes

`RenameFS` and `SyncWriterFile` together let callers implement crash-safe
atomic writes (temp file + sync + rename) entirely through the wfs
abstraction. The pattern works unchanged across `osfs` (where Rename and
Sync delegate to the OS) and `memfs` (where Rename moves the entry under
the filesystem mutex and Sync is a no-op).

```go
func atomicWrite(fsys fs.FS, name string, src io.Reader) error {
tmp := name + ".tmp-xxxxxx" // caller generates a unique suffix
f, err := wfs.CreateFile(fsys, tmp, 0o644)
if err != nil {
return err
}
// Best-effort cleanup; no-op after successful rename.
defer func() { _ = wfs.RemoveFile(fsys, tmp) }()

if _, err := io.Copy(f, src); err != nil {
_ = f.Close()
return err
}
if sf, ok := f.(wfs.SyncWriterFile); ok {
if err := sf.Sync(); err != nil {
_ = f.Close()
return err
}
}
if err := f.Close(); err != nil {
return err
}
return wfs.Rename(fsys, tmp, name)
}
```

## memfs limitations

`memfs` is intended for tests and small in-process workflows. A few
behaviors differ from `osfs` and are worth knowing:

- **Writes are visible only after `Close`.** `MemFile` buffers writes
locally; other readers do not see the new contents until `Close`
returns successfully. `osfs` makes writes visible immediately.
- **`Sync` is a no-op.** It exists so that atomic-write helpers can share
one code path across backends. On `memfs` it does *not* publish the
buffered bytes — only `Close` does.
- **`Rename` supports files only.** Renaming a directory currently
returns a `*fs.PathError`. `osfs.Rename` delegates to `os.Rename` and
therefore handles directories on POSIX systems.

This is one of the solutions to an [issue](https://github.com/golang/go/issues/45757) of github.com/golango/go.

The following packages are an implementation of wfs.
Expand Down
Loading