Skip to content

Commit

Permalink
Add an option --extract-to-source-path to 'borg extract' command: ext…
Browse files Browse the repository at this point in the history
…ract the content to its source path where it was backup from. 'borg extract' default behaviour always writes to current directory. This option overrides that default behaviour
  • Loading branch information
MarkJoy committed Nov 2, 2023
1 parent d25cc1b commit f00aef2
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 11 deletions.
16 changes: 12 additions & 4 deletions src/borg/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ def extract_item(
*,
restore_attrs=True,
dry_run=False,
ex2src=False,
stdout=False,
sparse=False,
hlm=None,
Expand All @@ -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 ex2src: extract the content to its source 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
Expand Down Expand Up @@ -851,8 +853,14 @@ 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 ex2src:
original_path = item.source_path
dest = ''
path = item.source_path
else:
original_path = item.path
dest = self.cwd
path = os.path.join(dest, item.path)
# Attempt to remove existing files, ignore errors on failure
try:
st = os.stat(path, follow_symlinks=False)
Expand Down Expand Up @@ -885,7 +893,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(original_path)])
with backup_io("write"):
if sparse and zeros.startswith(data):
# all-zero chunk: create a hole in a sparse file
Expand Down Expand Up @@ -1378,7 +1386,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(path=sanitized_path, source_path=path)
hardlinked = hardlinkable and st.st_nlink > 1
hl_chunks = None
update_map = False
Expand Down
18 changes: 12 additions & 6 deletions src/borg/archiver/extract_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
ex2src = args.ex2src
stdout = args.stdout
sparse = args.sparse
strip_components = args.strip_components
Expand All @@ -63,21 +64,24 @@ 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, ex2src=ex2src)
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 ex2src:
logging.getLogger("borg.output.list").info(remove_surrogates(item.source_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, ex2src=ex2src)
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, ex2src=ex2src)
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, ex2src=ex2src
)
except (BackupOSError, BackupError) as e:
self.print_warning("%s: %s", remove_surrogates(orig_path), e)
Expand All @@ -93,7 +97,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, ex2src=ex2src)
except BackupOSError as e:
self.print_warning("%s: %s", remove_surrogates(dir_item.path), e)
for pattern in matcher.get_unmatched_include_patterns():
Expand Down Expand Up @@ -160,6 +164,8 @@ 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("--extract-to-source-path", dest="ex2src", action="store_true",
help="extract the content back to its source path where it was backup from")
subparser.add_argument(
"--stdout", dest="stdout", action="store_true", help="write all extracted data to stdout"
)
Expand Down
2 changes: 1 addition & 1 deletion src/borg/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 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',
ITEM_KEYS = frozenset(['path', 'source_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'])
Expand Down
1 change: 1 addition & 0 deletions src/borg/item.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ cdef class Item(PropDict):
# properties statically defined, so that IDEs can know their names:

path = PropDictProperty(str, 'surrogate-escaped str', encode=assert_sanitized_path, decode=to_sanitized_path)
source_path = PropDictProperty(str, 'surrogate-escaped str')
source = PropDictProperty(str, 'surrogate-escaped str') # legacy borg 1.x. borg 2: see .target
target = PropDictProperty(str, 'surrogate-escaped str')
user = PropDictProperty(str, 'surrogate-escaped str')
Expand Down

0 comments on commit f00aef2

Please sign in to comment.