package unionfs import ( "fmt" "github.com/hanwen/go-fuse/fuse" "io/ioutil" "log" "os" "path/filepath" "strings" "sync" "syscall" "time" ) type knownFs struct { *UnionFs *fuse.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 { fuse.DefaultFileSystem lock sync.RWMutex knownFileSystems map[string]knownFs nameRootMap map[string]string root string nodeFs *fuse.PathNodeFs options *AutoUnionFsOptions mountState *fuse.MountState connector *fuse.FileSystemConnector } type AutoUnionFsOptions struct { UnionFsOptions fuse.FileSystemOptions fuse.PathNodeFsOptions // If set, run updateKnownFses() after mounting. UpdateOnMount bool // If set hides the _READONLY file. HideReadonly bool } const ( _READONLY = "READONLY" _STATUS = "status" _CONFIG = "config" _DEBUG = "debug" _ROOT = "root" _VERSION = "gounionfs_version" _SCAN_CONFIG = ".scan_config" ) func NewAutoUnionFs(directory string, options AutoUnionFsOptions) *AutoUnionFs { if options.HideReadonly { options.HiddenFiles = append(options.HiddenFiles, _READONLY) } a := new(AutoUnionFs) a.knownFileSystems = make(map[string]knownFs) a.nameRootMap = make(map[string]string) a.options = &options 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 *fuse.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() 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 := fuse.NewPathNodeFs(ufs, &fs.options.PathNodeFsOptions) code := fs.nodeFs.Mount(name, nfs, &fs.options.FileSystemOptions) 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() known := fs.knownFileSystems[name] if known.UnionFs == nil { return fuse.ENOENT } code = fs.nodeFs.Unmount(name) if code.Ok() { delete(fs.knownFileSystems, name) delete(fs.nameRootMap, name) } else { log.Printf("Unmount failed for %s. Code %v", name, code) } return code } func (fs *AutoUnionFs) addFs(name string, roots []string) (code fuse.Status) { if name == _CONFIG || name == _STATUS || name == _SCAN_CONFIG { log.Println("Illegal name for overlay", roots) 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() { log.Println("Looking for new filesystems") // 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) }) } } } log.Println("Done looking") } 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) *UnionFs { 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.ENODATA } func (fs *AutoUnionFs) GetAttr(path string, context *fuse.Context) (*fuse.Attr, fuse.Status) { if path == "" || path == _CONFIG || path == _STATUS { a := &fuse.Attr{ Mode: fuse.S_IFDIR | 0755, } return a, fuse.OK } if path == filepath.Join(_STATUS, _VERSION) { a := &fuse.Attr{ Mode: fuse.S_IFREG | 0644, Size: uint64(len(fuse.Version())), } return a, fuse.OK } if path == filepath.Join(_STATUS, _DEBUG) { a := &fuse.Attr{ Mode: fuse.S_IFREG | 0644, Size: uint64(len(fs.DebugData())), } return a, fuse.OK } if path == filepath.Join(_STATUS, _ROOT) { a := &fuse.Attr{ Mode: syscall.S_IFLNK | 0644, } return a, fuse.OK } if path == filepath.Join(_CONFIG, _SCAN_CONFIG) { a := &fuse.Attr{ 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 := &fuse.Attr{ Mode: syscall.S_IFLNK | 0644, } return a, fuse.OK } return nil, fuse.ENOENT } func (fs *AutoUnionFs) StatusDir() (stream chan fuse.DirEntry, status fuse.Status) { stream = make(chan fuse.DirEntry, 10) stream <- fuse.DirEntry{ Name: _VERSION, Mode: fuse.S_IFREG | 0644, } stream <- fuse.DirEntry{ Name: _DEBUG, Mode: fuse.S_IFREG | 0644, } stream <- fuse.DirEntry{ Name: _ROOT, Mode: syscall.S_IFLNK | 0644, } close(stream) return stream, fuse.OK } // SetMountState stores the MountState, which is necessary for // retrieving debug data. func (fs *AutoUnionFs) SetMountState(state *fuse.MountState) { fs.mountState = state } func (fs *AutoUnionFs) SetFileSystemConnector(conn *fuse.FileSystemConnector) { fs.connector = conn } func (fs *AutoUnionFs) DebugData() string { if fs.mountState == nil { return "AutoUnionFs.mountState not set" } setting := fs.mountState.KernelSettings() msg := fmt.Sprintf( "Version: %v\n"+ "Bufferpool: %v\n"+ "Kernel: %v\n", fuse.Version(), fs.mountState.BufferPoolStats(), &setting) lat := fs.mountState.Latencies() if len(lat) > 0 { msg += fmt.Sprintf("Latencies: %v\n", lat) } counts := fs.mountState.OperationCounts() if len(counts) > 0 { msg += fmt.Sprintf("Op counts: %v\n", counts) } if fs.connector != nil { msg += fmt.Sprintf("Live inodes: %d\n", fs.connector.InodeHandleCount()) } return msg } func (fs *AutoUnionFs) Open(path string, flags uint32, context *fuse.Context) (fuse.File, fuse.Status) { if path == filepath.Join(_STATUS, _DEBUG) { if flags&fuse.O_ANYWRITE != 0 { return nil, fuse.EPERM } return fuse.NewDataFile([]byte(fs.DebugData())), fuse.OK } if path == filepath.Join(_STATUS, _VERSION) { if flags&fuse.O_ANYWRITE != 0 { return nil, fuse.EPERM } return fuse.NewDataFile([]byte(fuse.Version())), fuse.OK } if path == filepath.Join(_CONFIG, _SCAN_CONFIG) { if flags&fuse.O_ANYWRITE != 0 { fs.updateKnownFses() } return fuse.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 chan 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(chan fuse.DirEntry, len(fs.knownFileSystems)+5) if name == _CONFIG { for k := range fs.knownFileSystems { stream <- fuse.DirEntry{ Name: k, Mode: syscall.S_IFLNK | 0644, } } } if name == "" { stream <- fuse.DirEntry{ Name: _CONFIG, Mode: uint32(fuse.S_IFDIR | 0755), } stream <- fuse.DirEntry{ Name: _STATUS, Mode: uint32(fuse.S_IFDIR | 0755), } } close(stream) return stream, status } func (fs *AutoUnionFs) StatFs(name string) *fuse.StatfsOut { return &fuse.StatfsOut{} }