21700: Install Bundler system-wide in Rails postinst
[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         "net/http"
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         byID     map[string]inode
34         byIDLock sync.Mutex
35         byIDRoot *treenode
36 }
37
38 func (c *Client) CustomFileSystem(kc keepClient) CustomFileSystem {
39         root := &vdirnode{}
40         fs := &customFileSystem{
41                 root: root,
42                 fileSystem: fileSystem{
43                         fsBackend: keepBackend{apiClient: c, keepClient: kc},
44                         root:      root,
45                         thr:       newThrottle(concurrentWriters),
46                 },
47         }
48         root.treenode = treenode{
49                 fs:     fs,
50                 parent: root,
51                 fileinfo: fileinfo{
52                         name:    "/",
53                         mode:    os.ModeDir | 0755,
54                         modTime: time.Now(),
55                 },
56                 inodes: make(map[string]inode),
57         }
58         fs.byID = map[string]inode{}
59         fs.byIDRoot = &treenode{
60                 fs:     fs,
61                 parent: root,
62                 inodes: make(map[string]inode),
63                 fileinfo: fileinfo{
64                         name:    "_internal_by_id",
65                         modTime: time.Now(),
66                         mode:    0755 | os.ModeDir,
67                 },
68         }
69         return fs
70 }
71
72 func (fs *customFileSystem) MountByID(mount string) {
73         fs.root.treenode.Lock()
74         defer fs.root.treenode.Unlock()
75         fs.root.treenode.Child(mount, func(inode) (inode, error) {
76                 return &vdirnode{
77                         treenode: treenode{
78                                 fs:     fs,
79                                 parent: fs.root,
80                                 inodes: make(map[string]inode),
81                                 fileinfo: fileinfo{
82                                         name:    mount,
83                                         modTime: time.Now(),
84                                         mode:    0755 | os.ModeDir,
85                                 },
86                         },
87                         create: fs.newCollectionOrProjectHardlink,
88                 }, nil
89         })
90 }
91
92 func (fs *customFileSystem) MountProject(mount, uuid string) {
93         fs.root.treenode.Lock()
94         defer fs.root.treenode.Unlock()
95         fs.root.treenode.Child(mount, func(inode) (inode, error) {
96                 return fs.newProjectDir(fs.root, mount, uuid, nil), nil
97         })
98 }
99
100 func (fs *customFileSystem) MountUsers(mount string) {
101         fs.root.treenode.Lock()
102         defer fs.root.treenode.Unlock()
103         fs.root.treenode.Child(mount, func(inode) (inode, error) {
104                 return &lookupnode{
105                         stale:   fs.Stale,
106                         loadOne: fs.usersLoadOne,
107                         loadAll: fs.usersLoadAll,
108                         treenode: treenode{
109                                 fs:     fs,
110                                 parent: fs.root,
111                                 inodes: make(map[string]inode),
112                                 fileinfo: fileinfo{
113                                         name:    mount,
114                                         modTime: time.Now(),
115                                         mode:    0755 | os.ModeDir,
116                                 },
117                         },
118                 }, nil
119         })
120 }
121
122 func (fs *customFileSystem) ForwardSlashNameSubstitution(repl string) {
123         fs.forwardSlashNameSubstitution = repl
124 }
125
126 func (fs *customFileSystem) MemorySize() int64 {
127         return fs.fileSystem.MemorySize() + fs.byIDRoot.MemorySize()
128 }
129
130 // SiteFileSystem returns a FileSystem that maps collections and other
131 // Arvados objects onto a filesystem layout.
132 //
133 // This is experimental: the filesystem layout is not stable, and
134 // there are significant known bugs and shortcomings. For example,
135 // writes are not persisted until Sync() is called.
136 func (c *Client) SiteFileSystem(kc keepClient) CustomFileSystem {
137         fs := c.CustomFileSystem(kc)
138         fs.MountByID("by_id")
139         fs.MountUsers("users")
140         return fs
141 }
142
143 func (fs *customFileSystem) Sync() error {
144         return fs.byIDRoot.Sync()
145 }
146
147 // Stale returns true if information obtained at time t should be
148 // considered stale.
149 func (fs *customFileSystem) Stale(t time.Time) bool {
150         fs.staleLock.Lock()
151         defer fs.staleLock.Unlock()
152         return !fs.staleThreshold.Before(t)
153 }
154
155 func (fs *customFileSystem) newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error) {
156         return nil, ErrInvalidOperation
157 }
158
159 func (fs *customFileSystem) newCollectionOrProjectHardlink(parent inode, id string) (inode, error) {
160         if strings.Contains(id, "-4zz18-") || pdhRegexp.MatchString(id) {
161                 node, err := fs.collectionSingleton(id)
162                 if os.IsNotExist(err) {
163                         return nil, nil
164                 } else if err != nil {
165                         return nil, err
166                 }
167                 return &hardlink{inode: node, parent: parent, name: id}, nil
168         } else if strings.Contains(id, "-j7d0g-") || strings.Contains(id, "-tpzed-") {
169                 fs.byIDLock.Lock()
170                 node := fs.byID[id]
171                 fs.byIDLock.Unlock()
172                 if node == nil {
173                         // Look up the project synchronously before
174                         // calling projectSingleton (otherwise we
175                         // wouldn't detect a nonexistent project until
176                         // it's too late to return ErrNotExist).
177                         proj, err := fs.getProject(id)
178                         if os.IsNotExist(err) {
179                                 return nil, nil
180                         } else if err != nil {
181                                 return nil, err
182                         }
183                         node = fs.projectSingleton(id, proj)
184                 }
185                 return &hardlink{inode: node, parent: parent, name: id}, nil
186         } else {
187                 return nil, nil
188         }
189 }
190
191 func (fs *customFileSystem) projectSingleton(uuid string, proj *Group) inode {
192         fs.byIDLock.Lock()
193         defer fs.byIDLock.Unlock()
194         if n := fs.byID[uuid]; n != nil {
195                 return n
196         }
197         name := uuid
198         if name == "" {
199                 // special case uuid=="" implements the "home project"
200                 // (owner_uuid == current user uuid)
201                 name = "home"
202         }
203         var projLoading sync.Mutex
204         n := &lookupnode{
205                 stale:   fs.Stale,
206                 loadOne: func(parent inode, name string) (inode, error) { return fs.projectsLoadOne(parent, uuid, name) },
207                 loadAll: func(parent inode) ([]inode, error) { return fs.projectsLoadAll(parent, uuid) },
208                 treenode: treenode{
209                         fs:     fs,
210                         parent: fs.byIDRoot,
211                         inodes: make(map[string]inode),
212                         fileinfo: fileinfo{
213                                 name:    name,
214                                 modTime: time.Now(),
215                                 mode:    0755 | os.ModeDir,
216                                 sys: func() interface{} {
217                                         projLoading.Lock()
218                                         defer projLoading.Unlock()
219                                         if proj != nil {
220                                                 return proj
221                                         }
222                                         g, err := fs.getProject(uuid)
223                                         if err != nil {
224                                                 return err
225                                         }
226                                         proj = g
227                                         return proj
228                                 },
229                         },
230                 },
231         }
232         fs.byID[uuid] = n
233         return n
234 }
235
236 func (fs *customFileSystem) getProject(uuid string) (*Group, error) {
237         var g Group
238         err := fs.RequestAndDecode(&g, "GET", "arvados/v1/groups/"+uuid, nil, nil)
239         if statusErr, ok := err.(interface{ HTTPStatus() int }); ok && statusErr.HTTPStatus() == http.StatusNotFound {
240                 return nil, os.ErrNotExist
241         } else if err != nil {
242                 return nil, err
243         }
244         return &g, err
245 }
246
247 func (fs *customFileSystem) collectionSingleton(id string) (inode, error) {
248         // Return existing singleton, if we have it
249         fs.byIDLock.Lock()
250         existing := fs.byID[id]
251         fs.byIDLock.Unlock()
252         if existing != nil {
253                 return existing, nil
254         }
255
256         coll, err := fs.getCollection(id)
257         if err != nil {
258                 return nil, err
259         }
260         newfs, err := coll.FileSystem(fs, fs)
261         if err != nil {
262                 return nil, err
263         }
264         cfs := newfs.(*collectionFileSystem)
265         cfs.SetParent(fs.byIDRoot, id)
266
267         // Check again in case another goroutine has added a node to
268         // fs.byID since we checked above.
269         fs.byIDLock.Lock()
270         defer fs.byIDLock.Unlock()
271         if existing = fs.byID[id]; existing != nil {
272                 // Other goroutine won the race. Discard the node we
273                 // just made, and return the race winner.
274                 return existing, nil
275         }
276         // We won the race. Save the new node in fs.byID and
277         // fs.byIDRoot.
278         fs.byID[id] = cfs
279         fs.byIDRoot.Lock()
280         defer fs.byIDRoot.Unlock()
281         fs.byIDRoot.Child(id, func(inode) (inode, error) { return cfs, nil })
282         return cfs, nil
283 }
284
285 func (fs *customFileSystem) getCollection(id string) (*Collection, error) {
286         var coll Collection
287         err := fs.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+id, nil, nil)
288         if statusErr, ok := err.(interface{ HTTPStatus() int }); ok && statusErr.HTTPStatus() == http.StatusNotFound {
289                 return nil, os.ErrNotExist
290         } else if err != nil {
291                 return nil, err
292         }
293         if len(id) != 27 {
294                 // This means id is a PDH, and controller/railsapi
295                 // returned one of (possibly) many collections with
296                 // that PDH. Even if controller returns more fields
297                 // besides PDH and manifest text (which are equal for
298                 // all matching collections), we don't want to expose
299                 // them (e.g., through Sys()).
300                 coll = Collection{
301                         PortableDataHash: coll.PortableDataHash,
302                         ManifestText:     coll.ManifestText,
303                 }
304         }
305         return &coll, nil
306 }
307
308 // vdirnode wraps an inode by rejecting (with ErrInvalidOperation)
309 // calls that add/replace children directly, instead calling a
310 // create() func when a non-existing child is looked up.
311 //
312 // create() can return either a new node, which will be added to the
313 // treenode, or nil for ENOENT.
314 type vdirnode struct {
315         treenode
316         create func(parent inode, name string) (inode, error)
317 }
318
319 func (vn *vdirnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
320         return vn.treenode.Child(name, func(existing inode) (inode, error) {
321                 if existing == nil && vn.create != nil {
322                         newnode, err := vn.create(vn, name)
323                         if err != nil {
324                                 return nil, err
325                         }
326                         if newnode != nil {
327                                 newnode.SetParent(vn, name)
328                                 existing = newnode
329                                 vn.treenode.fileinfo.modTime = time.Now()
330                         }
331                 }
332                 if replace == nil {
333                         return existing, nil
334                 } else if tryRepl, err := replace(existing); err != nil {
335                         return existing, err
336                 } else if tryRepl != existing {
337                         return existing, ErrInvalidOperation
338                 } else {
339                         return existing, nil
340                 }
341         })
342 }
343
344 // A hardlink can be used to mount an existing node at an additional
345 // point in the same filesystem.
346 type hardlink struct {
347         inode
348         parent inode
349         name   string
350 }
351
352 // If the wrapped inode is a filesystem, rootnode returns the wrapped
353 // fs's rootnode, otherwise inode itself. This allows
354 // (*fileSystem)Rename() to lock the root node of a hardlink-wrapped
355 // filesystem.
356 func (hl *hardlink) rootnode() inode {
357         if node, ok := hl.inode.(interface{ rootnode() inode }); ok {
358                 return node.rootnode()
359         } else {
360                 return hl.inode
361         }
362 }
363
364 func (hl *hardlink) Sync() error {
365         if node, ok := hl.inode.(syncer); ok {
366                 return node.Sync()
367         } else {
368                 return ErrInvalidOperation
369         }
370 }
371
372 func (hl *hardlink) SetParent(parent inode, name string) {
373         hl.Lock()
374         defer hl.Unlock()
375         hl.parent = parent
376         hl.name = name
377 }
378
379 func (hl *hardlink) Parent() inode {
380         hl.RLock()
381         defer hl.RUnlock()
382         return hl.parent
383 }
384
385 func (hl *hardlink) FileInfo() os.FileInfo {
386         fi := hl.inode.FileInfo()
387         if fi, ok := fi.(fileinfo); ok {
388                 fi.name = hl.name
389                 return fi
390         }
391         return fi
392 }
393
394 func (hl *hardlink) MemorySize() int64 {
395         return 64 + int64(len(hl.name))
396 }