tractor.dev/toolkit-go@v0.0.0-20241010005851-214d91207d07/engine/fs/mountablefs/mountablefs.go (about)

     1  package mountablefs
     2  
     3  import (
     4  	"errors"
     5  	"path/filepath"
     6  	"slices"
     7  	"strings"
     8  	"syscall"
     9  	"time"
    10  
    11  	"tractor.dev/toolkit-go/engine/fs"
    12  	"tractor.dev/toolkit-go/engine/fs/fsutil"
    13  )
    14  
    15  type mountedFSDir struct {
    16  	fsys       fs.FS
    17  	mountPoint string
    18  }
    19  
    20  type FS struct {
    21  	fs.MutableFS
    22  	mounts []mountedFSDir
    23  }
    24  
    25  func New(fsys fs.MutableFS) *FS {
    26  	return &FS{MutableFS: fsys, mounts: make([]mountedFSDir, 0, 1)}
    27  }
    28  
    29  func (host *FS) Mount(fsys fs.FS, dirPath string) error {
    30  	dirPath = cleanPath(dirPath)
    31  
    32  	fi, err := fs.Stat(host, dirPath)
    33  	if err != nil {
    34  		return err
    35  	}
    36  
    37  	if !fi.IsDir() {
    38  		return &fs.PathError{Op: "mount", Path: dirPath, Err: fs.ErrInvalid}
    39  	}
    40  	if found, _ := host.isPathInMount(dirPath); found {
    41  		return &fs.PathError{Op: "mount", Path: dirPath, Err: fs.ErrExist}
    42  	}
    43  
    44  	host.mounts = append(host.mounts, mountedFSDir{fsys: fsys, mountPoint: dirPath})
    45  	return nil
    46  }
    47  
    48  func (host *FS) Unmount(path string) error {
    49  	path = cleanPath(path)
    50  	for i, m := range host.mounts {
    51  		if path == m.mountPoint {
    52  			host.mounts = remove(host.mounts, i)
    53  			return nil
    54  		}
    55  	}
    56  
    57  	return &fs.PathError{Op: "unmount", Path: path, Err: fs.ErrInvalid}
    58  }
    59  
    60  func remove(s []mountedFSDir, i int) []mountedFSDir {
    61  	s[i] = s[len(s)-1]
    62  	return s[:len(s)-1]
    63  }
    64  
    65  func (host *FS) isPathInMount(path string) (bool, *mountedFSDir) {
    66  	for i, m := range host.mounts {
    67  		if strings.HasPrefix(path, m.mountPoint) {
    68  			return true, &host.mounts[i]
    69  		}
    70  	}
    71  	return false, nil
    72  }
    73  
    74  func cleanPath(p string) string {
    75  	return filepath.Clean(strings.TrimLeft(p, "/\\"))
    76  }
    77  
    78  func trimMountPoint(path string, mntPoint string) string {
    79  	result := strings.TrimPrefix(path, mntPoint)
    80  	result = strings.TrimPrefix(result, string(filepath.Separator))
    81  
    82  	if result == "" {
    83  		return "."
    84  	} else {
    85  		return result
    86  	}
    87  }
    88  
    89  func (host *FS) Chmod(name string, mode fs.FileMode) error {
    90  	name = cleanPath(name)
    91  	var fsys fs.FS
    92  	prefix := ""
    93  
    94  	if found, mount := host.isPathInMount(name); found {
    95  		fsys = mount.fsys
    96  		prefix = mount.mountPoint
    97  	} else {
    98  		fsys = host.MutableFS
    99  	}
   100  
   101  	chmodableFS, ok := fsys.(interface {
   102  		Chmod(name string, mode fs.FileMode) error
   103  	})
   104  	if !ok {
   105  		return &fs.PathError{Op: "chmod", Path: name, Err: errors.ErrUnsupported}
   106  	}
   107  	return chmodableFS.Chmod(trimMountPoint(name, prefix), mode)
   108  }
   109  
   110  func (host *FS) Chown(name string, uid, gid int) error {
   111  	name = cleanPath(name)
   112  	var fsys fs.FS
   113  	prefix := ""
   114  
   115  	if found, mount := host.isPathInMount(name); found {
   116  		fsys = mount.fsys
   117  		prefix = mount.mountPoint
   118  	} else {
   119  		fsys = host.MutableFS
   120  	}
   121  
   122  	chownableFS, ok := fsys.(interface {
   123  		Chown(name string, uid, gid int) error
   124  	})
   125  	if !ok {
   126  		return &fs.PathError{Op: "chown", Path: name, Err: errors.ErrUnsupported}
   127  	}
   128  	return chownableFS.Chown(trimMountPoint(name, prefix), uid, gid)
   129  }
   130  
   131  func (host *FS) Chtimes(name string, atime time.Time, mtime time.Time) error {
   132  	name = cleanPath(name)
   133  	var fsys fs.FS
   134  	prefix := ""
   135  
   136  	if found, mount := host.isPathInMount(name); found {
   137  		fsys = mount.fsys
   138  		prefix = mount.mountPoint
   139  	} else {
   140  		fsys = host.MutableFS
   141  	}
   142  
   143  	chtimesableFS, ok := fsys.(interface {
   144  		Chtimes(name string, atime time.Time, mtime time.Time) error
   145  	})
   146  	if !ok {
   147  		return &fs.PathError{Op: "chtimes", Path: name, Err: errors.ErrUnsupported}
   148  	}
   149  	return chtimesableFS.Chtimes(trimMountPoint(name, prefix), atime, mtime)
   150  }
   151  
   152  func (host *FS) Create(name string) (fs.File, error) {
   153  	name = cleanPath(name)
   154  	var fsys fs.FS
   155  	prefix := ""
   156  
   157  	if found, mount := host.isPathInMount(name); found {
   158  		fsys = mount.fsys
   159  		prefix = mount.mountPoint
   160  	} else {
   161  		fsys = host.MutableFS
   162  	}
   163  
   164  	createableFS, ok := fsys.(interface {
   165  		Create(name string) (fs.File, error)
   166  	})
   167  	if !ok {
   168  		return nil, &fs.PathError{Op: "create", Path: name, Err: errors.ErrUnsupported}
   169  	}
   170  	return createableFS.Create(trimMountPoint(name, prefix))
   171  }
   172  
   173  func (host *FS) Mkdir(name string, perm fs.FileMode) error {
   174  	name = cleanPath(name)
   175  	var fsys fs.FS
   176  	prefix := ""
   177  
   178  	if found, mount := host.isPathInMount(name); found {
   179  		fsys = mount.fsys
   180  		prefix = mount.mountPoint
   181  	} else {
   182  		fsys = host.MutableFS
   183  	}
   184  
   185  	mkdirableFS, ok := fsys.(interface {
   186  		Mkdir(name string, perm fs.FileMode) error
   187  	})
   188  	if !ok {
   189  		return &fs.PathError{Op: "mkdir", Path: name, Err: errors.ErrUnsupported}
   190  	}
   191  	return mkdirableFS.Mkdir(trimMountPoint(name, prefix), perm)
   192  }
   193  
   194  func (host *FS) MkdirAll(path string, perm fs.FileMode) error {
   195  	path = cleanPath(path)
   196  	var fsys fs.FS
   197  	prefix := ""
   198  
   199  	if found, mount := host.isPathInMount(path); found {
   200  		fsys = mount.fsys
   201  		prefix = mount.mountPoint
   202  	} else {
   203  		fsys = host.MutableFS
   204  	}
   205  
   206  	mkdirableFS, ok := fsys.(interface {
   207  		MkdirAll(path string, perm fs.FileMode) error
   208  	})
   209  	if !ok {
   210  		return &fs.PathError{Op: "mkdirAll", Path: path, Err: errors.ErrUnsupported}
   211  	}
   212  	return mkdirableFS.MkdirAll(trimMountPoint(path, prefix), perm)
   213  }
   214  
   215  func (host *FS) Open(name string) (fs.File, error) {
   216  	name = cleanPath(name)
   217  	if found, mount := host.isPathInMount(name); found {
   218  		return mount.fsys.Open(trimMountPoint(name, mount.mountPoint))
   219  	}
   220  
   221  	return host.MutableFS.Open(name)
   222  }
   223  
   224  func (host *FS) OpenFile(name string, flag int, perm fs.FileMode) (fs.File, error) {
   225  	if found, mount := host.isPathInMount(name); found {
   226  		return fsutil.OpenFile(mount.fsys, trimMountPoint(name, mount.mountPoint), flag, perm)
   227  	} else {
   228  		return fsutil.OpenFile(host.MutableFS, name, flag, perm)
   229  	}
   230  }
   231  
   232  type removableFS interface {
   233  	fs.FS
   234  	Remove(name string) error
   235  }
   236  
   237  func (host *FS) Remove(name string) error {
   238  	name = cleanPath(name)
   239  	var fsys fs.FS
   240  	prefix := ""
   241  
   242  	if found, mount := host.isPathInMount(name); found {
   243  		if name == mount.mountPoint {
   244  			return &fs.PathError{Op: "remove", Path: name, Err: syscall.EBUSY}
   245  		}
   246  
   247  		fsys = mount.fsys
   248  		prefix = mount.mountPoint
   249  	} else {
   250  		fsys = host.MutableFS
   251  	}
   252  
   253  	if removableFS, ok := fsys.(removableFS); ok {
   254  		return removableFS.Remove(trimMountPoint(name, prefix))
   255  	} else {
   256  		return &fs.PathError{Op: "remove", Path: name, Err: errors.ErrUnsupported}
   257  	}
   258  }
   259  
   260  func (host *FS) RemoveAll(path string) error {
   261  	path = cleanPath(path)
   262  	var fsys fs.FS
   263  	prefix := ""
   264  
   265  	if found, mount := host.isPathInMount(path); found {
   266  		if path == mount.mountPoint {
   267  			return &fs.PathError{Op: "removeAll", Path: path, Err: syscall.EBUSY}
   268  		}
   269  
   270  		fsys = mount.fsys
   271  		prefix = mount.mountPoint
   272  	} else {
   273  		fsys = host.MutableFS
   274  		// check if path contains any mountpoints, and call a custom removeAll
   275  		// if it does.
   276  		var mntPoints []string
   277  		for _, m := range host.mounts {
   278  			if path == "." || strings.HasPrefix(m.mountPoint, path) {
   279  				mntPoints = append(mntPoints, m.mountPoint)
   280  			}
   281  		}
   282  
   283  		if len(mntPoints) > 0 {
   284  			return removeAll(host, path, mntPoints)
   285  		}
   286  	}
   287  
   288  	rmAllFS, ok := fsys.(interface {
   289  		RemoveAll(path string) error
   290  	})
   291  	if !ok {
   292  		if rmFS, ok := fsys.(removableFS); ok {
   293  			return removeAll(rmFS, path, nil)
   294  		} else {
   295  			return &fs.PathError{Op: "removeAll", Path: path, Err: errors.ErrUnsupported}
   296  		}
   297  	}
   298  	return rmAllFS.RemoveAll(trimMountPoint(path, prefix))
   299  }
   300  
   301  // RemoveAll removes path and any children it contains. It removes everything
   302  // it can but returns the first error it encounters. If the path does not exist,
   303  // RemoveAll returns nil (no error). If there is an error, it will be of type *PathError.
   304  // Additionally, this function errors if attempting to remove a mountpoint.
   305  func removeAll(fsys removableFS, path string, mntPoints []string) error {
   306  	path = filepath.Clean(path)
   307  
   308  	if exists, err := fsutil.Exists(fsys, path); !exists || err != nil {
   309  		return err
   310  	}
   311  
   312  	return rmRecurse(fsys, path, mntPoints)
   313  
   314  }
   315  
   316  func rmRecurse(fsys removableFS, path string, mntPoints []string) error {
   317  	if mntPoints != nil && slices.Contains(mntPoints, path) {
   318  		return &fs.PathError{Op: "remove", Path: path, Err: syscall.EBUSY}
   319  	}
   320  
   321  	isdir, dirErr := fsutil.IsDir(fsys, path)
   322  	if dirErr != nil {
   323  		return dirErr
   324  	}
   325  
   326  	if isdir {
   327  		if entries, err := fs.ReadDir(fsys, path); err == nil {
   328  			for _, entry := range entries {
   329  				entryPath := filepath.Join(path, entry.Name())
   330  
   331  				if err := rmRecurse(fsys, entryPath, mntPoints); err != nil {
   332  					return err
   333  				}
   334  
   335  				if err := fsys.Remove(entryPath); err != nil {
   336  					return err
   337  				}
   338  			}
   339  		} else {
   340  			return err
   341  		}
   342  	}
   343  
   344  	return fsys.Remove(path)
   345  }
   346  
   347  func (host *FS) Rename(oldname, newname string) error {
   348  	oldname = cleanPath(oldname)
   349  	newname = cleanPath(newname)
   350  	var fsys fs.FS
   351  	prefix := ""
   352  
   353  	// error if both paths aren't in the same filesystem
   354  	if found, oldMount := host.isPathInMount(oldname); found {
   355  		if found, newMount := host.isPathInMount(newname); found {
   356  			if oldMount != newMount {
   357  				return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: syscall.EXDEV}
   358  			}
   359  
   360  			if oldname == oldMount.mountPoint || newname == newMount.mountPoint {
   361  				return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: syscall.EBUSY}
   362  			}
   363  
   364  			fsys = newMount.fsys
   365  			prefix = newMount.mountPoint
   366  		} else {
   367  			return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: syscall.EXDEV}
   368  		}
   369  	} else {
   370  		if found, _ := host.isPathInMount(newname); found {
   371  			return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: syscall.EXDEV}
   372  		}
   373  
   374  		fsys = host.MutableFS
   375  	}
   376  
   377  	renameableFS, ok := fsys.(interface {
   378  		Rename(oldname, newname string) error
   379  	})
   380  	if !ok {
   381  		return &fs.PathError{Op: "rename", Path: oldname + " -> " + newname, Err: errors.ErrUnsupported}
   382  	}
   383  	return renameableFS.Rename(trimMountPoint(oldname, prefix), trimMountPoint(newname, prefix))
   384  }