package arvados
import (
+ "log"
"os"
- "sync"
+ "strings"
"time"
)
-type staleChecker struct {
- mtx sync.Mutex
- last time.Time
-}
-
-func (sc *staleChecker) DoIfStale(fn func(), staleFunc func(time.Time) bool) {
- sc.mtx.Lock()
- defer sc.mtx.Unlock()
- if !staleFunc(sc.last) {
- return
+func (fs *customFileSystem) defaultUUID(uuid string) (string, error) {
+ if uuid != "" {
+ return uuid, nil
}
- sc.last = time.Now()
- fn()
-}
-
-// projectnode exposes an Arvados project as a filesystem directory.
-type projectnode struct {
- inode
- staleChecker
- uuid string
- err error
+ var resp User
+ err := fs.RequestAndDecode(&resp, "GET", "arvados/v1/users/current", nil, nil)
+ if err != nil {
+ return "", err
+ }
+ return resp.UUID, nil
}
-func (pn *projectnode) load() {
- fs := pn.FS().(*customFileSystem)
-
- 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
+// loadOneChild loads only the named child, if it exists.
+func (fs *customFileSystem) projectsLoadOne(parent inode, uuid, name string) (inode, error) {
+ uuid, err := fs.defaultUUID(uuid)
+ if err != nil {
+ return nil, err
}
- 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 {
- return
+
+ var contents CollectionList
+ for _, subst := range []string{"/", fs.forwardSlashNameSubstitution} {
+ contents = CollectionList{}
+ err = fs.RequestAndDecode(&contents, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, ResourceListParams{
+ Count: "none",
+ Filters: []Filter{
+ {"name", "=", strings.Replace(name, subst, "/", -1)},
+ {"uuid", "is_a", []string{"arvados#collection", "arvados#group"}},
+ {"groups.group_class", "=", "project"},
+ },
+ Select: []string{"uuid", "name", "modified_at", "properties"},
+ })
+ if err != nil {
+ return nil, err
}
- if len(resp.Items) == 0 {
+ if len(contents.Items) > 0 || fs.forwardSlashNameSubstitution == "/" || fs.forwardSlashNameSubstitution == "" || !strings.Contains(name, fs.forwardSlashNameSubstitution) {
break
}
- for _, i := range resp.Items {
- coll := i
- if coll.Name == "" {
- continue
- }
- pn.inode.Child(coll.Name, func(inode) (inode, error) {
- return deferredCollectionFS(fs, pn, coll), nil
- })
- }
- params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
+ // If the requested name contains the configured "/"
+ // replacement string and didn't match a
+ // project/collection exactly, we'll try again with
+ // "/" in its place, so a lookup of a munged name
+ // works regardless of whether the directory listing
+ // has been populated with escaped names.
+ //
+ // Note this doesn't handle items whose names contain
+ // both "/" and the substitution string.
+ }
+ if len(contents.Items) == 0 {
+ return nil, nil
+ }
+ coll := contents.Items[0]
+
+ if strings.Contains(coll.UUID, "-j7d0g-") {
+ // Group item was loaded into a Collection var -- but
+ // we only need the Name and UUID anyway, so it's OK.
+ return &hardlink{
+ inode: fs.projectSingleton(coll.UUID, &Group{
+ UUID: coll.UUID,
+ Name: coll.Name,
+ ModifiedAt: coll.ModifiedAt,
+ Properties: coll.Properties,
+ }),
+ parent: parent,
+ name: coll.Name,
+ }, nil
+ } else if strings.Contains(coll.UUID, "-4zz18-") {
+ return fs.newDeferredCollectionDir(parent, name, coll.UUID, coll.ModifiedAt, coll.Properties), nil
+ } else {
+ log.Printf("group contents: unrecognized UUID in response: %q", coll.UUID)
+ return nil, ErrInvalidArgument
+ }
+}
+
+func (fs *customFileSystem) projectsLoadAll(parent inode, uuid string) ([]inode, error) {
+ uuid, err := fs.defaultUUID(uuid)
+ if err != nil {
+ return nil, err
}
- 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 {
- return
+ pagesize := 100000
+ var inodes []inode
+
+ // When #17424 is resolved, remove the outer loop here and use
+ // []string{"arvados#collection", "arvados#group"} directly as the uuid
+ // filter.
+ for _, class := range []string{"arvados#collection", "arvados#group"} {
+ // Note: the "filters" slice's backing array might be reused
+ // by append(filters,...) below. This isn't goroutine safe,
+ // but all accesses are in the same goroutine, so it's OK.
+ filters := []Filter{
+ {"uuid", "is_a", class},
}
- if len(resp.Items) == 0 {
- break
+ if class == "arvados#group" {
+ filters = append(filters, Filter{"group_class", "=", "project"})
}
- for _, group := range resp.Items {
- if group.Name == "" || group.Name == "." || group.Name == ".." {
- continue
+
+ params := ResourceListParams{
+ Count: "none",
+ Filters: filters,
+ Order: "uuid",
+ Select: []string{"uuid", "name", "modified_at", "properties"},
+ Limit: &pagesize,
+ }
+
+ for {
+ // The groups content endpoint returns
+ // Collection and Group (project)
+ // objects. This function only accesses the
+ // UUID, Name, and ModifiedAt fields. Both
+ // collections and groups have those fields,
+ // so it is easier to just treat the
+ // ObjectList that comes back as a
+ // CollectionList.
+ var resp CollectionList
+ err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, params)
+ if err != nil {
+ return nil, err
+ }
+ if len(resp.Items) == 0 {
+ break
}
- pn.inode.Child(group.Name, func(inode) (inode, error) {
- return fs.newProjectNode(pn, group.Name, group.UUID), nil
- })
+ for _, i := range resp.Items {
+ if fs.forwardSlashNameSubstitution != "" {
+ i.Name = strings.Replace(i.Name, "/", fs.forwardSlashNameSubstitution, -1)
+ }
+ if !permittedName(i.Name) {
+ continue
+ }
+ if strings.Contains(i.UUID, "-j7d0g-") {
+ inodes = append(inodes, fs.newProjectDir(parent, i.Name, i.UUID, &Group{
+ UUID: i.UUID,
+ Name: i.Name,
+ ModifiedAt: i.ModifiedAt,
+ Properties: i.Properties,
+ }))
+ } else if strings.Contains(i.UUID, "-4zz18-") {
+ inodes = append(inodes, fs.newDeferredCollectionDir(parent, i.Name, i.UUID, i.ModifiedAt, i.Properties))
+ } else {
+ log.Printf("group contents: unrecognized UUID in response: %q", i.UUID)
+ return nil, ErrInvalidArgument
+ }
+ }
+ params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
}
- params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
}
- pn.err = nil
+ return inodes, nil
}
-func (pn *projectnode) Readdir() ([]os.FileInfo, error) {
- pn.staleChecker.DoIfStale(pn.load, pn.FS().(*customFileSystem).Stale)
- if pn.err != nil {
- return nil, pn.err
- }
- return pn.inode.Readdir()
+func (fs *customFileSystem) newProjectDir(parent inode, name, uuid string, proj *Group) inode {
+ return &hardlink{inode: fs.projectSingleton(uuid, proj), parent: parent, name: name}
}
-func (pn *projectnode) Child(name string, replace func(inode) (inode, error)) (inode, error) {
- pn.staleChecker.DoIfStale(pn.load, pn.FS().(*customFileSystem).Stale)
- if pn.err != nil {
- return nil, pn.err
+func (fs *customFileSystem) newDeferredCollectionDir(parent inode, name, uuid string, modTime time.Time, props map[string]interface{}) inode {
+ if modTime.IsZero() {
+ modTime = time.Now()
}
- if replace == nil {
- // lookup
- return pn.inode.Child(name, nil)
+ placeholder := &treenode{
+ fs: fs,
+ parent: parent,
+ inodes: nil,
+ fileinfo: fileinfo{
+ name: name,
+ modTime: modTime,
+ mode: 0755 | os.ModeDir,
+ sys: func() interface{} { return &Collection{UUID: uuid, Name: name, ModifiedAt: modTime, Properties: props} },
+ },
}
- return pn.inode.Child(name, func(existing inode) (inode, error) {
- if repl, err := replace(existing); err != nil {
- return existing, err
- } else if repl == nil {
- if existing == nil {
- return nil, nil
- }
- // rmdir
- // (TODO)
- return existing, ErrInvalidArgument
- } else if existing != nil {
- // clobber
- return existing, ErrInvalidArgument
- } else if repl.FileInfo().IsDir() {
- // mkdir
- // TODO: repl.SetParent(pn, name), etc.
- return existing, ErrInvalidArgument
- } else {
- // create file
- return existing, ErrInvalidArgument
+ return &deferrednode{wrapped: placeholder, create: func() inode {
+ node, err := fs.collectionSingleton(uuid)
+ if err != nil {
+ log.Printf("BUG: unhandled error: %s", err)
+ return placeholder
}
- })
+ return &hardlink{inode: node, parent: parent, name: name}
+ }}
}