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