configbilly

package module
v0.1.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 5 Imported by: 0

README

config-billy

Read and write configuration through a go-billy filesystem with config

Go Reference Pipeline Coverage phpboyscout Go toolkit

Part of the phpboyscout Go toolkit — small, framework-free Go modules extracted from go-tool-base. Documented with the parent module at config.go.phpboyscout.uk


config defines its own six-method filesystem interface, config.FS, so that importing it does not oblige you to import a filesystem library chosen for you. go-billy is the storage-agnostic filesystem abstraction underneath go-git and a large family of Git tooling. If your tool already holds a billy.Filesystem — an on-disk tree (osfs), an in-memory one (memfs), a chrooted subtree, or your own implementation — config-billy is the bridge that hands it to a config Store unchanged.

import (
	"github.com/go-git/go-billy/v5/osfs"
	"gitlab.com/phpboyscout/go/config"
	configbilly "gitlab.com/phpboyscout/go/config-billy"
)

bfs := osfs.New("/etc/app")

store, err := config.NewStore(ctx,
	config.WithFiles(configbilly.Wrap(bfs), "config.yaml"))

configbilly.Wrap(bfs) returns a config.FS you pass to WithFiles exactly as you would config.OS(). It reads and writes: all six methods work, and a config write (store.Apply(...)) lands back through the wrapped filesystem.

If all you need is the real operating-system filesystem, you do not need this module — config.OS() is that, with no dependency. And a test wanting a throwaway directory wants config.Dir over t.TempDir(), which gives real file semantics including native watching.

Paths are relative to the wrapped filesystem's root

go-billy names are forward-slash and interpreted relative to the wrapped filesystem's root — an osfs.New("/etc/app") reads "config.yaml", not "/etc/app/config.yaml" — like config.Dir, unlike config.OS(). Names pass through Wrap unchanged: the adapter does not normalise or validate them, and the Store composes paths itself. Do not pass an absolute operating-system path expecting config.OS() semantics.

Writing: a native rename, atomic per backend

A config write is committed the way the whole module commits one — the Store stages the new content beside the target and renames over it, through the config.FS methods. config-billy maps that rename to go-billy's native Rename (a real filesystem-level rename, never a copy-then-delete emulation), so the atomicity is whatever the wrapped filesystem gives: on osfs a same-directory rename is os.Rename (atomic on POSIX); on memfs it is an in-process map swap. Either way a reader never observes a half-written file.

The overwrite semantics of Rename — whether renaming over an existing target succeeds or errors — vary by billy backend. The Store's stage-beside-and-rename-over model assumes rename over an existing target succeeds, which osfs and memfs both honour; a backend that refuses it would need its own handling.

What it claims, and what it declines

  • It is polled, not watched natively. A billy.Filesystem cannot honestly report an operating-system path through its interface — Root() returns a logical root (memfs reports "/"), the concrete type is the same wrapper for a disk-backed and an in-memory filesystem alike, and capabilities describe file-I/O features, not whether a path is real. So the adapter does not satisfy config.RealPather, and config watches it with its poll watcher on the responsive default cadence — cheap for both osfs and memfs.
  • It resolves symlinks. Readlink is mandatory on every billy.Filesystem, so the adapter satisfies config.LinkReader unconditionally, and a write to a symlinked configuration file lands on the target rather than replacing the link. Where the path is a regular file (not a link), billy returns an honest "not a link" error and config falls back to writing through the path as given.
  • A read-only billy filesystem is honoured. When billy.Capabilities() reports no WriteCapability — a filesystem mounted read-only — the write methods return config.ErrReadOnlyFS up front, so errors.Is(err, config.ErrReadOnlyFS) holds whether a filesystem is read-only by mount or by design. A genuinely writable filesystem that then fails still surfaces the underlying error unchanged.

What it costs

The config graph plus exactly one third-party module, github.com/go-git/go-billy/v5 — asserted by an allowlist test so an unforeseen transitive addition fails the build rather than arriving quietly. go-billy's disk-backed osfs sub-package pulls a little more, but osfs is reached only in tests, so it never enters the graph a consumer inherits. testify is a test-only dependency and reaches no consumer.

License

See LICENSE.

Documentation

Overview

Package configbilly adapts a billy.Filesystem to the config.FS interface, so code already holding a go-billy filesystem — the storage abstraction underneath go-git and a large family of Git tooling — can hand it to a config Store unchanged.

The config module defines its own six-method filesystem interface rather than depending on go-billy, so that importing it does not oblige a consumer to import a filesystem library chosen for them. This adapter is for consumers who have made that choice already — a tool built on go-billy, holding an on-disk tree (osfs), an in-memory one (memfs), a chrooted subtree, or its own implementation, that needs configuration read through it.

store, err := config.NewStore(ctx,
	config.WithFiles(configbilly.Wrap(bfs), "config.yaml"))

If all you need is the real filesystem, you do not need this package: config.OS is that, with no dependency. And a test wanting a throwaway directory wants config.Dir over t.TempDir(), which gives real file semantics including watching.

Paths are relative to the wrapped filesystem's root

go-billy names are forward-slash and interpreted relative to the wrapped filesystem's root — an osfs.New("/etc/app") reads "config.yaml", not "/etc/app/config.yaml" — like config.Dir, unlike config.OS. Names pass through Wrap unchanged; the adapter does not normalise or validate them, and the Store composes paths itself. Do not pass an absolute operating-system path expecting config.OS semantics.

What this adapter claims, and what it declines

Six methods map onto go-billy almost directly. The part worth not rediscovering is which optional interfaces the adapter may claim.

It does NOT satisfy config.RealPather. A billy.Filesystem cannot honestly report an operating-system path through its interface: Root() returns a logical root (memfs reports "/"), the concrete type is the same wrapper for a disk-backed and an in-memory filesystem alike, and capabilities describe file-I/O features, not whether a path is real. Claiming a real path it cannot prove would make every wrapped filesystem look watchable, so native notification would be selected and the absence of anything to watch discovered one path at a time. The adapter is therefore polled by config's core watcher, which is cheap for both osfs and memfs.

It DOES satisfy config.LinkReader unconditionally, because Readlink is mandatory on every billy.Filesystem (via the billy.Symlink interface). Where the target is not a link, Readlink returns an honest "not a link" error and the Store falls back to writing through the path as given.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Wrap

func Wrap(bfs billy.Filesystem) config.FS

Wrap adapts a go-billy filesystem to config.FS.

The result reads and writes through the wrapped filesystem and is watched by polling — a billy.Filesystem cannot honestly report an operating-system path (see the package doc), so it does not satisfy config.RealPather. It always satisfies config.LinkReader, because Readlink is mandatory on every billy.Filesystem.

A billy filesystem mounted read-only is honoured: when billy.Capabilities reports no WriteCapability, the write methods return config.ErrReadOnlyFS up front, so a read-only filesystem gives a consumer a consistent errors.Is result. A genuinely writable filesystem that then fails still surfaces the underlying error unchanged.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL