From fb5133e3156476f37ded76258edcb0e15d66aed7 Mon Sep 17 00:00:00 2001 From: MarkJoy Date: Sat, 4 Nov 2023 02:25:52 +0700 Subject: [PATCH] Add option --use-original-path to 'borg extract' command: extract the content to its original path where it was backup from. 'borg extract' default behaviour always writes to current directory. This option overrides the default behaviour --- src/borg/archive.py | 14 ++++++++++---- src/borg/archiver/extract_cmd.py | 21 +++++++++++++++------ src/borg/constants.py | 7 +++---- src/borg/item.pyx | 4 +++- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index b048d5c160..bf463722f2 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -783,6 +783,7 @@ def extract_item( *, restore_attrs=True, dry_run=False, + useop=False, stdout=False, sparse=False, hlm=None, @@ -795,6 +796,7 @@ def extract_item( :param item: the item to extract :param restore_attrs: restore file attributes :param dry_run: do not write any data + :param useop: extract the content to its original path where it was backup from :param stdout: write extracted data to stdout :param sparse: write sparse files (chunk-granularity, independent of the original being sparse) :param hlm: maps hlid to link_target for extracting subtrees with hardlinks correctly @@ -851,8 +853,12 @@ def same_item(item, st): raise BackupError("File has damaged (all-zero) chunks. Try running borg check --repair.") return - dest = self.cwd - path = os.path.join(dest, item.path) + if useop: + saved_path = item.orig_path + path = item.orig_path + else: + saved_path = item.path + path = os.path.join(self.cwd, item.path) # Attempt to remove existing files, ignore errors on failure try: st = os.stat(path, follow_symlinks=False) @@ -885,7 +891,7 @@ def make_parent(path): ids = [c.id for c in item.chunks] for data in self.pipeline.fetch_many(ids, is_preloaded=True, ro_type=ROBJ_FILE_STREAM): if pi: - pi.show(increase=len(data), info=[remove_surrogates(item.path)]) + pi.show(increase=len(data), info=[remove_surrogates(saved_path)]) with backup_io("write"): if sparse and zeros.startswith(data): # all-zero chunk: create a hole in a sparse file @@ -1378,7 +1384,7 @@ def __init__( @contextmanager def create_helper(self, path, st, status=None, hardlinkable=True): sanitized_path = remove_dotdot_prefixes(path) - item = Item(path=sanitized_path) + item = Item(orig_path=path, path=sanitized_path) hardlinked = hardlinkable and st.st_nlink > 1 hl_chunks = None update_map = False diff --git a/src/borg/archiver/extract_cmd.py b/src/borg/archiver/extract_cmd.py index 7be8a32fe5..90ab220785 100644 --- a/src/borg/archiver/extract_cmd.py +++ b/src/borg/archiver/extract_cmd.py @@ -39,6 +39,7 @@ def do_extract(self, args, repository, manifest, archive): progress = args.progress output_list = args.output_list dry_run = args.dry_run + useop = args.useop stdout = args.stdout sparse = args.sparse strip_components = args.strip_components @@ -63,21 +64,25 @@ def do_extract(self, args, repository, manifest, archive): while dirs and not item.path.startswith(dirs[-1].path): dir_item = dirs.pop(-1) try: - archive.extract_item(dir_item, stdout=stdout) + archive.extract_item(dir_item, stdout=stdout, useop=useop) except BackupOSError as e: self.print_warning("%s: %s", remove_surrogates(dir_item.path), e) if output_list: - logging.getLogger("borg.output.list").info(remove_surrogates(item.path)) + if useop: + logging.getLogger("borg.output.list").info(remove_surrogates(item.orig_path)) + else: + logging.getLogger("borg.output.list").info(remove_surrogates(item.path)) try: if dry_run: - archive.extract_item(item, dry_run=True, hlm=hlm, pi=pi) + archive.extract_item(item, dry_run=True, hlm=hlm, pi=pi, useop=useop) else: if stat.S_ISDIR(item.mode): dirs.append(item) - archive.extract_item(item, stdout=stdout, restore_attrs=False) + archive.extract_item(item, stdout=stdout, restore_attrs=False, useop=useop) else: archive.extract_item( - item, stdout=stdout, sparse=sparse, hlm=hlm, pi=pi, continue_extraction=continue_extraction + item, stdout=stdout, sparse=sparse, hlm=hlm, pi=pi, + continue_extraction=continue_extraction, useop=useop ) except (BackupOSError, BackupError) as e: self.print_warning("%s: %s", remove_surrogates(orig_path), e) @@ -93,7 +98,7 @@ def do_extract(self, args, repository, manifest, archive): pi.show() dir_item = dirs.pop(-1) try: - archive.extract_item(dir_item, stdout=stdout) + archive.extract_item(dir_item, stdout=stdout, useop=useop) except BackupOSError as e: self.print_warning("%s: %s", remove_surrogates(dir_item.path), e) for pattern in matcher.get_unmatched_include_patterns(): @@ -160,6 +165,10 @@ def build_parser_extract(self, subparsers, common_parser, mid_common_parser): ) subparser.add_argument("--noacls", dest="noacls", action="store_true", help="do not extract/set ACLs") subparser.add_argument("--noxattrs", dest="noxattrs", action="store_true", help="do not extract/set xattrs") + subparser.add_argument( + "--use-original-path", dest="useop", action="store_true", + help="extract the content back to its original path where it was backup from" + ) subparser.add_argument( "--stdout", dest="stdout", action="store_true", help="write all extracted data to stdout" ) diff --git a/src/borg/constants.py b/src/borg/constants.py index 7f4cbc31d6..53dcae1b35 100644 --- a/src/borg/constants.py +++ b/src/borg/constants.py @@ -1,9 +1,8 @@ # this set must be kept complete, otherwise the RobustUnpacker might malfunction: # fmt: off -ITEM_KEYS = frozenset(['path', 'source', 'target', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master', 'hlid', - 'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'birthtime', 'size', - 'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended', - 'part']) +ITEM_KEYS = frozenset(['orig_path', 'path', 'source', 'target', 'rdev', 'chunks', 'chunks_healthy', 'hardlink_master', + 'hlid', 'mode', 'user', 'group', 'uid', 'gid', 'mtime', 'atime', 'ctime', 'birthtime', 'size', + 'xattrs', 'bsdflags', 'acl_nfs4', 'acl_access', 'acl_default', 'acl_extended', 'part']) # fmt: on # this is the set of keys that are always present in items: diff --git a/src/borg/item.pyx b/src/borg/item.pyx index 04dd884f29..703ef2980a 100644 --- a/src/borg/item.pyx +++ b/src/borg/item.pyx @@ -263,8 +263,10 @@ cdef class Item(PropDict): # properties statically defined, so that IDEs can know their names: + orig_path = PropDictProperty(str, 'surrogate-escaped str') path = PropDictProperty(str, 'surrogate-escaped str', encode=assert_sanitized_path, decode=to_sanitized_path) - source = PropDictProperty(str, 'surrogate-escaped str') # legacy borg 1.x. borg 2: see .target + # legacy borg 1.x. borg 2: see .target https://github.com/borgbackup/borg/issues/7245 + source = PropDictProperty(str, 'surrogate-escaped str') target = PropDictProperty(str, 'surrogate-escaped str') user = PropDictProperty(str, 'surrogate-escaped str') group = PropDictProperty(str, 'surrogate-escaped str')