228 lines
5.1 KiB
Go
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
|
|
}
|