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) {
25 if len(opts.Select) > 0 {
26 // We need to know IsTrashed and TrashAt to implement
27 // signing properly, even if the caller doesn't want
29 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
31 resp, err := conn.railsProxy.CollectionGet(ctx, opts)
35 conn.signCollection(ctx, &resp)
39 // CollectionList defers to railsProxy for everything except blob
41 func (conn *Conn) CollectionList(ctx context.Context, opts arvados.ListOptions) (arvados.CollectionList, error) {
42 if len(opts.Select) > 0 {
43 // We need to know IsTrashed and TrashAt to implement
44 // signing properly, even if the caller doesn't want
46 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
48 resp, err := conn.railsProxy.CollectionList(ctx, opts)
52 for i := range resp.Items {
53 conn.signCollection(ctx, &resp.Items[i])
58 // CollectionCreate defers to railsProxy for everything except blob
59 // signatures and vocabulary checking.
60 func (conn *Conn) CollectionCreate(ctx context.Context, opts arvados.CreateOptions) (arvados.Collection, error) {
61 err := conn.checkProperties(ctx, opts.Attrs["properties"])
63 return arvados.Collection{}, err
65 if len(opts.Select) > 0 {
66 // We need to know IsTrashed and TrashAt to implement
67 // signing properly, even if the caller doesn't want
69 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
71 if err := conn.applySplices(ctx, "", opts.Attrs); err != nil {
72 return arvados.Collection{}, err
74 resp, err := conn.railsProxy.CollectionCreate(ctx, opts)
78 conn.signCollection(ctx, &resp)
82 // CollectionUpdate defers to railsProxy for everything except blob
83 // signatures and vocabulary checking.
84 func (conn *Conn) CollectionUpdate(ctx context.Context, opts arvados.UpdateOptions) (arvados.Collection, error) {
85 err := conn.checkProperties(ctx, opts.Attrs["properties"])
87 return arvados.Collection{}, err
89 if len(opts.Select) > 0 {
90 // We need to know IsTrashed and TrashAt to implement
91 // signing properly, even if the caller doesn't want
93 opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
95 if err := conn.applySplices(ctx, opts.UUID, opts.Attrs); err != nil {
96 return arvados.Collection{}, err
98 resp, err := conn.railsProxy.CollectionUpdate(ctx, opts)
102 conn.signCollection(ctx, &resp)
106 func (conn *Conn) signCollection(ctx context.Context, coll *arvados.Collection) {
107 if coll.IsTrashed || coll.ManifestText == "" || !conn.cluster.Collections.BlobSigning {
111 if creds, ok := auth.FromContext(ctx); ok && len(creds.Tokens) > 0 {
112 token = creds.Tokens[0]
117 ttl := conn.cluster.Collections.BlobSigningTTL.Duration()
118 exp := time.Now().Add(ttl)
119 if coll.TrashAt != nil && !coll.TrashAt.IsZero() && coll.TrashAt.Before(exp) {
122 coll.ManifestText = arvados.SignManifest(coll.ManifestText, token, exp, ttl, []byte(conn.cluster.Collections.BlobSigningKey))
125 // If attrs["splices"] is present, populate attrs["manifest_text"] by
126 // starting with the content of fromUUID (or an empty collection if
127 // fromUUID is empty) and applying the specified splice operations.
128 func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[string]interface{}) error {
129 var splices map[string]string
131 // Validate the incoming attrs, and return early if the
132 // request doesn't ask for any splices.
133 if sp, ok := attrs["splices"]; !ok {
136 switch sp := sp.(type) {
138 return httpserver.Errorf(http.StatusBadRequest, "invalid type %T for splices parameter", sp)
141 case map[string]string:
143 case map[string]interface{}:
144 splices = make(map[string]string, len(sp))
145 for dst, src := range sp {
146 if src, ok := src.(string); ok {
149 return httpserver.Errorf(http.StatusBadRequest, "invalid source type for splice target %q: %v", dst, src)
153 if len(splices) == 0 {
155 } else if mtxt, ok := attrs["manifest_text"].(string); ok && len(mtxt) > 0 {
156 return httpserver.Errorf(http.StatusBadRequest, "ambiguous request: both 'splices' and 'manifest_text' values provided")
160 // Load the current collection (if any) and set up an
161 // in-memory filesystem.
162 var dst arvados.Collection
163 if _, rootsplice := splices["/"]; !rootsplice && fromUUID != "" {
164 src, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: fromUUID})
170 dstfs, err := dst.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
175 // Sort splices by source collection to avoid redundant
176 // reloads when a source collection is used more than
177 // once. Note empty sources (which mean "delete target path")
179 dstTodo := make([]string, 0, len(splices))
181 srcid := make(map[string]string, len(splices))
182 for dst, src := range splices {
183 dstTodo = append(dstTodo, dst)
184 if i := strings.IndexRune(src, '/'); i > 0 {
188 sort.Slice(dstTodo, func(i, j int) bool {
189 return srcid[dstTodo[i]] < srcid[dstTodo[j]]
193 // Reject attempt to splice a node as well as its descendant
194 // (e.g., a/ and a/b/), which is unsupported, except where the
195 // source for a/ is empty (i.e., delete).
196 for _, dst := range dstTodo {
197 if dst != "/" && (strings.HasSuffix(dst, "/") ||
198 strings.HasSuffix(dst, "/.") ||
199 strings.HasSuffix(dst, "/..") ||
200 strings.Contains(dst, "//") ||
201 strings.Contains(dst, "/./") ||
202 strings.Contains(dst, "/../") ||
203 !strings.HasPrefix(dst, "/")) {
204 return httpserver.Errorf(http.StatusBadRequest, "invalid splice target: %q", dst)
206 for i := 0; i < len(dst)-1; i++ {
214 if outersrc := splices[outerdst]; outersrc != "" {
215 return httpserver.Errorf(http.StatusBadRequest, "cannot splice at target %q with non-empty splice at %q", dst, outerdst)
220 var srcidloaded string
221 var srcfs arvados.FileSystem
222 // Apply the requested splices.
223 for _, dst := range dstTodo {
227 // In this case we started with a
228 // blank manifest, so there can't be
229 // anything to delete.
232 err := dstfs.RemoveAll(dst)
234 return fmt.Errorf("RemoveAll(%s): %w", dst, err)
238 srcspec := strings.SplitN(src, "/", 2)
239 srcid, srcpath := srcspec[0], "/"
240 if !arvadosclient.PDHMatch(srcid) {
241 return httpserver.Errorf(http.StatusBadRequest, "invalid source %q for splices[%q]: must be \"\" or \"PDH\" or \"PDH/path\"", src, dst)
243 if len(srcspec) == 2 && srcspec[1] != "" {
246 if srcidloaded != srcid {
248 srccoll, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: srcid})
252 // We use StubClient here because we don't
253 // want srcfs to read/write any file data or
254 // sync collection state to/from the database.
255 srcfs, err = srccoll.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
261 snap, err := arvados.Snapshot(srcfs, srcpath)
263 return httpserver.Errorf(http.StatusBadRequest, "error getting snapshot of %q from %q: %w", srcpath, srcid, err)
265 // Create intermediate dirs, in case dst is
266 // "newdir1/newdir2/dst".
267 for i := 1; i < len(dst)-1; i++ {
269 err = dstfs.Mkdir(dst[:i], 0777)
270 if err != nil && !os.IsExist(err) {
271 return httpserver.Errorf(http.StatusBadRequest, "error creating parent dirs for %q: %w", dst, err)
275 err = arvados.Splice(dstfs, dst, snap)
277 return fmt.Errorf("error splicing snapshot onto path %q: %w", dst, err)
280 mtxt, err := dstfs.MarshalManifest(".")
284 delete(attrs, "splices")
285 attrs["manifest_text"] = mtxt