An afero.Fs implementation that wraps a
remote afero.Fs with an in-memory write-through / read-through cache.
Every write goes to the remote filesystem — synchronously by default, or
asynchronously if configured — and is mirrored into an in-memory
afero.MemMapFs for a configurable TTL. Reads served within the TTL never
touch the remote fs. Once a cache entry expires, a background sweeper
reclaims the memory, and the next read transparently falls through to the
remote fs and repopulates the cache.
go get github.com/kdihalas/write-through-cache
go get github.com/spf13/aferoimport (
"time"
cache "github.com/kdihalas/write-through-cache"
"github.com/spf13/afero"
)
remote := afero.NewOsFs() // or any other afero.Fs: S3, SFTP, NFS, ...
fs := cache.New(remote, 2*time.Second)
defer fs.Close() // stops the background eviction sweeper
afero.WriteFile(fs, "/data/hello.txt", []byte("hi"), 0o644) // hits remote + cache
afero.ReadFile(fs, "/data/hello.txt") // served from cache, TTL permittingSee example/example.go for a runnable end-to-end demo
(write, cached read, and read-through after TTL expiry):
go run ./example- TTL counts from write time, refreshed on every write to the same path.
- Write mode is configurable:
- Default: synchronous write-through —
Write/Closeonly return once the remote write completes. cache.WithAsync(onError): write-behind — the call returns as soon as the in-memory write succeeds; the remote write happens in the background, andonError(path, err)fires if it fails.
- Default: synchronous write-through —
- Reads are read-through: a cache miss or expired entry falls through to the remote fs, repopulating the cache on success.
- Eviction is handled by a background sweeper goroutine on a ticker
(default interval:
ttl/4, minimum 1s; override withcache.WithSweepInterval). CallFs.Close()to stop it on shutdown. Remove,RemoveAll,Rename,Chmod,Chown, andChtimesare applied to the remote fs (source of truth) and mirrored to the cache/expiry state.
go build ./...
go vet ./...
go test ./... -raceVersioning and CHANGELOG.md are managed by
release-please (see
.github/workflows/release-please.yml). It watches main, and on every push
opens or updates a "release PR" summarizing pending changes; merging that PR
tags a release and publishes a GitHub Release. Since this is a Go module,
there's nothing further to "publish" — consumers pull the new version
straight from the git tag via the Go module proxy.
Commit messages on main must follow
Conventional Commits (fix:,
feat:, feat!: / BREAKING CHANGE:, etc.) — release-please uses them to
decide the next version bump and changelog entries.