13111: Clear up some type assertions.
[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         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)
57
58         // analogous to os.Stat()
59         Stat(name string) (os.FileInfo, error)
60
61         // analogous to os.Create(): create/truncate a file and open it O_RDWR.
62         Create(name string) (File, error)
63
64         // Like os.OpenFile(): create or open a file or directory.
65         //
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
69         // exist.
70         //
71         // When creating a new item, perm&os.ModeDir determines
72         // whether it is a file or a directory.
73         //
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)
78
79         Mkdir(name string, perm os.FileMode) error
80         Remove(name string) error
81         RemoveAll(name string) error
82         Rename(oldname, newname string) error
83         Sync() error
84 }
85
86 type inode interface {
87         SetParent(inode)
88         Parent() inode
89         FS() FileSystem
90         Read([]byte, filenodePtr) (int, filenodePtr, error)
91         Write([]byte, filenodePtr) (int, filenodePtr, error)
92         Truncate(int64) error
93         IsDir() bool
94         Readdir() []os.FileInfo
95         Size() int64
96         FileInfo() os.FileInfo
97
98         // Child() performs lookups and updates of named child nodes.
99         //
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
103         // replace().
104         //
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.
108         //
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
112         // nil.
113         //
114         // Child() returns the child, if any, with the given name: if
115         // a child was added or changed, the new child is returned.
116         //
117         // Caller must have lock (or rlock if replace is nil).
118         Child(name string, replace func(inode) inode) inode
119
120         sync.Locker
121         RLock()
122         RUnlock()
123 }
124
125 type fileinfo struct {
126         name    string
127         mode    os.FileMode
128         size    int64
129         modTime time.Time
130 }
131
132 // Name implements os.FileInfo.
133 func (fi fileinfo) Name() string {
134         return fi.name
135 }
136
137 // ModTime implements os.FileInfo.
138 func (fi fileinfo) ModTime() time.Time {
139         return fi.modTime
140 }
141
142 // Mode implements os.FileInfo.
143 func (fi fileinfo) Mode() os.FileMode {
144         return fi.mode
145 }
146
147 // IsDir implements os.FileInfo.
148 func (fi fileinfo) IsDir() bool {
149         return fi.mode&os.ModeDir != 0
150 }
151
152 // Size implements os.FileInfo.
153 func (fi fileinfo) Size() int64 {
154         return fi.size
155 }
156
157 // Sys implements os.FileInfo.
158 func (fi fileinfo) Sys() interface{} {
159         return nil
160 }
161
162 type nullnode struct{}
163
164 func (*nullnode) Mkdir(string, os.FileMode) error {
165         return ErrInvalidOperation
166 }
167
168 func (*nullnode) Read([]byte, filenodePtr) (int, filenodePtr, error) {
169         return 0, filenodePtr{}, ErrInvalidOperation
170 }
171
172 func (*nullnode) Write([]byte, filenodePtr) (int, filenodePtr, error) {
173         return 0, filenodePtr{}, ErrInvalidOperation
174 }
175
176 func (*nullnode) Truncate(int64) error {
177         return ErrInvalidOperation
178 }
179
180 func (*nullnode) FileInfo() os.FileInfo {
181         return fileinfo{}
182 }
183
184 func (*nullnode) IsDir() bool {
185         return false
186 }
187
188 func (*nullnode) Readdir() []os.FileInfo {
189         return nil
190 }
191
192 func (*nullnode) Child(name string, replace func(inode) inode) inode {
193         return nil
194 }
195
196 type treenode struct {
197         fs       FileSystem
198         parent   inode
199         inodes   map[string]inode
200         fileinfo fileinfo
201         sync.RWMutex
202         nullnode
203 }
204
205 func (n *treenode) FS() FileSystem {
206         return n.fs
207 }
208
209 func (n *treenode) SetParent(p inode) {
210         n.RLock()
211         defer n.RUnlock()
212         n.parent = p
213 }
214
215 func (n *treenode) Parent() inode {
216         n.RLock()
217         defer n.RUnlock()
218         return n.parent
219 }
220
221 func (n *treenode) IsDir() bool {
222         return true
223 }
224
225 func (n *treenode) Child(name string, replace func(inode) inode) (child inode) {
226         // TODO: special treatment for "", ".", ".."
227         child = n.inodes[name]
228         if replace != nil {
229                 newchild := replace(child)
230                 if newchild == nil {
231                         delete(n.inodes, name)
232                 } else if newchild != child {
233                         n.inodes[name] = newchild
234                         newchild.SetParent(n)
235                         child = newchild
236                 }
237         }
238         return
239 }
240
241 func (n *treenode) Size() int64 {
242         return n.FileInfo().Size()
243 }
244
245 func (n *treenode) FileInfo() os.FileInfo {
246         n.Lock()
247         defer n.Unlock()
248         n.fileinfo.size = int64(len(n.inodes))
249         return n.fileinfo
250 }
251
252 func (n *treenode) Readdir() (fi []os.FileInfo) {
253         n.RLock()
254         defer n.RUnlock()
255         fi = make([]os.FileInfo, 0, len(n.inodes))
256         for _, inode := range n.inodes {
257                 fi = append(fi, inode.FileInfo())
258         }
259         return
260 }
261
262 type fileSystem struct {
263         root inode
264         fsBackend
265 }
266
267 func (fs *fileSystem) rootnode() inode {
268         return fs.root
269 }
270
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)
274 }
275
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
279         }
280         dirname, name := path.Split(name)
281         parent := rlookup(fs.root, dirname)
282         if parent == nil {
283                 return nil, os.ErrNotExist
284         }
285         var readable, writable bool
286         switch flag & (os.O_RDWR | os.O_RDONLY | os.O_WRONLY) {
287         case os.O_RDWR:
288                 readable = true
289                 writable = true
290         case os.O_RDONLY:
291                 readable = true
292         case os.O_WRONLY:
293                 writable = true
294         default:
295                 return nil, fmt.Errorf("invalid flags 0x%x", flag)
296         }
297         if !writable && parent.IsDir() {
298                 // A directory can be opened via "foo/", "foo/.", or
299                 // "foo/..".
300                 switch name {
301                 case ".", "":
302                         return &filehandle{inode: parent}, nil
303                 case "..":
304                         return &filehandle{inode: parent.Parent()}, nil
305                 }
306         }
307         createMode := flag&os.O_CREATE != 0
308         if createMode {
309                 parent.Lock()
310                 defer parent.Unlock()
311         } else {
312                 parent.RLock()
313                 defer parent.RUnlock()
314         }
315         n := parent.Child(name, nil)
316         if n == nil {
317                 if !createMode {
318                         return nil, os.ErrNotExist
319                 }
320                 var err error
321                 n = parent.Child(name, func(inode) inode {
322                         if perm.IsDir() {
323                                 n, err = parent.FS().newDirnode(parent, name, perm|0755, time.Now())
324                         } else {
325                                 n, err = parent.FS().newFilenode(parent, name, perm|0755, time.Now())
326                         }
327                         return n
328                 })
329                 if err != nil {
330                         return nil, err
331                 } else if n == nil {
332                         // parent rejected new child
333                         return nil, ErrInvalidOperation
334                 }
335         } else if flag&os.O_EXCL != 0 {
336                 return nil, ErrFileExists
337         } else if flag&os.O_TRUNC != 0 {
338                 if !writable {
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 {
343                         return nil, err
344                 }
345         }
346         return &filehandle{
347                 inode:    n,
348                 append:   flag&os.O_APPEND != 0,
349                 readable: readable,
350                 writable: writable,
351         }, nil
352 }
353
354 func (fs *fileSystem) Open(name string) (http.File, error) {
355         return fs.OpenFile(name, os.O_RDONLY, 0)
356 }
357
358 func (fs *fileSystem) Create(name string) (File, error) {
359         return fs.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0)
360 }
361
362 func (fs *fileSystem) Mkdir(name string, perm os.FileMode) (err error) {
363         dirname, name := path.Split(name)
364         n := rlookup(fs.root, dirname)
365         if n == nil {
366                 return os.ErrNotExist
367         }
368         n.Lock()
369         defer n.Unlock()
370         if n.Child(name, nil) != nil {
371                 return os.ErrExist
372         }
373         child := n.Child(name, func(inode) (child inode) {
374                 child, err = n.FS().newDirnode(n, name, perm, time.Now())
375                 return
376         })
377         if err != nil {
378                 return err
379         } else if child == nil {
380                 return ErrInvalidArgument
381         }
382         return nil
383 }
384
385 func (fs *fileSystem) Stat(name string) (fi os.FileInfo, err error) {
386         node := rlookup(fs.root, name)
387         if node == nil {
388                 err = os.ErrNotExist
389         } else {
390                 fi = node.FileInfo()
391         }
392         return
393 }
394
395 func (fs *fileSystem) Rename(oldname, newname string) error {
396         olddir, oldname := path.Split(oldname)
397         if oldname == "" || oldname == "." || oldname == ".." {
398                 return ErrInvalidArgument
399         }
400         olddirf, err := fs.openFile(olddir+".", os.O_RDONLY, 0)
401         if err != nil {
402                 return fmt.Errorf("%q: %s", olddir, err)
403         }
404         defer olddirf.Close()
405
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")
411                 newname = oldname
412         }
413         newdirf, err := fs.openFile(newdir+".", os.O_RDONLY, 0)
414         if err != nil {
415                 return fmt.Errorf("%q: %s", newdir, err)
416         }
417         defer newdirf.Close()
418
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
424         // nodes.
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() {
429                         node = node.Parent()
430                         needLock = append(needLock, node)
431                 }
432         }
433         locked := map[sync.Locker]bool{}
434         for i := len(needLock) - 1; i >= 0; i-- {
435                 if n := needLock[i]; !locked[n] {
436                         n.Lock()
437                         defer n.Unlock()
438                         locked[n] = true
439                 }
440         }
441
442         err = nil
443         olddirf.inode.Child(oldname, func(oldinode inode) inode {
444                 if oldinode == nil {
445                         err = os.ErrNotExist
446                         return nil
447                 }
448                 newdirf.inode.Child(newname, func(existing inode) inode {
449                         if existing != nil && existing.IsDir() {
450                                 err = ErrIsDirectory
451                                 return existing
452                         }
453                         return oldinode
454                 })
455                 if err != nil {
456                         return oldinode
457                 }
458                 oldinode.Lock()
459                 defer oldinode.Unlock()
460                 switch n := oldinode.(type) {
461                 case *dirnode:
462                         n.parent = newdirf.inode
463                         n.fileinfo.name = newname
464                 case *filenode:
465                         n.parent = newdirf.inode
466                         n.fileinfo.name = newname
467                 default:
468                         panic(fmt.Sprintf("bad inode type %T", n))
469                 }
470                 //TODO: olddirf.setModTime(time.Now())
471                 //TODO: newdirf.setModTime(time.Now())
472                 return nil
473         })
474         return err
475 }
476
477 func (fs *fileSystem) Remove(name string) error {
478         return fs.remove(strings.TrimRight(name, "/"), false)
479 }
480
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)
486                 err = nil
487         }
488         return err
489 }
490
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
495         }
496         dir := rlookup(fs.root, dirname)
497         if dir == nil {
498                 return os.ErrNotExist
499         }
500         dir.Lock()
501         defer dir.Unlock()
502         dir.Child(name, func(node inode) inode {
503                 if node == nil {
504                         err = os.ErrNotExist
505                         return nil
506                 }
507                 if !recursive && node.IsDir() && node.Size() > 0 {
508                         err = ErrDirectoryNotEmpty
509                         return node
510                 }
511                 return nil
512         })
513         return err
514 }
515
516 func (fs *fileSystem) Sync() error {
517         log.Printf("TODO: sync fileSystem")
518         return ErrInvalidOperation
519 }
520
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) {
525         node = start
526         for _, name := range strings.Split(path, "/") {
527                 if node == nil {
528                         break
529                 }
530                 if node.IsDir() {
531                         if name == "." || name == "" {
532                                 continue
533                         }
534                         if name == ".." {
535                                 node = node.Parent()
536                                 continue
537                         }
538                 }
539                 node = func() inode {
540                         node.RLock()
541                         defer node.RUnlock()
542                         return node.Child(name, nil)
543                 }()
544         }
545         return
546 }