18600: Add Snapshot and Splice methods.
[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         ErrNotADirectory     = errors.New("not a directory")
31         ErrPermission        = os.ErrPermission
32         DebugLocksPanicMode  = false
33 )
34
35 type syncer interface {
36         Sync() error
37 }
38
39 func debugPanicIfNotLocked(l sync.Locker, writing bool) {
40         if !DebugLocksPanicMode {
41                 return
42         }
43         race := false
44         if rl, ok := l.(interface {
45                 RLock()
46                 RUnlock()
47         }); ok && writing {
48                 go func() {
49                         // Fail if we can grab the read lock during an
50                         // operation that purportedly has write lock.
51                         rl.RLock()
52                         race = true
53                         rl.RUnlock()
54                 }()
55         } else {
56                 go func() {
57                         l.Lock()
58                         race = true
59                         l.Unlock()
60                 }()
61         }
62         time.Sleep(100)
63         if race {
64                 panic("bug: caller-must-have-lock func called, but nobody has lock")
65         }
66 }
67
68 // A File is an *os.File-like interface for reading and writing files
69 // in a FileSystem.
70 type File interface {
71         io.Reader
72         io.Writer
73         io.Closer
74         io.Seeker
75         Size() int64
76         Readdir(int) ([]os.FileInfo, error)
77         Stat() (os.FileInfo, error)
78         Truncate(int64) error
79         Sync() error
80         // Create a snapshot of a file or directory tree, which can
81         // then be spliced onto a different path or a different
82         // collection.
83         Snapshot() (*Subtree, error)
84         // Replace this file or directory with the given snapshot. It
85         // is an error to replace a directory with a file. If snapshot
86         // is (or might be) a directory, remove the directory, create
87         // a file with the same name, and splice the file.
88         Splice(snapshot *Subtree) error
89 }
90
91 // A Subtree is a detached part of a filesystem tree that can be
92 // spliced into a filesystem via (File)Splice().
93 type Subtree struct {
94         inode inode
95 }
96
97 // A FileSystem is an http.Filesystem plus Stat() and support for
98 // opening writable files. All methods are safe to call from multiple
99 // goroutines.
100 type FileSystem interface {
101         http.FileSystem
102         fsBackend
103
104         rootnode() inode
105
106         // filesystem-wide lock: used by Rename() to prevent deadlock
107         // while locking multiple inodes.
108         locker() sync.Locker
109
110         // throttle for limiting concurrent background writers
111         throttle() *throttle
112
113         // create a new node with nil parent.
114         newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error)
115
116         // analogous to os.Stat()
117         Stat(name string) (os.FileInfo, error)
118
119         // analogous to os.Create(): create/truncate a file and open it O_RDWR.
120         Create(name string) (File, error)
121
122         // Like os.OpenFile(): create or open a file or directory.
123         //
124         // If flag&os.O_EXCL==0, it opens an existing file or
125         // directory if one exists. If flag&os.O_CREATE!=0, it creates
126         // a new empty file or directory if one does not already
127         // exist.
128         //
129         // When creating a new item, perm&os.ModeDir determines
130         // whether it is a file or a directory.
131         //
132         // A file can be opened multiple times and used concurrently
133         // from multiple goroutines. However, each File object should
134         // be used by only one goroutine at a time.
135         OpenFile(name string, flag int, perm os.FileMode) (File, error)
136
137         Mkdir(name string, perm os.FileMode) error
138         Remove(name string) error
139         RemoveAll(name string) error
140         Rename(oldname, newname string) error
141
142         // Write buffered data from memory to storage, returning when
143         // all updates have been saved to persistent storage.
144         Sync() error
145
146         // Write buffered data from memory to storage, but don't wait
147         // for all writes to finish before returning. If shortBlocks
148         // is true, flush everything; otherwise, if there's less than
149         // a full block of buffered data at the end of a stream, leave
150         // it buffered in memory in case more data can be appended. If
151         // path is "", flush all dirs/streams; otherwise, flush only
152         // the specified dir/stream.
153         Flush(path string, shortBlocks bool) error
154
155         // Estimate current memory usage.
156         MemorySize() int64
157 }
158
159 type inode interface {
160         SetParent(parent inode, name string)
161         Parent() inode
162         FS() FileSystem
163         Read([]byte, filenodePtr) (int, filenodePtr, error)
164         Write([]byte, filenodePtr) (int, filenodePtr, error)
165         Truncate(int64) error
166         IsDir() bool
167         Readdir() ([]os.FileInfo, error)
168         Size() int64
169         FileInfo() os.FileInfo
170         // Create a snapshot of this node and its descendants.
171         Snapshot() (inode, error)
172         // Replace this node with a copy of the provided snapshot.
173         // Caller may provide the same snapshot to multiple Splice
174         // calls, but must not modify the the snapshot concurrently.
175         Splice(inode) error
176
177         // Child() performs lookups and updates of named child nodes.
178         //
179         // (The term "child" here is used strictly. This means name is
180         // not "." or "..", and name does not contain "/".)
181         //
182         // If replace is non-nil, Child calls replace(x) where x is
183         // the current child inode with the given name. If possible,
184         // the child inode is replaced with the one returned by
185         // replace().
186         //
187         // If replace(x) returns an inode (besides x or nil) that is
188         // subsequently returned by Child(), then Child()'s caller
189         // must ensure the new child's name and parent are set/updated
190         // to Child()'s name argument and its receiver respectively.
191         // This is not necessarily done before replace(x) returns, but
192         // it must be done before Child()'s caller releases the
193         // parent's lock.
194         //
195         // Nil represents "no child". replace(nil) signifies that no
196         // child with this name exists yet. If replace() returns nil,
197         // the existing child should be deleted if possible.
198         //
199         // An implementation of Child() is permitted to ignore
200         // replace() or its return value. For example, a regular file
201         // inode does not have children, so Child() always returns
202         // nil.
203         //
204         // Child() returns the child, if any, with the given name: if
205         // a child was added or changed, the new child is returned.
206         //
207         // Caller must have lock (or rlock if replace is nil).
208         Child(name string, replace func(inode) (inode, error)) (inode, error)
209
210         sync.Locker
211         RLock()
212         RUnlock()
213         MemorySize() int64
214 }
215
216 type fileinfo struct {
217         name    string
218         mode    os.FileMode
219         size    int64
220         modTime time.Time
221 }
222
223 // Name implements os.FileInfo.
224 func (fi fileinfo) Name() string {
225         return fi.name
226 }
227
228 // ModTime implements os.FileInfo.
229 func (fi fileinfo) ModTime() time.Time {
230         return fi.modTime
231 }
232
233 // Mode implements os.FileInfo.
234 func (fi fileinfo) Mode() os.FileMode {
235         return fi.mode
236 }
237
238 // IsDir implements os.FileInfo.
239 func (fi fileinfo) IsDir() bool {
240         return fi.mode&os.ModeDir != 0
241 }
242
243 // Size implements os.FileInfo.
244 func (fi fileinfo) Size() int64 {
245         return fi.size
246 }
247
248 // Sys implements os.FileInfo.
249 func (fi fileinfo) Sys() interface{} {
250         return nil
251 }
252
253 type nullnode struct{}
254
255 func (*nullnode) Mkdir(string, os.FileMode) error {
256         return ErrInvalidOperation
257 }
258
259 func (*nullnode) Read([]byte, filenodePtr) (int, filenodePtr, error) {
260         return 0, filenodePtr{}, ErrInvalidOperation
261 }
262
263 func (*nullnode) Write([]byte, filenodePtr) (int, filenodePtr, error) {
264         return 0, filenodePtr{}, ErrInvalidOperation
265 }
266
267 func (*nullnode) Truncate(int64) error {
268         return ErrInvalidOperation
269 }
270
271 func (*nullnode) FileInfo() os.FileInfo {
272         return fileinfo{}
273 }
274
275 func (*nullnode) IsDir() bool {
276         return false
277 }
278
279 func (*nullnode) Readdir() ([]os.FileInfo, error) {
280         return nil, ErrInvalidOperation
281 }
282
283 func (*nullnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
284         return nil, ErrNotADirectory
285 }
286
287 func (*nullnode) MemorySize() int64 {
288         // Types that embed nullnode should report their own size, but
289         // if they don't, we at least report a non-zero size to ensure
290         // a large tree doesn't get reported as 0 bytes.
291         return 64
292 }
293
294 func (*nullnode) Snapshot() (inode, error) {
295         return nil, ErrInvalidOperation
296 }
297
298 func (*nullnode) Splice(inode) error {
299         return ErrInvalidOperation
300 }
301
302 type treenode struct {
303         fs       FileSystem
304         parent   inode
305         inodes   map[string]inode
306         fileinfo fileinfo
307         sync.RWMutex
308         nullnode
309 }
310
311 func (n *treenode) FS() FileSystem {
312         return n.fs
313 }
314
315 func (n *treenode) SetParent(p inode, name string) {
316         n.Lock()
317         defer n.Unlock()
318         n.parent = p
319         n.fileinfo.name = name
320 }
321
322 func (n *treenode) Parent() inode {
323         n.RLock()
324         defer n.RUnlock()
325         return n.parent
326 }
327
328 func (n *treenode) IsDir() bool {
329         return true
330 }
331
332 func (n *treenode) Child(name string, replace func(inode) (inode, error)) (child inode, err error) {
333         debugPanicIfNotLocked(n, false)
334         child = n.inodes[name]
335         if name == "" || name == "." || name == ".." {
336                 err = ErrInvalidArgument
337                 return
338         }
339         if replace == nil {
340                 return
341         }
342         newchild, err := replace(child)
343         if err != nil {
344                 return
345         }
346         if newchild == nil {
347                 debugPanicIfNotLocked(n, true)
348                 delete(n.inodes, name)
349         } else if newchild != child {
350                 debugPanicIfNotLocked(n, true)
351                 n.inodes[name] = newchild
352                 n.fileinfo.modTime = time.Now()
353                 child = newchild
354         }
355         return
356 }
357
358 func (n *treenode) Size() int64 {
359         return n.FileInfo().Size()
360 }
361
362 func (n *treenode) FileInfo() os.FileInfo {
363         n.Lock()
364         defer n.Unlock()
365         n.fileinfo.size = int64(len(n.inodes))
366         return n.fileinfo
367 }
368
369 func (n *treenode) Readdir() (fi []os.FileInfo, err error) {
370         n.RLock()
371         defer n.RUnlock()
372         fi = make([]os.FileInfo, 0, len(n.inodes))
373         for _, inode := range n.inodes {
374                 fi = append(fi, inode.FileInfo())
375         }
376         return
377 }
378
379 func (n *treenode) Sync() error {
380         n.RLock()
381         defer n.RUnlock()
382         for _, inode := range n.inodes {
383                 syncer, ok := inode.(syncer)
384                 if !ok {
385                         return ErrInvalidOperation
386                 }
387                 err := syncer.Sync()
388                 if err != nil {
389                         return err
390                 }
391         }
392         return nil
393 }
394
395 func (n *treenode) MemorySize() (size int64) {
396         n.RLock()
397         defer n.RUnlock()
398         debugPanicIfNotLocked(n, false)
399         for _, inode := range n.inodes {
400                 size += inode.MemorySize()
401         }
402         return
403 }
404
405 type fileSystem struct {
406         root inode
407         fsBackend
408         mutex sync.Mutex
409         thr   *throttle
410 }
411
412 func (fs *fileSystem) rootnode() inode {
413         return fs.root
414 }
415
416 func (fs *fileSystem) throttle() *throttle {
417         return fs.thr
418 }
419
420 func (fs *fileSystem) locker() sync.Locker {
421         return &fs.mutex
422 }
423
424 // OpenFile is analogous to os.OpenFile().
425 func (fs *fileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
426         return fs.openFile(name, flag, perm)
427 }
428
429 func (fs *fileSystem) openFile(name string, flag int, perm os.FileMode) (*filehandle, error) {
430         if flag&os.O_SYNC != 0 {
431                 return nil, ErrSyncNotSupported
432         }
433         dirname, name := path.Split(name)
434         parent, err := rlookup(fs.root, dirname)
435         if err != nil {
436                 return nil, err
437         }
438         var readable, writable bool
439         switch flag & (os.O_RDWR | os.O_RDONLY | os.O_WRONLY) {
440         case os.O_RDWR:
441                 readable = true
442                 writable = true
443         case os.O_RDONLY:
444                 readable = true
445         case os.O_WRONLY:
446                 writable = true
447         default:
448                 return nil, fmt.Errorf("invalid flags 0x%x", flag)
449         }
450         if !writable && parent.IsDir() {
451                 // A directory can be opened via "foo/", "foo/.", or
452                 // "foo/..".
453                 switch name {
454                 case ".", "":
455                         return &filehandle{inode: parent}, nil
456                 case "..":
457                         return &filehandle{inode: parent.Parent()}, nil
458                 }
459         }
460         createMode := flag&os.O_CREATE != 0
461         // We always need to take Lock() here, not just RLock(). Even
462         // if we know we won't be creating a file, parent might be a
463         // lookupnode, which sometimes populates its inodes map during
464         // a Child() call.
465         parent.Lock()
466         defer parent.Unlock()
467         n, err := parent.Child(name, nil)
468         if err != nil {
469                 return nil, err
470         } else if n == nil {
471                 if !createMode {
472                         return nil, os.ErrNotExist
473                 }
474                 n, err = parent.Child(name, func(inode) (repl inode, err error) {
475                         repl, err = parent.FS().newNode(name, perm|0755, time.Now())
476                         if err != nil {
477                                 return
478                         }
479                         repl.SetParent(parent, name)
480                         return
481                 })
482                 if err != nil {
483                         return nil, err
484                 } else if n == nil {
485                         // Parent rejected new child, but returned no error
486                         return nil, ErrInvalidArgument
487                 }
488         } else if flag&os.O_EXCL != 0 {
489                 return nil, ErrFileExists
490         } else if flag&os.O_TRUNC != 0 {
491                 if !writable {
492                         return nil, fmt.Errorf("invalid flag O_TRUNC in read-only mode")
493                 } else if n.IsDir() {
494                         return nil, fmt.Errorf("invalid flag O_TRUNC when opening directory")
495                 } else if err := n.Truncate(0); err != nil {
496                         return nil, err
497                 }
498         }
499         return &filehandle{
500                 inode:    n,
501                 append:   flag&os.O_APPEND != 0,
502                 readable: readable,
503                 writable: writable,
504         }, nil
505 }
506
507 func (fs *fileSystem) Open(name string) (http.File, error) {
508         return fs.OpenFile(name, os.O_RDONLY, 0)
509 }
510
511 func (fs *fileSystem) Create(name string) (File, error) {
512         return fs.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0)
513 }
514
515 func (fs *fileSystem) Mkdir(name string, perm os.FileMode) error {
516         dirname, name := path.Split(name)
517         n, err := rlookup(fs.root, dirname)
518         if err != nil {
519                 return err
520         }
521         n.Lock()
522         defer n.Unlock()
523         if child, err := n.Child(name, nil); err != nil {
524                 return err
525         } else if child != nil {
526                 return os.ErrExist
527         }
528
529         _, err = n.Child(name, func(inode) (repl inode, err error) {
530                 repl, err = n.FS().newNode(name, perm|os.ModeDir, time.Now())
531                 if err != nil {
532                         return
533                 }
534                 repl.SetParent(n, name)
535                 return
536         })
537         return err
538 }
539
540 func (fs *fileSystem) Stat(name string) (os.FileInfo, error) {
541         node, err := rlookup(fs.root, name)
542         if err != nil {
543                 return nil, err
544         }
545         return node.FileInfo(), nil
546 }
547
548 func (fs *fileSystem) Rename(oldname, newname string) error {
549         olddir, oldname := path.Split(oldname)
550         if oldname == "" || oldname == "." || oldname == ".." {
551                 return ErrInvalidArgument
552         }
553         olddirf, err := fs.openFile(olddir+".", os.O_RDONLY, 0)
554         if err != nil {
555                 return fmt.Errorf("%q: %s", olddir, err)
556         }
557         defer olddirf.Close()
558
559         newdir, newname := path.Split(newname)
560         if newname == "." || newname == ".." {
561                 return ErrInvalidArgument
562         } else if newname == "" {
563                 // Rename("a/b", "c/") means Rename("a/b", "c/b")
564                 newname = oldname
565         }
566         newdirf, err := fs.openFile(newdir+".", os.O_RDONLY, 0)
567         if err != nil {
568                 return fmt.Errorf("%q: %s", newdir, err)
569         }
570         defer newdirf.Close()
571
572         // TODO: If the nearest common ancestor ("nca") of olddirf and
573         // newdirf is on a different filesystem than fs, we should
574         // call nca.FS().Rename() instead of proceeding. Until then
575         // it's awkward for filesystems to implement their own Rename
576         // methods effectively: the only one that runs is the one on
577         // the root FileSystem exposed to the caller (webdav, fuse,
578         // etc).
579
580         // When acquiring locks on multiple inodes, avoid deadlock by
581         // locking the entire containing filesystem first.
582         cfs := olddirf.inode.FS()
583         cfs.locker().Lock()
584         defer cfs.locker().Unlock()
585
586         if cfs != newdirf.inode.FS() {
587                 // Moving inodes across filesystems is not (yet)
588                 // supported. Locking inodes from different
589                 // filesystems could deadlock, so we must error out
590                 // now.
591                 return ErrInvalidArgument
592         }
593
594         // To ensure we can test reliably whether we're about to move
595         // a directory into itself, lock all potential common
596         // ancestors of olddir and newdir.
597         needLock := []sync.Locker{}
598         for _, node := range []inode{olddirf.inode, newdirf.inode} {
599                 needLock = append(needLock, node)
600                 for node.Parent() != node && node.Parent().FS() == node.FS() {
601                         node = node.Parent()
602                         needLock = append(needLock, node)
603                 }
604         }
605         locked := map[sync.Locker]bool{}
606         for i := len(needLock) - 1; i >= 0; i-- {
607                 if n := needLock[i]; !locked[n] {
608                         n.Lock()
609                         defer n.Unlock()
610                         locked[n] = true
611                 }
612         }
613
614         _, err = olddirf.inode.Child(oldname, func(oldinode inode) (inode, error) {
615                 if oldinode == nil {
616                         return oldinode, os.ErrNotExist
617                 }
618                 if locked[oldinode] {
619                         // oldinode cannot become a descendant of itself.
620                         return oldinode, ErrInvalidArgument
621                 }
622                 if oldinode.FS() != cfs && newdirf.inode != olddirf.inode {
623                         // moving a mount point to a different parent
624                         // is not (yet) supported.
625                         return oldinode, ErrInvalidArgument
626                 }
627                 accepted, err := newdirf.inode.Child(newname, func(existing inode) (inode, error) {
628                         if existing != nil && existing.IsDir() {
629                                 return existing, ErrIsDirectory
630                         }
631                         return oldinode, nil
632                 })
633                 if err != nil {
634                         // Leave oldinode in olddir.
635                         return oldinode, err
636                 }
637                 accepted.SetParent(newdirf.inode, newname)
638                 return nil, nil
639         })
640         return err
641 }
642
643 func (fs *fileSystem) Remove(name string) error {
644         return fs.remove(strings.TrimRight(name, "/"), false)
645 }
646
647 func (fs *fileSystem) RemoveAll(name string) error {
648         err := fs.remove(strings.TrimRight(name, "/"), true)
649         if os.IsNotExist(err) {
650                 // "If the path does not exist, RemoveAll returns
651                 // nil." (see "os" pkg)
652                 err = nil
653         }
654         return err
655 }
656
657 func (fs *fileSystem) remove(name string, recursive bool) error {
658         dirname, name := path.Split(name)
659         if name == "" || name == "." || name == ".." {
660                 return ErrInvalidArgument
661         }
662         dir, err := rlookup(fs.root, dirname)
663         if err != nil {
664                 return err
665         }
666         dir.Lock()
667         defer dir.Unlock()
668         _, err = dir.Child(name, func(node inode) (inode, error) {
669                 if node == nil {
670                         return nil, os.ErrNotExist
671                 }
672                 if !recursive && node.IsDir() && node.Size() > 0 {
673                         return node, ErrDirectoryNotEmpty
674                 }
675                 return nil, nil
676         })
677         return err
678 }
679
680 func (fs *fileSystem) Sync() error {
681         if syncer, ok := fs.root.(syncer); ok {
682                 return syncer.Sync()
683         }
684         return ErrInvalidOperation
685 }
686
687 func (fs *fileSystem) Flush(string, bool) error {
688         log.Printf("TODO: flush fileSystem")
689         return ErrInvalidOperation
690 }
691
692 func (fs *fileSystem) MemorySize() int64 {
693         return fs.root.MemorySize()
694 }
695
696 // rlookup (recursive lookup) returns the inode for the file/directory
697 // with the given name (which may contain "/" separators). If no such
698 // file/directory exists, the returned node is nil.
699 func rlookup(start inode, path string) (node inode, err error) {
700         node = start
701         for _, name := range strings.Split(path, "/") {
702                 if node.IsDir() {
703                         if name == "." || name == "" {
704                                 continue
705                         }
706                         if name == ".." {
707                                 node = node.Parent()
708                                 continue
709                         }
710                 }
711                 node, err = func() (inode, error) {
712                         node.Lock()
713                         defer node.Unlock()
714                         return node.Child(name, nil)
715                 }()
716                 if node == nil || err != nil {
717                         break
718                 }
719         }
720         if node == nil && err == nil {
721                 err = os.ErrNotExist
722         }
723         return
724 }
725
726 func permittedName(name string) bool {
727         return name != "" && name != "." && name != ".." && !strings.Contains(name, "/")
728 }
729
730 // Snapshot returns a Subtree that's a copy of the given path. It
731 // returns an error if the path is not inside a collection.
732 func Snapshot(fs FileSystem, path string) (*Subtree, error) {
733         f, err := fs.OpenFile(path, os.O_RDONLY, 0)
734         if err != nil {
735                 return nil, err
736         }
737         defer f.Close()
738         return f.Snapshot()
739 }
740
741 // Splice inserts newsubtree at the indicated target path.
742 //
743 // Splice returns an error if target is not inside a collection.
744 //
745 // Splice returns an error if target is an existing directory and
746 // newsubtree is a snapshot of a file.
747 func Splice(fs FileSystem, target string, newsubtree *Subtree) error {
748         f, err := fs.OpenFile(target, os.O_WRONLY, 0)
749         if os.IsNotExist(err) {
750                 f, err = fs.OpenFile(target, os.O_CREATE|os.O_WRONLY, 0700)
751         }
752         if err != nil {
753                 return err
754         }
755         defer f.Close()
756         return f.Splice(newsubtree)
757 }