13111: Propagate errors in Readdir().
[arvados.git] / sdk / go / arvados / fs_base.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package arvados
6
7 import (
8         "errors"
9         "fmt"
10         "io"
11         "log"
12         "net/http"
13         "os"
14         "path"
15         "strings"
16         "sync"
17         "time"
18 )
19
20 var (
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
31 )
32
33 // A File is an *os.File-like interface for reading and writing files
34 // in a FileSystem.
35 type File interface {
36         io.Reader
37         io.Writer
38         io.Closer
39         io.Seeker
40         Size() int64
41         Readdir(int) ([]os.FileInfo, error)
42         Stat() (os.FileInfo, error)
43         Truncate(int64) error
44         Sync() error
45 }
46
47 // A FileSystem is an http.Filesystem plus Stat() and support for
48 // opening writable files. All methods are safe to call from multiple
49 // goroutines.
50 type FileSystem interface {
51         http.FileSystem
52         fsBackend
53
54         rootnode() inode
55
56         // filesystem-wide lock: used by Rename() to prevent deadlock
57         // while locking multiple inodes.
58         locker() sync.Locker
59
60         // create a new node with nil parent.
61         newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error)
62
63         // analogous to os.Stat()
64         Stat(name string) (os.FileInfo, error)
65
66         // analogous to os.Create(): create/truncate a file and open it O_RDWR.
67         Create(name string) (File, error)
68
69         // Like os.OpenFile(): create or open a file or directory.
70         //
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
74         // exist.
75         //
76         // When creating a new item, perm&os.ModeDir determines
77         // whether it is a file or a directory.
78         //
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)
83
84         Mkdir(name string, perm os.FileMode) error
85         Remove(name string) error
86         RemoveAll(name string) error
87         Rename(oldname, newname string) error
88         Sync() error
89 }
90
91 type inode interface {
92         SetParent(parent inode, name string)
93         Parent() inode
94         FS() FileSystem
95         Read([]byte, filenodePtr) (int, filenodePtr, error)
96         Write([]byte, filenodePtr) (int, filenodePtr, error)
97         Truncate(int64) error
98         IsDir() bool
99         Readdir() ([]os.FileInfo, error)
100         Size() int64
101         FileInfo() os.FileInfo
102
103         // Child() performs lookups and updates of named child nodes.
104         //
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
108         // replace().
109         //
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
116         // parent's lock.
117         //
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.
121         //
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
125         // nil.
126         //
127         // Child() returns the child, if any, with the given name: if
128         // a child was added or changed, the new child is returned.
129         //
130         // Caller must have lock (or rlock if replace is nil).
131         Child(name string, replace func(inode) inode) inode
132
133         sync.Locker
134         RLock()
135         RUnlock()
136 }
137
138 type fileinfo struct {
139         name    string
140         mode    os.FileMode
141         size    int64
142         modTime time.Time
143 }
144
145 // Name implements os.FileInfo.
146 func (fi fileinfo) Name() string {
147         return fi.name
148 }
149
150 // ModTime implements os.FileInfo.
151 func (fi fileinfo) ModTime() time.Time {
152         return fi.modTime
153 }
154
155 // Mode implements os.FileInfo.
156 func (fi fileinfo) Mode() os.FileMode {
157         return fi.mode
158 }
159
160 // IsDir implements os.FileInfo.
161 func (fi fileinfo) IsDir() bool {
162         return fi.mode&os.ModeDir != 0
163 }
164
165 // Size implements os.FileInfo.
166 func (fi fileinfo) Size() int64 {
167         return fi.size
168 }
169
170 // Sys implements os.FileInfo.
171 func (fi fileinfo) Sys() interface{} {
172         return nil
173 }
174
175 type nullnode struct{}
176
177 func (*nullnode) Mkdir(string, os.FileMode) error {
178         return ErrInvalidOperation
179 }
180
181 func (*nullnode) Read([]byte, filenodePtr) (int, filenodePtr, error) {
182         return 0, filenodePtr{}, ErrInvalidOperation
183 }
184
185 func (*nullnode) Write([]byte, filenodePtr) (int, filenodePtr, error) {
186         return 0, filenodePtr{}, ErrInvalidOperation
187 }
188
189 func (*nullnode) Truncate(int64) error {
190         return ErrInvalidOperation
191 }
192
193 func (*nullnode) FileInfo() os.FileInfo {
194         return fileinfo{}
195 }
196
197 func (*nullnode) IsDir() bool {
198         return false
199 }
200
201 func (*nullnode) Readdir() ([]os.FileInfo, error) {
202         return nil, ErrInvalidOperation
203 }
204
205 func (*nullnode) Child(name string, replace func(inode) inode) inode {
206         return nil
207 }
208
209 type treenode struct {
210         fs       FileSystem
211         parent   inode
212         inodes   map[string]inode
213         fileinfo fileinfo
214         sync.RWMutex
215         nullnode
216 }
217
218 func (n *treenode) FS() FileSystem {
219         return n.fs
220 }
221
222 func (n *treenode) SetParent(p inode, name string) {
223         n.Lock()
224         defer n.Unlock()
225         n.parent = p
226         n.fileinfo.name = name
227 }
228
229 func (n *treenode) Parent() inode {
230         n.RLock()
231         defer n.RUnlock()
232         return n.parent
233 }
234
235 func (n *treenode) IsDir() bool {
236         return true
237 }
238
239 func (n *treenode) Child(name string, replace func(inode) inode) (child inode) {
240         // TODO: special treatment for "", ".", ".."
241         child = n.inodes[name]
242         if replace != nil {
243                 newchild := replace(child)
244                 if newchild == nil {
245                         delete(n.inodes, name)
246                 } else if newchild != child {
247                         n.inodes[name] = newchild
248                         n.fileinfo.modTime = time.Now()
249                         child = newchild
250                 }
251         }
252         return
253 }
254
255 func (n *treenode) Size() int64 {
256         return n.FileInfo().Size()
257 }
258
259 func (n *treenode) FileInfo() os.FileInfo {
260         n.Lock()
261         defer n.Unlock()
262         n.fileinfo.size = int64(len(n.inodes))
263         return n.fileinfo
264 }
265
266 func (n *treenode) Readdir() (fi []os.FileInfo, err error) {
267         n.RLock()
268         defer n.RUnlock()
269         fi = make([]os.FileInfo, 0, len(n.inodes))
270         for _, inode := range n.inodes {
271                 fi = append(fi, inode.FileInfo())
272         }
273         return
274 }
275
276 type fileSystem struct {
277         root inode
278         fsBackend
279         mutex sync.Mutex
280 }
281
282 func (fs *fileSystem) rootnode() inode {
283         return fs.root
284 }
285
286 func (fs *fileSystem) locker() sync.Locker {
287         return &fs.mutex
288 }
289
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)
293 }
294
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
298         }
299         dirname, name := path.Split(name)
300         parent := rlookup(fs.root, dirname)
301         if parent == nil {
302                 return nil, os.ErrNotExist
303         }
304         var readable, writable bool
305         switch flag & (os.O_RDWR | os.O_RDONLY | os.O_WRONLY) {
306         case os.O_RDWR:
307                 readable = true
308                 writable = true
309         case os.O_RDONLY:
310                 readable = true
311         case os.O_WRONLY:
312                 writable = true
313         default:
314                 return nil, fmt.Errorf("invalid flags 0x%x", flag)
315         }
316         if !writable && parent.IsDir() {
317                 // A directory can be opened via "foo/", "foo/.", or
318                 // "foo/..".
319                 switch name {
320                 case ".", "":
321                         return &filehandle{inode: parent}, nil
322                 case "..":
323                         return &filehandle{inode: parent.Parent()}, nil
324                 }
325         }
326         createMode := flag&os.O_CREATE != 0
327         if createMode {
328                 parent.Lock()
329                 defer parent.Unlock()
330         } else {
331                 parent.RLock()
332                 defer parent.RUnlock()
333         }
334         n := parent.Child(name, nil)
335         if n == nil {
336                 if !createMode {
337                         return nil, os.ErrNotExist
338                 }
339                 var err error
340                 n = parent.Child(name, func(inode) inode {
341                         n, err = parent.FS().newNode(name, perm|0755, time.Now())
342                         n.SetParent(parent, name)
343                         return n
344                 })
345                 if err != nil {
346                         return nil, err
347                 } else if n == nil {
348                         // parent rejected new child
349                         return nil, ErrInvalidOperation
350                 }
351         } else if flag&os.O_EXCL != 0 {
352                 return nil, ErrFileExists
353         } else if flag&os.O_TRUNC != 0 {
354                 if !writable {
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 {
359                         return nil, err
360                 }
361         }
362         return &filehandle{
363                 inode:    n,
364                 append:   flag&os.O_APPEND != 0,
365                 readable: readable,
366                 writable: writable,
367         }, nil
368 }
369
370 func (fs *fileSystem) Open(name string) (http.File, error) {
371         return fs.OpenFile(name, os.O_RDONLY, 0)
372 }
373
374 func (fs *fileSystem) Create(name string) (File, error) {
375         return fs.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0)
376 }
377
378 func (fs *fileSystem) Mkdir(name string, perm os.FileMode) (err error) {
379         dirname, name := path.Split(name)
380         n := rlookup(fs.root, dirname)
381         if n == nil {
382                 return os.ErrNotExist
383         }
384         n.Lock()
385         defer n.Unlock()
386         if n.Child(name, nil) != nil {
387                 return os.ErrExist
388         }
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)
392                 return
393         })
394         if err != nil {
395                 return err
396         } else if child == nil {
397                 return ErrInvalidArgument
398         }
399         return nil
400 }
401
402 func (fs *fileSystem) Stat(name string) (fi os.FileInfo, err error) {
403         node := rlookup(fs.root, name)
404         if node == nil {
405                 err = os.ErrNotExist
406         } else {
407                 fi = node.FileInfo()
408         }
409         return
410 }
411
412 func (fs *fileSystem) Rename(oldname, newname string) error {
413         olddir, oldname := path.Split(oldname)
414         if oldname == "" || oldname == "." || oldname == ".." {
415                 return ErrInvalidArgument
416         }
417         olddirf, err := fs.openFile(olddir+".", os.O_RDONLY, 0)
418         if err != nil {
419                 return fmt.Errorf("%q: %s", olddir, err)
420         }
421         defer olddirf.Close()
422
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")
428                 newname = oldname
429         }
430         newdirf, err := fs.openFile(newdir+".", os.O_RDONLY, 0)
431         if err != nil {
432                 return fmt.Errorf("%q: %s", newdir, err)
433         }
434         defer newdirf.Close()
435
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,
442         // etc).
443
444         // When acquiring locks on multiple inodes, avoid deadlock by
445         // locking the entire containing filesystem first.
446         cfs := olddirf.inode.FS()
447         cfs.locker().Lock()
448         defer cfs.locker().Unlock()
449
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
454                 // now.
455                 return ErrInvalidArgument
456         }
457
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() {
465                         node = node.Parent()
466                         needLock = append(needLock, node)
467                 }
468         }
469         locked := map[sync.Locker]bool{}
470         for i := len(needLock) - 1; i >= 0; i-- {
471                 if n := needLock[i]; !locked[n] {
472                         n.Lock()
473                         defer n.Unlock()
474                         locked[n] = true
475                 }
476         }
477
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 {
482                 err = nil
483                 if oldinode == nil {
484                         err = os.ErrNotExist
485                         return nil
486                 }
487                 if locked[oldinode] {
488                         // oldinode cannot become a descendant of itself.
489                         err = ErrInvalidArgument
490                         return oldinode
491                 }
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
496                         return oldinode
497                 }
498                 accepted := newdirf.inode.Child(newname, func(existing inode) inode {
499                         if existing != nil && existing.IsDir() {
500                                 err = ErrIsDirectory
501                                 return existing
502                         }
503                         return oldinode
504                 })
505                 if accepted != oldinode {
506                         if err == nil {
507                                 // newdirf didn't accept oldinode.
508                                 err = ErrInvalidArgument
509                         }
510                         // Leave oldinode in olddir.
511                         return oldinode
512                 }
513                 accepted.SetParent(newdirf.inode, newname)
514                 return nil
515         })
516         return err
517 }
518
519 func (fs *fileSystem) Remove(name string) error {
520         return fs.remove(strings.TrimRight(name, "/"), false)
521 }
522
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)
528                 err = nil
529         }
530         return err
531 }
532
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
537         }
538         dir := rlookup(fs.root, dirname)
539         if dir == nil {
540                 return os.ErrNotExist
541         }
542         dir.Lock()
543         defer dir.Unlock()
544         dir.Child(name, func(node inode) inode {
545                 if node == nil {
546                         err = os.ErrNotExist
547                         return nil
548                 }
549                 if !recursive && node.IsDir() && node.Size() > 0 {
550                         err = ErrDirectoryNotEmpty
551                         return node
552                 }
553                 return nil
554         })
555         return err
556 }
557
558 func (fs *fileSystem) Sync() error {
559         log.Printf("TODO: sync fileSystem")
560         return ErrInvalidOperation
561 }
562
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) {
567         node = start
568         for _, name := range strings.Split(path, "/") {
569                 if node == nil {
570                         break
571                 }
572                 if node.IsDir() {
573                         if name == "." || name == "" {
574                                 continue
575                         }
576                         if name == ".." {
577                                 node = node.Parent()
578                                 continue
579                         }
580                 }
581                 node = func() inode {
582                         node.RLock()
583                         defer node.RUnlock()
584                         return node.Child(name, nil)
585                 }()
586         }
587         return
588 }