buildr/internal/storage/unionfs_unix.go
Peter 1261932bdc
All checks were successful
continuous-integration/drone/push Build is passing
refactor: apply golangci-lint findings
2023-06-22 19:16:00 +02:00

228 lines
5.1 KiB
Go

//go:build linux || darwin
package storage
import (
"context"
"io/fs"
"os"
"path/filepath"
"strings"
"syscall"
unionfs "github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
)
type Layer struct {
Source string
Target string
}
type Layers []Layer
func MountUnionFS(rootNode *UnionFSNode, mountDir string, layers Layers) (*fuse.Server, error) {
options := &unionfs.Options{
MountOptions: fuse.MountOptions{
AllowOther: false,
Options: nil,
FsName: "buildr-fs",
Name: "buildr",
Debug: false,
DirectMount: true,
},
OnAdd: func(ctx context.Context) {
rootNode.onAdd(ctx, layers)
},
}
return unionfs.Mount(mountDir, rootNode, options)
}
func NewUnionFS(outDir string) *UnionFSNode {
return &UnionFSNode{
OutDir: outDir,
}
}
var (
_ unionfs.NodeCreater = (*UnionFSNode)(nil)
_ unionfs.NodeMkdirer = (*UnionFSNode)(nil)
)
type UnionFSNode struct {
unionfs.Inode
OutDir string
}
func (u *UnionFSNode) Create(
ctx context.Context,
name string,
flags, mode uint32,
out *fuse.EntryOut,
) (node *unionfs.Inode, fh unionfs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
realPath := filepath.Join(u.OutDir, name)
flags &^= syscall.O_APPEND
fd, err := syscall.Open(realPath, int(flags)|os.O_CREATE, mode)
if err != nil {
return nil, nil, 0, unionfs.ToErrno(err)
}
_ = u.preserveOwner(ctx, realPath)
st := syscall.Stat_t{}
if err := syscall.Fstat(fd, &st); err != nil {
_ = syscall.Close(fd)
return nil, nil, 0, unionfs.ToErrno(err)
}
out.FromStat(&st)
fh = &osFileHandle{file: os.NewFile(uintptr(fd), name)}
ufsFile := &unionFsFile{targetPath: filepath.Join(u.OutDir, name), readWrite: true}
ch := u.Inode.NewInode(ctx, ufsFile, unionfs.StableAttr{})
return ch, fh, 0, 0
}
func (u *UnionFSNode) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*unionfs.Inode, syscall.Errno) {
p := filepath.Join(u.OutDir, name)
err := os.Mkdir(p, os.FileMode(mode))
if err != nil {
return nil, unionfs.ToErrno(err)
}
_ = u.preserveOwner(ctx, p)
st := syscall.Stat_t{}
if err := syscall.Lstat(p, &st); err != nil {
_ = syscall.Rmdir(p)
return nil, unionfs.ToErrno(err)
}
out.Attr.FromStat(&st)
node := &UnionFSNode{OutDir: p}
ch := u.NewInode(ctx, node, u.idFromStat(&st))
return ch, 0
}
func (u *UnionFSNode) onAdd(ctx context.Context, layers Layers) {
for _, layer := range layers {
p := &u.Inode
if layer.Target == "." {
layer.Target = ""
}
addDir := func(dirToAdd string) {
for _, component := range strings.Split(dirToAdd, "/") {
if len(component) == 0 {
continue
}
ch := p.GetChild(component)
if ch == nil {
// Create a directory
ch = p.NewPersistentInode(ctx,
&UnionFSNode{OutDir: u.OutDir},
unionfs.StableAttr{Mode: syscall.S_IFDIR})
// Add it
p.AddChild(component, ch, true)
}
p = ch
}
}
addDir(layer.Target)
// Make a file out of the content bytes. This type
// provides the open/read/flush methods.
dirFs := os.DirFS(layer.Source)
_ = fs.WalkDir(dirFs, ".", func(path string, d fs.DirEntry, err error) error {
if d.IsDir() || path == "." {
return nil
}
dir, file := filepath.Split(path)
if ch := getChild(ctx, p, dir); ch != nil {
fileNode := &unionFsFile{
targetPath: filepath.Join(layer.Source, path),
}
// Create the file. The Inode must be persistent,
// because its life time is not under control of the
// kernel.
child := p.NewPersistentInode(ctx, fileNode, unionfs.StableAttr{})
// And add it
ch.AddChild(file, child, true)
}
return nil
})
}
}
func (u *UnionFSNode) idFromStat(st *syscall.Stat_t) unionfs.StableAttr {
// We compose an inode number by the underlying inode, and
// mixing in the device number. In traditional filesystems,
// the inode numbers are small. The device numbers are also
// small (typically 16 bit). Finally, we mask out the root
// device number of the root, so a loopback FS that does not
// encompass multiple mounts will reflect the inode numbers of
// the underlying filesystem
swapped := (st.Dev << 32) | (st.Dev >> 32)
return unionfs.StableAttr{
Mode: st.Mode,
Gen: 1,
// This should work well for traditional backing FSes,
// not so much for other go-fuse FS-es
Ino: swapped ^ st.Ino,
}
}
func (u *UnionFSNode) preserveOwner(ctx context.Context, path string) error {
if os.Getuid() != 0 {
return nil
}
caller, ok := fuse.FromContext(ctx)
if !ok {
return nil
}
return syscall.Lchown(path, int(caller.Uid), int(caller.Gid))
}
func getChild(ctx context.Context, node *unionfs.Inode, path string) *unionfs.Inode {
if path == "" {
return node
}
if strings.HasSuffix(path, "/") {
path = filepath.Dir(path)
}
var (
parent = node
current *unionfs.Inode
)
for _, component := range strings.Split(path, "/") {
current = parent.GetChild(component)
if current == nil {
current = node.NewPersistentInode(
ctx,
&unionfs.Inode{},
unionfs.StableAttr{Mode: syscall.S_IFDIR},
)
parent.AddChild(component, current, true)
}
parent = current
}
return current
}