1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
21 ErrReadOnlyFile = errors.New("read-only file")
22 ErrNegativeOffset = errors.New("cannot seek to negative offset")
23 ErrFileExists = errors.New("file exists")
24 ErrInvalidOperation = errors.New("invalid operation")
25 ErrInvalidArgument = errors.New("invalid argument")
26 ErrDirectoryNotEmpty = errors.New("directory not empty")
27 ErrWriteOnlyMode = errors.New("file is O_WRONLY")
28 ErrSyncNotSupported = errors.New("O_SYNC flag is not supported")
29 ErrIsDirectory = errors.New("cannot rename file to overwrite existing directory")
30 ErrPermission = os.ErrPermission
33 // A File is an *os.File-like interface for reading and writing files
41 Readdir(int) ([]os.FileInfo, error)
42 Stat() (os.FileInfo, error)
47 // A FileSystem is an http.Filesystem plus Stat() and support for
48 // opening writable files. All methods are safe to call from multiple
50 type FileSystem interface {
56 // filesystem-wide lock: used by Rename() to prevent deadlock
57 // while locking multiple inodes.
60 // create a new node with nil parent.
61 newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error)
63 // analogous to os.Stat()
64 Stat(name string) (os.FileInfo, error)
66 // analogous to os.Create(): create/truncate a file and open it O_RDWR.
67 Create(name string) (File, error)
69 // Like os.OpenFile(): create or open a file or directory.
71 // If flag&os.O_EXCL==0, it opens an existing file or
72 // directory if one exists. If flag&os.O_CREATE!=0, it creates
73 // a new empty file or directory if one does not already
76 // When creating a new item, perm&os.ModeDir determines
77 // whether it is a file or a directory.
79 // A file can be opened multiple times and used concurrently
80 // from multiple goroutines. However, each File object should
81 // be used by only one goroutine at a time.
82 OpenFile(name string, flag int, perm os.FileMode) (File, error)
84 Mkdir(name string, perm os.FileMode) error
85 Remove(name string) error
86 RemoveAll(name string) error
87 Rename(oldname, newname string) error
91 type inode interface {
92 SetParent(parent inode, name string)
95 Read([]byte, filenodePtr) (int, filenodePtr, error)
96 Write([]byte, filenodePtr) (int, filenodePtr, error)
99 Readdir() ([]os.FileInfo, error)
101 FileInfo() os.FileInfo
103 // Child() performs lookups and updates of named child nodes.
105 // If replace is non-nil, Child calls replace(x) where x is
106 // the current child inode with the given name. If possible,
107 // the child inode is replaced with the one returned by
110 // If replace(x) returns an inode (besides x or nil) that is
111 // subsequently returned by Child(), then Child()'s caller
112 // must ensure the new child's name and parent are set/updated
113 // to Child()'s name argument and its receiver respectively.
114 // This is not necessarily done before replace(x) returns, but
115 // it must be done before Child()'s caller releases the
118 // Nil represents "no child". replace(nil) signifies that no
119 // child with this name exists yet. If replace() returns nil,
120 // the existing child should be deleted if possible.
122 // An implementation of Child() is permitted to ignore
123 // replace() or its return value. For example, a regular file
124 // inode does not have children, so Child() always returns
127 // Child() returns the child, if any, with the given name: if
128 // a child was added or changed, the new child is returned.
130 // Caller must have lock (or rlock if replace is nil).
131 Child(name string, replace func(inode) inode) inode
138 type fileinfo struct {
145 // Name implements os.FileInfo.
146 func (fi fileinfo) Name() string {
150 // ModTime implements os.FileInfo.
151 func (fi fileinfo) ModTime() time.Time {
155 // Mode implements os.FileInfo.
156 func (fi fileinfo) Mode() os.FileMode {
160 // IsDir implements os.FileInfo.
161 func (fi fileinfo) IsDir() bool {
162 return fi.mode&os.ModeDir != 0
165 // Size implements os.FileInfo.
166 func (fi fileinfo) Size() int64 {
170 // Sys implements os.FileInfo.
171 func (fi fileinfo) Sys() interface{} {
175 type nullnode struct{}
177 func (*nullnode) Mkdir(string, os.FileMode) error {
178 return ErrInvalidOperation
181 func (*nullnode) Read([]byte, filenodePtr) (int, filenodePtr, error) {
182 return 0, filenodePtr{}, ErrInvalidOperation
185 func (*nullnode) Write([]byte, filenodePtr) (int, filenodePtr, error) {
186 return 0, filenodePtr{}, ErrInvalidOperation
189 func (*nullnode) Truncate(int64) error {
190 return ErrInvalidOperation
193 func (*nullnode) FileInfo() os.FileInfo {
197 func (*nullnode) IsDir() bool {
201 func (*nullnode) Readdir() ([]os.FileInfo, error) {
202 return nil, ErrInvalidOperation
205 func (*nullnode) Child(name string, replace func(inode) inode) inode {
209 type treenode struct {
212 inodes map[string]inode
218 func (n *treenode) FS() FileSystem {
222 func (n *treenode) SetParent(p inode, name string) {
226 n.fileinfo.name = name
229 func (n *treenode) Parent() inode {
235 func (n *treenode) IsDir() bool {
239 func (n *treenode) Child(name string, replace func(inode) inode) (child inode) {
240 // TODO: special treatment for "", ".", ".."
241 child = n.inodes[name]
243 newchild := replace(child)
245 delete(n.inodes, name)
246 } else if newchild != child {
247 n.inodes[name] = newchild
248 n.fileinfo.modTime = time.Now()
255 func (n *treenode) Size() int64 {
256 return n.FileInfo().Size()
259 func (n *treenode) FileInfo() os.FileInfo {
262 n.fileinfo.size = int64(len(n.inodes))
266 func (n *treenode) Readdir() (fi []os.FileInfo, err error) {
269 fi = make([]os.FileInfo, 0, len(n.inodes))
270 for _, inode := range n.inodes {
271 fi = append(fi, inode.FileInfo())
276 type fileSystem struct {
282 func (fs *fileSystem) rootnode() inode {
286 func (fs *fileSystem) locker() sync.Locker {
290 // OpenFile is analogous to os.OpenFile().
291 func (fs *fileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
292 return fs.openFile(name, flag, perm)
295 func (fs *fileSystem) openFile(name string, flag int, perm os.FileMode) (*filehandle, error) {
296 if flag&os.O_SYNC != 0 {
297 return nil, ErrSyncNotSupported
299 dirname, name := path.Split(name)
300 parent := rlookup(fs.root, dirname)
302 return nil, os.ErrNotExist
304 var readable, writable bool
305 switch flag & (os.O_RDWR | os.O_RDONLY | os.O_WRONLY) {
314 return nil, fmt.Errorf("invalid flags 0x%x", flag)
316 if !writable && parent.IsDir() {
317 // A directory can be opened via "foo/", "foo/.", or
321 return &filehandle{inode: parent}, nil
323 return &filehandle{inode: parent.Parent()}, nil
326 createMode := flag&os.O_CREATE != 0
329 defer parent.Unlock()
332 defer parent.RUnlock()
334 n := parent.Child(name, nil)
337 return nil, os.ErrNotExist
340 n = parent.Child(name, func(inode) inode {
341 n, err = parent.FS().newNode(name, perm|0755, time.Now())
342 n.SetParent(parent, name)
348 // parent rejected new child
349 return nil, ErrInvalidOperation
351 } else if flag&os.O_EXCL != 0 {
352 return nil, ErrFileExists
353 } else if flag&os.O_TRUNC != 0 {
355 return nil, fmt.Errorf("invalid flag O_TRUNC in read-only mode")
356 } else if n.IsDir() {
357 return nil, fmt.Errorf("invalid flag O_TRUNC when opening directory")
358 } else if err := n.Truncate(0); err != nil {
364 append: flag&os.O_APPEND != 0,
370 func (fs *fileSystem) Open(name string) (http.File, error) {
371 return fs.OpenFile(name, os.O_RDONLY, 0)
374 func (fs *fileSystem) Create(name string) (File, error) {
375 return fs.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0)
378 func (fs *fileSystem) Mkdir(name string, perm os.FileMode) (err error) {
379 dirname, name := path.Split(name)
380 n := rlookup(fs.root, dirname)
382 return os.ErrNotExist
386 if n.Child(name, nil) != nil {
389 child := n.Child(name, func(inode) (child inode) {
390 child, err = n.FS().newNode(name, perm|os.ModeDir, time.Now())
391 child.SetParent(n, name)
396 } else if child == nil {
397 return ErrInvalidArgument
402 func (fs *fileSystem) Stat(name string) (fi os.FileInfo, err error) {
403 node := rlookup(fs.root, name)
412 func (fs *fileSystem) Rename(oldname, newname string) error {
413 olddir, oldname := path.Split(oldname)
414 if oldname == "" || oldname == "." || oldname == ".." {
415 return ErrInvalidArgument
417 olddirf, err := fs.openFile(olddir+".", os.O_RDONLY, 0)
419 return fmt.Errorf("%q: %s", olddir, err)
421 defer olddirf.Close()
423 newdir, newname := path.Split(newname)
424 if newname == "." || newname == ".." {
425 return ErrInvalidArgument
426 } else if newname == "" {
427 // Rename("a/b", "c/") means Rename("a/b", "c/b")
430 newdirf, err := fs.openFile(newdir+".", os.O_RDONLY, 0)
432 return fmt.Errorf("%q: %s", newdir, err)
434 defer newdirf.Close()
436 // TODO: If the nearest common ancestor ("nca") of olddirf and
437 // newdirf is on a different filesystem than fs, we should
438 // call nca.FS().Rename() instead of proceeding. Until then
439 // it's awkward for filesystems to implement their own Rename
440 // methods effectively: the only one that runs is the one on
441 // the root filesystem exposed to the caller (webdav, fuse,
444 // When acquiring locks on multiple inodes, avoid deadlock by
445 // locking the entire containing filesystem first.
446 cfs := olddirf.inode.FS()
448 defer cfs.locker().Unlock()
450 if cfs != newdirf.inode.FS() {
451 // Moving inodes across filesystems is not (yet)
452 // supported. Locking inodes from different
453 // filesystems could deadlock, so we must error out
455 return ErrInvalidArgument
458 // To ensure we can test reliably whether we're about to move
459 // a directory into itself, lock all potential common
460 // ancestors of olddir and newdir.
461 needLock := []sync.Locker{}
462 for _, node := range []inode{olddirf.inode, newdirf.inode} {
463 needLock = append(needLock, node)
464 for node.Parent() != node && node.Parent().FS() == node.FS() {
466 needLock = append(needLock, node)
469 locked := map[sync.Locker]bool{}
470 for i := len(needLock) - 1; i >= 0; i-- {
471 if n := needLock[i]; !locked[n] {
478 // Return ErrInvalidOperation if olddirf.inode doesn't even
479 // bother calling our "remove oldname entry" replacer func.
480 err = ErrInvalidArgument
481 olddirf.inode.Child(oldname, func(oldinode inode) inode {
487 if locked[oldinode] {
488 // oldinode cannot become a descendant of itself.
489 err = ErrInvalidArgument
492 if oldinode.FS() != cfs && newdirf.inode != olddirf.inode {
493 // moving a mount point to a different parent
494 // is not (yet) supported.
495 err = ErrInvalidArgument
498 accepted := newdirf.inode.Child(newname, func(existing inode) inode {
499 if existing != nil && existing.IsDir() {
505 if accepted != oldinode {
507 // newdirf didn't accept oldinode.
508 err = ErrInvalidArgument
510 // Leave oldinode in olddir.
513 accepted.SetParent(newdirf.inode, newname)
519 func (fs *fileSystem) Remove(name string) error {
520 return fs.remove(strings.TrimRight(name, "/"), false)
523 func (fs *fileSystem) RemoveAll(name string) error {
524 err := fs.remove(strings.TrimRight(name, "/"), true)
525 if os.IsNotExist(err) {
526 // "If the path does not exist, RemoveAll returns
527 // nil." (see "os" pkg)
533 func (fs *fileSystem) remove(name string, recursive bool) (err error) {
534 dirname, name := path.Split(name)
535 if name == "" || name == "." || name == ".." {
536 return ErrInvalidArgument
538 dir := rlookup(fs.root, dirname)
540 return os.ErrNotExist
544 dir.Child(name, func(node inode) inode {
549 if !recursive && node.IsDir() && node.Size() > 0 {
550 err = ErrDirectoryNotEmpty
558 func (fs *fileSystem) Sync() error {
559 log.Printf("TODO: sync fileSystem")
560 return ErrInvalidOperation
563 // rlookup (recursive lookup) returns the inode for the file/directory
564 // with the given name (which may contain "/" separators). If no such
565 // file/directory exists, the returned node is nil.
566 func rlookup(start inode, path string) (node inode) {
568 for _, name := range strings.Split(path, "/") {
573 if name == "." || name == "" {
581 node = func() inode {
584 return node.Child(name, nil)