Documentation
¶
Overview ¶
Package configiofs adapts any io/fs.FS to the read-only config.FS interface, so configuration compiled into a binary with embed.FS, packed in a zip or tar archive, mounted under os.DirFS, or built in an [fstest.MapFS] can be read by a config Store without copying it onto disk first.
//go:embed defaults/config.yaml var defaults embed.FS store, err := config.NewStore(ctx, config.WithFiles(configiofs.Wrap(defaults), "defaults/config.yaml"))
The headline case is a tool shipping its own defaults compiled in: an embed.FS never changes at runtime, so reading through it is exactly reading bytes the binary already carries.
Read-only by design ¶
io/fs has no write, rename or remove — it is read-only by design — so this adapter is too. The two read methods map onto the two stdlib helpers (fs.ReadFile and fs.Stat); the four write methods each return config.ErrReadOnlyFS, the shared sentinel a caller can errors.Is regardless of which read-only filesystem produced it. A write routed at a compiled-in embed.FS fails loudly at the point of the write, which is correct: there is nothing to write to.
Names are io/fs names, not OS paths ¶
io/fs uses unrooted, forward-slash-separated names with no leading slash: pass "defaults/config.yaml", not "/etc/app.yaml". A name fs.ValidPath rejects — a leading slash, a ".." segment, a backslash — is refused early with an error wrapping fs.ErrInvalid, so a fat-fingered absolute path surfaces as the mistake it is rather than reading as a normal missing source. A valid-but-absent name still returns the fs.ErrNotExist-shaped error the Store treats as a normal missing overlay.
A DirLister where io/fs can list, never a RealPather or LinkReader ¶
The wrapped filesystem satisfies config.DirLister, so a directory-shaped backend such as config-filekv can enumerate it, whenever the io/fs.FS can honour fs.ReadDir: it is an fs.ReadDirFS, or its root opens as an fs.ReadDirFile. Every standard-library filesystem qualifies. One that does neither gets a type without ReadDir rather than one that fails on every call.
io/fs exposes no operating-system path and no symlink resolution, so the wrapped filesystem deliberately satisfies neither config.RealPather nor config.LinkReader. Because it is not a config.RealPather, a watched filesystem is polled rather than watched natively — which is harmless for the headline embed.FS case, where a poll simply re-reads the same bytes and stays quiet.
If you want native watching over a real directory, you already have the right tool: config.Dir, which is a config.RealPather by construction. This package is for the cases that genuinely have no OS path.
To scope a wrapped filesystem to a sub-directory, compose fs.Sub before Wrap — configiofs.Wrap(must(fs.Sub(fsys, "conf"))) — which needs no help from this module.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Wrap ¶
Wrap adapts any io/fs.FS to a read-only config.FS. ReadFile and Stat map onto fs.ReadFile and fs.Stat; the four write methods — WriteFile, Rename, Remove and MkdirAll — return config.ErrReadOnlyFS.
The returned value satisfies config.DirLister when the filesystem can list (see the package doc), and never config.RealPather or config.LinkReader: an io/fs.FS has no operating-system path and no symlink resolution to honour them with.
Example ¶
ExampleWrap reads configuration compiled into the binary. An embed.FS — here the test's own fixture — satisfies io/fs.FS, so Wrap hands it to a config Store as a read-only source. Names are io/fs names: pass the embedded path, not an OS path.
store, err := config.NewStore(context.Background(),
config.WithFiles(configiofs.Wrap(embedded), "testdata/defaults/config.yaml"))
if err != nil {
log.Fatal(err)
}
fmt.Println(store.View().GetInt("server.port"))
Output: 8080
Types ¶
This section is empty.