13111: Move SetParent() responsibility from Child() to caller.
[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         // create a new node with nil parent.
57         newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error)
58
59         // analogous to os.Stat()
60         Stat(name string) (os.FileInfo, error)
61
62         // analogous to os.Create(): create/truncate a file and open it O_RDWR.
63         Create(name string) (File, error)
64
65         // Like os.OpenFile(): create or open a file or directory.
66         //
67         // If flag&os.O_EXCL==0, it opens an existing file or
68         // directory if one exists. If flag&os.O_CREATE!=0, it creates
69         // a new empty file or directory if one does not already
70         // exist.
71         //
72         // When creating a new item, perm&os.ModeDir determines
73         // whether it is a file or a directory.
74         //
75         // A file can be opened multiple times and used concurrently
76         // from multiple goroutines. However, each File object should
77         // be used by only one goroutine at a time.
78         OpenFile(name string, flag int, perm os.FileMode) (File, error)
79
80         Mkdir(name string, perm os.FileMode) error
81         Remove(name string) error
82         RemoveAll(name string) error
83         Rename(oldname, newname string) error
84         Sync() error
85 }
86
87 type inode interface {
88         SetParent(parent inode, name string)
89         Parent() inode
90         FS() FileSystem
91         Read([]byte, filenodePtr) (int, filenodePtr, error)
92         Write([]byte, filenodePtr) (int, filenodePtr, error)
93         Truncate(int64) error
94         IsDir() bool
95         Readdir() []os.FileInfo
96         Size() int64
97         FileInfo() os.FileInfo
98
99         // Child() performs lookups and updates of named child nodes.
100         //
101         // If replace is non-nil, Child calls replace(x) where x is
102         // the current child inode with the given name. If possible,
103         // the child inode is replaced with the one returned by
104         // replace().
105         //
106         // If replace(x) returns an inode (besides x or nil) that is
107         // subsequently returned by Child(), then Child()'s caller
108         // must ensure the new child's name and parent are set/updated
109         // to Child()'s name argument and its receiver respectively.
110         // This is not necessarily done before replace(x) returns, but
111         // it must be done before Child()'s caller releases the
112         // parent's lock.
113         //
114         // Nil represents "no child". replace(nil) signifies that no
115         // child with this name exists yet. If replace() returns nil,
116         // the existing child should be deleted if possible.
117         //
118         // An implementation of Child() is permitted to ignore
119         // replace() or its return value. For example, a regular file
120         // inode does not have children, so Child() always returns
121         // nil.
122         //
123         // Child() returns the child, if any, with the given name: if
124         // a child was added or changed, the new child is returned.
125         //
126         // Caller must have lock (or rlock if replace is nil).
127         Child(name string, replace func(inode) inode) inode
128
129         sync.Locker
130         RLock()
131         RUnlock()
132 }
133
134 type fileinfo struct {
135         name    string
136         mode    os.FileMode
137         size    int64
138         modTime time.Time
139 }
140
141 // Name implements os.FileInfo.
142 func (fi fileinfo) Name() string {
143         return fi.name
144 }
145
146 // ModTime implements os.FileInfo.
147 func (fi fileinfo) ModTime() time.Time {
148         return fi.modTime
149 }
150
151 // Mode implements os.FileInfo.
152 func (fi fileinfo) Mode() os.FileMode {
153         return fi.mode
154 }
155
156 // IsDir implements os.FileInfo.
157 func (fi fileinfo) IsDir() bool {
158         return fi.mode&os.ModeDir != 0
159 }
160
161 // Size implements os.FileInfo.
162 func (fi fileinfo) Size() int64 {
163         return fi.size
164 }
165
166 // Sys implements os.FileInfo.
167 func (fi fileinfo) Sys() interface{} {
168         return nil
169 }
170
171 type nullnode struct{}
172
173 func (*nullnode) Mkdir(string, os.FileMode) error {
174         return ErrInvalidOperation
175 }
176
177 func (*nullnode) Read([]byte, filenodePtr) (int, filenodePtr, error) {
178         return 0, filenodePtr{}, ErrInvalidOperation
179 }
180
181 func (*nullnode) Write([]byte, filenodePtr) (int, filenodePtr, error) {
182         return 0, filenodePtr{}, ErrInvalidOperation
183 }
184
185 func (*nullnode) Truncate(int64) error {
186         return ErrInvalidOperation
187 }
188
189 func (*nullnode) FileInfo() os.FileInfo {
190         return fileinfo{}
191 }
192
193 func (*nullnode) IsDir() bool {
194         return false
195 }
196
197 func (*nullnode) Readdir() []os.FileInfo {
198         return nil
199 }
200
201 func (*nullnode) Child(name string, replace func(inode) inode) inode {
202         return nil
203 }
204
205 type treenode struct {
206         fs       FileSystem
207         parent   inode
208         inodes   map[string]inode
209         fileinfo fileinfo
210         sync.RWMutex
211         nullnode
212 }
213
214 func (n *treenode) FS() FileSystem {
215         return n.fs
216 }
217
218 func (n *treenode) SetParent(p inode, name string) {
219         n.Lock()
220         defer n.Unlock()
221         n.parent = p
222         n.fileinfo.name = name
223 }
224
225 func (n *treenode) Parent() inode {
226         n.RLock()
227         defer n.RUnlock()
228         return n.parent
229 }
230
231 func (n *treenode) IsDir() bool {
232         return true
233 }
234
235 func (n *treenode) Child(name string, replace func(inode) inode) (child inode) {
236         // TODO: special treatment for "", ".", ".."
237         child = n.inodes[name]
238         if replace != nil {
239                 newchild := replace(child)
240                 if newchild == nil {
241                         delete(n.inodes, name)
242                 } else if newchild != child {
243                         n.inodes[name] = newchild
244                         n.fileinfo.modTime = time.Now()
245                         child = newchild
246                 }
247         }
248         return
249 }
250
251 func (n *treenode) Size() int64 {
252         return n.FileInfo().Size()
253 }
254
255 func (n *treenode) FileInfo() os.FileInfo {
256         n.Lock()
257         defer n.Unlock()
258         n.fileinfo.size = int64(len(n.inodes))
259         return n.fileinfo
260 }
261
262 func (n *treenode) Readdir() (fi []os.FileInfo) {
263         n.RLock()
264         defer n.RUnlock()
265         fi = make([]os.FileInfo, 0, len(n.inodes))
266         for _, inode := range n.inodes {
267                 fi = append(fi, inode.FileInfo())
268         }
269         return
270 }
271
272 type fileSystem struct {
273         root inode
274         fsBackend
275 }
276
277 func (fs *fileSystem) rootnode() inode {
278         return fs.root
279 }
280
281 // OpenFile is analogous to os.OpenFile().
282 func (fs *fileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
283         return fs.openFile(name, flag, perm)
284 }
285
286 func (fs *fileSystem) openFile(name string, flag int, perm os.FileMode) (*filehandle, error) {
287         if flag&os.O_SYNC != 0 {
288                 return nil, ErrSyncNotSupported
289         }
290         dirname, name := path.Split(name)
291         parent := rlookup(fs.root, dirname)
292         if parent == nil {
293                 return nil, os.ErrNotExist
294         }
295         var readable, writable bool
296         switch flag & (os.O_RDWR | os.O_RDONLY | os.O_WRONLY) {
297         case os.O_RDWR:
298                 readable = true
299                 writable = true
300         case os.O_RDONLY:
301                 readable = true
302         case os.O_WRONLY:
303                 writable = true
304         default:
305                 return nil, fmt.Errorf("invalid flags 0x%x", flag)
306         }
307         if !writable && parent.IsDir() {
308                 // A directory can be opened via "foo/", "foo/.", or
309                 // "foo/..".
310                 switch name {
311                 case ".", "":
312                         return &filehandle{inode: parent}, nil
313                 case "..":
314                         return &filehandle{inode: parent.Parent()}, nil
315                 }
316         }
317         createMode := flag&os.O_CREATE != 0
318         if createMode {
319                 parent.Lock()
320                 defer parent.Unlock()
321         } else {
322                 parent.RLock()
323                 defer parent.RUnlock()
324         }
325         n := parent.Child(name, nil)
326         if n == nil {
327                 if !createMode {
328                         return nil, os.ErrNotExist
329                 }
330                 var err error
331                 n = parent.Child(name, func(inode) inode {
332                         n, err = parent.FS().newNode(name, perm|0755, time.Now())
333                         n.SetParent(parent, name)
334                         return n
335                 })
336                 if err != nil {
337                         return nil, err
338                 } else if n == nil {
339                         // parent rejected new child
340                         return nil, ErrInvalidOperation
341                 }
342         } else if flag&os.O_EXCL != 0 {
343                 return nil, ErrFileExists
344         } else if flag&os.O_TRUNC != 0 {
345                 if !writable {
346                         return nil, fmt.Errorf("invalid flag O_TRUNC in read-only mode")
347                 } else if n.IsDir() {
348                         return nil, fmt.Errorf("invalid flag O_TRUNC when opening directory")
349                 } else if err := n.Truncate(0); err != nil {
350                         return nil, err
351                 }
352         }
353         return &filehandle{
354                 inode:    n,
355                 append:   flag&os.O_APPEND != 0,
356                 readable: readable,
357                 writable: writable,
358         }, nil
359 }
360
361 func (fs *fileSystem) Open(name string) (http.File, error) {
362         return fs.OpenFile(name, os.O_RDONLY, 0)
363 }
364
365 func (fs *fileSystem) Create(name string) (File, error) {
366         return fs.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0)
367 }
368
369 func (fs *fileSystem) Mkdir(name string, perm os.FileMode) (err error) {
370         dirname, name := path.Split(name)
371         n := rlookup(fs.root, dirname)
372         if n == nil {
373                 return os.ErrNotExist
374         }
375         n.Lock()
376         defer n.Unlock()
377         if n.Child(name, nil) != nil {
378                 return os.ErrExist
379         }
380         child := n.Child(name, func(inode) (child inode) {
381                 child, err = n.FS().newNode(name, perm|os.ModeDir, time.Now())
382                 child.SetParent(n, name)
383                 return
384         })
385         if err != nil {
386                 return err
387         } else if child == nil {
388                 return ErrInvalidArgument
389         }
390         return nil
391 }
392
393 func (fs *fileSystem) Stat(name string) (fi os.FileInfo, err error) {
394         node := rlookup(fs.root, name)
395         if node == nil {
396                 err = os.ErrNotExist
397         } else {
398                 fi = node.FileInfo()
399         }
400         return
401 }
402
403 func (fs *fileSystem) Rename(oldname, newname string) error {
404         olddir, oldname := path.Split(oldname)
405         if oldname == "" || oldname == "." || oldname == ".." {
406                 return ErrInvalidArgument
407         }
408         olddirf, err := fs.openFile(olddir+".", os.O_RDONLY, 0)
409         if err != nil {
410                 return fmt.Errorf("%q: %s", olddir, err)
411         }
412         defer olddirf.Close()
413
414         newdir, newname := path.Split(newname)
415         if newname == "." || newname == ".." {
416                 return ErrInvalidArgument
417         } else if newname == "" {
418                 // Rename("a/b", "c/") means Rename("a/b", "c/b")
419                 newname = oldname
420         }
421         newdirf, err := fs.openFile(newdir+".", os.O_RDONLY, 0)
422         if err != nil {
423                 return fmt.Errorf("%q: %s", newdir, err)
424         }
425         defer newdirf.Close()
426
427         // When acquiring locks on multiple nodes, all common
428         // ancestors must be locked first in order to avoid
429         // deadlock. This is assured by locking the path from
430         // filesystem root to newdir, then locking the path from
431         // filesystem root to olddir, skipping any already-locked
432         // nodes.
433         needLock := []sync.Locker{}
434         for _, node := range []inode{olddirf.inode, newdirf.inode} {
435                 needLock = append(needLock, node)
436                 for node.Parent() != node && node.Parent().FS() == node.FS() {
437                         node = node.Parent()
438                         needLock = append(needLock, node)
439                 }
440         }
441         locked := map[sync.Locker]bool{}
442         for i := len(needLock) - 1; i >= 0; i-- {
443                 if n := needLock[i]; !locked[n] {
444                         n.Lock()
445                         defer n.Unlock()
446                         locked[n] = true
447                 }
448         }
449
450         err = nil
451         olddirf.inode.Child(oldname, func(oldinode inode) inode {
452                 if oldinode == nil {
453                         err = os.ErrNotExist
454                         return nil
455                 }
456                 if locked[oldinode] {
457                         // oldinode cannot become a descendant of itself.
458                         err = ErrInvalidArgument
459                         return oldinode
460                 }
461                 accepted := newdirf.inode.Child(newname, func(existing inode) inode {
462                         if existing != nil && existing.IsDir() {
463                                 err = ErrIsDirectory
464                                 return existing
465                         }
466                         return oldinode
467                 })
468                 if accepted != oldinode {
469                         if err == nil {
470                                 // newdirf didn't accept oldinode.
471                                 err = ErrInvalidArgument
472                         }
473                         // Leave oldinode in olddir.
474                         return oldinode
475                 }
476                 accepted.SetParent(newdirf.inode, newname)
477                 return nil
478         })
479         return err
480 }
481
482 func (fs *fileSystem) Remove(name string) error {
483         return fs.remove(strings.TrimRight(name, "/"), false)
484 }
485
486 func (fs *fileSystem) RemoveAll(name string) error {
487         err := fs.remove(strings.TrimRight(name, "/"), true)
488         if os.IsNotExist(err) {
489                 // "If the path does not exist, RemoveAll returns
490                 // nil." (see "os" pkg)
491                 err = nil
492         }
493         return err
494 }
495
496 func (fs *fileSystem) remove(name string, recursive bool) (err error) {
497         dirname, name := path.Split(name)
498         if name == "" || name == "." || name == ".." {
499                 return ErrInvalidArgument
500         }
501         dir := rlookup(fs.root, dirname)
502         if dir == nil {
503                 return os.ErrNotExist
504         }
505         dir.Lock()
506         defer dir.Unlock()
507         dir.Child(name, func(node inode) inode {
508                 if node == nil {
509                         err = os.ErrNotExist
510                         return nil
511                 }
512                 if !recursive && node.IsDir() && node.Size() > 0 {
513                         err = ErrDirectoryNotEmpty
514                         return node
515                 }
516                 return nil
517         })
518         return err
519 }
520
521 func (fs *fileSystem) Sync() error {
522         log.Printf("TODO: sync fileSystem")
523         return ErrInvalidOperation
524 }
525
526 // rlookup (recursive lookup) returns the inode for the file/directory
527 // with the given name (which may contain "/" separators). If no such
528 // file/directory exists, the returned node is nil.
529 func rlookup(start inode, path string) (node inode) {
530         node = start
531         for _, name := range strings.Split(path, "/") {
532                 if node == nil {
533                         break
534                 }
535                 if node.IsDir() {
536                         if name == "." || name == "" {
537                                 continue
538                         }
539                         if name == ".." {
540                                 node = node.Parent()
541                                 continue
542                         }
543                 }
544                 node = func() inode {
545                         node.RLock()
546                         defer node.RUnlock()
547                         return node.Child(name, nil)
548                 }()
549         }
550         return
551 }