13111: Add projects hierarchy under "home".
authorTom Clegg <tclegg@veritasgenetics.com>
Wed, 31 Jan 2018 21:57:30 +0000 (16:57 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Wed, 31 Jan 2018 21:57:30 +0000 (16:57 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

sdk/go/arvados/fs_deferred.go [new file with mode: 0644]
sdk/go/arvados/fs_project.go [new file with mode: 0644]
sdk/go/arvados/fs_site.go

diff --git a/sdk/go/arvados/fs_deferred.go b/sdk/go/arvados/fs_deferred.go
new file mode 100644 (file)
index 0000000..e638838
--- /dev/null
@@ -0,0 +1,103 @@
+// 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() }
diff --git a/sdk/go/arvados/fs_project.go b/sdk/go/arvados/fs_project.go
new file mode 100644 (file)
index 0000000..4dd8699
--- /dev/null
@@ -0,0 +1,113 @@
+// 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
+               }
+       })
+}
index 37ec8a33133d9c4b5ead41094e634d09d0743ddd..c8d7360ae420bfdd46b8325c5a6abc7eb9c058b3 100644 (file)
@@ -38,8 +38,7 @@ func (c *Client) SiteFileSystem(kc keepClient) FileSystem {
                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,
@@ -52,7 +51,9 @@ func (c *Client) SiteFileSystem(kc keepClient) FileSystem {
                        },
                        create: fs.mountCollection,
                }
-               return vn
+       })
+       root.inode.Child("home", func(inode) inode {
+               return fs.newProjectNode(fs.root, "home", "")
        })
        return fs
 }
@@ -76,6 +77,22 @@ func (fs *siteFileSystem) mountCollection(parent inode, id string) inode {
        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.