borg export-tar (#2519)

This commit is contained in:
enkore 2017-05-17 00:09:41 +02:00 committed by GitHub
parent 98e4e55141
commit 5788219ff4
7 changed files with 574 additions and 7 deletions

142
docs/man/borg-export-tar.1 Normal file
View File

@ -0,0 +1,142 @@
.\" Man page generated from reStructuredText.
.
.TH BORG-EXPORT-TAR 1 "2017-05-16" "" "borg backup tool"
.SH NAME
borg-export-tar \- Export archive contents as a tarball
.
.nr rst2man-indent-level 0
.
.de1 rstReportMargin
\\$1 \\n[an-margin]
level \\n[rst2man-indent-level]
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
-
\\n[rst2man-indent0]
\\n[rst2man-indent1]
\\n[rst2man-indent2]
..
.de1 INDENT
.\" .rstReportMargin pre:
. RS \\$1
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
. nr rst2man-indent-level +1
.\" .rstReportMargin post:
..
.de UNINDENT
. RE
.\" indent \\n[an-margin]
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
.nr rst2man-indent-level -1
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.SH SYNOPSIS
.sp
borg export\-tar <options> ARCHIVE FILE PATH
.SH DESCRIPTION
.sp
This command creates a tarball from an archive.
.sp
When giving \(aq\-\(aq as the output FILE, Borg will write a tar stream to standard output.
.sp
By default (\-\-tar\-filter=auto) Borg will detect whether the FILE should be compressed
based on its file extension and pipe the tarball through an appropriate filter
before writing it to FILE:
.INDENT 0.0
.IP \(bu 2
\&.tar.gz: gzip
.IP \(bu 2
\&.tar.bz2: bzip2
.IP \(bu 2
\&.tar.xz: xz
.UNINDENT
.sp
Alternatively a \-\-tar\-filter program may be explicitly specified. It should
read the uncompressed tar stream from stdin and write a compressed/filtered
tar stream to stdout.
.sp
The generated tarball uses the GNU tar format.
.sp
export\-tar is a lossy conversion:
BSD flags, ACLs, extended attributes (xattrs), atime and ctime are not exported.
Timestamp resolution is limited to whole seconds, not the nanosecond resolution
otherwise supported by Borg.
.sp
A \-\-sparse option (as found in borg extract) is not supported.
.sp
By default the entire archive is extracted but a subset of files and directories
can be selected by passing a list of \fBPATHs\fP as arguments.
The file selection can further be restricted by using the \fB\-\-exclude\fP option.
.sp
See the output of the "borg help patterns" command for more help on exclude patterns.
.sp
\fB\-\-progress\fP can be slower than no progress display, since it makes one additional
pass over the archive metadata.
.SH OPTIONS
.sp
See \fIborg\-common(1)\fP for common options of Borg commands.
.SS arguments
.INDENT 0.0
.TP
.B ARCHIVE
archive to export
.TP
.B FILE
output tar file. "\-" to write to stdout instead.
.TP
.B PATH
paths to extract; patterns are supported
.UNINDENT
.SS optional arguments
.INDENT 0.0
.TP
.B \-\-tar\-filter
filter program to pipe data through
.TP
.B \-\-list
output verbose list of items (files, dirs, ...)
.TP
.BI \-e \ PATTERN\fP,\fB \ \-\-exclude \ PATTERN
exclude paths matching PATTERN
.TP
.BI \-\-exclude\-from \ EXCLUDEFILE
read exclude patterns from EXCLUDEFILE, one per line
.TP
.BI \-\-pattern \ PATTERN
include/exclude paths matching PATTERN
.TP
.BI \-\-patterns\-from \ PATTERNFILE
read include/exclude patterns from PATTERNFILE, one per line
.TP
.BI \-\-strip\-components \ NUMBER
Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.
.UNINDENT
.SH EXAMPLES
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
# export as uncompressed tar
$ borg export\-tar /path/to/repo::Monday Monday.tar
# exclude some types, compress using gzip
$ borg export\-tar /path/to/repo::Monday Monday.tar.gz \-\-exclude \(aq*.so\(aq
# use higher compression level with gzip
$ borg export\-tar testrepo::linux \-\-tar\-filter="gzip \-9" Monday.tar.gz
# export a gzipped tar, but instead of storing it on disk,
# upload it to a remote site using curl.
$ borg export\-tar ... \-\-tar\-filter="gzip" \- | curl \-\-data\-binary @\- https://somewhere/to/POST
.ft P
.fi
.UNINDENT
.UNINDENT
.SH SEE ALSO
.sp
\fIborg\-common(1)\fP
.SH AUTHOR
The Borg Collective
.\" Generated by docutils manpage writer.
.

View File

@ -492,6 +492,24 @@ Examples
Comment: This is a better comment
...
.. include:: usage/export-tar.rst.inc
Examples
~~~~~~~~
::
# export as uncompressed tar
$ borg export-tar /path/to/repo::Monday Monday.tar
# exclude some types, compress using gzip
$ borg export-tar /path/to/repo::Monday Monday.tar.gz --exclude '*.so'
# use higher compression level with gzip
$ borg export-tar testrepo::linux --tar-filter="gzip -9" Monday.tar.gz
# export a gzipped tar, but instead of storing it on disk,
# upload it to a remote site using curl.
$ borg export-tar ... --tar-filter="gzip" - | curl --data-binary @- https://somewhere/to/POST
.. include:: usage/with-lock.rst.inc

View File

@ -0,0 +1,73 @@
.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!
.. _borg_export-tar:
borg export-tar
---------------
::
borg export-tar <options> ARCHIVE FILE PATH
positional arguments
ARCHIVE
archive to export
FILE
output tar file. "-" to write to stdout instead.
PATH
paths to extract; patterns are supported
optional arguments
``--tar-filter``
| filter program to pipe data through
``--list``
| output verbose list of items (files, dirs, ...)
``-e PATTERN``, ``--exclude PATTERN``
| exclude paths matching PATTERN
``--exclude-from EXCLUDEFILE``
| read exclude patterns from EXCLUDEFILE, one per line
``--pattern PATTERN``
| include/exclude paths matching PATTERN
``--patterns-from PATTERNFILE``
| read include/exclude patterns from PATTERNFILE, one per line
``--strip-components NUMBER``
| Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.
`Common options`_
|
Description
~~~~~~~~~~~
This command creates a tarball from an archive.
When giving '-' as the output FILE, Borg will write a tar stream to standard output.
By default (--tar-filter=auto) Borg will detect whether the FILE should be compressed
based on its file extension and pipe the tarball through an appropriate filter
before writing it to FILE:
- .tar.gz: gzip
- .tar.bz2: bzip2
- .tar.xz: xz
Alternatively a --tar-filter program may be explicitly specified. It should
read the uncompressed tar stream from stdin and write a compressed/filtered
tar stream to stdout.
The generated tarball uses the GNU tar format.
export-tar is a lossy conversion:
BSD flags, ACLs, extended attributes (xattrs), atime and ctime are not exported.
Timestamp resolution is limited to whole seconds, not the nanosecond resolution
otherwise supported by Borg.
A --sparse option (as found in borg extract) is not supported.
By default the entire archive is extracted but a subset of files and directories
can be selected by passing a list of ``PATHs`` as arguments.
The file selection can further be restricted by using the ``--exclude`` option.
See the output of the "borg help patterns" command for more help on exclude patterns.
``--progress`` can be slower than no progress display, since it makes one additional
pass over the archive metadata.

View File

@ -15,6 +15,7 @@ import signal
import stat
import subprocess
import sys
import tarfile
import textwrap
import time
import traceback
@ -61,6 +62,7 @@ from .helpers import ErrorIgnoringTextIOWrapper
from .helpers import ProgressIndicatorPercent
from .helpers import basic_json_data, json_print
from .helpers import replace_placeholders
from .helpers import ChunkIteratorFileWrapper
from .patterns import ArgparsePatternAction, ArgparseExcludeFileAction, ArgparsePatternFileAction, parse_exclude_pattern
from .patterns import PatternMatcher
from .item import Item
@ -694,6 +696,219 @@ class Archiver:
pi.finish()
return self.exit_code
@with_repository()
@with_archive
def do_export_tar(self, args, repository, manifest, key, archive):
"""Export archive contents as a tarball"""
self.output_list = args.output_list
# A quick note about the general design of tar_filter and tarfile;
# The tarfile module of Python can provide some compression mechanisms
# by itself, using the builtin gzip, bz2 and lzma modules (and "tarmodes"
# such as "w:xz").
#
# Doing so would have three major drawbacks:
# For one the compressor runs on the same thread as the program using the
# tarfile, stealing valuable CPU time from Borg and thus reducing throughput.
# Then this limits the available options - what about lz4? Brotli? zstd?
# The third issue is that systems can ship more optimized versions than those
# built into Python, e.g. pigz or pxz, which can use more than one thread for
# compression.
#
# Therefore we externalize compression by using a filter program, which has
# none of these drawbacks. The only issue of using an external filter is
# that it has to be installed -- hardly a problem, considering that
# the decompressor must be installed as well to make use of the exported tarball!
filter = None
if args.tar_filter == 'auto':
# Note that filter remains None if tarfile is '-'.
if args.tarfile.endswith('.tar.gz'):
filter = 'gzip'
elif args.tarfile.endswith('.tar.bz2'):
filter = 'bzip2'
elif args.tarfile.endswith('.tar.xz'):
filter = 'xz'
logger.debug('Automatically determined tar filter: %s', filter)
else:
filter = args.tar_filter
if args.tarfile == '-':
tarstream, tarstream_close = sys.stdout.buffer, False
else:
tarstream, tarstream_close = open(args.tarfile, 'wb'), True
if filter:
# When we put a filter between us and the final destination,
# the selected output (tarstream until now) becomes the output of the filter (=filterout).
# The decision whether to close that or not remains the same.
filterout = tarstream
filterout_close = tarstream_close
# There is no deadlock potential here (the subprocess docs warn about this), because
# communication with the process is a one-way road, i.e. the process can never block
# for us to do something while we block on the process for something different.
filtercmd = shlex.split(filter)
logger.debug('--tar-filter command line: %s', filtercmd)
filterproc = subprocess.Popen(filtercmd, stdin=subprocess.PIPE, stdout=filterout)
# Always close the pipe, otherwise the filter process would not notice when we are done.
tarstream = filterproc.stdin
tarstream_close = True
# The | (pipe) symbol instructs tarfile to use a streaming mode of operation
# where it never seeks on the passed fileobj.
tar = tarfile.open(fileobj=tarstream, mode='w|')
self._export_tar(args, archive, tar)
# This does not close the fileobj (tarstream) we passed to it -- a side effect of the | mode.
tar.close()
if tarstream_close:
tarstream.close()
if filter:
logger.debug('Done creating tar, waiting for filter to die...')
rc = filterproc.wait()
if rc:
logger.error('--tar-filter exited with code %d, output file is likely unusable!', rc)
self.exit_code = set_ec(EXIT_ERROR)
else:
logger.debug('filter exited with code %d', rc)
if filterout_close:
filterout.close()
return self.exit_code
def _export_tar(self, args, archive, tar):
matcher = self.build_matcher(args.patterns, args.paths)
progress = args.progress
output_list = args.output_list
strip_components = args.strip_components
partial_extract = not matcher.empty() or strip_components
hardlink_masters = {} if partial_extract else None
def peek_and_store_hardlink_masters(item, matched):
if (partial_extract and not matched and hardlinkable(item.mode) and
item.get('hardlink_master', True) and 'source' not in item):
hardlink_masters[item.get('path')] = (item.get('chunks'), None)
filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components)
if progress:
pi = ProgressIndicatorPercent(msg='%5.1f%% Processing: %s', step=0.1, msgid='extract')
pi.output('Calculating size')
extracted_size = sum(item.get_size(hardlink_masters) for item in archive.iter_items(filter))
pi.total = extracted_size
else:
pi = None
def item_content_stream(item):
"""
Return a file-like object that reads from the chunks of *item*.
"""
chunk_iterator = archive.pipeline.fetch_many([chunk_id for chunk_id, _, _ in item.chunks])
if pi:
info = [remove_surrogates(item.path)]
return ChunkIteratorFileWrapper(chunk_iterator,
lambda read_bytes: pi.show(increase=len(read_bytes), info=info))
else:
return ChunkIteratorFileWrapper(chunk_iterator)
def item_to_tarinfo(item, original_path):
"""
Transform a Borg *item* into a tarfile.TarInfo object.
Return a tuple (tarinfo, stream), where stream may be a file-like object that represents
the file contents, if any, and is None otherwise. When *tarinfo* is None, the *item*
cannot be represented as a TarInfo object and should be skipped.
"""
# If we would use the PAX (POSIX) format (which we currently don't),
# we can support most things that aren't possible with classic tar
# formats, including GNU tar, such as:
# atime, ctime, possibly Linux capabilities (security.* xattrs)
# and various additions supported by GNU tar in POSIX mode.
stream = None
tarinfo = tarfile.TarInfo()
tarinfo.name = item.path
tarinfo.mtime = item.mtime / 1e9
tarinfo.mode = stat.S_IMODE(item.mode)
tarinfo.uid = item.uid
tarinfo.gid = item.gid
tarinfo.uname = item.user or ''
tarinfo.gname = item.group or ''
# The linkname in tar has the same dual use the 'source' attribute of Borg items,
# i.e. for symlinks it means the destination, while for hardlinks it refers to the
# file.
# Since hardlinks in tar have a different type code (LNKTYPE) the format might
# support hardlinking arbitrary objects (including symlinks and directories), but
# whether implementations actually support that is a whole different question...
tarinfo.linkname = ""
modebits = stat.S_IFMT(item.mode)
if modebits == stat.S_IFREG:
tarinfo.type = tarfile.REGTYPE
if 'source' in item:
source = os.sep.join(item.source.split(os.sep)[strip_components:])
if hardlink_masters is None:
linkname = source
else:
chunks, linkname = hardlink_masters.get(item.source, (None, source))
if linkname:
# Master was already added to the archive, add a hardlink reference to it.
tarinfo.type = tarfile.LNKTYPE
tarinfo.linkname = linkname
elif chunks is not None:
# The item which has the chunks was not put into the tar, therefore
# we do that now and update hardlink_masters to reflect that.
item.chunks = chunks
tarinfo.size = item.get_size()
stream = item_content_stream(item)
hardlink_masters[item.get('source') or original_path] = (None, item.path)
else:
tarinfo.size = item.get_size()
stream = item_content_stream(item)
elif modebits == stat.S_IFDIR:
tarinfo.type = tarfile.DIRTYPE
elif modebits == stat.S_IFLNK:
tarinfo.type = tarfile.SYMTYPE
tarinfo.linkname = item.source
elif modebits == stat.S_IFBLK:
tarinfo.type = tarfile.BLKTYPE
tarinfo.devmajor = os.major(item.rdev)
tarinfo.devminor = os.minor(item.rdev)
elif modebits == stat.S_IFCHR:
tarinfo.type = tarfile.CHRTYPE
tarinfo.devmajor = os.major(item.rdev)
tarinfo.devminor = os.minor(item.rdev)
elif modebits == stat.S_IFIFO:
tarinfo.type = tarfile.FIFOTYPE
else:
self.print_warning('%s: unsupported file type %o for tar export', remove_surrogates(item.path), modebits)
set_ec(EXIT_WARNING)
return None, stream
return tarinfo, stream
for item in archive.iter_items(filter, preload=True):
orig_path = item.path
if strip_components:
item.path = os.sep.join(orig_path.split(os.sep)[strip_components:])
tarinfo, stream = item_to_tarinfo(item, orig_path)
if tarinfo:
if output_list:
logging.getLogger('borg.output.list').info(remove_surrogates(orig_path))
tar.addfile(tarinfo, stream)
if pi:
pi.finish()
for pattern in matcher.get_unmatched_include_patterns():
self.print_warning("Include pattern '%s' never matched.", pattern)
return self.exit_code
@with_repository()
@with_archive
def do_diff(self, args, repository, manifest, key, archive):
@ -2605,6 +2820,72 @@ class Archiver:
subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
help='paths to extract; patterns are supported')
export_tar_epilog = process_epilog("""
This command creates a tarball from an archive.
When giving '-' as the output FILE, Borg will write a tar stream to standard output.
By default (--tar-filter=auto) Borg will detect whether the FILE should be compressed
based on its file extension and pipe the tarball through an appropriate filter
before writing it to FILE:
- .tar.gz: gzip
- .tar.bz2: bzip2
- .tar.xz: xz
Alternatively a --tar-filter program may be explicitly specified. It should
read the uncompressed tar stream from stdin and write a compressed/filtered
tar stream to stdout.
The generated tarball uses the GNU tar format.
export-tar is a lossy conversion:
BSD flags, ACLs, extended attributes (xattrs), atime and ctime are not exported.
Timestamp resolution is limited to whole seconds, not the nanosecond resolution
otherwise supported by Borg.
A --sparse option (as found in borg extract) is not supported.
By default the entire archive is extracted but a subset of files and directories
can be selected by passing a list of ``PATHs`` as arguments.
The file selection can further be restricted by using the ``--exclude`` option.
See the output of the "borg help patterns" command for more help on exclude patterns.
``--progress`` can be slower than no progress display, since it makes one additional
pass over the archive metadata.
""")
subparser = subparsers.add_parser('export-tar', parents=[common_parser], add_help=False,
description=self.do_export_tar.__doc__,
epilog=export_tar_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter,
help='create tarball from archive')
subparser.set_defaults(func=self.do_export_tar)
subparser.add_argument('--tar-filter', dest='tar_filter', default='auto',
help='filter program to pipe data through')
subparser.add_argument('--list', dest='output_list',
action='store_true', default=False,
help='output verbose list of items (files, dirs, ...)')
subparser.add_argument('-e', '--exclude', dest='patterns',
type=parse_exclude_pattern, action='append',
metavar="PATTERN", help='exclude paths matching PATTERN')
subparser.add_argument('--exclude-from', action=ArgparseExcludeFileAction,
metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
subparser.add_argument('--pattern', action=ArgparsePatternAction,
metavar="PATTERN", help='include/exclude paths matching PATTERN')
subparser.add_argument('--patterns-from', action=ArgparsePatternFileAction,
metavar='PATTERNFILE', help='read include/exclude patterns from PATTERNFILE, one per line')
subparser.add_argument('--strip-components', dest='strip_components',
type=int, default=0, metavar='NUMBER',
help='Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.')
subparser.add_argument('location', metavar='ARCHIVE',
type=location_validator(archive=True),
help='archive to export')
subparser.add_argument('tarfile', metavar='FILE',
help='output tar file. "-" to write to stdout instead.')
subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
help='paths to extract; patterns are supported')
diff_epilog = process_epilog("""
This command finds differences (file contents, user/group/mode) between archives.

View File

@ -1622,11 +1622,20 @@ class ItemFormatter(BaseFormatter):
class ChunkIteratorFileWrapper:
"""File-like wrapper for chunk iterators"""
def __init__(self, chunk_iterator):
def __init__(self, chunk_iterator, read_callback=None):
"""
*chunk_iterator* should be an iterator yielding bytes. These will be buffered
internally as necessary to satisfy .read() calls.
*read_callback* will be called with one argument, some byte string that has
just been read and will be subsequently returned to a caller of .read().
It can be used to update a progress display.
"""
self.chunk_iterator = chunk_iterator
self.chunk_offset = 0
self.chunk = b''
self.exhausted = False
self.read_callback = read_callback
def _refill(self):
remaining = len(self.chunk) - self.chunk_offset
@ -1655,6 +1664,8 @@ class ChunkIteratorFileWrapper:
read_data = self._read(nbytes)
nbytes -= len(read_data)
parts.append(read_data)
if self.read_callback:
self.read_callback(read_data)
return b''.join(parts)

View File

@ -150,7 +150,7 @@ class BaseTestCase(unittest.TestCase):
diff = filecmp.dircmp(dir1, dir2)
self._assert_dirs_equal_cmp(diff, **kwargs)
def _assert_dirs_equal_cmp(self, diff, ignore_bsdflags=False, ignore_xattrs=False):
def _assert_dirs_equal_cmp(self, diff, ignore_bsdflags=False, ignore_xattrs=False, ignore_ns=False):
self.assert_equal(diff.left_only, [])
self.assert_equal(diff.right_only, [])
self.assert_equal(diff.diff_files, [])
@ -162,25 +162,30 @@ class BaseTestCase(unittest.TestCase):
s2 = os.lstat(path2)
# Assume path2 is on FUSE if st_dev is different
fuse = s1.st_dev != s2.st_dev
attrs = ['st_mode', 'st_uid', 'st_gid', 'st_rdev']
attrs = ['st_uid', 'st_gid', 'st_rdev']
if not fuse or not os.path.isdir(path1):
# dir nlink is always 1 on our fuse filesystem
attrs.append('st_nlink')
d1 = [filename] + [getattr(s1, a) for a in attrs]
d2 = [filename] + [getattr(s2, a) for a in attrs]
d1.insert(1, oct(s1.st_mode))
d2.insert(1, oct(s2.st_mode))
if not ignore_bsdflags:
d1.append(get_flags(path1, s1))
d2.append(get_flags(path2, s2))
# ignore st_rdev if file is not a block/char device, fixes #203
if not stat.S_ISCHR(d1[1]) and not stat.S_ISBLK(d1[1]):
if not stat.S_ISCHR(s1.st_mode) and not stat.S_ISBLK(s1.st_mode):
d1[4] = None
if not stat.S_ISCHR(d2[1]) and not stat.S_ISBLK(d2[1]):
if not stat.S_ISCHR(s2.st_mode) and not stat.S_ISBLK(s2.st_mode):
d2[4] = None
# If utime isn't fully supported, borg can't set mtime.
# Therefore, we shouldn't test it in that case.
if is_utime_fully_supported():
# Older versions of llfuse do not support ns precision properly
if fuse and not have_fuse_mtime_ns:
if ignore_ns:
d1.append(int(s1.st_mtime_ns / 1e9))
d2.append(int(s2.st_mtime_ns / 1e9))
elif fuse and not have_fuse_mtime_ns:
d1.append(round(s1.st_mtime_ns, -4))
d2.append(round(s2.st_mtime_ns, -4))
else:
@ -191,7 +196,7 @@ class BaseTestCase(unittest.TestCase):
d2.append(no_selinux(get_all(path2, follow_symlinks=False)))
self.assert_equal(d1, d2)
for sub_diff in diff.subdirs.values():
self._assert_dirs_equal_cmp(sub_diff, ignore_bsdflags=ignore_bsdflags, ignore_xattrs=ignore_xattrs)
self._assert_dirs_equal_cmp(sub_diff, ignore_bsdflags=ignore_bsdflags, ignore_xattrs=ignore_xattrs, ignore_ns=ignore_ns)
@contextmanager
def fuse_mount(self, location, mountpoint, *options):

View File

@ -96,6 +96,14 @@ def exec_cmd(*args, archiver=None, fork=False, exe=None, **kw):
sys.stdin, sys.stdout, sys.stderr = stdin, stdout, stderr
def have_gnutar():
if not shutil.which('tar'):
return False
popen = subprocess.Popen(['tar', '--version'], stdout=subprocess.PIPE)
stdout, stderr = popen.communicate()
return b'GNU tar' in stdout
# check if the binary "borg.exe" is available (for local testing a symlink to virtualenv/bin/borg should do)
try:
exec_cmd('help', exe='borg.exe', fork=True)
@ -2354,6 +2362,35 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02
assert '_meta' in result
assert '_items' in result
requires_gnutar = pytest.mark.skipif(not have_gnutar(), reason='GNU tar must be installed for this test.')
requires_gzip = pytest.mark.skipif(not shutil.which('gzip'), reason='gzip must be installed for this test.')
@requires_gnutar
def test_export_tar(self):
self.create_test_files()
os.unlink('input/flagfile')
self.cmd('init', '--encryption=repokey', self.repository_location)
self.cmd('create', self.repository_location + '::test', 'input')
self.cmd('export-tar', self.repository_location + '::test', 'simple.tar')
with changedir('output'):
# This probably assumes GNU tar. Note -p switch to extract permissions regardless of umask.
subprocess.check_output(['tar', 'xpf', '../simple.tar'])
self.assert_dirs_equal('input', 'output/input', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True)
@requires_gnutar
@requires_gzip
def test_export_tar_gz(self):
if not shutil.which('gzip'):
pytest.skip('gzip is not installed')
self.create_test_files()
os.unlink('input/flagfile')
self.cmd('init', '--encryption=repokey', self.repository_location)
self.cmd('create', self.repository_location + '::test', 'input')
self.cmd('export-tar', self.repository_location + '::test', 'simple.tar.gz')
with changedir('output'):
subprocess.check_output(['tar', 'xpf', '../simple.tar.gz'])
self.assert_dirs_equal('input', 'output/input', ignore_bsdflags=True, ignore_xattrs=True, ignore_ns=True)
@unittest.skipUnless('binary' in BORG_EXES, 'no borg.exe available')
class ArchiverTestCaseBinary(ArchiverTestCase):