Merge branch '18600-update-files'
[arvados.git] / lib / controller / localdb / collection.go
index 965b009f45c0996edc954ae708261e45984c2b5a..868e466e9e281bf7f4f5eaf8b4f7a530956653cf 100644 (file)
@@ -68,7 +68,7 @@ func (conn *Conn) CollectionCreate(ctx context.Context, opts arvados.CreateOptio
                // them.
                opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
        }
-       if err := conn.applySplices(ctx, "", opts.Attrs); err != nil {
+       if opts.Attrs, err = conn.applyReplaceFilesOption(ctx, "", opts.Attrs, opts.ReplaceFiles); err != nil {
                return arvados.Collection{}, err
        }
        resp, err := conn.railsProxy.CollectionCreate(ctx, opts)
@@ -92,7 +92,7 @@ func (conn *Conn) CollectionUpdate(ctx context.Context, opts arvados.UpdateOptio
                // them.
                opts.Select = append([]string{"is_trashed", "trash_at"}, opts.Select...)
        }
-       if err := conn.applySplices(ctx, opts.UUID, opts.Attrs); err != nil {
+       if opts.Attrs, err = conn.applyReplaceFilesOption(ctx, opts.UUID, opts.Attrs, opts.ReplaceFiles); err != nil {
                return arvados.Collection{}, err
        }
        resp, err := conn.railsProxy.CollectionUpdate(ctx, opts)
@@ -122,64 +122,42 @@ func (conn *Conn) signCollection(ctx context.Context, coll *arvados.Collection)
        coll.ManifestText = arvados.SignManifest(coll.ManifestText, token, exp, ttl, []byte(conn.cluster.Collections.BlobSigningKey))
 }
 
-// If attrs["splices"] is present, populate attrs["manifest_text"] by
+// If replaceFiles is non-empty, populate attrs["manifest_text"] by
 // starting with the content of fromUUID (or an empty collection if
-// fromUUID is empty) and applying the specified splice operations.
-func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[string]interface{}) error {
-       var splices map[string]string
-
-       // Validate the incoming attrs, and return early if the
-       // request doesn't ask for any splices.
-       if sp, ok := attrs["splices"]; !ok {
-               return nil
-       } else {
-               switch sp := sp.(type) {
-               default:
-                       return httpserver.Errorf(http.StatusBadRequest, "invalid type %T for splices parameter", sp)
-               case nil:
-                       return nil
-               case map[string]string:
-                       splices = sp
-               case map[string]interface{}:
-                       splices = make(map[string]string, len(sp))
-                       for dst, src := range sp {
-                               if src, ok := src.(string); ok {
-                                       splices[dst] = src
-                               } else {
-                                       return httpserver.Errorf(http.StatusBadRequest, "invalid source type for splice target %q: %v", dst, src)
-                               }
-                       }
-               }
-               if len(splices) == 0 {
-                       return nil
-               } else if mtxt, ok := attrs["manifest_text"].(string); ok && len(mtxt) > 0 {
-                       return httpserver.Errorf(http.StatusBadRequest, "ambiguous request: both 'splices' and 'manifest_text' values provided")
-               }
+// fromUUID is empty) and applying the specified file/directory
+// replacements.
+//
+// Return value is the (possibly modified) attrs map.
+func (conn *Conn) applyReplaceFilesOption(ctx context.Context, fromUUID string, attrs map[string]interface{}, replaceFiles map[string]string) (map[string]interface{}, error) {
+       if len(replaceFiles) == 0 {
+               return attrs, nil
+       } else if mtxt, ok := attrs["manifest_text"].(string); ok && len(mtxt) > 0 {
+               return nil, httpserver.Errorf(http.StatusBadRequest, "ambiguous request: both 'replace_files' and attrs['manifest_text'] values provided")
        }
 
        // Load the current collection (if any) and set up an
        // in-memory filesystem.
        var dst arvados.Collection
-       if _, rootsplice := splices["/"]; !rootsplice && fromUUID != "" {
+       if _, replacingRoot := replaceFiles["/"]; !replacingRoot && fromUUID != "" {
                src, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: fromUUID})
                if err != nil {
-                       return err
+                       return nil, err
                }
                dst = src
        }
        dstfs, err := dst.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
        if err != nil {
-               return err
+               return nil, err
        }
 
-       // Sort splices by source collection to avoid redundant
+       // Sort replacements by source collection to avoid redundant
        // reloads when a source collection is used more than
        // once. Note empty sources (which mean "delete target path")
        // sort first.
-       dstTodo := make([]string, 0, len(splices))
+       dstTodo := make([]string, 0, len(replaceFiles))
        {
-               srcid := make(map[string]string, len(splices))
-               for dst, src := range splices {
+               srcid := make(map[string]string, len(replaceFiles))
+               for dst, src := range replaceFiles {
                        dstTodo = append(dstTodo, dst)
                        if i := strings.IndexRune(src, '/'); i > 0 {
                                srcid[dst] = src[:i]
@@ -190,7 +168,7 @@ func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[s
                })
        }
 
-       // Reject attempt to splice a node as well as its descendant
+       // Reject attempt to replace a node as well as its descendant
        // (e.g., a/ and a/b/), which is unsupported, except where the
        // source for a/ is empty (i.e., delete).
        for _, dst := range dstTodo {
@@ -201,7 +179,7 @@ func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[s
                        strings.Contains(dst, "/./") ||
                        strings.Contains(dst, "/../") ||
                        !strings.HasPrefix(dst, "/")) {
-                       return httpserver.Errorf(http.StatusBadRequest, "invalid splice target: %q", dst)
+                       return nil, httpserver.Errorf(http.StatusBadRequest, "invalid replace_files target: %q", dst)
                }
                for i := 0; i < len(dst)-1; i++ {
                        if dst[i] != '/' {
@@ -211,17 +189,17 @@ func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[s
                        if outerdst == "" {
                                outerdst = "/"
                        }
-                       if outersrc := splices[outerdst]; outersrc != "" {
-                               return httpserver.Errorf(http.StatusBadRequest, "cannot splice at target %q with non-empty splice at %q", dst, outerdst)
+                       if outersrc := replaceFiles[outerdst]; outersrc != "" {
+                               return nil, httpserver.Errorf(http.StatusBadRequest, "replace_files: cannot operate on target %q inside non-empty target %q", dst, outerdst)
                        }
                }
        }
 
        var srcidloaded string
        var srcfs arvados.FileSystem
-       // Apply the requested splices.
+       // Apply the requested replacements.
        for _, dst := range dstTodo {
-               src := splices[dst]
+               src := replaceFiles[dst]
                if src == "" {
                        if dst == "/" {
                                // In this case we started with a
@@ -231,14 +209,14 @@ func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[s
                        }
                        err := dstfs.RemoveAll(dst)
                        if err != nil {
-                               return fmt.Errorf("RemoveAll(%s): %w", dst, err)
+                               return nil, fmt.Errorf("RemoveAll(%s): %w", dst, err)
                        }
                        continue
                }
                srcspec := strings.SplitN(src, "/", 2)
                srcid, srcpath := srcspec[0], "/"
                if !arvadosclient.PDHMatch(srcid) {
-                       return httpserver.Errorf(http.StatusBadRequest, "invalid source %q for splices[%q]: must be \"\" or \"PDH[/path]\"", src, dst)
+                       return nil, httpserver.Errorf(http.StatusBadRequest, "invalid source %q for replace_files[%q]: must be \"\" or \"PDH\" or \"PDH/path\"", src, dst)
                }
                if len(srcspec) == 2 && srcspec[1] != "" {
                        srcpath = srcspec[1]
@@ -247,20 +225,20 @@ func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[s
                        srcfs = nil
                        srccoll, err := conn.CollectionGet(ctx, arvados.GetOptions{UUID: srcid})
                        if err != nil {
-                               return err
+                               return nil, err
                        }
                        // We use StubClient here because we don't
                        // want srcfs to read/write any file data or
                        // sync collection state to/from the database.
                        srcfs, err = srccoll.FileSystem(&arvados.StubClient{}, &arvados.StubClient{})
                        if err != nil {
-                               return err
+                               return nil, err
                        }
                        srcidloaded = srcid
                }
                snap, err := arvados.Snapshot(srcfs, srcpath)
                if err != nil {
-                       return httpserver.Errorf(http.StatusBadRequest, "error getting snapshot of %q from %q: %w", srcpath, srcid, err)
+                       return nil, httpserver.Errorf(http.StatusBadRequest, "error getting snapshot of %q from %q: %w", srcpath, srcid, err)
                }
                // Create intermediate dirs, in case dst is
                // "newdir1/newdir2/dst".
@@ -268,20 +246,22 @@ func (conn *Conn) applySplices(ctx context.Context, fromUUID string, attrs map[s
                        if dst[i] == '/' {
                                err = dstfs.Mkdir(dst[:i], 0777)
                                if err != nil && !os.IsExist(err) {
-                                       return httpserver.Errorf(http.StatusBadRequest, "error creating parent dirs for %q: %w", dst, err)
+                                       return nil, httpserver.Errorf(http.StatusBadRequest, "error creating parent dirs for %q: %w", dst, err)
                                }
                        }
                }
                err = arvados.Splice(dstfs, dst, snap)
                if err != nil {
-                       return fmt.Errorf("error splicing snapshot onto path %q: %w", dst, err)
+                       return nil, fmt.Errorf("error splicing snapshot onto path %q: %w", dst, err)
                }
        }
        mtxt, err := dstfs.MarshalManifest(".")
        if err != nil {
-               return err
+               return nil, err
+       }
+       if attrs == nil {
+               attrs = make(map[string]interface{}, 1)
        }
-       delete(attrs, "splices")
        attrs["manifest_text"] = mtxt
-       return nil
+       return attrs, nil
 }