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