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 }