452 lines
10 KiB
Go
452 lines
10 KiB
Go
// Copyright 2016 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 (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/hanwen/go-fuse/fuse"
|
|
"github.com/hanwen/go-fuse/fuse/nodefs"
|
|
"github.com/hanwen/go-fuse/fuse/pathfs"
|
|
)
|
|
|
|
type knownFs struct {
|
|
unionFS pathfs.FileSystem
|
|
nodeFS *pathfs.PathNodeFs
|
|
}
|
|
|
|
// Creates unions for all files under a given directory,
|
|
// walking the tree and looking for directories D which have a
|
|
// D/READONLY symlink.
|
|
//
|
|
// A union for A/B/C will placed under directory A-B-C.
|
|
type autoUnionFs struct {
|
|
pathfs.FileSystem
|
|
debug bool
|
|
|
|
lock sync.RWMutex
|
|
zombies map[string]bool
|
|
knownFileSystems map[string]knownFs
|
|
nameRootMap map[string]string
|
|
root string
|
|
|
|
nodeFs *pathfs.PathNodeFs
|
|
options *AutoUnionFsOptions
|
|
}
|
|
|
|
type AutoUnionFsOptions struct {
|
|
UnionFsOptions
|
|
|
|
nodefs.Options
|
|
pathfs.PathNodeFsOptions
|
|
|
|
// If set, run updateKnownFses() after mounting.
|
|
UpdateOnMount bool
|
|
|
|
// If set hides the _READONLY file.
|
|
HideReadonly bool
|
|
|
|
// Expose this version in /status/gounionfs_version
|
|
Version string
|
|
}
|
|
|
|
const (
|
|
_READONLY = "READONLY"
|
|
_STATUS = "status"
|
|
_CONFIG = "config"
|
|
_DEBUG = "debug"
|
|
_ROOT = "root"
|
|
_VERSION = "gounionfs_version"
|
|
_SCAN_CONFIG = ".scan_config"
|
|
)
|
|
|
|
func NewAutoUnionFs(directory string, options AutoUnionFsOptions) pathfs.FileSystem {
|
|
if options.HideReadonly {
|
|
options.HiddenFiles = append(options.HiddenFiles, _READONLY)
|
|
}
|
|
a := &autoUnionFs{
|
|
knownFileSystems: make(map[string]knownFs),
|
|
nameRootMap: make(map[string]string),
|
|
zombies: make(map[string]bool),
|
|
options: &options,
|
|
FileSystem: pathfs.NewDefaultFileSystem(),
|
|
}
|
|
|
|
directory, err := filepath.Abs(directory)
|
|
if err != nil {
|
|
panic("filepath.Abs returned err")
|
|
}
|
|
a.root = directory
|
|
return a
|
|
}
|
|
|
|
func (fs *autoUnionFs) String() string {
|
|
return fmt.Sprintf("autoUnionFs(%s)", fs.root)
|
|
}
|
|
|
|
func (fs *autoUnionFs) OnMount(nodeFs *pathfs.PathNodeFs) {
|
|
fs.nodeFs = nodeFs
|
|
if fs.options.UpdateOnMount {
|
|
time.AfterFunc(100*time.Millisecond, func() { fs.updateKnownFses() })
|
|
}
|
|
}
|
|
|
|
func (fs *autoUnionFs) addAutomaticFs(roots []string) {
|
|
relative := strings.TrimLeft(strings.Replace(roots[0], fs.root, "", -1), "/")
|
|
name := strings.Replace(relative, "/", "-", -1)
|
|
|
|
if fs.getUnionFs(name) == nil {
|
|
fs.addFs(name, roots)
|
|
}
|
|
}
|
|
|
|
func (fs *autoUnionFs) createFs(name string, roots []string) fuse.Status {
|
|
fs.lock.Lock()
|
|
defer fs.lock.Unlock()
|
|
|
|
if fs.zombies[name] {
|
|
log.Printf("filesystem named %q is being removed", name)
|
|
return fuse.EBUSY
|
|
}
|
|
|
|
for workspace, root := range fs.nameRootMap {
|
|
if root == roots[0] && workspace != name {
|
|
log.Printf("Already have a union FS for directory %s in workspace %s",
|
|
roots[0], workspace)
|
|
return fuse.EBUSY
|
|
}
|
|
}
|
|
|
|
known := fs.knownFileSystems[name]
|
|
if known.unionFS != nil {
|
|
log.Println("Already have a workspace:", name)
|
|
return fuse.EBUSY
|
|
}
|
|
|
|
ufs, err := NewUnionFsFromRoots(roots, &fs.options.UnionFsOptions, true)
|
|
if err != nil {
|
|
log.Println("Could not create UnionFs:", err)
|
|
return fuse.EPERM
|
|
}
|
|
|
|
log.Printf("Adding workspace %v for roots %v", name, ufs.String())
|
|
nfs := pathfs.NewPathNodeFs(ufs, &fs.options.PathNodeFsOptions)
|
|
code := fs.nodeFs.Mount(name, nfs.Root(), &fs.options.Options)
|
|
if code.Ok() {
|
|
fs.knownFileSystems[name] = knownFs{
|
|
ufs,
|
|
nfs,
|
|
}
|
|
fs.nameRootMap[name] = roots[0]
|
|
}
|
|
return code
|
|
}
|
|
|
|
func (fs *autoUnionFs) rmFs(name string) (code fuse.Status) {
|
|
fs.lock.Lock()
|
|
defer fs.lock.Unlock()
|
|
|
|
if fs.zombies[name] {
|
|
return fuse.ENOENT
|
|
}
|
|
|
|
known := fs.knownFileSystems[name]
|
|
if known.unionFS == nil {
|
|
return fuse.ENOENT
|
|
}
|
|
|
|
root := fs.nameRootMap[name]
|
|
delete(fs.knownFileSystems, name)
|
|
delete(fs.nameRootMap, name)
|
|
fs.zombies[name] = true
|
|
fs.lock.Unlock()
|
|
code = fs.nodeFs.Unmount(name)
|
|
|
|
fs.lock.Lock()
|
|
delete(fs.zombies, name)
|
|
if !code.Ok() {
|
|
// Reinstate.
|
|
log.Printf("Unmount failed for %s. Code %v", name, code)
|
|
fs.knownFileSystems[name] = known
|
|
fs.nameRootMap[name] = root
|
|
}
|
|
return code
|
|
}
|
|
|
|
func (fs *autoUnionFs) addFs(name string, roots []string) (code fuse.Status) {
|
|
if name == _CONFIG || name == _STATUS || name == _SCAN_CONFIG {
|
|
return fuse.EINVAL
|
|
}
|
|
return fs.createFs(name, roots)
|
|
}
|
|
|
|
func (fs *autoUnionFs) getRoots(path string) []string {
|
|
ro := filepath.Join(path, _READONLY)
|
|
fi, err := os.Lstat(ro)
|
|
fiDir, errDir := os.Stat(ro)
|
|
if err != nil || errDir != nil {
|
|
return nil
|
|
}
|
|
|
|
if fi.Mode()&os.ModeSymlink != 0 && fiDir.IsDir() {
|
|
// TODO - should recurse and chain all READONLYs
|
|
// together.
|
|
return []string{path, ro}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fs *autoUnionFs) visit(path string, fi os.FileInfo, err error) error {
|
|
if fi != nil && fi.IsDir() {
|
|
roots := fs.getRoots(path)
|
|
if roots != nil {
|
|
fs.addAutomaticFs(roots)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fs *autoUnionFs) updateKnownFses() {
|
|
// We unroll the first level of entries in the root manually in order
|
|
// to allow symbolic links on that level.
|
|
directoryEntries, err := ioutil.ReadDir(fs.root)
|
|
if err == nil {
|
|
for _, dir := range directoryEntries {
|
|
if dir.IsDir() || dir.Mode()&os.ModeSymlink != 0 {
|
|
path := filepath.Join(fs.root, dir.Name())
|
|
dir, _ = os.Stat(path)
|
|
fs.visit(path, dir, nil)
|
|
filepath.Walk(path,
|
|
func(path string, fi os.FileInfo, err error) error {
|
|
return fs.visit(path, fi, err)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fs *autoUnionFs) Readlink(path string, context *fuse.Context) (out string, code fuse.Status) {
|
|
comps := strings.Split(path, string(filepath.Separator))
|
|
if comps[0] == _STATUS && comps[1] == _ROOT {
|
|
return fs.root, fuse.OK
|
|
}
|
|
|
|
if comps[0] != _CONFIG {
|
|
return "", fuse.ENOENT
|
|
}
|
|
|
|
name := comps[1]
|
|
|
|
fs.lock.RLock()
|
|
defer fs.lock.RUnlock()
|
|
|
|
root, ok := fs.nameRootMap[name]
|
|
if ok {
|
|
return root, fuse.OK
|
|
}
|
|
return "", fuse.ENOENT
|
|
}
|
|
|
|
func (fs *autoUnionFs) getUnionFs(name string) pathfs.FileSystem {
|
|
fs.lock.RLock()
|
|
defer fs.lock.RUnlock()
|
|
return fs.knownFileSystems[name].unionFS
|
|
}
|
|
|
|
func (fs *autoUnionFs) Symlink(pointedTo string, linkName string, context *fuse.Context) (code fuse.Status) {
|
|
comps := strings.Split(linkName, "/")
|
|
if len(comps) != 2 {
|
|
return fuse.EPERM
|
|
}
|
|
|
|
if comps[0] == _CONFIG {
|
|
roots := fs.getRoots(pointedTo)
|
|
if roots == nil {
|
|
return fuse.Status(syscall.ENOTDIR)
|
|
}
|
|
|
|
name := comps[1]
|
|
return fs.addFs(name, roots)
|
|
}
|
|
return fuse.EPERM
|
|
}
|
|
|
|
func (fs *autoUnionFs) Unlink(path string, context *fuse.Context) (code fuse.Status) {
|
|
comps := strings.Split(path, "/")
|
|
if len(comps) != 2 {
|
|
return fuse.EPERM
|
|
}
|
|
|
|
if comps[0] == _CONFIG && comps[1] != _SCAN_CONFIG {
|
|
code = fs.rmFs(comps[1])
|
|
} else {
|
|
code = fuse.ENOENT
|
|
}
|
|
return code
|
|
}
|
|
|
|
// Must define this, because ENOSYS will suspend all GetXAttr calls.
|
|
func (fs *autoUnionFs) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
|
|
return nil, fuse.ENOATTR
|
|
}
|
|
|
|
func (fs *autoUnionFs) GetAttr(path string, context *fuse.Context) (*fuse.Attr, fuse.Status) {
|
|
a := &fuse.Attr{
|
|
Owner: *fuse.CurrentOwner(),
|
|
}
|
|
if path == "" || path == _CONFIG || path == _STATUS {
|
|
a.Mode = fuse.S_IFDIR | 0755
|
|
return a, fuse.OK
|
|
}
|
|
|
|
if path == filepath.Join(_STATUS, _VERSION) {
|
|
a.Mode = fuse.S_IFREG | 0644
|
|
a.Size = uint64(len(fs.options.Version))
|
|
return a, fuse.OK
|
|
}
|
|
|
|
if path == filepath.Join(_STATUS, _DEBUG) {
|
|
a.Mode = fuse.S_IFREG | 0644
|
|
a.Size = uint64(len(fs.DebugData()))
|
|
return a, fuse.OK
|
|
}
|
|
|
|
if path == filepath.Join(_STATUS, _ROOT) {
|
|
a.Mode = syscall.S_IFLNK | 0644
|
|
return a, fuse.OK
|
|
}
|
|
|
|
if path == filepath.Join(_CONFIG, _SCAN_CONFIG) {
|
|
a.Mode = fuse.S_IFREG | 0644
|
|
return a, fuse.OK
|
|
}
|
|
comps := strings.Split(path, string(filepath.Separator))
|
|
|
|
if len(comps) > 1 && comps[0] == _CONFIG {
|
|
fs := fs.getUnionFs(comps[1])
|
|
|
|
if fs == nil {
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
a.Mode = syscall.S_IFLNK | 0644
|
|
return a, fuse.OK
|
|
}
|
|
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
func (fs *autoUnionFs) StatusDir() (stream []fuse.DirEntry, status fuse.Status) {
|
|
stream = make([]fuse.DirEntry, 0, 10)
|
|
stream = []fuse.DirEntry{
|
|
{Name: _VERSION, Mode: fuse.S_IFREG | 0644},
|
|
{Name: _DEBUG, Mode: fuse.S_IFREG | 0644},
|
|
{Name: _ROOT, Mode: syscall.S_IFLNK | 0644},
|
|
}
|
|
return stream, fuse.OK
|
|
}
|
|
|
|
func (fs *autoUnionFs) DebugData() string {
|
|
conn := fs.nodeFs.Connector()
|
|
if conn.Server() == nil {
|
|
return "autoUnionFs.mountState not set"
|
|
}
|
|
setting := conn.Server().KernelSettings()
|
|
msg := fmt.Sprintf(
|
|
"Version: %v\n"+
|
|
"Bufferpool: %v\n"+
|
|
"Kernel: %v\n",
|
|
fs.options.Version,
|
|
conn.Server().DebugData(),
|
|
&setting)
|
|
|
|
if conn != nil {
|
|
msg += fmt.Sprintf("Live inodes: %d\n", conn.InodeHandleCount())
|
|
}
|
|
|
|
return msg
|
|
}
|
|
|
|
func (fs *autoUnionFs) Open(path string, flags uint32, context *fuse.Context) (nodefs.File, fuse.Status) {
|
|
if path == filepath.Join(_STATUS, _DEBUG) {
|
|
if flags&fuse.O_ANYWRITE != 0 {
|
|
return nil, fuse.EPERM
|
|
}
|
|
|
|
return nodefs.NewDataFile([]byte(fs.DebugData())), fuse.OK
|
|
}
|
|
if path == filepath.Join(_STATUS, _VERSION) {
|
|
if flags&fuse.O_ANYWRITE != 0 {
|
|
return nil, fuse.EPERM
|
|
}
|
|
return nodefs.NewDataFile([]byte(fs.options.Version)), fuse.OK
|
|
}
|
|
if path == filepath.Join(_CONFIG, _SCAN_CONFIG) {
|
|
if flags&fuse.O_ANYWRITE != 0 {
|
|
fs.updateKnownFses()
|
|
}
|
|
return nodefs.NewDevNullFile(), fuse.OK
|
|
}
|
|
return nil, fuse.ENOENT
|
|
}
|
|
|
|
func (fs *autoUnionFs) Truncate(name string, offset uint64, context *fuse.Context) (code fuse.Status) {
|
|
if name != filepath.Join(_CONFIG, _SCAN_CONFIG) {
|
|
log.Println("Huh? Truncating unsupported write file", name)
|
|
return fuse.EPERM
|
|
}
|
|
return fuse.OK
|
|
}
|
|
|
|
func (fs *autoUnionFs) OpenDir(name string, context *fuse.Context) (stream []fuse.DirEntry, status fuse.Status) {
|
|
switch name {
|
|
case _STATUS:
|
|
return fs.StatusDir()
|
|
case _CONFIG:
|
|
case "/":
|
|
name = ""
|
|
case "":
|
|
default:
|
|
log.Printf("Argh! Don't know how to list dir %v", name)
|
|
return nil, fuse.ENOSYS
|
|
}
|
|
|
|
fs.lock.RLock()
|
|
defer fs.lock.RUnlock()
|
|
|
|
stream = make([]fuse.DirEntry, 0, len(fs.knownFileSystems)+5)
|
|
if name == _CONFIG {
|
|
for k := range fs.knownFileSystems {
|
|
stream = append(stream, fuse.DirEntry{
|
|
Name: k,
|
|
Mode: syscall.S_IFLNK | 0644,
|
|
})
|
|
}
|
|
}
|
|
|
|
if name == "" {
|
|
stream = append(stream, fuse.DirEntry{
|
|
Name: _CONFIG,
|
|
Mode: uint32(fuse.S_IFDIR | 0755),
|
|
},
|
|
fuse.DirEntry{
|
|
Name: _STATUS,
|
|
Mode: uint32(fuse.S_IFDIR | 0755),
|
|
})
|
|
}
|
|
return stream, status
|
|
}
|
|
|
|
func (fs *autoUnionFs) StatFs(name string) *fuse.StatfsOut {
|
|
return &fuse.StatfsOut{}
|
|
}
|