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