16535: Add s3 endpoint.
[arvados.git] / sdk / go / arvados / fs_site.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         "fmt"
9         "os"
10         "strings"
11         "sync"
12         "time"
13 )
14
15 type CustomFileSystem interface {
16         FileSystem
17         MountByID(mount string)
18         MountProject(mount, uuid string)
19         MountUsers(mount string)
20         ForwardSlashNameSubstitution(string)
21 }
22
23 type customFileSystem struct {
24         fileSystem
25         root *vdirnode
26         thr  *throttle
27
28         staleThreshold time.Time
29         staleLock      sync.Mutex
30
31         forwardSlashNameSubstitution string
32 }
33
34 func (c *Client) CustomFileSystem(kc keepClient) CustomFileSystem {
35         root := &vdirnode{}
36         fs := &customFileSystem{
37                 root: root,
38                 fileSystem: fileSystem{
39                         fsBackend: keepBackend{apiClient: c, keepClient: kc},
40                         root:      root,
41                         thr:       newThrottle(concurrentWriters),
42                 },
43         }
44         root.inode = &treenode{
45                 fs:     fs,
46                 parent: root,
47                 fileinfo: fileinfo{
48                         name:    "/",
49                         mode:    os.ModeDir | 0755,
50                         modTime: time.Now(),
51                 },
52                 inodes: make(map[string]inode),
53         }
54         return fs
55 }
56
57 func (fs *customFileSystem) MountByID(mount string) {
58         fs.root.inode.Child(mount, func(inode) (inode, error) {
59                 return &vdirnode{
60                         inode: &treenode{
61                                 fs:     fs,
62                                 parent: fs.root,
63                                 inodes: make(map[string]inode),
64                                 fileinfo: fileinfo{
65                                         name:    mount,
66                                         modTime: time.Now(),
67                                         mode:    0755 | os.ModeDir,
68                                 },
69                         },
70                         create: fs.mountByID,
71                 }, nil
72         })
73 }
74
75 func (fs *customFileSystem) MountProject(mount, uuid string) {
76         fs.root.inode.Child(mount, func(inode) (inode, error) {
77                 return fs.newProjectNode(fs.root, mount, uuid), nil
78         })
79 }
80
81 func (fs *customFileSystem) MountUsers(mount string) {
82         fs.root.inode.Child(mount, func(inode) (inode, error) {
83                 return &lookupnode{
84                         stale:   fs.Stale,
85                         loadOne: fs.usersLoadOne,
86                         loadAll: fs.usersLoadAll,
87                         inode: &treenode{
88                                 fs:     fs,
89                                 parent: fs.root,
90                                 inodes: make(map[string]inode),
91                                 fileinfo: fileinfo{
92                                         name:    mount,
93                                         modTime: time.Now(),
94                                         mode:    0755 | os.ModeDir,
95                                 },
96                         },
97                 }, nil
98         })
99 }
100
101 func (fs *customFileSystem) ForwardSlashNameSubstitution(repl string) {
102         fs.forwardSlashNameSubstitution = repl
103 }
104
105 // SiteFileSystem returns a FileSystem that maps collections and other
106 // Arvados objects onto a filesystem layout.
107 //
108 // This is experimental: the filesystem layout is not stable, and
109 // there are significant known bugs and shortcomings. For example,
110 // writes are not persisted until Sync() is called.
111 func (c *Client) SiteFileSystem(kc keepClient) CustomFileSystem {
112         fs := c.CustomFileSystem(kc)
113         fs.MountByID("by_id")
114         fs.MountUsers("users")
115         return fs
116 }
117
118 func (fs *customFileSystem) Sync() error {
119         fs.staleLock.Lock()
120         fs.staleThreshold = time.Now()
121         fs.staleLock.Unlock()
122         return fs.syncTree("/", fs.root.inode)
123 }
124
125 // syncTree calls node.Sync() if it has its own Sync method, otherwise
126 // it calls syncTree() on all of node's children.
127 //
128 // Returns ErrInvalidArgument if node does not implement Sync() and
129 // isn't a directory (or if Readdir() fails for any other reason).
130 func (fs *customFileSystem) syncTree(path string, node inode) error {
131         if vn, ok := node.(*vdirnode); ok {
132                 node = vn.inode
133         }
134         if syncer, ok := node.(interface{ Sync() error }); ok {
135                 err := syncer.Sync()
136                 if err != nil {
137                         return fmt.Errorf("%s: %T: %w", path, syncer, err)
138                 }
139                 return nil
140         }
141         fis, err := node.Readdir()
142         if err != nil {
143                 return fmt.Errorf("%s: %T: %w", path, node, ErrInvalidArgument)
144         }
145         for _, fi := range fis {
146                 child, err := node.Child(fi.Name(), nil)
147                 if err != nil {
148                         continue
149                 }
150                 err = fs.syncTree(path+"/"+fi.Name(), child)
151                 if err != nil {
152                         return err
153                 }
154         }
155         return nil
156 }
157
158 // Stale returns true if information obtained at time t should be
159 // considered stale.
160 func (fs *customFileSystem) Stale(t time.Time) bool {
161         fs.staleLock.Lock()
162         defer fs.staleLock.Unlock()
163         return !fs.staleThreshold.Before(t)
164 }
165
166 func (fs *customFileSystem) newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error) {
167         return nil, ErrInvalidOperation
168 }
169
170 func (fs *customFileSystem) mountByID(parent inode, id string) inode {
171         if strings.Contains(id, "-4zz18-") || pdhRegexp.MatchString(id) {
172                 return fs.mountCollection(parent, id)
173         } else if strings.Contains(id, "-j7d0g-") {
174                 return fs.newProjectNode(fs.root, id, id)
175         } else {
176                 return nil
177         }
178 }
179
180 func (fs *customFileSystem) mountCollection(parent inode, id string) inode {
181         var coll Collection
182         err := fs.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+id, nil, nil)
183         if err != nil {
184                 return nil
185         }
186         cfs, err := coll.FileSystem(fs, fs)
187         if err != nil {
188                 return nil
189         }
190         return cfs.(*collectionFileSystem).asChildNode(parent, id)
191 }
192
193 func (fs *customFileSystem) newProjectNode(root inode, name, uuid string) inode {
194         return &lookupnode{
195                 stale:   fs.Stale,
196                 loadOne: func(parent inode, name string) (inode, error) { return fs.projectsLoadOne(parent, uuid, name) },
197                 loadAll: func(parent inode) ([]inode, error) { return fs.projectsLoadAll(parent, uuid) },
198                 inode: &treenode{
199                         fs:     fs,
200                         parent: root,
201                         inodes: make(map[string]inode),
202                         fileinfo: fileinfo{
203                                 name:    name,
204                                 modTime: time.Now(),
205                                 mode:    0755 | os.ModeDir,
206                         },
207                 },
208         }
209 }
210
211 // vdirnode wraps an inode by ignoring any requests to add/replace
212 // children, and calling a create() func when a non-existing child is
213 // looked up.
214 //
215 // create() can return either a new node, which will be added to the
216 // treenode, or nil for ENOENT.
217 type vdirnode struct {
218         inode
219         create func(parent inode, name string) inode
220 }
221
222 func (vn *vdirnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
223         return vn.inode.Child(name, func(existing inode) (inode, error) {
224                 if existing == nil && vn.create != nil {
225                         existing = vn.create(vn, name)
226                         if existing != nil {
227                                 existing.SetParent(vn, name)
228                                 vn.inode.(*treenode).fileinfo.modTime = time.Now()
229                         }
230                 }
231                 if replace == nil {
232                         return existing, nil
233                 } else if tryRepl, err := replace(existing); err != nil {
234                         return existing, err
235                 } else if tryRepl != existing {
236                         return existing, ErrInvalidArgument
237                 } else {
238                         return existing, nil
239                 }
240         })
241 }