20318: Use one tidying goroutine and filehandle pool per cache dir.
[arvados.git] / sdk / go / arvados / fs_project.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         "log"
9         "os"
10         "strings"
11         "time"
12 )
13
14 func (fs *customFileSystem) defaultUUID(uuid string) (string, error) {
15         if uuid != "" {
16                 return uuid, nil
17         }
18         var resp User
19         err := fs.RequestAndDecode(&resp, "GET", "arvados/v1/users/current", nil, nil)
20         if err != nil {
21                 return "", err
22         }
23         return resp.UUID, nil
24 }
25
26 // loadOneChild loads only the named child, if it exists.
27 func (fs *customFileSystem) projectsLoadOne(parent inode, uuid, name string) (inode, error) {
28         uuid, err := fs.defaultUUID(uuid)
29         if err != nil {
30                 return nil, err
31         }
32
33         var contents CollectionList
34         for _, subst := range []string{"/", fs.forwardSlashNameSubstitution} {
35                 contents = CollectionList{}
36                 err = fs.RequestAndDecode(&contents, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, ResourceListParams{
37                         Count: "none",
38                         Order: "uuid",
39                         Filters: []Filter{
40                                 {"name", "=", strings.Replace(name, subst, "/", -1)},
41                                 {"uuid", "is_a", []string{"arvados#collection", "arvados#group"}},
42                                 {"groups.group_class", "in", []string{"project", "filter"}},
43                         },
44                         Select: []string{"uuid", "name", "modified_at", "properties"},
45                 })
46                 if err != nil {
47                         return nil, err
48                 }
49                 if len(contents.Items) > 0 || fs.forwardSlashNameSubstitution == "/" || fs.forwardSlashNameSubstitution == "" || !strings.Contains(name, fs.forwardSlashNameSubstitution) {
50                         break
51                 }
52                 // If the requested name contains the configured "/"
53                 // replacement string and didn't match a
54                 // project/collection exactly, we'll try again with
55                 // "/" in its place, so a lookup of a munged name
56                 // works regardless of whether the directory listing
57                 // has been populated with escaped names.
58                 //
59                 // Note this doesn't handle items whose names contain
60                 // both "/" and the substitution string.
61         }
62         if len(contents.Items) == 0 {
63                 return nil, nil
64         }
65         coll := contents.Items[0]
66
67         if strings.Contains(coll.UUID, "-j7d0g-") {
68                 // Group item was loaded into a Collection var -- but
69                 // we only need the Name and UUID anyway, so it's OK.
70                 return &hardlink{
71                         inode: fs.projectSingleton(coll.UUID, &Group{
72                                 UUID:       coll.UUID,
73                                 Name:       coll.Name,
74                                 ModifiedAt: coll.ModifiedAt,
75                                 Properties: coll.Properties,
76                         }),
77                         parent: parent,
78                         name:   coll.Name,
79                 }, nil
80         } else if strings.Contains(coll.UUID, "-4zz18-") {
81                 return fs.newDeferredCollectionDir(parent, name, coll.UUID, coll.ModifiedAt, coll.Properties), nil
82         } else {
83                 log.Printf("group contents: unrecognized UUID in response: %q", coll.UUID)
84                 return nil, ErrInvalidArgument
85         }
86 }
87
88 func (fs *customFileSystem) projectsLoadAll(parent inode, uuid string) ([]inode, error) {
89         uuid, err := fs.defaultUUID(uuid)
90         if err != nil {
91                 return nil, err
92         }
93
94         pagesize := 100000
95         var inodes []inode
96
97         // When #17424 is resolved, remove the outer loop here and use
98         // []string{"arvados#collection", "arvados#group"} directly as the uuid
99         // filter.
100         for _, class := range []string{"arvados#collection", "arvados#group"} {
101                 // Note: the "filters" slice's backing array might be reused
102                 // by append(filters,...) below. This isn't goroutine safe,
103                 // but all accesses are in the same goroutine, so it's OK.
104                 filters := []Filter{
105                         {"uuid", "is_a", class},
106                 }
107                 if class == "arvados#group" {
108                         filters = append(filters, Filter{"groups.group_class", "in", []string{"project", "filter"}})
109                 }
110
111                 params := ResourceListParams{
112                         Count:   "none",
113                         Filters: filters,
114                         Order:   "uuid",
115                         Select:  []string{"uuid", "name", "modified_at", "properties"},
116                         Limit:   &pagesize,
117                 }
118
119                 for {
120                         // The groups content endpoint returns
121                         // Collection and Group (project)
122                         // objects. This function only accesses the
123                         // UUID, Name, and ModifiedAt fields. Both
124                         // collections and groups have those fields,
125                         // so it is easier to just treat the
126                         // ObjectList that comes back as a
127                         // CollectionList.
128                         var resp CollectionList
129                         err = fs.RequestAndDecode(&resp, "GET", "arvados/v1/groups/"+uuid+"/contents", nil, params)
130                         if err != nil {
131                                 return nil, err
132                         }
133                         if len(resp.Items) == 0 {
134                                 break
135                         }
136                         for _, i := range resp.Items {
137                                 if fs.forwardSlashNameSubstitution != "" {
138                                         i.Name = strings.Replace(i.Name, "/", fs.forwardSlashNameSubstitution, -1)
139                                 }
140                                 if !permittedName(i.Name) {
141                                         continue
142                                 }
143                                 if strings.Contains(i.UUID, "-j7d0g-") {
144                                         inodes = append(inodes, fs.newProjectDir(parent, i.Name, i.UUID, &Group{
145                                                 UUID:       i.UUID,
146                                                 Name:       i.Name,
147                                                 ModifiedAt: i.ModifiedAt,
148                                                 Properties: i.Properties,
149                                         }))
150                                 } else if strings.Contains(i.UUID, "-4zz18-") {
151                                         inodes = append(inodes, fs.newDeferredCollectionDir(parent, i.Name, i.UUID, i.ModifiedAt, i.Properties))
152                                 } else {
153                                         log.Printf("group contents: unrecognized UUID in response: %q", i.UUID)
154                                         return nil, ErrInvalidArgument
155                                 }
156                         }
157                         params.Filters = append(filters, Filter{"uuid", ">", resp.Items[len(resp.Items)-1].UUID})
158                 }
159         }
160         return inodes, nil
161 }
162
163 func (fs *customFileSystem) newProjectDir(parent inode, name, uuid string, proj *Group) inode {
164         return &hardlink{inode: fs.projectSingleton(uuid, proj), parent: parent, name: name}
165 }
166
167 func (fs *customFileSystem) newDeferredCollectionDir(parent inode, name, uuid string, modTime time.Time, props map[string]interface{}) inode {
168         if modTime.IsZero() {
169                 modTime = time.Now()
170         }
171         placeholder := &treenode{
172                 fs:     fs,
173                 parent: parent,
174                 inodes: nil,
175                 fileinfo: fileinfo{
176                         name:    name,
177                         modTime: modTime,
178                         mode:    0755 | os.ModeDir,
179                         sys:     func() interface{} { return &Collection{UUID: uuid, Name: name, ModifiedAt: modTime, Properties: props} },
180                 },
181         }
182         return &deferrednode{wrapped: placeholder, create: func() inode {
183                 node, err := fs.collectionSingleton(uuid)
184                 if err != nil {
185                         log.Printf("BUG: unhandled error: %s", err)
186                         return placeholder
187                 }
188                 return &hardlink{inode: node, parent: parent, name: name}
189         }}
190 }