billy backend support¶
The adapter delegates to whatever billy.Filesystem you hand it, so several answers to
"does this work?" are really answers about the backend. This page states what each of the
common backends honours, and which behaviour is the adapter's and which is billy's.
Facts on this page were checked against go-billy v5.9.1, the version this module requires. A different billy release may change them.
The capability matrix¶
memfs.New() |
osfs.New(dir) |
osfs.New(dir, osfs.WithBoundOS()) |
|
|---|---|---|---|
| Concrete type | *chroot.ChrootHelper |
*chroot.ChrootHelper |
*osfs.BoundOS |
Name() returns |
/ |
dir |
dir |
Implements billy.Chmod |
yes | yes | yes |
Implements billy.Change |
no | no | no |
Chmod observable |
in memory | on disk | on disk |
| File mode on create | recorded | on disk | on disk |
Directory mode from Mkdir |
honoured | ignored, always 0755 |
honoured |
| Symlinks | yes | yes | relative targets only |
Files satisfy io.WriterAt |
no | no | yes |
File.Sync() flushes |
no | no | no — the adapter never calls it |
memfs.New() and osfs.New(dir) both wrap themselves in billy's chroot helper, which
is why they share a concrete type and several of these answers.
Which backend should you use?¶
memfs.New()for tests and for anything that must not touch disk. It is what go-git gives you for an in-memory clone.osfs.New(dir)for a local git worktree. This is what go-git uses for a disk-backed clone, so it is the backend the flagship use case actually runs on.osfs.New(dir, osfs.WithBoundOS())when you want the base directory enforced at the OS boundary. billy marks the defaultChrootOSas deprecated in favour of it, but the behaviour differs in ways listed below, so it is not a drop-in swap.chroot.New(fs, base)to narrow any of the above to a subtree, then wrap the result.
Nothing here is a security boundary — see what this does not do.
Are file permissions honoured?¶
Yes on all three, both at create time and through Chmod.
fs := aferobilly.New(osfs.New(dir))
_ = afero.WriteFile(fs, "script.sh", []byte("#!/bin/sh\n"), 0o644)
_ = fs.Chmod("script.sh", 0o755) // observable as 0755 on disk
This matters for git: go-git derives a committed blob's mode (100755 vs 100644) from
the worktree file's mode, so a scaffolder that writes a script through the adapter and
chmods it executable produces an executable blob. Before v0.1.2 Chmod was an
unconditional no-op and it did not.
memfs records the mode and reports it back through Stat, but nothing enforces it —
there is no inode and no kernel involved.
Are directory permissions honoured?¶
Not on osfs.New(dir). billy's ChrootOS.MkdirAll ignores the perm argument and
calls os.MkdirAll(path, 0755) unconditionally:
func (fs *ChrootOS) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, defaultDirectoryMode) // defaultDirectoryMode = 0o755
}
So fs.MkdirAll("/private", 0o700) produces a 0755 directory on disk. memfs and
osfs with WithBoundOS() both honour the mode you pass.
If you need a restricted directory on the default osfs, create it and then Chmod it —
Chmod is delegated and does reach disk.
Do Chown and Chtimes work anywhere?¶
No, on any backend, ever. Both return nil without doing anything. go-billy exposes no
interface for either operation: billy.Change — which adds Chown, Lchown and
Chtimes on top of Chmod — is declared but implemented by no backend in the
library, so there would be nothing to delegate to even if the adapter tried.
If ownership or timestamps matter to your code, this adapter cannot carry them. Operate
on the real path with os directly.
Do symlinks work?¶
memfs and osfs.New(dir) handle both relative and absolute targets.
WithBoundOS() rejects absolute targets. Creating the link succeeds, and reading it
back fails:
fs := aferobilly.New(osfs.New(dir, osfs.WithBoundOS()))
sl := fs.(afero.Symlinker)
_ = sl.SymlinkIfPossible("/t", "/l") // succeeds
_, err := sl.ReadlinkIfPossible("/l") // "/l": path outside base dir "…": file does not exist
_ = sl.SymlinkIfPossible("t", "l") // relative target
got, err := sl.ReadlinkIfPossible("l") // "t", nil
The same absolute pair works under osfs.New(dir). Use relative targets if you might
switch backends.
Why is WriteAt emulated on memfs and osfs?¶
Because billy's chroot helper wraps every file it returns in a struct that embeds the
billy.File interface, not the concrete file. Embedding an interface promotes only
that interface's methods, and billy.File deliberately omits io.WriterAt — upstream
has it commented out with // TODO: Add io.WriterAt for v6.
memfs.New() and osfs.New(dir) both go through chroot, so their files never satisfy
io.WriterAt from the outside even though an *os.File sits underneath. The adapter
falls back to seek, write, seek back. osfs.New(dir, osfs.WithBoundOS()) returns an
unwrapped *osfs.file, which does satisfy it.
The observable result is identical; only the number of backend calls differs. Details on the fallback are in filesystem operations.
What if the backend does not implement a capability?¶
A billy.Filesystem assembled from a billy.Basic — for instance via
chroot.New(basic, "/") — goes through billy's polyfill helper, which answers any
missing capability with billy.ErrNotSupported ("feature not supported"). That error
travels back through the adapter unchanged:
MkdirAll, ReadDir, TempFile, Symlink, Lstat, Readlink and Chmod can all
return it. Chmod is the one exception where the adapter has its own fallback: if the
value you passed to New does not itself satisfy billy.Chmod, the adapter returns nil
and does nothing rather than delegating. A chroot-wrapped filesystem always satisfies
billy.Chmod, so in that case you get ErrNotSupported rather than the silent no-op.
Does go-git's worktree filesystem work?¶
Yes, and it is the case the module exists for. repo.Worktree().Filesystem is a billy
filesystem — the package documentation names osfs as what a local clone gives you, and an
in-memory clone gives you memfs. Both are covered above. The adapter never imports
go-git; a dependency guard test keeps it out of the graph. See
work with a go-git worktree.
Related¶
- Filesystem operations — behaviour that is the same on every backend.
- API reference —
New,Option,WithLocker.