mirror of
https://github.com/hanzoai/go-fuse.git
synced 2026-10-30 14:46:18 +00:00
517 lines
11 KiB
Go
517 lines
11 KiB
Go
// Copyright 2019 the Go-FUSE Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package unionfs
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"syscall"
|
|
|
|
"github.com/hanzoai/go-fuse/v2/fs"
|
|
"github.com/hanzoai/go-fuse/v2/fuse"
|
|
)
|
|
|
|
func filePathHash(path string) string {
|
|
dir, base := filepath.Split(path)
|
|
|
|
h := md5.New()
|
|
h.Write([]byte(dir))
|
|
return fmt.Sprintf("%x-%s", h.Sum(nil)[:8], base)
|
|
}
|
|
|
|
type unionFSRoot struct {
|
|
unionFSNode
|
|
|
|
roots []string
|
|
}
|
|
|
|
type unionFSNode struct {
|
|
fs.Inode
|
|
}
|
|
|
|
const delDir = "DELETIONS"
|
|
|
|
var delDirHash = filePathHash(delDir)
|
|
|
|
func (r *unionFSRoot) allMarkers(result map[string]struct{}) syscall.Errno {
|
|
dir := filepath.Join(r.roots[0], delDir)
|
|
|
|
ds, errno := fs.NewLoopbackDirStream(dir)
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
|
|
defer ds.Close()
|
|
for ds.HasNext() {
|
|
e, errno := ds.Next()
|
|
if errno != 0 {
|
|
return errno
|
|
}
|
|
if e.Mode != syscall.S_IFREG {
|
|
continue
|
|
}
|
|
result[e.Name] = struct{}{}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (r *unionFSRoot) rmMarker(name string) syscall.Errno {
|
|
err := syscall.Unlink(r.markerPath(name))
|
|
if err != nil {
|
|
return err.(syscall.Errno)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (r *unionFSRoot) writeMarker(name string) syscall.Errno {
|
|
dir := filepath.Join(r.roots[0], delDir)
|
|
var st syscall.Stat_t
|
|
if err := syscall.Stat(dir, &st); err == syscall.ENOENT {
|
|
if err := syscall.Mkdir(dir, 0755); err != nil {
|
|
log.Printf("Mkdir %q: %v", dir, err)
|
|
return syscall.EIO
|
|
}
|
|
} else if err != nil {
|
|
return err.(syscall.Errno)
|
|
}
|
|
|
|
dest := r.markerPath(name)
|
|
|
|
err := os.WriteFile(dest, []byte(name), 0644)
|
|
return fs.ToErrno(err)
|
|
}
|
|
|
|
func (r *unionFSRoot) markerPath(name string) string {
|
|
return filepath.Join(r.roots[0], delDir, filePathHash(name))
|
|
}
|
|
|
|
func (r *unionFSRoot) isDeleted(name string) bool {
|
|
var st syscall.Stat_t
|
|
err := syscall.Stat(r.markerPath(name), &st)
|
|
return err == nil
|
|
}
|
|
|
|
func (n *unionFSNode) root() *unionFSRoot {
|
|
return n.Root().Operations().(*unionFSRoot)
|
|
}
|
|
|
|
var _ = (fs.NodeSetattrer)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno {
|
|
if errno := n.promote(); errno != 0 {
|
|
return errno
|
|
}
|
|
|
|
if fh != nil {
|
|
return fh.(fs.FileSetattrer).Setattr(ctx, in, out)
|
|
}
|
|
|
|
p := filepath.Join(n.root().roots[0], n.Path(nil))
|
|
fsa, ok := fh.(fs.FileSetattrer)
|
|
if ok && fsa != nil {
|
|
fsa.Setattr(ctx, in, out)
|
|
} else {
|
|
if m, ok := in.GetMode(); ok {
|
|
if err := syscall.Chmod(p, m); err != nil {
|
|
return fs.ToErrno(err)
|
|
}
|
|
}
|
|
|
|
uid, uok := in.GetUID()
|
|
gid, gok := in.GetGID()
|
|
if uok || gok {
|
|
suid := -1
|
|
sgid := -1
|
|
if uok {
|
|
suid = int(uid)
|
|
}
|
|
if gok {
|
|
sgid = int(gid)
|
|
}
|
|
if err := syscall.Chown(p, suid, sgid); err != nil {
|
|
return fs.ToErrno(err)
|
|
}
|
|
}
|
|
|
|
mtime, mok := in.GetMTime()
|
|
atime, aok := in.GetATime()
|
|
|
|
if mok || aok {
|
|
|
|
ap := &atime
|
|
mp := &mtime
|
|
if !aok {
|
|
ap = nil
|
|
}
|
|
if !mok {
|
|
mp = nil
|
|
}
|
|
var ts [2]syscall.Timespec
|
|
ts[0] = fuse.UtimeToTimespec(ap)
|
|
ts[1] = fuse.UtimeToTimespec(mp)
|
|
|
|
if err := syscall.UtimesNano(p, ts[:]); err != nil {
|
|
return fs.ToErrno(err)
|
|
}
|
|
}
|
|
|
|
if sz, ok := in.GetSize(); ok {
|
|
if err := syscall.Truncate(p, int64(sz)); err != nil {
|
|
return fs.ToErrno(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
fga, ok := fh.(fs.FileGetattrer)
|
|
if ok && fga != nil {
|
|
fga.Getattr(ctx, out)
|
|
} else {
|
|
st := syscall.Stat_t{}
|
|
err := syscall.Lstat(p, &st)
|
|
if err != nil {
|
|
return fs.ToErrno(err)
|
|
}
|
|
out.FromStat(&st)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
var _ = (fs.NodeCreater)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) {
|
|
if n.IsRoot() && name == delDir {
|
|
return nil, nil, 0, syscall.EPERM
|
|
}
|
|
|
|
var st syscall.Stat_t
|
|
dirName, idx := n.getBranch(&st)
|
|
if idx > 0 {
|
|
if errno := n.promote(); errno != 0 {
|
|
return nil, nil, 0, errno
|
|
}
|
|
idx = 0
|
|
}
|
|
fullPath := filepath.Join(dirName, name)
|
|
r := n.root()
|
|
if errno := r.rmMarker(fullPath); errno != 0 && errno != syscall.ENOENT {
|
|
return nil, nil, 0, errno
|
|
}
|
|
|
|
abs := filepath.Join(n.root().roots[0], fullPath)
|
|
fd, err := syscall.Creat(abs, mode)
|
|
if err != nil {
|
|
return nil, nil, 0, err.(syscall.Errno)
|
|
}
|
|
|
|
if err := syscall.Fstat(fd, &st); err != nil {
|
|
// now what?
|
|
syscall.Close(fd)
|
|
syscall.Unlink(abs)
|
|
return nil, nil, 0, err.(syscall.Errno)
|
|
}
|
|
|
|
ch := n.NewInode(ctx, &unionFSNode{}, fs.StableAttr{Mode: st.Mode, Ino: st.Ino})
|
|
out.FromStat(&st)
|
|
|
|
return ch, fs.NewLoopbackFile(fd), 0, 0
|
|
}
|
|
|
|
var _ = (fs.NodeOpener)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) {
|
|
isWR := (flags&syscall.O_RDWR != 0) || (flags&syscall.O_WRONLY != 0)
|
|
|
|
var st syscall.Stat_t
|
|
nm, idx := n.getBranch(&st)
|
|
if isWR && idx > 0 {
|
|
if errno := n.promote(); errno != 0 {
|
|
return nil, 0, errno
|
|
}
|
|
idx = 0
|
|
}
|
|
|
|
fd, err := syscall.Open(filepath.Join(n.root().roots[idx], nm), int(flags), 0)
|
|
if err != nil {
|
|
return nil, 0, err.(syscall.Errno)
|
|
}
|
|
|
|
return fs.NewLoopbackFile(fd), 0, 0
|
|
}
|
|
|
|
var _ = (fs.NodeGetattrer)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
|
|
var st syscall.Stat_t
|
|
_, idx := n.getBranch(&st)
|
|
if idx < 0 {
|
|
return syscall.ENOENT
|
|
}
|
|
|
|
out.FromStat(&st)
|
|
return 0
|
|
}
|
|
|
|
var _ = (fs.NodeLookuper)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
|
|
if n.IsRoot() && name == delDir {
|
|
return nil, syscall.ENOENT
|
|
}
|
|
|
|
var st syscall.Stat_t
|
|
|
|
p := filepath.Join(n.Path(nil), name)
|
|
idx := n.root().getBranch(p, &st)
|
|
if idx >= 0 {
|
|
// XXX use idx in Ino?
|
|
ch := n.NewInode(ctx, &unionFSNode{}, fs.StableAttr{Mode: st.Mode, Ino: st.Ino})
|
|
out.FromStat(&st)
|
|
out.Mode |= 0111
|
|
return ch, 0
|
|
}
|
|
return nil, syscall.ENOENT
|
|
}
|
|
|
|
var _ = (fs.NodeUnlinker)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Unlink(ctx context.Context, name string) syscall.Errno {
|
|
return n.root().delPath(filepath.Join(n.Path(nil), name))
|
|
}
|
|
|
|
var _ = (fs.NodeRmdirer)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Rmdir(ctx context.Context, name string) syscall.Errno {
|
|
return n.root().delPath(filepath.Join(n.Path(nil), name))
|
|
}
|
|
|
|
var _ = (fs.NodeSymlinker)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
|
|
n.promote()
|
|
path := filepath.Join(n.root().roots[0], n.Path(nil), name)
|
|
err := syscall.Symlink(target, path)
|
|
|
|
if err != nil {
|
|
return nil, err.(syscall.Errno)
|
|
}
|
|
|
|
var st syscall.Stat_t
|
|
if err := syscall.Lstat(path, &st); err != nil {
|
|
return nil, err.(syscall.Errno)
|
|
}
|
|
|
|
out.FromStat(&st)
|
|
|
|
ch := n.NewInode(ctx, &unionFSNode{}, fs.StableAttr{
|
|
Mode: syscall.S_IFLNK,
|
|
Ino: st.Ino,
|
|
})
|
|
return ch, 0
|
|
}
|
|
|
|
var _ = (fs.NodeReadlinker)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
|
|
nm, idx := n.getBranch(nil)
|
|
|
|
var buf [1024]byte
|
|
count, err := syscall.Readlink(filepath.Join(n.root().roots[idx], nm), buf[:])
|
|
if err != nil {
|
|
return nil, err.(syscall.Errno)
|
|
}
|
|
|
|
return buf[:count], 0
|
|
}
|
|
|
|
var _ = (fs.NodeReaddirer)((*unionFSNode)(nil))
|
|
|
|
func (n *unionFSNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) {
|
|
root := n.root()
|
|
|
|
markers := map[string]struct{}{delDirHash: {}}
|
|
// ignore error: assume no markers
|
|
root.allMarkers(markers)
|
|
|
|
dir := n.Path(nil)
|
|
|
|
names := map[string]uint32{}
|
|
for i := range root.roots {
|
|
// deepest root first.
|
|
readRoot(root.roots[len(root.roots)-i-1], dir, names)
|
|
}
|
|
result := make([]fuse.DirEntry, 0, len(names))
|
|
for nm, mode := range names {
|
|
marker := filePathHash(filepath.Join(dir, nm))
|
|
if _, ok := markers[marker]; ok {
|
|
continue
|
|
}
|
|
|
|
result = append(result, fuse.DirEntry{
|
|
Name: nm,
|
|
Mode: mode,
|
|
})
|
|
}
|
|
sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
|
|
return fs.NewListDirStream(result), 0
|
|
}
|
|
|
|
func readRoot(root string, dir string, result map[string]uint32) {
|
|
ds, errno := fs.NewLoopbackDirStream(filepath.Join(root, dir))
|
|
if errno != 0 {
|
|
return
|
|
}
|
|
defer ds.Close()
|
|
for ds.HasNext() {
|
|
e, errno := ds.Next()
|
|
if errno != 0 {
|
|
return
|
|
}
|
|
|
|
result[e.Name] = e.Mode
|
|
}
|
|
}
|
|
|
|
// getBranch returns the root where we can find the given file. It
|
|
// will check the deletion markers in roots[0].
|
|
func (n *unionFSNode) getBranch(st *syscall.Stat_t) (string, int) {
|
|
name := n.Path(nil)
|
|
return name, n.root().getBranch(name, st)
|
|
}
|
|
|
|
func (r *unionFSRoot) getBranch(name string, st *syscall.Stat_t) int {
|
|
if r.isDeleted(name) {
|
|
return -1
|
|
}
|
|
if st == nil {
|
|
st = &syscall.Stat_t{}
|
|
}
|
|
for i, root := range r.roots {
|
|
p := filepath.Join(root, name)
|
|
err := syscall.Lstat(p, st)
|
|
if err == nil {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (n *unionFSRoot) delPath(p string) syscall.Errno {
|
|
var st syscall.Stat_t
|
|
r := n.root()
|
|
idx := r.getBranch(p, &st)
|
|
|
|
if idx < 0 {
|
|
return 0
|
|
}
|
|
if idx == 0 {
|
|
err := syscall.Unlink(filepath.Join(r.roots[idx], p))
|
|
if err != nil {
|
|
return fs.ToErrno(err)
|
|
}
|
|
idx = r.getBranch(p, &st)
|
|
}
|
|
if idx > 0 {
|
|
return r.writeMarker(p)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (n *unionFSNode) promote() syscall.Errno {
|
|
p := &n.Inode
|
|
r := n.root()
|
|
|
|
type tup struct {
|
|
*unionFSNode
|
|
name string
|
|
idx int
|
|
st syscall.Stat_t
|
|
}
|
|
|
|
var parents []tup
|
|
for p != nil && p != &r.Inode {
|
|
asUN := p.Operations().(*unionFSNode)
|
|
|
|
var st syscall.Stat_t
|
|
name, idx := asUN.getBranch(&st)
|
|
if idx == 0 {
|
|
break
|
|
}
|
|
if idx < 0 {
|
|
log.Println("promote called on nonexistent file")
|
|
return syscall.EIO
|
|
}
|
|
|
|
parents = append(parents, tup{asUN, name, idx, st})
|
|
_, p = p.Parent()
|
|
}
|
|
|
|
for i := len(parents) - 1; i >= 0; i-- {
|
|
t := parents[i]
|
|
|
|
path := t.Path(nil)
|
|
if t.IsDir() {
|
|
if err := syscall.Mkdir(filepath.Join(r.roots[0], path), t.st.Mode); err != nil {
|
|
return err.(syscall.Errno)
|
|
}
|
|
} else if t.Mode()&syscall.S_IFREG != 0 {
|
|
if errno := r.promoteRegularFile(path, t.idx, &t.st); errno != 0 {
|
|
return errno
|
|
}
|
|
} else {
|
|
log.Panicf("don't know how to handle mode %o", t.Mode())
|
|
}
|
|
var ts [2]syscall.Timespec
|
|
ts[0] = t.st.Atim
|
|
ts[1] = t.st.Mtim
|
|
|
|
// ignore error.
|
|
syscall.UtimesNano(path, ts[:])
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (r *unionFSRoot) promoteRegularFile(p string, idx int, st *syscall.Stat_t) syscall.Errno {
|
|
dest, err := syscall.Creat(filepath.Join(r.roots[0], p), st.Mode)
|
|
if err != nil {
|
|
return err.(syscall.Errno)
|
|
}
|
|
src, err := syscall.Open(filepath.Join(r.roots[idx], p), syscall.O_RDONLY, 0)
|
|
if err != nil {
|
|
syscall.Close(dest)
|
|
return err.(syscall.Errno)
|
|
}
|
|
|
|
var ret syscall.Errno
|
|
var buf [128 >> 10]byte
|
|
for {
|
|
n, err := syscall.Read(src, buf[:])
|
|
if n == 0 {
|
|
break
|
|
}
|
|
if err != nil {
|
|
ret = err.(syscall.Errno)
|
|
break
|
|
}
|
|
|
|
if _, err := syscall.Write(dest, buf[:n]); err != nil {
|
|
ret = err.(syscall.Errno)
|
|
break
|
|
}
|
|
}
|
|
syscall.Close(src)
|
|
|
|
if err := syscall.Close(dest); err != nil && ret == 0 {
|
|
ret = err.(syscall.Errno)
|
|
}
|
|
return ret
|
|
}
|