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 {
55 newDirnode(parent inode, name string, perm os.FileMode, modTime time.Time) (node inode, err error)
56 newFilenode(parent inode, name string, perm os.FileMode, modTime time.Time) (node inode, err error)
58 // analogous to os.Stat()
59 Stat(name string) (os.FileInfo, error)
61 // analogous to os.Create(): create/truncate a file and open it O_RDWR.
62 Create(name string) (File, error)
64 // Like os.OpenFile(): create or open a file or directory.
66 // If flag&os.O_EXCL==0, it opens an existing file or
67 // directory if one exists. If flag&os.O_CREATE!=0, it creates
68 // a new empty file or directory if one does not already
71 // When creating a new item, perm&os.ModeDir determines
72 // whether it is a file or a directory.
74 // A file can be opened multiple times and used concurrently
75 // from multiple goroutines. However, each File object should
76 // be used by only one goroutine at a time.
77 OpenFile(name string, flag int, perm os.FileMode) (File, error)
79 Mkdir(name string, perm os.FileMode) error
80 Remove(name string) error
81 RemoveAll(name string) error
82 Rename(oldname, newname string) error
86 type inode interface {
90 Read([]byte, filenodePtr) (int, filenodePtr, error)
91 Write([]byte, filenodePtr) (int, filenodePtr, error)
94 Readdir() []os.FileInfo
96 FileInfo() os.FileInfo
98 // Child() performs lookups and updates of named child nodes.
100 // If replace is non-nil, Child calls replace(x) where x is
101 // the current child inode with the given name. If possible,
102 // the child inode is replaced with the one returned by
105 // Nil represents "no child". replace(nil) signifies that no
106 // child with this name exists yet. If replace() returns nil,
107 // the existing child should be deleted if possible.
109 // An implementation of Child() is permitted to ignore
110 // replace() or its return value. For example, a regular file
111 // inode does not have children, so Child() always returns
114 // Child() returns the child, if any, with the given name: if
115 // a child was added or changed, the new child is returned.
117 // Caller must have lock (or rlock if replace is nil).
118 Child(name string, replace func(inode) inode) inode
125 type fileinfo struct {
132 // Name implements os.FileInfo.
133 func (fi fileinfo) Name() string {
137 // ModTime implements os.FileInfo.
138 func (fi fileinfo) ModTime() time.Time {
142 // Mode implements os.FileInfo.
143 func (fi fileinfo) Mode() os.FileMode {
147 // IsDir implements os.FileInfo.
148 func (fi fileinfo) IsDir() bool {
149 return fi.mode&os.ModeDir != 0
152 // Size implements os.FileInfo.
153 func (fi fileinfo) Size() int64 {
157 // Sys implements os.FileInfo.
158 func (fi fileinfo) Sys() interface{} {
162 type nullnode struct{}
164 func (*nullnode) Mkdir(string, os.FileMode) error {
165 return ErrInvalidOperation
168 func (*nullnode) Read([]byte, filenodePtr) (int, filenodePtr, error) {
169 return 0, filenodePtr{}, ErrInvalidOperation
172 func (*nullnode) Write([]byte, filenodePtr) (int, filenodePtr, error) {
173 return 0, filenodePtr{}, ErrInvalidOperation
176 func (*nullnode) Truncate(int64) error {
177 return ErrInvalidOperation
180 func (*nullnode) FileInfo() os.FileInfo {
184 func (*nullnode) IsDir() bool {
188 func (*nullnode) Readdir() []os.FileInfo {
192 func (*nullnode) Child(name string, replace func(inode) inode) inode {
196 type treenode struct {
199 inodes map[string]inode
205 func (n *treenode) FS() FileSystem {
209 func (n *treenode) SetParent(p inode) {
215 func (n *treenode) Parent() inode {
221 func (n *treenode) IsDir() bool {
225 func (n *treenode) Child(name string, replace func(inode) inode) (child inode) {
226 // TODO: special treatment for "", ".", ".."
227 child = n.inodes[name]
229 newchild := replace(child)
231 delete(n.inodes, name)
232 } else if newchild != child {
233 n.inodes[name] = newchild
234 newchild.SetParent(n)
241 func (n *treenode) Size() int64 {
242 return n.FileInfo().Size()
245 func (n *treenode) FileInfo() os.FileInfo {
248 n.fileinfo.size = int64(len(n.inodes))
252 func (n *treenode) Readdir() (fi []os.FileInfo) {
255 fi = make([]os.FileInfo, 0, len(n.inodes))
256 for _, inode := range n.inodes {
257 fi = append(fi, inode.FileInfo())
262 type fileSystem struct {
267 func (fs *fileSystem) rootnode() inode {
271 // OpenFile is analogous to os.OpenFile().
272 func (fs *fileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
273 return fs.openFile(name, flag, perm)
276 func (fs *fileSystem) openFile(name string, flag int, perm os.FileMode) (*filehandle, error) {
277 if flag&os.O_SYNC != 0 {
278 return nil, ErrSyncNotSupported
280 dirname, name := path.Split(name)
281 parent := rlookup(fs.root, dirname)
283 return nil, os.ErrNotExist
285 var readable, writable bool
286 switch flag & (os.O_RDWR | os.O_RDONLY | os.O_WRONLY) {
295 return nil, fmt.Errorf("invalid flags 0x%x", flag)
297 if !writable && parent.IsDir() {
298 // A directory can be opened via "foo/", "foo/.", or
302 return &filehandle{inode: parent}, nil
304 return &filehandle{inode: parent.Parent()}, nil
307 createMode := flag&os.O_CREATE != 0
310 defer parent.Unlock()
313 defer parent.RUnlock()
315 n := parent.Child(name, nil)
318 return nil, os.ErrNotExist
321 n = parent.Child(name, func(inode) inode {
323 n, err = parent.FS().newDirnode(parent, name, perm|0755, time.Now())
325 n, err = parent.FS().newFilenode(parent, name, perm|0755, time.Now())
332 // parent rejected new child
333 return nil, ErrInvalidOperation
335 } else if flag&os.O_EXCL != 0 {
336 return nil, ErrFileExists
337 } else if flag&os.O_TRUNC != 0 {
339 return nil, fmt.Errorf("invalid flag O_TRUNC in read-only mode")
340 } else if n.IsDir() {
341 return nil, fmt.Errorf("invalid flag O_TRUNC when opening directory")
342 } else if err := n.Truncate(0); err != nil {
348 append: flag&os.O_APPEND != 0,
354 func (fs *fileSystem) Open(name string) (http.File, error) {
355 return fs.OpenFile(name, os.O_RDONLY, 0)
358 func (fs *fileSystem) Create(name string) (File, error) {
359 return fs.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0)
362 func (fs *fileSystem) Mkdir(name string, perm os.FileMode) (err error) {
363 dirname, name := path.Split(name)
364 n := rlookup(fs.root, dirname)
366 return os.ErrNotExist
370 if n.Child(name, nil) != nil {
373 child := n.Child(name, func(inode) (child inode) {
374 child, err = n.FS().newDirnode(n, name, perm, time.Now())
379 } else if child == nil {
380 return ErrInvalidArgument
385 func (fs *fileSystem) Stat(name string) (fi os.FileInfo, err error) {
386 node := rlookup(fs.root, name)
395 func (fs *fileSystem) Rename(oldname, newname string) error {
396 olddir, oldname := path.Split(oldname)
397 if oldname == "" || oldname == "." || oldname == ".." {
398 return ErrInvalidArgument
400 olddirf, err := fs.openFile(olddir+".", os.O_RDONLY, 0)
402 return fmt.Errorf("%q: %s", olddir, err)
404 defer olddirf.Close()
406 newdir, newname := path.Split(newname)
407 if newname == "." || newname == ".." {
408 return ErrInvalidArgument
409 } else if newname == "" {
410 // Rename("a/b", "c/") means Rename("a/b", "c/b")
413 newdirf, err := fs.openFile(newdir+".", os.O_RDONLY, 0)
415 return fmt.Errorf("%q: %s", newdir, err)
417 defer newdirf.Close()
419 // When acquiring locks on multiple nodes, all common
420 // ancestors must be locked first in order to avoid
421 // deadlock. This is assured by locking the path from
422 // filesystem root to newdir, then locking the path from
423 // filesystem root to olddir, skipping any already-locked
425 needLock := []sync.Locker{}
426 for _, node := range []inode{olddirf.inode, newdirf.inode} {
427 needLock = append(needLock, node)
428 for node.Parent() != node && node.Parent().FS() == node.FS() {
430 needLock = append(needLock, node)
433 locked := map[sync.Locker]bool{}
434 for i := len(needLock) - 1; i >= 0; i-- {
435 if n := needLock[i]; !locked[n] {
443 olddirf.inode.Child(oldname, func(oldinode inode) inode {
448 newdirf.inode.Child(newname, func(existing inode) inode {
449 if existing != nil && existing.IsDir() {
459 defer oldinode.Unlock()
460 switch n := oldinode.(type) {
462 n.parent = newdirf.inode
463 n.fileinfo.name = newname
465 n.parent = newdirf.inode
466 n.fileinfo.name = newname
468 panic(fmt.Sprintf("bad inode type %T", n))
470 //TODO: olddirf.setModTime(time.Now())
471 //TODO: newdirf.setModTime(time.Now())
477 func (fs *fileSystem) Remove(name string) error {
478 return fs.remove(strings.TrimRight(name, "/"), false)
481 func (fs *fileSystem) RemoveAll(name string) error {
482 err := fs.remove(strings.TrimRight(name, "/"), true)
483 if os.IsNotExist(err) {
484 // "If the path does not exist, RemoveAll returns
485 // nil." (see "os" pkg)
491 func (fs *fileSystem) remove(name string, recursive bool) (err error) {
492 dirname, name := path.Split(name)
493 if name == "" || name == "." || name == ".." {
494 return ErrInvalidArgument
496 dir := rlookup(fs.root, dirname)
498 return os.ErrNotExist
502 dir.Child(name, func(node inode) inode {
507 if !recursive && node.IsDir() && node.Size() > 0 {
508 err = ErrDirectoryNotEmpty
516 func (fs *fileSystem) Sync() error {
517 log.Printf("TODO: sync fileSystem")
518 return ErrInvalidOperation
521 // rlookup (recursive lookup) returns the inode for the file/directory
522 // with the given name (which may contain "/" separators). If no such
523 // file/directory exists, the returned node is nil.
524 func rlookup(start inode, path string) (node inode) {
526 for _, name := range strings.Split(path, "/") {
531 if name == "." || name == "" {
539 node = func() inode {
542 return node.Child(name, nil)