--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import (
+ "log"
+ "os"
+ "sync"
+ "time"
+)
+
+func deferredCollectionFS(fs FileSystem, parent inode, coll Collection) inode {
+ var modTime time.Time
+ if coll.ModifiedAt != nil {
+ modTime = *coll.ModifiedAt
+ } else {
+ modTime = time.Now()
+ }
+ placeholder := &treenode{
+ fs: fs,
+ parent: parent,
+ inodes: nil,
+ fileinfo: fileinfo{
+ name: coll.Name,
+ modTime: modTime,
+ mode: 0755 | os.ModeDir,
+ },
+ }
+ return &deferrednode{wrapped: placeholder, create: func() inode {
+ err := fs.RequestAndDecode(&coll, "GET", "arvados/v1/collections/"+coll.UUID, nil, nil)
+ if err != nil {
+ log.Printf("BUG: unhandled error: %s", err)
+ return placeholder
+ }
+ cfs, err := coll.FileSystem(fs, fs)
+ if err != nil {
+ log.Printf("BUG: unhandled error: %s", err)
+ return placeholder
+ }
+ root := cfs.rootnode()
+ root.SetParent(parent, coll.Name)
+ return root
+ }}
+}
+
+// A deferrednode wraps an inode that's expensive to build. Initially,
+// it responds to basic directory functions by proxying to the given
+// placeholder. If a caller uses a read/write/lock operation,
+// deferrednode calls the create() func to create the real inode, and
+// proxies to the real inode from then on.
+//
+// In practice, this means a deferrednode's parent's directory listing
+// can be generated using only the placeholder, instead of waiting for
+// create().
+type deferrednode struct {
+ wrapped inode
+ create func() inode
+ mtx sync.Mutex
+ created bool
+}
+
+func (dn *deferrednode) realinode() inode {
+ dn.mtx.Lock()
+ defer dn.mtx.Unlock()
+ if !dn.created {
+ dn.wrapped = dn.create()
+ dn.created = true
+ }
+ return dn.wrapped
+}
+
+func (dn *deferrednode) currentinode() inode {
+ dn.mtx.Lock()
+ defer dn.mtx.Unlock()
+ return dn.wrapped
+}
+
+func (dn *deferrednode) Read(p []byte, pos filenodePtr) (int, filenodePtr, error) {
+ return dn.realinode().Read(p, pos)
+}
+
+func (dn *deferrednode) Write(p []byte, pos filenodePtr) (int, filenodePtr, error) {
+ return dn.realinode().Write(p, pos)
+}
+
+func (dn *deferrednode) Child(name string, replace func(inode) inode) inode {
+ return dn.realinode().Child(name, replace)
+}
+
+func (dn *deferrednode) Truncate(size int64) error { return dn.realinode().Truncate(size) }
+func (dn *deferrednode) SetParent(p inode, name string) { dn.realinode().SetParent(p, name) }
+func (dn *deferrednode) IsDir() bool { return dn.currentinode().IsDir() }
+func (dn *deferrednode) Readdir() []os.FileInfo { return dn.realinode().Readdir() }
+func (dn *deferrednode) Size() int64 { return dn.currentinode().Size() }
+func (dn *deferrednode) FileInfo() os.FileInfo { return dn.currentinode().FileInfo() }
+func (dn *deferrednode) Lock() { dn.realinode().Lock() }
+func (dn *deferrednode) Unlock() { dn.realinode().Unlock() }
+func (dn *deferrednode) RLock() { dn.realinode().RLock() }
+func (dn *deferrednode) RUnlock() { dn.realinode().RUnlock() }
+func (dn *deferrednode) FS() FileSystem { return dn.currentinode().FS() }
+func (dn *deferrednode) Parent() inode { return dn.currentinode().Parent() }
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package arvados
+
+import (
+ "log"
+ "os"
+ "sync"
+)
+
+// projectnode exposes an Arvados project as a filesystem directory.
+type projectnode struct {
+ inode
+ uuid string
+ setupOnce sync.Once
+ err error
+}
+
+func (pn *projectnode) setup() {
+ fs := pn.FS().(*siteFileSystem)
+ if pn.uuid == "" {
+ var resp User
+ pn.err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/users/current", nil, nil)
+ if pn.err != nil {
+ return
+ }
+ pn.uuid = resp.UUID
+ }
+ filters := []Filter{{"owner_uuid", "=", pn.uuid}}
+ params := ResourceListParams{
+ Filters: filters,
+ Order: "uuid",
+ }
+ for {
+ var resp CollectionList
+ pn.err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/collections", nil, params)
+ if pn.err != nil {
+ // TODO: retry on next access, instead of returning the same error forever
+ return
+ }
+ if len(resp.Items) == 0 {
+ break
+ }
+ for _, i := range resp.Items {
+ coll := i
+ if coll.Name == "" {
+ continue
+ }
+ pn.inode.Child(coll.Name, func(inode) inode {
+ return deferredCollectionFS(fs, pn, coll)
+ })
+ }
+ params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
+ }
+
+ filters = append(filters, Filter{"group_class", "=", "project"})
+ params.Filters = filters
+ for {
+ var resp GroupList
+ pn.err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/groups", nil, params)
+ if pn.err != nil {
+ // TODO: retry on next access, instead of returning the same error forever
+ return
+ }
+ if len(resp.Items) == 0 {
+ break
+ }
+ for _, group := range resp.Items {
+ if group.Name == "" || group.Name == "." || group.Name == ".." {
+ continue
+ }
+ pn.inode.Child(group.Name, func(inode) inode {
+ return fs.newProjectNode(pn, group.Name, group.UUID)
+ })
+ }
+ params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
+ }
+}
+
+func (pn *projectnode) Readdir() []os.FileInfo {
+ pn.setupOnce.Do(pn.setup)
+ return pn.inode.Readdir()
+}
+
+func (pn *projectnode) Child(name string, replace func(inode) inode) inode {
+ pn.setupOnce.Do(pn.setup)
+ if pn.err != nil {
+ log.Printf("BUG: not propagating error setting up %T %v: %s", pn, pn, pn.err)
+ // TODO: propagate error, instead of just being empty
+ return nil
+ }
+ if replace == nil {
+ // lookup
+ return pn.inode.Child(name, nil)
+ }
+ return pn.inode.Child(name, func(existing inode) inode {
+ if repl := replace(existing); repl == nil {
+ // delete
+ // (TODO)
+ return pn.Child(name, nil) // not implemented
+ } else if repl.FileInfo().IsDir() {
+ // mkdir
+ // TODO: repl.SetParent(pn, name), etc.
+ return pn.Child(name, nil) // not implemented
+ } else {
+ // create file
+ // TODO: repl.SetParent(pn, name), etc.
+ return pn.Child(name, nil) // not implemented
+ }
+ })
+}
inodes: make(map[string]inode),
}
root.inode.Child("by_id", func(inode) inode {
- var vn inode
- vn = &vdirnode{
+ return &vdirnode{
inode: &treenode{
fs: fs,
parent: fs.root,
},
create: fs.mountCollection,
}
- return vn
+ })
+ root.inode.Child("home", func(inode) inode {
+ return fs.newProjectNode(fs.root, "home", "")
})
return fs
}
return root
}
+func (fs *siteFileSystem) newProjectNode(root inode, name, uuid string) inode {
+ return &projectnode{
+ uuid: uuid,
+ inode: &treenode{
+ fs: fs,
+ parent: root,
+ inodes: make(map[string]inode),
+ fileinfo: fileinfo{
+ name: name,
+ modTime: time.Now(),
+ mode: 0755 | os.ModeDir,
+ },
+ },
+ }
+}
+
// vdirnode wraps an inode by ignoring any requests to add/replace
// children, and calling a create() func when a non-existing child is
// looked up.