Merge branch '16698-debug-ports-arvbox' into master
[arvados.git] / sdk / go / arvados / fs_base.go
index 3be3f5e2f64335b00088d0665719fac4b9f91523..5e57fed3beab3281b1d498936edb4eed813398ec 100644 (file)
@@ -8,6 +8,7 @@ import (
        "errors"
        "fmt"
        "io"
+       "log"
        "net/http"
        "os"
        "path"
@@ -26,9 +27,14 @@ var (
        ErrWriteOnlyMode     = errors.New("file is O_WRONLY")
        ErrSyncNotSupported  = errors.New("O_SYNC flag is not supported")
        ErrIsDirectory       = errors.New("cannot rename file to overwrite existing directory")
+       ErrNotADirectory     = errors.New("not a directory")
        ErrPermission        = os.ErrPermission
 )
 
+type syncer interface {
+       Sync() error
+}
+
 // A File is an *os.File-like interface for reading and writing files
 // in a FileSystem.
 type File interface {
@@ -40,6 +46,7 @@ type File interface {
        Readdir(int) ([]os.FileInfo, error)
        Stat() (os.FileInfo, error)
        Truncate(int64) error
+       Sync() error
 }
 
 // A FileSystem is an http.Filesystem plus Stat() and support for
@@ -47,8 +54,19 @@ type File interface {
 // goroutines.
 type FileSystem interface {
        http.FileSystem
+       fsBackend
+
+       rootnode() inode
 
-       inode
+       // filesystem-wide lock: used by Rename() to prevent deadlock
+       // while locking multiple inodes.
+       locker() sync.Locker
+
+       // throttle for limiting concurrent background writers
+       throttle() *throttle
+
+       // create a new node with nil parent.
+       newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error)
 
        // analogous to os.Stat()
        Stat(name string) (os.FileInfo, error)
@@ -75,25 +93,51 @@ type FileSystem interface {
        Remove(name string) error
        RemoveAll(name string) error
        Rename(oldname, newname string) error
+
+       // Write buffered data from memory to storage, returning when
+       // all updates have been saved to persistent storage.
+       Sync() error
+
+       // Write buffered data from memory to storage, but don't wait
+       // for all writes to finish before returning. If shortBlocks
+       // is true, flush everything; otherwise, if there's less than
+       // a full block of buffered data at the end of a stream, leave
+       // it buffered in memory in case more data can be appended. If
+       // path is "", flush all dirs/streams; otherwise, flush only
+       // the specified dir/stream.
+       Flush(path string, shortBlocks bool) error
 }
 
 type inode interface {
+       SetParent(parent inode, name string)
        Parent() inode
+       FS() FileSystem
        Read([]byte, filenodePtr) (int, filenodePtr, error)
        Write([]byte, filenodePtr) (int, filenodePtr, error)
        Truncate(int64) error
        IsDir() bool
-       Readdir() []os.FileInfo
+       Readdir() ([]os.FileInfo, error)
        Size() int64
        FileInfo() os.FileInfo
 
        // Child() performs lookups and updates of named child nodes.
        //
+       // (The term "child" here is used strictly. This means name is
+       // not "." or "..", and name does not contain "/".)
+       //
        // If replace is non-nil, Child calls replace(x) where x is
        // the current child inode with the given name. If possible,
        // the child inode is replaced with the one returned by
        // replace().
        //
+       // If replace(x) returns an inode (besides x or nil) that is
+       // subsequently returned by Child(), then Child()'s caller
+       // must ensure the new child's name and parent are set/updated
+       // to Child()'s name argument and its receiver respectively.
+       // This is not necessarily done before replace(x) returns, but
+       // it must be done before Child()'s caller releases the
+       // parent's lock.
+       //
        // Nil represents "no child". replace(nil) signifies that no
        // child with this name exists yet. If replace() returns nil,
        // the existing child should be deleted if possible.
@@ -107,7 +151,7 @@ type inode interface {
        // a child was added or changed, the new child is returned.
        //
        // Caller must have lock (or rlock if replace is nil).
-       Child(name string, replace func(inode) inode) inode
+       Child(name string, replace func(inode) (inode, error)) (inode, error)
 
        sync.Locker
        RLock()
@@ -177,15 +221,16 @@ func (*nullnode) IsDir() bool {
        return false
 }
 
-func (*nullnode) Readdir() []os.FileInfo {
-       return nil
+func (*nullnode) Readdir() ([]os.FileInfo, error) {
+       return nil, ErrInvalidOperation
 }
 
-func (*nullnode) Child(name string, replace func(inode) inode) inode {
-       return nil
+func (*nullnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
+       return nil, ErrNotADirectory
 }
 
 type treenode struct {
+       fs       FileSystem
        parent   inode
        inodes   map[string]inode
        fileinfo fileinfo
@@ -193,6 +238,17 @@ type treenode struct {
        nullnode
 }
 
+func (n *treenode) FS() FileSystem {
+       return n.fs
+}
+
+func (n *treenode) SetParent(p inode, name string) {
+       n.Lock()
+       defer n.Unlock()
+       n.parent = p
+       n.fileinfo.name = name
+}
+
 func (n *treenode) Parent() inode {
        n.RLock()
        defer n.RUnlock()
@@ -203,16 +259,25 @@ func (n *treenode) IsDir() bool {
        return true
 }
 
-func (n *treenode) Child(name string, replace func(inode) inode) (child inode) {
-       // TODO: special treatment for "", ".", ".."
+func (n *treenode) Child(name string, replace func(inode) (inode, error)) (child inode, err error) {
        child = n.inodes[name]
-       if replace != nil {
-               child = replace(child)
-               if child == nil {
-                       delete(n.inodes, name)
-               } else {
-                       n.inodes[name] = child
-               }
+       if name == "" || name == "." || name == ".." {
+               err = ErrInvalidArgument
+               return
+       }
+       if replace == nil {
+               return
+       }
+       newchild, err := replace(child)
+       if err != nil {
+               return
+       }
+       if newchild == nil {
+               delete(n.inodes, name)
+       } else if newchild != child {
+               n.inodes[name] = newchild
+               n.fileinfo.modTime = time.Now()
+               child = newchild
        }
        return
 }
@@ -228,7 +293,7 @@ func (n *treenode) FileInfo() os.FileInfo {
        return n.fileinfo
 }
 
-func (n *treenode) Readdir() (fi []os.FileInfo) {
+func (n *treenode) Readdir() (fi []os.FileInfo, err error) {
        n.RLock()
        defer n.RUnlock()
        fi = make([]os.FileInfo, 0, len(n.inodes))
@@ -238,8 +303,39 @@ func (n *treenode) Readdir() (fi []os.FileInfo) {
        return
 }
 
+func (n *treenode) Sync() error {
+       n.RLock()
+       defer n.RUnlock()
+       for _, inode := range n.inodes {
+               syncer, ok := inode.(syncer)
+               if !ok {
+                       return ErrInvalidOperation
+               }
+               err := syncer.Sync()
+               if err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
 type fileSystem struct {
-       inode
+       root inode
+       fsBackend
+       mutex sync.Mutex
+       thr   *throttle
+}
+
+func (fs *fileSystem) rootnode() inode {
+       return fs.root
+}
+
+func (fs *fileSystem) throttle() *throttle {
+       return fs.thr
+}
+
+func (fs *fileSystem) locker() sync.Locker {
+       return &fs.mutex
 }
 
 // OpenFile is analogous to os.OpenFile().
@@ -248,14 +344,13 @@ func (fs *fileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, e
 }
 
 func (fs *fileSystem) openFile(name string, flag int, perm os.FileMode) (*filehandle, error) {
-       var dn inode = fs.inode
        if flag&os.O_SYNC != 0 {
                return nil, ErrSyncNotSupported
        }
        dirname, name := path.Split(name)
-       parent := rlookup(dn, dirname)
-       if parent == nil {
-               return nil, os.ErrNotExist
+       parent, err := rlookup(fs.root, dirname)
+       if err != nil {
+               return nil, err
        }
        var readable, writable bool
        switch flag & (os.O_RDWR | os.O_RDONLY | os.O_WRONLY) {
@@ -287,45 +382,36 @@ func (fs *fileSystem) openFile(name string, flag int, perm os.FileMode) (*fileha
                parent.RLock()
                defer parent.RUnlock()
        }
-       n := parent.Child(name, nil)
-       if n == nil {
+       n, err := parent.Child(name, nil)
+       if err != nil {
+               return nil, err
+       } else if n == nil {
                if !createMode {
                        return nil, os.ErrNotExist
                }
-               var err error
-               n = parent.Child(name, func(inode) inode {
-                       var dn *dirnode
-                       switch parent := parent.(type) {
-                       case *dirnode:
-                               dn = parent
-                       case *collectionFileSystem:
-                               dn = parent.inode.(*dirnode)
-                       default:
-                               err = ErrInvalidArgument
-                               return nil
-                       }
-                       if perm.IsDir() {
-                               n, err = dn.newDirnode(dn, name, perm|0755, time.Now())
-                       } else {
-                               n, err = dn.newFilenode(dn, name, perm|0755, time.Now())
+               n, err = parent.Child(name, func(inode) (repl inode, err error) {
+                       repl, err = parent.FS().newNode(name, perm|0755, time.Now())
+                       if err != nil {
+                               return
                        }
-                       return n
+                       repl.SetParent(parent, name)
+                       return
                })
                if err != nil {
                        return nil, err
                } else if n == nil {
-                       // parent rejected new child
-                       return nil, ErrInvalidOperation
+                       // Parent rejected new child, but returned no error
+                       return nil, ErrInvalidArgument
                }
        } else if flag&os.O_EXCL != 0 {
                return nil, ErrFileExists
        } else if flag&os.O_TRUNC != 0 {
                if !writable {
                        return nil, fmt.Errorf("invalid flag O_TRUNC in read-only mode")
-               } else if fn, ok := n.(*filenode); !ok {
+               } else if n.IsDir() {
                        return nil, fmt.Errorf("invalid flag O_TRUNC when opening directory")
-               } else {
-                       fn.Truncate(0)
+               } else if err := n.Truncate(0); err != nil {
+                       return nil, err
                }
        }
        return &filehandle{
@@ -344,41 +430,37 @@ func (fs *fileSystem) Create(name string) (File, error) {
        return fs.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0)
 }
 
-func (fs *fileSystem) Mkdir(name string, perm os.FileMode) (err error) {
+func (fs *fileSystem) Mkdir(name string, perm os.FileMode) error {
        dirname, name := path.Split(name)
-       n := rlookup(fs.inode, dirname)
-       if n == nil {
-               return os.ErrNotExist
+       n, err := rlookup(fs.root, dirname)
+       if err != nil {
+               return err
        }
        n.Lock()
        defer n.Unlock()
-       if n.Child(name, nil) != nil {
+       if child, err := n.Child(name, nil); err != nil {
+               return err
+       } else if child != nil {
                return os.ErrExist
        }
-       dn, ok := n.(*dirnode)
-       if !ok {
-               return ErrInvalidArgument
-       }
-       child := n.Child(name, func(inode) (child inode) {
-               child, err = dn.newDirnode(dn, name, perm, time.Now())
+
+       _, err = n.Child(name, func(inode) (repl inode, err error) {
+               repl, err = n.FS().newNode(name, perm|os.ModeDir, time.Now())
+               if err != nil {
+                       return
+               }
+               repl.SetParent(n, name)
                return
        })
-       if err != nil {
-               return err
-       } else if child == nil {
-               return ErrInvalidArgument
-       }
-       return nil
+       return err
 }
 
-func (fs *fileSystem) Stat(name string) (fi os.FileInfo, err error) {
-       node := rlookup(fs.inode, name)
-       if node == nil {
-               err = os.ErrNotExist
-       } else {
-               fi = node.FileInfo()
+func (fs *fileSystem) Stat(name string) (os.FileInfo, error) {
+       node, err := rlookup(fs.root, name)
+       if err != nil {
+               return nil, err
        }
-       return
+       return node.FileInfo(), nil
 }
 
 func (fs *fileSystem) Rename(oldname, newname string) error {
@@ -405,16 +487,35 @@ func (fs *fileSystem) Rename(oldname, newname string) error {
        }
        defer newdirf.Close()
 
-       // When acquiring locks on multiple nodes, all common
-       // ancestors must be locked first in order to avoid
-       // deadlock. This is assured by locking the path from root to
-       // newdir, then locking the path from root to olddir, skipping
-       // any already-locked nodes.
+       // TODO: If the nearest common ancestor ("nca") of olddirf and
+       // newdirf is on a different filesystem than fs, we should
+       // call nca.FS().Rename() instead of proceeding. Until then
+       // it's awkward for filesystems to implement their own Rename
+       // methods effectively: the only one that runs is the one on
+       // the root FileSystem exposed to the caller (webdav, fuse,
+       // etc).
+
+       // When acquiring locks on multiple inodes, avoid deadlock by
+       // locking the entire containing filesystem first.
+       cfs := olddirf.inode.FS()
+       cfs.locker().Lock()
+       defer cfs.locker().Unlock()
+
+       if cfs != newdirf.inode.FS() {
+               // Moving inodes across filesystems is not (yet)
+               // supported. Locking inodes from different
+               // filesystems could deadlock, so we must error out
+               // now.
+               return ErrInvalidArgument
+       }
+
+       // To ensure we can test reliably whether we're about to move
+       // a directory into itself, lock all potential common
+       // ancestors of olddir and newdir.
        needLock := []sync.Locker{}
-       for _, f := range []*filehandle{olddirf, newdirf} {
-               node := f.inode
+       for _, node := range []inode{olddirf.inode, newdirf.inode} {
                needLock = append(needLock, node)
-               for node.Parent() != node {
+               for node.Parent() != node && node.Parent().FS() == node.FS() {
                        node = node.Parent()
                        needLock = append(needLock, node)
                }
@@ -428,43 +529,31 @@ func (fs *fileSystem) Rename(oldname, newname string) error {
                }
        }
 
-       if _, ok := newdirf.inode.(*dirnode); !ok {
-               return ErrInvalidOperation
-       }
-
-       err = nil
-       olddirf.inode.Child(oldname, func(oldinode inode) inode {
+       _, err = olddirf.inode.Child(oldname, func(oldinode inode) (inode, error) {
                if oldinode == nil {
-                       err = os.ErrNotExist
-                       return nil
+                       return oldinode, os.ErrNotExist
+               }
+               if locked[oldinode] {
+                       // oldinode cannot become a descendant of itself.
+                       return oldinode, ErrInvalidArgument
+               }
+               if oldinode.FS() != cfs && newdirf.inode != olddirf.inode {
+                       // moving a mount point to a different parent
+                       // is not (yet) supported.
+                       return oldinode, ErrInvalidArgument
                }
-               newdirf.inode.Child(newname, func(existing inode) inode {
+               accepted, err := newdirf.inode.Child(newname, func(existing inode) (inode, error) {
                        if existing != nil && existing.IsDir() {
-                               err = ErrIsDirectory
-                               return existing
+                               return existing, ErrIsDirectory
                        }
-                       return oldinode
+                       return oldinode, nil
                })
                if err != nil {
-                       return oldinode
+                       // Leave oldinode in olddir.
+                       return oldinode, err
                }
-               oldinode.Lock()
-               defer oldinode.Unlock()
-               olddn := olddirf.inode.(*dirnode)
-               newdn := newdirf.inode.(*dirnode)
-               switch n := oldinode.(type) {
-               case *dirnode:
-                       n.parent = newdirf.inode
-                       n.treenode.fileinfo.name = newname
-               case *filenode:
-                       n.parent = newdn
-                       n.fileinfo.name = newname
-               default:
-                       panic(fmt.Sprintf("bad inode type %T", n))
-               }
-               olddn.treenode.fileinfo.modTime = time.Now()
-               newdn.treenode.fileinfo.modTime = time.Now()
-               return nil
+               accepted.SetParent(newdirf.inode, newname)
+               return nil, nil
        })
        return err
 }
@@ -483,27 +572,72 @@ func (fs *fileSystem) RemoveAll(name string) error {
        return err
 }
 
-func (fs *fileSystem) remove(name string, recursive bool) (err error) {
+func (fs *fileSystem) remove(name string, recursive bool) error {
        dirname, name := path.Split(name)
        if name == "" || name == "." || name == ".." {
                return ErrInvalidArgument
        }
-       dir := rlookup(fs, dirname)
-       if dir == nil {
-               return os.ErrNotExist
+       dir, err := rlookup(fs.root, dirname)
+       if err != nil {
+               return err
        }
        dir.Lock()
        defer dir.Unlock()
-       dir.Child(name, func(node inode) inode {
+       _, err = dir.Child(name, func(node inode) (inode, error) {
                if node == nil {
-                       err = os.ErrNotExist
-                       return nil
+                       return nil, os.ErrNotExist
                }
                if !recursive && node.IsDir() && node.Size() > 0 {
-                       err = ErrDirectoryNotEmpty
-                       return node
+                       return node, ErrDirectoryNotEmpty
                }
-               return nil
+               return nil, nil
        })
        return err
 }
+
+func (fs *fileSystem) Sync() error {
+       if syncer, ok := fs.root.(syncer); ok {
+               return syncer.Sync()
+       } else {
+               return ErrInvalidOperation
+       }
+}
+
+func (fs *fileSystem) Flush(string, bool) error {
+       log.Printf("TODO: flush fileSystem")
+       return ErrInvalidOperation
+}
+
+// rlookup (recursive lookup) returns the inode for the file/directory
+// with the given name (which may contain "/" separators). If no such
+// file/directory exists, the returned node is nil.
+func rlookup(start inode, path string) (node inode, err error) {
+       node = start
+       for _, name := range strings.Split(path, "/") {
+               if node.IsDir() {
+                       if name == "." || name == "" {
+                               continue
+                       }
+                       if name == ".." {
+                               node = node.Parent()
+                               continue
+                       }
+               }
+               node, err = func() (inode, error) {
+                       node.RLock()
+                       defer node.RUnlock()
+                       return node.Child(name, nil)
+               }()
+               if node == nil || err != nil {
+                       break
+               }
+       }
+       if node == nil && err == nil {
+               err = os.ErrNotExist
+       }
+       return
+}
+
+func permittedName(name string) bool {
+       return name != "" && name != "." && name != ".." && !strings.Contains(name, "/")
+}