| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- // Package mount contains an implementation of hackpadfs.MountFS.
- package mount
- import (
- "io"
- "path"
- "strings"
- "sync"
- "github.com/hack-pad/hackpadfs"
- )
- var (
- _ interface {
- hackpadfs.FS
- hackpadfs.MountFS
- hackpadfs.RenameFS
- } = &FS{}
- )
- // FS is mesh of several file systems mounted at different paths.
- // Mount a file system with AddMount().
- //
- // For ease of use, call the standard operations via hackpadfs.OpenFile(fs, ...), hackpadfs.Mkdir(fs, ...), etc.
- type FS struct {
- rootFS hackpadfs.FS
- mounts sync.Map // map[string]hackpadfs.FS
- mountMu sync.Mutex
- }
- // NewFS returns a new FS.
- func NewFS(rootFS hackpadfs.FS) (*FS, error) {
- return &FS{
- rootFS: rootFS,
- }, nil
- }
- // AddMount mounts 'mount' at 'path'. The mount point must already exist as a directory.
- func (fs *FS) AddMount(path string, mount hackpadfs.FS) error {
- err := fs.addMount(path, mount)
- if err != nil {
- return &hackpadfs.PathError{Op: "mount", Path: path, Err: err}
- }
- return nil
- }
- func (fs *FS) addMount(p string, mountFS hackpadfs.FS) error {
- if !hackpadfs.ValidPath(p) || p == "." {
- return hackpadfs.ErrInvalid
- }
- _, loaded := fs.mounts.Load(p)
- if loaded {
- // cannot mount at same point as existing mount
- return hackpadfs.ErrExist
- }
- fs.mountMu.Lock()
- defer fs.mountMu.Unlock()
- dir, base := path.Split(p)
- parentFS, subPath := fs.Mount(dir) // get this mount point's parent mount, verify dir exists
- f, err := parentFS.Open(path.Join(subPath, base))
- if err != nil {
- return err
- }
- defer func() { _ = f.Close() }()
- info, err := f.Stat()
- if err != nil {
- return err
- }
- if !info.IsDir() {
- return hackpadfs.ErrNotDir
- }
- // TODO Handle data race when directory is removed or becomes a file between the Stat and the mount.
- _, loaded = fs.mounts.LoadOrStore(p, mountFS)
- if loaded {
- // cannot mount at same point as existing mount
- return hackpadfs.ErrExist
- }
- return nil
- }
- // Mount implements hackpadfs.MountFS
- func (fs *FS) Mount(path string) (mount hackpadfs.FS, subPath string) {
- mount, mountPath, subPath := fs.mountPoint(path)
- if mountPath == "." {
- return mount, path
- }
- return mount, subPath
- }
- func (fs *FS) mountPoint(path string) (_ hackpadfs.FS, mountPoint, subPath string) {
- var resultPath string
- resultFS := fs.rootFS
- fs.mounts.Range(func(key, value interface{}) bool {
- mountPath, mountFS := key.(string), value.(hackpadfs.FS)
- switch {
- case strings.HasPrefix(path, mountPath+"/"):
- if len(mountPath) > len(resultPath) {
- resultPath, resultFS = mountPath, mountFS
- }
- return true
- case mountPath == path:
- // exact match
- resultPath, resultFS = mountPath, mountFS
- return false
- default:
- return true
- }
- })
- subPath = path
- subPath = strings.TrimPrefix(subPath, resultPath)
- subPath = strings.TrimPrefix(subPath, "/")
- if resultPath == "" {
- resultPath = "."
- }
- if subPath == "" {
- subPath = "."
- }
- return resultFS, resultPath, subPath
- }
- // Open implements hackpadfs.FS
- func (fs *FS) Open(name string) (hackpadfs.File, error) {
- mountFS, subPath := fs.Mount(name)
- return mountFS.Open(subPath)
- }
- // Point represents a mount point, including any relevant metadata
- type Point struct {
- Path string
- }
- // MountPoints returns a slice of mount points every mounted file system.
- func (fs *FS) MountPoints() []Point {
- var points []Point
- fs.mounts.Range(func(key, _ interface{}) bool {
- path := key.(string)
- points = append(points, Point{path})
- return true
- })
- return points
- }
- // Rename implements hackpadfs.RenameFS
- func (fs *FS) Rename(oldname, newname string) error {
- oldMount, oldPoint, oldSubPath := fs.mountPoint(oldname)
- newMount, newPoint, newSubPath := fs.mountPoint(newname)
- oldInfo, err := hackpadfs.Stat(oldMount, oldSubPath)
- if err != nil {
- return &hackpadfs.LinkError{Op: "rename", Old: oldname, New: newname, Err: err}
- }
- if oldname == newname {
- if !oldInfo.IsDir() {
- return nil
- }
- return &hackpadfs.LinkError{Op: "rename", Old: oldname, New: newname, Err: hackpadfs.ErrExist}
- }
- if oldPoint == newPoint {
- return hackpadfs.Rename(oldMount, oldSubPath, newSubPath)
- }
- if oldInfo.IsDir() {
- // TODO support renaming directories
- return &hackpadfs.LinkError{Op: "rename", Old: oldname, New: newname, Err: hackpadfs.ErrNotImplemented}
- }
- oldFile, err := oldMount.Open(oldSubPath)
- if err != nil {
- return err
- }
- defer func() { _ = oldFile.Close() }()
- newFile, err := hackpadfs.OpenFile(newMount, newSubPath, hackpadfs.FlagWriteOnly|hackpadfs.FlagCreate|hackpadfs.FlagTruncate, oldInfo.Mode())
- if err != nil {
- return err
- }
- newFileWriter, ok := newFile.(io.Writer)
- if !ok {
- return &hackpadfs.LinkError{Op: "rename", Old: oldname, New: newname, Err: hackpadfs.ErrPermission}
- }
- defer func() { _ = newFile.Close() }()
- _, err = io.Copy(newFileWriter, oldFile)
- if err != nil {
- _ = hackpadfs.Remove(newMount, newSubPath)
- return err
- }
- return hackpadfs.Remove(oldMount, oldSubPath)
- }
|