1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
16 "git.arvados.org/arvados.git/lib/ctrlctx"
17 "git.arvados.org/arvados.git/sdk/go/arvados"
18 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
19 "git.arvados.org/arvados.git/sdk/go/auth"
20 "git.arvados.org/arvados.git/sdk/go/httpserver"
23 // CollectionGet defers to railsProxy for everything except blob
25 func (conn *Conn) CollectionGet(ctx context.Context, opts arvados.GetOptions) (arvados.Collection, error) {
27 if len(opts.Select) > 0 {
28 // We need to know IsTrashed and TrashAt to implement
29 // signing properly, even if the caller doesn't want
31 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
33 resp, err := conn.railsProxy.CollectionGet(ctx, opts)
37 conn.signCollection(ctx, &resp)
41 // CollectionList defers to railsProxy for everything except blob
43 func (conn *Conn) CollectionList(ctx context.Context, opts arvados.ListOptions) (arvados.CollectionList, error) {
45 if len(opts.Select) > 0 {
46 // We need to know IsTrashed and TrashAt to implement
47 // signing properly, even if the caller doesn't want
49 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
51 resp, err := conn.railsProxy.CollectionList(ctx, opts)
55 for i := range resp.Items {
56 conn.signCollection(ctx, &resp.Items[i])
61 // CollectionCreate defers to railsProxy for everything except blob
62 // signatures and vocabulary checking.
63 func (conn *Conn) CollectionCreate(ctx context.Context, opts arvados.CreateOptions) (arvados.Collection, error) {
65 err := conn.checkProperties(ctx, opts.Attrs["properties"])
67 return arvados.Collection{}, err
69 if len(opts.Select) > 0 {
70 // We need to know IsTrashed and TrashAt to implement
71 // signing properly, even if the caller doesn't want
73 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
75 if opts.Attrs, err = conn.applyReplaceFilesOption(ctx, "", opts.Attrs, opts.ReplaceFiles); err != nil {
76 return arvados.Collection{}, err
78 resp, err := conn.railsProxy.CollectionCreate(ctx, opts)
82 conn.signCollection(ctx, &resp)
86 // CollectionUpdate defers to railsProxy for everything except blob
87 // signatures and vocabulary checking.
88 func (conn *Conn) CollectionUpdate(ctx context.Context, opts arvados.UpdateOptions) (arvados.Collection, error) {
90 err := conn.checkProperties(ctx, opts.Attrs["properties"])
92 return arvados.Collection{}, err
94 if len(opts.Select) > 0 {
95 // We need to know IsTrashed and TrashAt to implement
96 // signing properly, even if the caller doesn't want
98 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
100 err = conn.lockUUID(ctx, opts.UUID)
102 return arvados.Collection{}, err
104 if opts.Attrs, err = conn.applyReplaceFilesOption(ctx, opts.UUID, opts.Attrs, opts.ReplaceFiles); err != nil {
105 return arvados.Collection{}, err
107 resp, err := conn.railsProxy.CollectionUpdate(ctx, opts)
111 conn.signCollection(ctx, &resp)
115 func (conn *Conn) signCollection(ctx context.Context, coll *arvados.Collection) {
116 if coll.IsTrashed || coll.ManifestText == "" || !conn.cluster.Collections.BlobSigning {
120 if creds, ok := auth.FromContext(ctx); ok && len(creds.Tokens) > 0 {
121 token = creds.Tokens[0]
126 ttl := conn.cluster.Collections.BlobSigningTTL.Duration()
127 exp := time.Now().Add(ttl)
128 if coll.TrashAt != nil && !coll.TrashAt.IsZero() && coll.TrashAt.Before(exp) {
131 coll.ManifestText = arvados.SignManifest(coll.ManifestText, token, exp, ttl, []byte(conn.cluster.Collections.BlobSigningKey))
134 func (conn *Conn) lockUUID(ctx context.Context, uuid string) error {
135 tx, err := ctrlctx.CurrentTx(ctx)
139 _, err = tx.ExecContext(ctx, `insert into uuid_locks (uuid) values ($1) on conflict (uuid) do update set n=uuid_locks.n+1`, uuid)
146 // If replaceFiles is non-empty, populate attrs["manifest_text"] by
147 // starting with the content of fromUUID (or an empty collection if
148 // fromUUID is empty) and applying the specified file/directory
151 // Return value is the (possibly modified) attrs map.
152 func (conn *Conn) applyReplaceFilesOption(ctx context.Context, fromUUID string, attrs map[string]interface{}, replaceFiles map[string]string) (map[string]interface{}, error) {
153 if len(replaceFiles) == 0 {
157 providedManifestText, _ := attrs["manifest_text"].(string)
158 if providedManifestText != "" {
160 for _, src := range replaceFiles {
161 if strings.HasPrefix(src, "manifest_text/") {
167 return nil, httpserver.Errorf(http.StatusBadRequest, "invalid request: attrs['manifest_text'] was provided, but would not be used because it is not referenced by any 'replace_files' entry")
171 // Load the current collection (if any) and set up an
172 // in-memory filesystem.
173 var dst arvados.Collection
174 if _, replacingRoot := replaceFiles["/"]; !replacingRoot && fromUUID != "" {
175 src, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: fromUUID})
181 dstfs, err := dst.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
186 // Sort replacements by source collection to avoid redundant
187 // reloads when a source collection is used more than
188 // once. Note empty sources (which mean "delete target path")
190 dstTodo := make([]string, 0, len(replaceFiles))
192 srcid := make(map[string]string, len(replaceFiles))
193 for dst, src := range replaceFiles {
194 dstTodo = append(dstTodo, dst)
195 if i := strings.IndexRune(src, '/'); i > 0 {
199 sort.Slice(dstTodo, func(i, j int) bool {
200 return srcid[dstTodo[i]] < srcid[dstTodo[j]]
204 // Reject attempt to replace a node as well as its descendant
205 // (e.g., a/ and a/b/), which is unsupported, except where the
206 // source for a/ is empty (i.e., delete).
207 for _, dst := range dstTodo {
208 if dst != "/" && (strings.HasSuffix(dst, "/") ||
209 strings.HasSuffix(dst, "/.") ||
210 strings.HasSuffix(dst, "/..") ||
211 strings.Contains(dst, "//") ||
212 strings.Contains(dst, "/./") ||
213 strings.Contains(dst, "/../") ||
214 !strings.HasPrefix(dst, "/")) {
215 return nil, httpserver.Errorf(http.StatusBadRequest, "invalid replace_files target: %q", dst)
217 for i := 0; i < len(dst)-1; i++ {
225 if outersrc := replaceFiles[outerdst]; outersrc != "" {
226 return nil, httpserver.Errorf(http.StatusBadRequest, "replace_files: cannot operate on target %q inside non-empty target %q", dst, outerdst)
231 current := make(map[string]*arvados.Subtree)
232 // Check whether any sources are "current/...", and if so,
233 // populate current with the relevant snapshot. Doing this
234 // ahead of time, before making any modifications to dstfs
235 // below, ensures that even instructions like {/a: current/b,
236 // b: current/a} will be handled correctly.
237 for _, src := range replaceFiles {
238 if strings.HasPrefix(src, "current/") && current[src] == nil {
239 current[src], err = arvados.Snapshot(dstfs, src[8:])
241 return nil, fmt.Errorf("%s: %w", src, err)
246 var srcidloaded string
247 var srcfs arvados.FileSystem
248 // Apply the requested replacements.
249 for _, dst := range dstTodo {
250 src := replaceFiles[dst]
253 // In this case we started with a
254 // blank manifest, so there can't be
255 // anything to delete.
258 err := dstfs.RemoveAll(dst)
260 return nil, fmt.Errorf("RemoveAll(%s): %w", dst, err)
264 var snap *arvados.Subtree
265 srcspec := strings.SplitN(src, "/", 2)
266 srcid, srcpath := srcspec[0], "/"
267 if len(srcspec) == 2 && srcspec[1] != "" {
271 case srcid == "current":
274 return nil, fmt.Errorf("internal error: current[%s] == nil", src)
276 case srcid == "manifest_text":
277 if srcidloaded == srcid {
281 srccoll := &arvados.Collection{ManifestText: providedManifestText}
282 srcfs, err = srccoll.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
287 case arvadosclient.PDHMatch(srcid):
288 if srcidloaded == srcid {
292 srccoll, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: srcid})
296 // We use StubClient here because we don't
297 // want srcfs to read/write any file data or
298 // sync collection state to/from the database.
299 srcfs, err = srccoll.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
305 return nil, httpserver.Errorf(http.StatusBadRequest, "invalid source %q for replace_files[%q]: must be \"\" or \"SRC\" or \"SRC/path\" where SRC is \"current\", \"manifest_text\", or a portable data hash", src, dst)
308 snap, err = arvados.Snapshot(srcfs, srcpath)
310 return nil, httpserver.Errorf(http.StatusBadRequest, "error getting snapshot of %q from %q: %w", srcpath, srcid, err)
313 // Create intermediate dirs, in case dst is
314 // "newdir1/newdir2/dst".
315 for i := 1; i < len(dst)-1; i++ {
317 err = dstfs.Mkdir(dst[:i], 0777)
318 if err != nil && !os.IsExist(err) {
319 return nil, httpserver.Errorf(http.StatusBadRequest, "error creating parent dirs for %q: %w", dst, err)
323 err = arvados.Splice(dstfs, dst, snap)
325 return nil, fmt.Errorf("error splicing snapshot onto path %q: %w", dst, err)
328 mtxt, err := dstfs.MarshalManifest(".")
333 attrs = make(map[string]interface{}, 1)
335 attrs["manifest_text"] = mtxt