1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
15 type CustomFileSystem interface {
17 MountByID(mount string)
18 MountProject(mount, uuid string)
19 MountUsers(mount string)
20 ForwardSlashNameSubstitution(string)
23 type customFileSystem struct {
28 staleThreshold time.Time
31 forwardSlashNameSubstitution string
38 func (c *Client) CustomFileSystem(kc keepClient) CustomFileSystem {
40 fs := &customFileSystem{
42 fileSystem: fileSystem{
43 fsBackend: keepBackend{apiClient: c, keepClient: kc},
45 thr: newThrottle(concurrentWriters),
48 root.treenode = treenode{
53 mode: os.ModeDir | 0755,
56 inodes: make(map[string]inode),
58 fs.byID = map[string]inode{}
59 fs.byIDRoot = &treenode{
62 inodes: make(map[string]inode),
64 name: "_internal_by_id",
66 mode: 0755 | os.ModeDir,
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) {
80 inodes: make(map[string]inode),
84 mode: 0755 | os.ModeDir,
87 create: fs.newCollectionOrProjectHardlink,
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
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) {
106 loadOne: fs.usersLoadOne,
107 loadAll: fs.usersLoadAll,
111 inodes: make(map[string]inode),
115 mode: 0755 | os.ModeDir,
122 func (fs *customFileSystem) ForwardSlashNameSubstitution(repl string) {
123 fs.forwardSlashNameSubstitution = repl
126 func (fs *customFileSystem) MemorySize() int64 {
127 return fs.fileSystem.MemorySize() + fs.byIDRoot.MemorySize()
130 // SiteFileSystem returns a FileSystem that maps collections and other
131 // Arvados objects onto a filesystem layout.
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")
143 func (fs *customFileSystem) Sync() error {
144 return fs.byIDRoot.Sync()
147 // Stale returns true if information obtained at time t should be
149 func (fs *customFileSystem) Stale(t time.Time) bool {
151 defer fs.staleLock.Unlock()
152 return !fs.staleThreshold.Before(t)
155 func (fs *customFileSystem) newNode(name string, perm os.FileMode, modTime time.Time) (node inode, err error) {
156 return nil, ErrInvalidOperation
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) {
164 } else if err != nil {
167 return &hardlink{inode: node, parent: parent, name: id}, nil
168 } else if strings.Contains(id, "-j7d0g-") || strings.Contains(id, "-tpzed-") {
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) {
180 } else if err != nil {
183 node = fs.projectSingleton(id, proj)
185 return &hardlink{inode: node, parent: parent, name: id}, nil
191 func (fs *customFileSystem) projectSingleton(uuid string, proj *Group) inode {
193 defer fs.byIDLock.Unlock()
194 if n := fs.byID[uuid]; n != nil {
199 // special case uuid=="" implements the "home project"
200 // (owner_uuid == current user uuid)
203 var projLoading sync.Mutex
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) },
211 inodes: make(map[string]inode),
215 mode: 0755 | os.ModeDir,
216 sys: func() interface{} {
218 defer projLoading.Unlock()
222 g, err := fs.getProject(uuid)
236 func (fs *customFileSystem) getProject(uuid string) (*Group, error) {
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 {
247 func (fs *customFileSystem) collectionSingleton(id string) (inode, error) {
248 // Return existing singleton, if we have it
250 existing := fs.byID[id]
256 coll, err := fs.getCollection(id)
260 newfs, err := coll.FileSystem(fs, fs)
264 cfs := newfs.(*collectionFileSystem)
265 cfs.SetParent(fs.byIDRoot, id)
267 // Check again in case another goroutine has added a node to
268 // fs.byID since we checked above.
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.
276 // We won the race. Save the new node in fs.byID and
280 defer fs.byIDRoot.Unlock()
281 fs.byIDRoot.Child(id, func(inode) (inode, error) { return cfs, nil })
285 func (fs *customFileSystem) getCollection(id string) (*Collection, error) {
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 {
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()).
301 PortableDataHash: coll.PortableDataHash,
302 ManifestText: coll.ManifestText,
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.
312 // create() can return either a new node, which will be added to the
313 // treenode, or nil for ENOENT.
314 type vdirnode struct {
316 create func(parent inode, name string) (inode, error)
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)
327 newnode.SetParent(vn, name)
329 vn.treenode.fileinfo.modTime = time.Now()
334 } else if tryRepl, err := replace(existing); err != nil {
336 } else if tryRepl != existing {
337 return existing, ErrInvalidOperation
344 // A hardlink can be used to mount an existing node at an additional
345 // point in the same filesystem.
346 type hardlink struct {
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
356 func (hl *hardlink) rootnode() inode {
357 if node, ok := hl.inode.(interface{ rootnode() inode }); ok {
358 return node.rootnode()
364 func (hl *hardlink) Sync() error {
365 if node, ok := hl.inode.(syncer); ok {
368 return ErrInvalidOperation
372 func (hl *hardlink) SetParent(parent inode, name string) {
379 func (hl *hardlink) Parent() inode {
385 func (hl *hardlink) FileInfo() os.FileInfo {
386 fi := hl.inode.FileInfo()
387 if fi, ok := fi.(fileinfo); ok {
394 func (hl *hardlink) MemorySize() int64 {
395 return 64 + int64(len(hl.name))