1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
16 "git.arvados.org/arvados.git/sdk/go/arvados"
17 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
18 "git.arvados.org/arvados.git/sdk/go/auth"
19 "git.arvados.org/arvados.git/sdk/go/httpserver"
22 // CollectionGet defers to railsProxy for everything except blob
24 func (conn *Conn) CollectionGet(ctx context.Context, opts arvados.GetOptions) (arvados.Collection, error) {
26 if len(opts.Select) > 0 {
27 // We need to know IsTrashed and TrashAt to implement
28 // signing properly, even if the caller doesn't want
30 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
32 resp, err := conn.railsProxy.CollectionGet(ctx, opts)
36 conn.signCollection(ctx, &resp)
40 // CollectionList defers to railsProxy for everything except blob
42 func (conn *Conn) CollectionList(ctx context.Context, opts arvados.ListOptions) (arvados.CollectionList, error) {
44 if len(opts.Select) > 0 {
45 // We need to know IsTrashed and TrashAt to implement
46 // signing properly, even if the caller doesn't want
48 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
50 resp, err := conn.railsProxy.CollectionList(ctx, opts)
54 for i := range resp.Items {
55 conn.signCollection(ctx, &resp.Items[i])
60 // CollectionCreate defers to railsProxy for everything except blob
61 // signatures and vocabulary checking.
62 func (conn *Conn) CollectionCreate(ctx context.Context, opts arvados.CreateOptions) (arvados.Collection, error) {
64 err := conn.checkProperties(ctx, opts.Attrs["properties"])
66 return arvados.Collection{}, err
68 if len(opts.Select) > 0 {
69 // We need to know IsTrashed and TrashAt to implement
70 // signing properly, even if the caller doesn't want
72 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
74 if opts.Attrs, err = conn.applyReplaceFilesOption(ctx, "", opts.Attrs, opts.ReplaceFiles); err != nil {
75 return arvados.Collection{}, err
77 resp, err := conn.railsProxy.CollectionCreate(ctx, opts)
81 conn.signCollection(ctx, &resp)
85 // CollectionUpdate defers to railsProxy for everything except blob
86 // signatures and vocabulary checking.
87 func (conn *Conn) CollectionUpdate(ctx context.Context, opts arvados.UpdateOptions) (arvados.Collection, error) {
89 err := conn.checkProperties(ctx, opts.Attrs["properties"])
91 return arvados.Collection{}, err
93 if len(opts.Select) > 0 {
94 // We need to know IsTrashed and TrashAt to implement
95 // signing properly, even if the caller doesn't want
97 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
99 if opts.Attrs, err = conn.applyReplaceFilesOption(ctx, opts.UUID, opts.Attrs, opts.ReplaceFiles); err != nil {
100 return arvados.Collection{}, err
102 resp, err := conn.railsProxy.CollectionUpdate(ctx, opts)
106 conn.signCollection(ctx, &resp)
110 func (conn *Conn) signCollection(ctx context.Context, coll *arvados.Collection) {
111 if coll.IsTrashed || coll.ManifestText == "" || !conn.cluster.Collections.BlobSigning {
115 if creds, ok := auth.FromContext(ctx); ok && len(creds.Tokens) > 0 {
116 token = creds.Tokens[0]
121 ttl := conn.cluster.Collections.BlobSigningTTL.Duration()
122 exp := time.Now().Add(ttl)
123 if coll.TrashAt != nil && !coll.TrashAt.IsZero() && coll.TrashAt.Before(exp) {
126 coll.ManifestText = arvados.SignManifest(coll.ManifestText, token, exp, ttl, []byte(conn.cluster.Collections.BlobSigningKey))
129 // If replaceFiles is non-empty, populate attrs["manifest_text"] by
130 // starting with the content of fromUUID (or an empty collection if
131 // fromUUID is empty) and applying the specified file/directory
134 // Return value is the (possibly modified) attrs map.
135 func (conn *Conn) applyReplaceFilesOption(ctx context.Context, fromUUID string, attrs map[string]interface{}, replaceFiles map[string]string) (map[string]interface{}, error) {
136 if len(replaceFiles) == 0 {
138 } else if mtxt, ok := attrs["manifest_text"].(string); ok && len(mtxt) > 0 {
139 return nil, httpserver.Errorf(http.StatusBadRequest, "ambiguous request: both 'replace_files' and attrs['manifest_text'] values provided")
142 // Load the current collection (if any) and set up an
143 // in-memory filesystem.
144 var dst arvados.Collection
145 if _, replacingRoot := replaceFiles["/"]; !replacingRoot && fromUUID != "" {
146 src, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: fromUUID})
152 dstfs, err := dst.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
157 // Sort replacements by source collection to avoid redundant
158 // reloads when a source collection is used more than
159 // once. Note empty sources (which mean "delete target path")
161 dstTodo := make([]string, 0, len(replaceFiles))
163 srcid := make(map[string]string, len(replaceFiles))
164 for dst, src := range replaceFiles {
165 dstTodo = append(dstTodo, dst)
166 if i := strings.IndexRune(src, '/'); i > 0 {
170 sort.Slice(dstTodo, func(i, j int) bool {
171 return srcid[dstTodo[i]] < srcid[dstTodo[j]]
175 // Reject attempt to replace a node as well as its descendant
176 // (e.g., a/ and a/b/), which is unsupported, except where the
177 // source for a/ is empty (i.e., delete).
178 for _, dst := range dstTodo {
179 if dst != "/" && (strings.HasSuffix(dst, "/") ||
180 strings.HasSuffix(dst, "/.") ||
181 strings.HasSuffix(dst, "/..") ||
182 strings.Contains(dst, "//") ||
183 strings.Contains(dst, "/./") ||
184 strings.Contains(dst, "/../") ||
185 !strings.HasPrefix(dst, "/")) {
186 return nil, httpserver.Errorf(http.StatusBadRequest, "invalid replace_files target: %q", dst)
188 for i := 0; i < len(dst)-1; i++ {
196 if outersrc := replaceFiles[outerdst]; outersrc != "" {
197 return nil, httpserver.Errorf(http.StatusBadRequest, "replace_files: cannot operate on target %q inside non-empty target %q", dst, outerdst)
202 var srcidloaded string
203 var srcfs arvados.FileSystem
204 // Apply the requested replacements.
205 for _, dst := range dstTodo {
206 src := replaceFiles[dst]
209 // In this case we started with a
210 // blank manifest, so there can't be
211 // anything to delete.
214 err := dstfs.RemoveAll(dst)
216 return nil, fmt.Errorf("RemoveAll(%s): %w", dst, err)
220 srcspec := strings.SplitN(src, "/", 2)
221 srcid, srcpath := srcspec[0], "/"
222 if !arvadosclient.PDHMatch(srcid) {
223 return nil, httpserver.Errorf(http.StatusBadRequest, "invalid source %q for replace_files[%q]: must be \"\" or \"PDH\" or \"PDH/path\"", src, dst)
225 if len(srcspec) == 2 && srcspec[1] != "" {
228 if srcidloaded != srcid {
230 srccoll, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: srcid})
234 // We use StubClient here because we don't
235 // want srcfs to read/write any file data or
236 // sync collection state to/from the database.
237 srcfs, err = srccoll.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
243 snap, err := arvados.Snapshot(srcfs, srcpath)
245 return nil, httpserver.Errorf(http.StatusBadRequest, "error getting snapshot of %q from %q: %w", srcpath, srcid, err)
247 // Create intermediate dirs, in case dst is
248 // "newdir1/newdir2/dst".
249 for i := 1; i < len(dst)-1; i++ {
251 err = dstfs.Mkdir(dst[:i], 0777)
252 if err != nil && !os.IsExist(err) {
253 return nil, httpserver.Errorf(http.StatusBadRequest, "error creating parent dirs for %q: %w", dst, err)
257 err = arvados.Splice(dstfs, dst, snap)
259 return nil, fmt.Errorf("error splicing snapshot onto path %q: %w", dst, err)
262 mtxt, err := dstfs.MarshalManifest(".")
267 attrs = make(map[string]interface{}, 1)
269 attrs["manifest_text"] = mtxt