From: Rob Browning Date: Sat, 18 Aug 2012 20:26:28 +0000 (-0500) Subject: Overhaul restore destination handling, and stripping/grafting behavior. X-Git-Tag: bup-0.25-rc2~75 X-Git-Url: https://arthur.barton.de/cgi-bin/gitweb.cgi?p=bup.git;a=commitdiff_plain;h=042eaac10b2650a71f7e8604cfac8213091ec1be;hp=66ff902e6318b118dfff03cbec3d9579f5bd248d Overhaul restore destination handling, and stripping/grafting behavior. Change restore to respond to source paths like this (assume outdir corresponds to "." if there no -C argument or to -C outdir): /foo/what/ever - extract ever to outdir/ever /foo/what/ever/ - extract ever/* to outdir/* /foo/what/ever/. - extract ever/. to outdir/. (i.e. outdir == ever). Also fix handling of top-level commit symlinks. Previously bup would just restore /foo/latest as a dummy symlink like this: latest -> ../.commit/SHA Instead, dereference latest and restore the target instead. Tighten up stripping/grafting with additional argument checks, and handle any root collisions by creating a fake root dir (see comments in save-cmd.py). Bup still doesn't handle other path collisions yet, i.e. if both /foo/bar and /bar are remapped to /bar. Signed-off-by: Rob Browning Reviewed-by: Zoran Zaric --- diff --git a/cmd/restore-cmd.py b/cmd/restore-cmd.py index 3377338..d38fe50 100755 --- a/cmd/restore-cmd.py +++ b/cmd/restore-cmd.py @@ -116,8 +116,41 @@ def write_file_content(fullname, n): outf.close() +def do_root(n): + # Very similar to do_node(), except that this function doesn't + # create a path for n's destination directory (and so ignores + # n.fullname). It assumes the destination is '.', and restores + # n's metadata and content there. + global total_restored, opt + meta_stream = None + try: + # Directory metadata is the first entry in any .bupm file in + # the directory. Get it. + mfile = n.metadata_file() # VFS file -- cannot close(). + if mfile: + meta_stream = mfile.open() + meta = metadata.Metadata.read(meta_stream) + print_info(n, '.') + total_restored += 1 + plog('Restoring: %d\r' % total_restored) + for sub in n: + m = None + # Don't get metadata if this is a dir -- handled in sub do_node(). + if meta_stream and not stat.S_ISDIR(sub.mode): + m = metadata.Metadata.read(meta_stream) + do_node(n, sub, m) + if meta: + meta.apply_to_path('.', restore_numeric_ids = opt.numeric_ids) + finally: + if meta_stream: + meta_stream.close() + + def do_node(top, n, meta=None): - # meta will be None for dirs, and when there is no .bupm (i.e. no metadata) + # Create n.fullname(), relative to the current directory, and + # restore all of its metadata, when available. The meta argument + # will be None for dirs, or when there is no .bupm (i.e. no + # metadata). global total_restored, opt meta_stream = None try: @@ -152,8 +185,7 @@ def do_node(top, n, meta=None): m = metadata.Metadata.read(meta_stream) do_node(top, sub, m) if meta and not created_hardlink: - meta.apply_to_path(fullname, - restore_numeric_ids=opt.numeric_ids) + meta.apply_to_path(fullname, restore_numeric_ids = opt.numeric_ids) finally: if meta_stream: meta_stream.close() @@ -183,15 +215,31 @@ for d in extra: continue isdir = stat.S_ISDIR(n.mode) if not name or name == '.': - # trailing slash: extract children to cwd + # Source is /foo/what/ever/ or /foo/what/ever/. -- extract + # what/ever/* to the current directory, and if name == '.' + # (i.e. /foo/what/ever/.), then also restore what/ever's + # metadata to the current directory. if not isdir: add_error('%r: not a directory' % d) else: - for sub in n: - do_node(n, sub) + if name == '.': + do_root(n) + else: + for sub in n: + do_node(n, sub) else: - # no trailing slash: extract node and its children to cwd - do_node(n.parent, n) + # Source is /foo/what/ever -- extract ./ever to cwd. + if isinstance(n, vfs.FakeSymlink): + # Source is actually /foo/what, i.e. a top-level commit + # like /foo/latest, which is a symlink to ../.commit/SHA. + # So dereference it, and restore ../.commit/SHA/. to + # "./what/.". + target = n.dereference() + mkdirp(n.name) + os.chdir(n.name) + do_root(target) + else: + do_node(n.parent, n) if not opt.quiet: progress('Restoring: %d, done.\n' % total_restored) diff --git a/cmd/save-cmd.py b/cmd/save-cmd.py index a8265bd..6b39599 100755 --- a/cmd/save-cmd.py +++ b/cmd/save-cmd.py @@ -100,23 +100,33 @@ def eatslash(dir): # created. The sort_key must be computed using the element's real # name and mode rather than the git mode and (possibly mangled) name. -parts = [''] -shalists = [[]] -metalists = [[]] +# Maintain a stack of information representing the current location in +# the archive being constructed. The current path is recorded in +# parts, which will be something like ['', 'home', 'someuser'], and +# the accumulated content and metadata for of the dirs in parts is +# stored in parallel stacks in shalists and metalists. + +parts = [] # Current archive position (stack of dir names). +shalists = [] # Hashes for each dir in paths. +metalists = [] # Metadata for each dir in paths. + def _push(part, metadata): - assert(part) + # Enter a new archive directory -- make it the current directory. parts.append(part) shalists.append([]) - # First entry is dir metadata, which is represented with an empty name. - metalists.append([('', metadata)]) + metalists.append([('', metadata)]) # This dir's metadata (no name). + -def _pop(force_tree): +def _pop(force_tree, dir_metadata=None): + # Leave the current archive directory and add its tree to its parent. assert(len(parts) >= 1) part = parts.pop() shalist = shalists.pop() metalist = metalists.pop() if metalist: + if dir_metadata: # Override the original metadata pushed for this dir. + metalist = [('', dir_metadata)] + metalist[1:] sorted_metalist = sorted(metalist, key = lambda x : x[0]) metadata = ''.join([m[1].encode() for m in sorted_metalist]) shalist.append((0100644, '.bupm', w.new_blob(metadata))) @@ -126,12 +136,9 @@ def _pop(force_tree): git.mangle_name(part, GIT_MODE_TREE, GIT_MODE_TREE), tree)) - else: - # This was the toplevel, so put it back for sanity (i.e. cd .. from /). - shalists.append(shalist) - metalists.append(metalist) return tree + lastremain = None def progress_report(n): global count, subcount, lastremain @@ -205,6 +212,19 @@ if opt.progress: progress('Reading index: %d, done.\n' % ftotal) hashsplit.progress_callback = progress_report +# Root collisions occur when strip or graft options map more than one +# path to the same directory (paths which originally had separate +# parents). When that situation is detected, use empty metadata for +# the parent. Otherwise, use the metadata for the common parent. +# Collision example: "bup save ... --strip /foo /foo/bar /bar". + +# FIXME: Add collision tests, or handle collisions some other way. + +# FIXME: Detect/handle strip/graft name collisions (other than root), +# i.e. if '/foo/bar' and '/bar' both map to '/'. + +first_root = None +root_collision = None tstart = time.time() count = subcount = fcount = 0 lastskip_name = None @@ -254,21 +274,42 @@ for (transname,ent) in r.filter(extra, wantrecurse=wantrecurse_during): else: dirp = path_components(dir) + # At this point, dirp contains a representation of the archive + # path that looks like [(archive_dir_name, real_fs_path), ...]. + # So given "bup save ... --strip /foo/bar /foo/bar/baz", dirp + # might look like this at some point: + # [('', '/foo/bar'), ('baz', '/foo/bar/baz'), ...]. + + # This dual representation supports stripping/grafting, where the + # archive path may not have a direct correspondence with the + # filesystem. The root directory is represented by an initial + # component named '', and any component that doesn't have a + # corresponding filesystem directory (due to grafting, for + # example) will have a real_fs_path of None, i.e. [('', None), + # ...]. + + if first_root == None: + dir_name, fs_path = dirp[0] + first_root = dirp[0] + meta = metadata.from_path(fs_path) if fs_path else metadata.Metadata() + _push(dir_name, meta) + elif first_root != dirp[0]: + root_collision = True + + # If switching to a new sub-tree, finish the current sub-tree. while parts > [x[0] for x in dirp]: _pop(force_tree = None) - if dir != '/': - for path_component in dirp[len(parts):]: - dir_name, fs_path = path_component - if fs_path: - meta = metadata.from_path(fs_path) - else: - meta = metadata.Metadata() - _push(dir_name, meta) + # If switching to a new sub-tree, start a new sub-tree. + for path_component in dirp[len(parts):]: + dir_name, fs_path = path_component + meta = metadata.from_path(fs_path) if fs_path else metadata.Metadata() + _push(dir_name, meta) if not file: - # no filename portion means this is a subdir. But - # sub/parentdirectories already handled in the pop/push() part above. + if len(parts) == 1: + continue # We're at the top level -- keep the current root dir + # Since there's no filename, this is a subdir -- finish it. oldtree = already_saved(ent) # may be None newtree = _pop(force_tree = oldtree) if not oldtree: @@ -346,19 +387,14 @@ if opt.progress: progress('Saving: %.2f%% (%d/%dk, %d/%d files), done. \n' % (pct, count/1024, total/1024, fcount, ftotal)) -while len(parts) > 1: # _pop() all the parts above the indexed items. +while len(parts) > 1: # _pop() all the parts above the root _pop(force_tree = None) assert(len(shalists) == 1) assert(len(metalists) == 1) -if not (opt.strip or opt.strip_path or graft_points): - # For now, only save metadata for the root directory when there - # isn't any path grafting or stripping that might create multiple - # roots. - shalist = shalists[-1] - metadata = ''.join([metadata.from_path('/').encode()]) - shalist.append((0100644, '.bupm', w.new_blob(metadata))) -tree = w.new_tree(shalists[-1]) +# Finish the root directory. +tree = _pop(force_tree = None, + dir_metadata = metadata.Metadata() if root_collision else None) if opt.tree: print tree.encode('hex') diff --git a/lib/bup/helpers.py b/lib/bup/helpers.py index f4a471f..8b9d0d2 100644 --- a/lib/bup/helpers.py +++ b/lib/bup/helpers.py @@ -647,6 +647,14 @@ def parse_date_or_fatal(str, fatal): return date +# FIXME: Carefully consider the use of functions (os.path.*, etc.) +# that resolve against the current filesystem in the strip/graft +# functions for example, but elsewhere as well. I suspect bup's not +# always being careful about that. For some cases, the contents of +# the current filesystem should be irrelevant, and consulting it might +# produce the wrong result, perhaps via unintended symlink resolution, +# for example. + def path_components(path): """Break path into a list of pairs of the form (name, full_path_to_name). Path must start with '/'. @@ -688,20 +696,47 @@ def stripped_path_components(path, strip_prefixes): def grafted_path_components(graft_points, path): - # Find the first '/' after the graft prefix, match that to the - # original source base dir, then move on. + # Create a result that consists of some number of faked graft + # directories before the graft point, followed by all of the real + # directories from path that are after the graft point. Arrange + # for the directory at the graft point in the result to correspond + # to the "orig" directory in --graft orig=new. See t/thelpers.py + # for some examples. + + # Note that given --graft orig=new, orig and new have *nothing* to + # do with each other, even if some of their component names + # match. i.e. --graft /foo/bar/baz=/foo/bar/bax is semantically + # equivalent to --graft /foo/bar/baz=/x/y/z, or even + # /foo/bar/baz=/x. + + # FIXME: This can't be the best solution... clean_path = os.path.abspath(path) for graft_point in graft_points: old_prefix, new_prefix = graft_point + # Expand prefixes iff not absolute paths. + old_prefix = os.path.normpath(old_prefix) + new_prefix = os.path.normpath(new_prefix) if clean_path.startswith(old_prefix): - grafted_path = re.sub(r'^' + old_prefix, new_prefix, - clean_path) - result = [(p, None) for p in grafted_path.split('/')] - result[-1] = (result[-1][0], clean_path) + escaped_prefix = re.escape(old_prefix) + grafted_path = re.sub(r'^' + escaped_prefix, new_prefix, clean_path) + # Handle /foo=/ (at least) -- which produces //whatever. + grafted_path = '/' + grafted_path.lstrip('/') + clean_path_components = path_components(clean_path) + # Count the components that were stripped. + strip_count = 0 if old_prefix == '/' else old_prefix.count('/') + new_prefix_parts = new_prefix.split('/') + result_prefix = grafted_path.split('/')[:new_prefix.count('/')] + result = [(p, None) for p in result_prefix] \ + + clean_path_components[strip_count:] + # Now set the graft point name to match the end of new_prefix. + graft_point = len(result_prefix) + result[graft_point] = \ + (new_prefix_parts[-1], clean_path_components[strip_count][1]) + if new_prefix == '/': # --graft ...=/ is a special case. + return result[1:] return result return path_components(clean_path) - # hashlib is only available in python 2.5 or higher, but the 'sha' module # produces a DeprecationWarning in python 2.6 or higher. We want to support # python 2.4 and above without any stupid warnings, so let's try using hashlib diff --git a/lib/bup/t/thelpers.py b/lib/bup/t/thelpers.py index e4e24cd..2e113c8 100644 --- a/lib/bup/t/thelpers.py +++ b/lib/bup/t/thelpers.py @@ -48,10 +48,32 @@ def test_stripped_path_components(): [('', '/foo/bar/baz')]) WVEXCEPT(Exception, stripped_path_components, 'foo', []) + @wvtest def test_grafted_path_components(): WVPASSEQ(grafted_path_components([('/chroot', '/')], '/foo'), [('', '/'), ('foo', '/foo')]) - WVPASSEQ(grafted_path_components([('/foo/bar', '')], '/foo/bar/baz/bax'), - [('', None), ('baz', None), ('bax', '/foo/bar/baz/bax')]) + WVPASSEQ(grafted_path_components([('/foo/bar', '/')], '/foo/bar/baz/bax'), + [('', '/foo/bar'), + ('baz', '/foo/bar/baz'), + ('bax', '/foo/bar/baz/bax')]) + WVPASSEQ(grafted_path_components([('/foo/bar/baz', '/bax')], + '/foo/bar/baz/1/2'), + [('', None), + ('bax', '/foo/bar/baz'), + ('1', '/foo/bar/baz/1'), + ('2', '/foo/bar/baz/1/2')]) + WVPASSEQ(grafted_path_components([('/foo', '/bar/baz/bax')], + '/foo/bar'), + [('', None), + ('bar', None), + ('baz', None), + ('bax', '/foo'), + ('bar', '/foo/bar')]) + WVPASSEQ(grafted_path_components([('/foo/bar/baz', '/a/b/c')], + '/foo/bar/baz'), + [('', None), ('a', None), ('b', None), ('c', '/foo/bar/baz')]) + WVPASSEQ(grafted_path_components([('/', '/a/b/c/')], '/foo/bar'), + [('', None), ('a', None), ('b', None), ('c', '/'), + ('foo', '/foo'), ('bar', '/foo/bar')]) WVEXCEPT(Exception, grafted_path_components, 'foo', []) diff --git a/lib/bup/vfs.py b/lib/bup/vfs.py index 0f9b0a2..b60a6ad 100644 --- a/lib/bup/vfs.py +++ b/lib/bup/vfs.py @@ -174,8 +174,8 @@ class Node: self._metadata = None def __repr__(self): - return "" \ - % (self.name, self.hash.encode('hex'), + return "<%s object at X - name:%r hash:%s parent:%r>" \ + % (self.__class__, self.name, self.hash.encode('hex'), self.parent.name if self.parent else None) def __cmp__(a, b): diff --git a/t/test.sh b/t/test.sh index e932ad7..5a03609 100755 --- a/t/test.sh +++ b/t/test.sh @@ -238,6 +238,53 @@ WVPASS bup restore -C buprestore.tmp "/master/latest/$TOP/$D/" touch $D/non-existent-file buprestore.tmp/non-existent-file # else diff fails WVPASS diff -ur $D/ buprestore.tmp/ +( + tmp=testrestore.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save --strip -n foo $tmp/src + + WVSTART "restore /foo/latest" + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/ $tmp/restore/latest/ + + WVSTART "restore /foo/latest/" + rm -rf "$tmp/restore" + WVPASS bup restore -C $tmp/restore /foo/latest/ + for x in $tmp/src/*; do + WVPASS t/compare-trees $x/ $tmp/restore/$(basename $x); + done + + WVSTART "restore /foo/latest/." + rm -rf "$tmp/restore" + WVPASS bup restore -C $tmp/restore /foo/latest/. + WVPASS t/compare-trees $tmp/src/ $tmp/restore/ + + WVSTART "restore /foo/latest/x" + rm -rf "$tmp/restore" + WVPASS bup restore -C $tmp/restore /foo/latest/x + WVPASS t/compare-trees $tmp/src/x/ $tmp/restore/x/ + + WVSTART "restore /foo/latest/x/" + rm -rf "$tmp/restore" + WVPASS bup restore -C $tmp/restore /foo/latest/x/ + for x in $tmp/src/x/*; do + WVPASS t/compare-trees $x/ $tmp/restore/$(basename $x); + done + + WVSTART "restore /foo/latest/x/." + rm -rf "$tmp/restore" + WVPASS bup restore -C $tmp/restore /foo/latest/x/. + WVPASS t/compare-trees $tmp/src/x/ $tmp/restore/ +) || WVFAIL + + WVSTART "ftp" WVPASS bup ftp "cat /master/latest/$TOP/$D/b" >$D/b.new WVPASS bup ftp "cat /master/latest/$TOP/$D/f" >$D/f.new @@ -347,63 +394,167 @@ b f" rm $EXCLUDE_FILE -WVSTART "strip" -D=strip.tmp -rm -rf $D -mkdir $D -export BUP_DIR="$D/.bup" -WVPASS bup init -touch $D/a -WVPASS bup random 128k >$D/b -mkdir $D/d $D/d/e -WVPASS bup random 512 >$D/f -WVPASS bup index -ux $D -bup save --strip -n strip $D -WVPASSEQ "$(bup ls strip/latest/)" "a -b -d/ -f" -WVSTART "strip-path" -D=strip-path.tmp -rm -rf $D -mkdir $D -export BUP_DIR="$D/.bup" -WVPASS bup init -touch $D/a -WVPASS bup random 128k >$D/b -mkdir $D/d $D/d/e -WVPASS bup random 512 >$D/f -WVPASS bup index -ux $D -bup save --strip-path $TOP -n strip-path $D -WVPASSEQ "$(bup ls strip-path/latest/$D/)" "a -b -d/ -f" +WVSTART "save --strip" +( + tmp=graft-points.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save --strip -n foo $tmp/src/x/y + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/x/y/ "$tmp/restore/latest/" +) || WVFAIL -WVSTART "graft_points" -D=graft-points.tmp -rm -rf $D -mkdir $D -export BUP_DIR="$D/.bup" -WVPASS bup init -touch $D/a -WVPASS bup random 128k >$D/b -mkdir $D/d $D/d/e -WVPASS bup random 512 >$D/f -WVPASS bup index -ux $D -WVFAIL bup save --graft =/grafted -n graft-point-absolute $D -WVFAIL bup save --graft $TOP/$D= -n graft-point-absolute $D -bup save --graft $TOP/$D=/grafted -n graft-point-absolute $D -WVPASSEQ "$(bup ls graft-point-absolute/latest/grafted/)" "a -b -d/ -f" -bup save --graft $D=grafted -n graft-point-relative $D -WVPASSEQ "$(bup ls graft-point-relative/latest/$TOP/grafted/)" "a -b -d/ -f" +WVSTART "save --strip-path (relative)" +( + tmp=graft-points.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save --strip-path $tmp/src -n foo $tmp/src/x + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/ "$tmp/restore/latest/" +) || WVFAIL + +WVSTART "save --strip-path (absolute)" +( + tmp=graft-points.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save --strip-path "$TOP" -n foo $tmp/src + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/ "$tmp/restore/latest/$tmp/src/" +) || WVFAIL + +WVSTART "save --strip-path (no match)" +( + tmp=graft-points.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save --strip-path $tmp/foo -n foo $tmp/src/x + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/ "$tmp/restore/latest/$TOP/$tmp/src/" +) || WVFAIL + +WVSTART "save --graft (empty graft points disallowed)" +( + tmp=graft-points.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + WVFAIL bup save --graft =/grafted -n graft-point-absolute $tmp + WVFAIL bup save --graft $TOP/$tmp= -n graft-point-absolute $tmp +) || WVFAIL + +WVSTART "save --graft /x/y=/a/b (relative paths)" +( + tmp=graft-points.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save --graft $tmp/src=x -n foo $tmp/src + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/ "$tmp/restore/latest/$TOP/x/" +) || WVFAIL + +WVSTART "save --graft /x/y=/a/b (matching structure)" +( + tmp=graft-points.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save -v --graft "$TOP/$tmp/src/x/y=$TOP/$tmp/src/a/b" \ + -n foo $tmp/src/x/y + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/x/y/ \ + "$tmp/restore/latest/$TOP/$tmp/src/a/b/" +) || WVFAIL + +WVSTART "save --graft /x/y=/a (shorter target)" +( + tmp=graft-points.tmp + rm -rf $tmp + mkdir $tmp + export BUP_DIR="$(pwd)/$tmp/bup" + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save -v --graft "$TOP/$tmp/src/x/y=/a" -n foo $tmp/src/x/y + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/x/y/ "$tmp/restore/latest/a/" +) || WVFAIL + +WVSTART "save --graft /x=/a/b (longer target)" +( + tmp=graft-points.tmp + export BUP_DIR="$(pwd)/$tmp/bup" + rm -rf $tmp + mkdir $tmp + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save -v --graft "$TOP/$tmp/src=$TOP/$tmp/src/a/b/c" \ + -n foo $tmp/src + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/ "$tmp/restore/latest/$TOP/$tmp/src/a/b/c/" +) || WVFAIL + +WVSTART "save --graft /x=/ (root target)" +( + tmp=graft-points.tmp + export BUP_DIR="$(pwd)/$tmp/bup" + rm -rf $tmp + mkdir $tmp + WVPASS bup init + mkdir -p $tmp/src/x/y/z + WVPASS bup random 8k > $tmp/src/x/y/random-1 + WVPASS bup random 8k > $tmp/src/x/y/z/random-2 + WVPASS bup index -u $tmp/src + WVPASS bup save -v --graft "$TOP/$tmp/src/x=/" -n foo $tmp/src/x + WVPASS bup restore -C $tmp/restore /foo/latest + WVPASS t/compare-trees $tmp/src/x/ "$tmp/restore/latest/" +) || WVFAIL + +#WVSTART "save --graft /=/x/ (root source)" +# FIXME: Not tested for now -- will require cleverness, or caution as root. WVSTART "indexfile" D=indexfile.tmp