|
@@ -3,23 +3,28 @@ from binascii import hexlify
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
from operator import attrgetter
|
|
from operator import attrgetter
|
|
import functools
|
|
import functools
|
|
|
|
+import inspect
|
|
import io
|
|
import io
|
|
import os
|
|
import os
|
|
|
|
+import signal
|
|
import stat
|
|
import stat
|
|
import sys
|
|
import sys
|
|
import textwrap
|
|
import textwrap
|
|
-
|
|
|
|
-from attic import __version__
|
|
|
|
-from attic.archive import Archive, ArchiveChecker
|
|
|
|
-from attic.repository import Repository
|
|
|
|
-from attic.cache import Cache
|
|
|
|
-from attic.key import key_creator
|
|
|
|
-from attic.helpers import Error, location_validator, format_time, \
|
|
|
|
- format_file_mode, ExcludePattern, exclude_path, adjust_patterns, to_localtime, \
|
|
|
|
|
|
+import traceback
|
|
|
|
+
|
|
|
|
+from . import __version__
|
|
|
|
+from .archive import Archive, ArchiveChecker, CHUNKER_PARAMS
|
|
|
|
+from .repository import Repository
|
|
|
|
+from .cache import Cache
|
|
|
|
+from .key import key_creator
|
|
|
|
+from .helpers import Error, location_validator, format_time, format_file_size, \
|
|
|
|
+ format_file_mode, ExcludePattern, exclude_path, adjust_patterns, to_localtime, timestamp, \
|
|
get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
|
|
get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \
|
|
Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
|
|
Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \
|
|
- is_cachedir, bigint_to_int
|
|
|
|
-from attic.remote import RepositoryServer, RemoteRepository
|
|
|
|
|
|
+ is_cachedir, bigint_to_int, ChunkerParams
|
|
|
|
+from .remote import RepositoryServer, RemoteRepository
|
|
|
|
+
|
|
|
|
+has_lchflags = hasattr(os, 'lchflags')
|
|
|
|
|
|
|
|
|
|
class Archiver:
|
|
class Archiver:
|
|
@@ -38,7 +43,7 @@ class Archiver:
|
|
def print_error(self, msg, *args):
|
|
def print_error(self, msg, *args):
|
|
msg = args and msg % args or msg
|
|
msg = args and msg % args or msg
|
|
self.exit_code = 1
|
|
self.exit_code = 1
|
|
- print('attic: ' + msg, file=sys.stderr)
|
|
|
|
|
|
+ print('borg: ' + msg, file=sys.stderr)
|
|
|
|
|
|
def print_verbose(self, msg, *args, **kw):
|
|
def print_verbose(self, msg, *args, **kw):
|
|
if self.verbose:
|
|
if self.verbose:
|
|
@@ -49,7 +54,7 @@ class Archiver:
|
|
print(msg, end=' ')
|
|
print(msg, end=' ')
|
|
|
|
|
|
def do_serve(self, args):
|
|
def do_serve(self, args):
|
|
- """Start Attic in server mode. This command is usually not used manually.
|
|
|
|
|
|
+ """Start in server mode. This command is usually not used manually.
|
|
"""
|
|
"""
|
|
return RepositoryServer(restrict_to_paths=args.restrict_to_paths).serve()
|
|
return RepositoryServer(restrict_to_paths=args.restrict_to_paths).serve()
|
|
|
|
|
|
@@ -69,7 +74,7 @@ class Archiver:
|
|
"""Check repository consistency"""
|
|
"""Check repository consistency"""
|
|
repository = self.open_repository(args.repository, exclusive=args.repair)
|
|
repository = self.open_repository(args.repository, exclusive=args.repair)
|
|
if args.repair:
|
|
if args.repair:
|
|
- while not os.environ.get('ATTIC_CHECK_I_KNOW_WHAT_I_AM_DOING'):
|
|
|
|
|
|
+ while not os.environ.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
|
|
self.print_error("""Warning: 'check --repair' is an experimental feature that might result
|
|
self.print_error("""Warning: 'check --repair' is an experimental feature that might result
|
|
in data loss.
|
|
in data loss.
|
|
|
|
|
|
@@ -82,8 +87,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
print('Repository check complete, no problems found.')
|
|
print('Repository check complete, no problems found.')
|
|
else:
|
|
else:
|
|
return 1
|
|
return 1
|
|
- if not args.repo_only and not ArchiveChecker().check(repository, repair=args.repair):
|
|
|
|
- return 1
|
|
|
|
|
|
+ if not args.repo_only and not ArchiveChecker().check(
|
|
|
|
+ repository, repair=args.repair, archive=args.repository.archive, last=args.last):
|
|
|
|
+ return 1
|
|
return 0
|
|
return 0
|
|
|
|
|
|
def do_change_passphrase(self, args):
|
|
def do_change_passphrase(self, args):
|
|
@@ -98,11 +104,13 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
t0 = datetime.now()
|
|
t0 = datetime.now()
|
|
repository = self.open_repository(args.archive, exclusive=True)
|
|
repository = self.open_repository(args.archive, exclusive=True)
|
|
manifest, key = Manifest.load(repository)
|
|
manifest, key = Manifest.load(repository)
|
|
- cache = Cache(repository, key, manifest)
|
|
|
|
|
|
+ key.compression_level = args.compression
|
|
|
|
+ cache = Cache(repository, key, manifest, do_files=args.cache_files)
|
|
archive = Archive(repository, key, manifest, args.archive.archive, cache=cache,
|
|
archive = Archive(repository, key, manifest, args.archive.archive, cache=cache,
|
|
create=True, checkpoint_interval=args.checkpoint_interval,
|
|
create=True, checkpoint_interval=args.checkpoint_interval,
|
|
- numeric_owner=args.numeric_owner)
|
|
|
|
- # Add Attic cache dir to inode_skip list
|
|
|
|
|
|
+ numeric_owner=args.numeric_owner, progress=args.progress,
|
|
|
|
+ chunker_params=args.chunker_params)
|
|
|
|
+ # Add cache dir to inode_skip list
|
|
skip_inodes = set()
|
|
skip_inodes = set()
|
|
try:
|
|
try:
|
|
st = os.stat(get_cache_dir())
|
|
st = os.stat(get_cache_dir())
|
|
@@ -117,6 +125,14 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
except IOError:
|
|
except IOError:
|
|
pass
|
|
pass
|
|
for path in args.paths:
|
|
for path in args.paths:
|
|
|
|
+ if path == '-': # stdin
|
|
|
|
+ path = 'stdin'
|
|
|
|
+ self.print_verbose(path)
|
|
|
|
+ try:
|
|
|
|
+ archive.process_stdin(path, cache)
|
|
|
|
+ except IOError as e:
|
|
|
|
+ self.print_error('%s: %s', path, e)
|
|
|
|
+ continue
|
|
path = os.path.normpath(path)
|
|
path = os.path.normpath(path)
|
|
if args.dontcross:
|
|
if args.dontcross:
|
|
try:
|
|
try:
|
|
@@ -127,7 +143,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
else:
|
|
else:
|
|
restrict_dev = None
|
|
restrict_dev = None
|
|
self._process(archive, cache, args.excludes, args.exclude_caches, skip_inodes, path, restrict_dev)
|
|
self._process(archive, cache, args.excludes, args.exclude_caches, skip_inodes, path, restrict_dev)
|
|
- archive.save()
|
|
|
|
|
|
+ archive.save(timestamp=args.timestamp)
|
|
|
|
+ if args.progress:
|
|
|
|
+ archive.stats.show_progress(final=True)
|
|
if args.stats:
|
|
if args.stats:
|
|
t = datetime.now()
|
|
t = datetime.now()
|
|
diff = t - t0
|
|
diff = t - t0
|
|
@@ -155,48 +173,67 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
# Entering a new filesystem?
|
|
# Entering a new filesystem?
|
|
if restrict_dev and st.st_dev != restrict_dev:
|
|
if restrict_dev and st.st_dev != restrict_dev:
|
|
return
|
|
return
|
|
- # Ignore unix sockets
|
|
|
|
- if stat.S_ISSOCK(st.st_mode):
|
|
|
|
|
|
+ status = None
|
|
|
|
+ # Ignore if nodump flag is set
|
|
|
|
+ if has_lchflags and (st.st_flags & stat.UF_NODUMP):
|
|
return
|
|
return
|
|
- self.print_verbose(remove_surrogates(path))
|
|
|
|
if stat.S_ISREG(st.st_mode):
|
|
if stat.S_ISREG(st.st_mode):
|
|
try:
|
|
try:
|
|
- archive.process_file(path, st, cache)
|
|
|
|
|
|
+ status = archive.process_file(path, st, cache)
|
|
except IOError as e:
|
|
except IOError as e:
|
|
self.print_error('%s: %s', path, e)
|
|
self.print_error('%s: %s', path, e)
|
|
elif stat.S_ISDIR(st.st_mode):
|
|
elif stat.S_ISDIR(st.st_mode):
|
|
if exclude_caches and is_cachedir(path):
|
|
if exclude_caches and is_cachedir(path):
|
|
return
|
|
return
|
|
- archive.process_item(path, st)
|
|
|
|
|
|
+ status = archive.process_dir(path, st)
|
|
try:
|
|
try:
|
|
entries = os.listdir(path)
|
|
entries = os.listdir(path)
|
|
except OSError as e:
|
|
except OSError as e:
|
|
self.print_error('%s: %s', path, e)
|
|
self.print_error('%s: %s', path, e)
|
|
else:
|
|
else:
|
|
for filename in sorted(entries):
|
|
for filename in sorted(entries):
|
|
|
|
+ entry_path = os.path.normpath(os.path.join(path, filename))
|
|
self._process(archive, cache, excludes, exclude_caches, skip_inodes,
|
|
self._process(archive, cache, excludes, exclude_caches, skip_inodes,
|
|
- os.path.join(path, filename), restrict_dev)
|
|
|
|
|
|
+ entry_path, restrict_dev)
|
|
elif stat.S_ISLNK(st.st_mode):
|
|
elif stat.S_ISLNK(st.st_mode):
|
|
- archive.process_symlink(path, st)
|
|
|
|
|
|
+ status = archive.process_symlink(path, st)
|
|
elif stat.S_ISFIFO(st.st_mode):
|
|
elif stat.S_ISFIFO(st.st_mode):
|
|
- archive.process_item(path, st)
|
|
|
|
|
|
+ status = archive.process_fifo(path, st)
|
|
elif stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
|
|
elif stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
|
|
- archive.process_dev(path, st)
|
|
|
|
|
|
+ status = archive.process_dev(path, st)
|
|
|
|
+ elif stat.S_ISSOCK(st.st_mode):
|
|
|
|
+ # Ignore unix sockets
|
|
|
|
+ return
|
|
else:
|
|
else:
|
|
self.print_error('Unknown file type: %s', path)
|
|
self.print_error('Unknown file type: %s', path)
|
|
|
|
+ return
|
|
|
|
+ # Status output
|
|
|
|
+ # A lowercase character means a file type other than a regular file,
|
|
|
|
+ # borg usually just stores them. E.g. (d)irectory.
|
|
|
|
+ # Hardlinks to already seen content are indicated by (h).
|
|
|
|
+ # A uppercase character means a regular file that was (A)dded,
|
|
|
|
+ # (M)odified or was (U)nchanged.
|
|
|
|
+ # Note: A/M/U is relative to the "files" cache, not to the repo.
|
|
|
|
+ # This would be an issue if the files cache is not used.
|
|
|
|
+ if status is None:
|
|
|
|
+ status = '?' # need to add a status code somewhere
|
|
|
|
+ # output ALL the stuff - it can be easily filtered using grep.
|
|
|
|
+ # even stuff considered unchanged might be interesting.
|
|
|
|
+ self.print_verbose("%1s %s", status, remove_surrogates(path))
|
|
|
|
|
|
def do_extract(self, args):
|
|
def do_extract(self, args):
|
|
"""Extract archive contents"""
|
|
"""Extract archive contents"""
|
|
# be restrictive when restoring files, restore permissions later
|
|
# be restrictive when restoring files, restore permissions later
|
|
if sys.getfilesystemencoding() == 'ascii':
|
|
if sys.getfilesystemencoding() == 'ascii':
|
|
print('Warning: File system encoding is "ascii", extracting non-ascii filenames will not be supported.')
|
|
print('Warning: File system encoding is "ascii", extracting non-ascii filenames will not be supported.')
|
|
- os.umask(0o077)
|
|
|
|
repository = self.open_repository(args.archive)
|
|
repository = self.open_repository(args.archive)
|
|
manifest, key = Manifest.load(repository)
|
|
manifest, key = Manifest.load(repository)
|
|
archive = Archive(repository, key, manifest, args.archive.archive,
|
|
archive = Archive(repository, key, manifest, args.archive.archive,
|
|
numeric_owner=args.numeric_owner)
|
|
numeric_owner=args.numeric_owner)
|
|
patterns = adjust_patterns(args.paths, args.excludes)
|
|
patterns = adjust_patterns(args.paths, args.excludes)
|
|
dry_run = args.dry_run
|
|
dry_run = args.dry_run
|
|
|
|
+ stdout = args.stdout
|
|
|
|
+ sparse = args.sparse
|
|
strip_components = args.strip_components
|
|
strip_components = args.strip_components
|
|
dirs = []
|
|
dirs = []
|
|
for item in archive.iter_items(lambda item: not exclude_path(item[b'path'], patterns), preload=True):
|
|
for item in archive.iter_items(lambda item: not exclude_path(item[b'path'], patterns), preload=True):
|
|
@@ -207,7 +244,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
continue
|
|
continue
|
|
if not args.dry_run:
|
|
if not args.dry_run:
|
|
while dirs and not item[b'path'].startswith(dirs[-1][b'path']):
|
|
while dirs and not item[b'path'].startswith(dirs[-1][b'path']):
|
|
- archive.extract_item(dirs.pop(-1))
|
|
|
|
|
|
+ archive.extract_item(dirs.pop(-1), stdout=stdout)
|
|
self.print_verbose(remove_surrogates(orig_path))
|
|
self.print_verbose(remove_surrogates(orig_path))
|
|
try:
|
|
try:
|
|
if dry_run:
|
|
if dry_run:
|
|
@@ -217,7 +254,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
dirs.append(item)
|
|
dirs.append(item)
|
|
archive.extract_item(item, restore_attrs=False)
|
|
archive.extract_item(item, restore_attrs=False)
|
|
else:
|
|
else:
|
|
- archive.extract_item(item)
|
|
|
|
|
|
+ archive.extract_item(item, stdout=stdout, sparse=sparse)
|
|
except IOError as e:
|
|
except IOError as e:
|
|
self.print_error('%s: %s', remove_surrogates(orig_path), e)
|
|
self.print_error('%s: %s', remove_surrogates(orig_path), e)
|
|
|
|
|
|
@@ -226,27 +263,51 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
archive.extract_item(dirs.pop(-1))
|
|
archive.extract_item(dirs.pop(-1))
|
|
return self.exit_code
|
|
return self.exit_code
|
|
|
|
|
|
- def do_delete(self, args):
|
|
|
|
- """Delete an existing archive"""
|
|
|
|
|
|
+ def do_rename(self, args):
|
|
|
|
+ """Rename an existing archive"""
|
|
repository = self.open_repository(args.archive, exclusive=True)
|
|
repository = self.open_repository(args.archive, exclusive=True)
|
|
manifest, key = Manifest.load(repository)
|
|
manifest, key = Manifest.load(repository)
|
|
cache = Cache(repository, key, manifest)
|
|
cache = Cache(repository, key, manifest)
|
|
archive = Archive(repository, key, manifest, args.archive.archive, cache=cache)
|
|
archive = Archive(repository, key, manifest, args.archive.archive, cache=cache)
|
|
- stats = Statistics()
|
|
|
|
- archive.delete(stats)
|
|
|
|
|
|
+ archive.rename(args.name)
|
|
manifest.write()
|
|
manifest.write()
|
|
repository.commit()
|
|
repository.commit()
|
|
cache.commit()
|
|
cache.commit()
|
|
- if args.stats:
|
|
|
|
- stats.print_('Deleted data:', cache)
|
|
|
|
|
|
+ return self.exit_code
|
|
|
|
+
|
|
|
|
+ def do_delete(self, args):
|
|
|
|
+ """Delete an existing repository or archive"""
|
|
|
|
+ repository = self.open_repository(args.target, exclusive=True)
|
|
|
|
+ manifest, key = Manifest.load(repository)
|
|
|
|
+ cache = Cache(repository, key, manifest, do_files=args.cache_files)
|
|
|
|
+ if args.target.archive:
|
|
|
|
+ archive = Archive(repository, key, manifest, args.target.archive, cache=cache)
|
|
|
|
+ stats = Statistics()
|
|
|
|
+ archive.delete(stats)
|
|
|
|
+ manifest.write()
|
|
|
|
+ repository.commit()
|
|
|
|
+ cache.commit()
|
|
|
|
+ if args.stats:
|
|
|
|
+ stats.print_('Deleted data:', cache)
|
|
|
|
+ else:
|
|
|
|
+ print("You requested to completely DELETE the repository *including* all archives it contains:")
|
|
|
|
+ for archive_info in manifest.list_archive_infos(sort_by='ts'):
|
|
|
|
+ print(format_archive(archive_info))
|
|
|
|
+ while not os.environ.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING'):
|
|
|
|
+ print("""Type "YES" if you understand this and want to continue.\n""")
|
|
|
|
+ if input('Do you want to continue? ') == 'YES':
|
|
|
|
+ break
|
|
|
|
+ repository.destroy()
|
|
|
|
+ cache.destroy()
|
|
|
|
+ print("Repository and corresponding cache were deleted.")
|
|
return self.exit_code
|
|
return self.exit_code
|
|
|
|
|
|
def do_mount(self, args):
|
|
def do_mount(self, args):
|
|
"""Mount archive or an entire repository as a FUSE fileystem"""
|
|
"""Mount archive or an entire repository as a FUSE fileystem"""
|
|
try:
|
|
try:
|
|
- from attic.fuse import AtticOperations
|
|
|
|
- except ImportError:
|
|
|
|
- self.print_error('the "llfuse" module is required to use this feature')
|
|
|
|
|
|
+ from .fuse import FuseOperations
|
|
|
|
+ except ImportError as e:
|
|
|
|
+ self.print_error('loading fuse support failed [ImportError: %s]' % str(e))
|
|
return self.exit_code
|
|
return self.exit_code
|
|
|
|
|
|
if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
|
|
if not os.path.isdir(args.mountpoint) or not os.access(args.mountpoint, os.R_OK | os.W_OK | os.X_OK):
|
|
@@ -259,7 +320,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
archive = Archive(repository, key, manifest, args.src.archive)
|
|
archive = Archive(repository, key, manifest, args.src.archive)
|
|
else:
|
|
else:
|
|
archive = None
|
|
archive = None
|
|
- operations = AtticOperations(key, repository, manifest, archive)
|
|
|
|
|
|
+ operations = FuseOperations(key, repository, manifest, archive)
|
|
self.print_verbose("Mounting filesystem")
|
|
self.print_verbose("Mounting filesystem")
|
|
try:
|
|
try:
|
|
operations.mount(args.mountpoint, args.options, args.foreground)
|
|
operations.mount(args.mountpoint, args.options, args.foreground)
|
|
@@ -284,7 +345,11 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
size = sum(size for _, size, _ in item[b'chunks'])
|
|
size = sum(size for _, size, _ in item[b'chunks'])
|
|
except KeyError:
|
|
except KeyError:
|
|
pass
|
|
pass
|
|
- mtime = format_time(datetime.fromtimestamp(bigint_to_int(item[b'mtime']) / 1e9))
|
|
|
|
|
|
+ try:
|
|
|
|
+ mtime = datetime.fromtimestamp(bigint_to_int(item[b'mtime']) / 1e9)
|
|
|
|
+ except ValueError:
|
|
|
|
+ # likely a broken mtime and datetime did not want to go beyond year 9999
|
|
|
|
+ mtime = datetime(9999, 12, 31, 23, 59, 59)
|
|
if b'source' in item:
|
|
if b'source' in item:
|
|
if type == 'l':
|
|
if type == 'l':
|
|
extra = ' -> %s' % item[b'source']
|
|
extra = ' -> %s' % item[b'source']
|
|
@@ -293,19 +358,20 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
extra = ' link to %s' % item[b'source']
|
|
extra = ' link to %s' % item[b'source']
|
|
else:
|
|
else:
|
|
extra = ''
|
|
extra = ''
|
|
- print('%s%s %-6s %-6s %8d %s %s%s' % (type, mode, item[b'user'] or item[b'uid'],
|
|
|
|
- item[b'group'] or item[b'gid'], size, mtime,
|
|
|
|
- remove_surrogates(item[b'path']), extra))
|
|
|
|
|
|
+ print('%s%s %-6s %-6s %8d %s %s%s' % (
|
|
|
|
+ type, mode, item[b'user'] or item[b'uid'],
|
|
|
|
+ item[b'group'] or item[b'gid'], size, format_time(mtime),
|
|
|
|
+ remove_surrogates(item[b'path']), extra))
|
|
else:
|
|
else:
|
|
- for archive in sorted(Archive.list_archives(repository, key, manifest), key=attrgetter('ts')):
|
|
|
|
- print(format_archive(archive))
|
|
|
|
|
|
+ for archive_info in manifest.list_archive_infos(sort_by='ts'):
|
|
|
|
+ print(format_archive(archive_info))
|
|
return self.exit_code
|
|
return self.exit_code
|
|
|
|
|
|
def do_info(self, args):
|
|
def do_info(self, args):
|
|
"""Show archive details such as disk space used"""
|
|
"""Show archive details such as disk space used"""
|
|
repository = self.open_repository(args.archive)
|
|
repository = self.open_repository(args.archive)
|
|
manifest, key = Manifest.load(repository)
|
|
manifest, key = Manifest.load(repository)
|
|
- cache = Cache(repository, key, manifest)
|
|
|
|
|
|
+ cache = Cache(repository, key, manifest, do_files=args.cache_files)
|
|
archive = Archive(repository, key, manifest, args.archive.archive, cache=cache)
|
|
archive = Archive(repository, key, manifest, args.archive.archive, cache=cache)
|
|
stats = archive.calc_stats(cache)
|
|
stats = archive.calc_stats(cache)
|
|
print('Name:', archive.name)
|
|
print('Name:', archive.name)
|
|
@@ -322,12 +388,11 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
"""Prune repository archives according to specified rules"""
|
|
"""Prune repository archives according to specified rules"""
|
|
repository = self.open_repository(args.repository, exclusive=True)
|
|
repository = self.open_repository(args.repository, exclusive=True)
|
|
manifest, key = Manifest.load(repository)
|
|
manifest, key = Manifest.load(repository)
|
|
- cache = Cache(repository, key, manifest)
|
|
|
|
- archives = list(sorted(Archive.list_archives(repository, key, manifest, cache),
|
|
|
|
- key=attrgetter('ts'), reverse=True))
|
|
|
|
|
|
+ cache = Cache(repository, key, manifest, do_files=args.cache_files)
|
|
|
|
+ archives = manifest.list_archive_infos(sort_by='ts', reverse=True) # just a ArchiveInfo list
|
|
if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
|
|
if args.hourly + args.daily + args.weekly + args.monthly + args.yearly == 0 and args.within is None:
|
|
- self.print_error('At least one of the "within", "hourly", "daily", "weekly", "monthly" or "yearly" '
|
|
|
|
- 'settings must be specified')
|
|
|
|
|
|
+ self.print_error('At least one of the "within", "keep-hourly", "keep-daily", "keep-weekly", '
|
|
|
|
+ '"keep-monthly" or "keep-yearly" settings must be specified')
|
|
return 1
|
|
return 1
|
|
if args.prefix:
|
|
if args.prefix:
|
|
archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
|
|
archives = [archive for archive in archives if archive.name.startswith(args.prefix)]
|
|
@@ -355,7 +420,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
self.print_verbose('Would prune: %s' % format_archive(archive))
|
|
self.print_verbose('Would prune: %s' % format_archive(archive))
|
|
else:
|
|
else:
|
|
self.print_verbose('Pruning archive: %s' % format_archive(archive))
|
|
self.print_verbose('Pruning archive: %s' % format_archive(archive))
|
|
- archive.delete(stats)
|
|
|
|
|
|
+ Archive(repository, key, manifest, archive.name, cache).delete(stats)
|
|
if to_delete and not args.dry_run:
|
|
if to_delete and not args.dry_run:
|
|
manifest.write()
|
|
manifest.write()
|
|
repository.commit()
|
|
repository.commit()
|
|
@@ -381,17 +446,17 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
Examples:
|
|
Examples:
|
|
|
|
|
|
# Exclude '/home/user/file.o' but not '/home/user/file.odt':
|
|
# Exclude '/home/user/file.o' but not '/home/user/file.odt':
|
|
- $ attic create -e '*.o' repo.attic /
|
|
|
|
|
|
+ $ borg create -e '*.o' backup /
|
|
|
|
|
|
# Exclude '/home/user/junk' and '/home/user/subdir/junk' but
|
|
# Exclude '/home/user/junk' and '/home/user/subdir/junk' but
|
|
# not '/home/user/importantjunk' or '/etc/junk':
|
|
# not '/home/user/importantjunk' or '/etc/junk':
|
|
- $ attic create -e '/home/*/junk' repo.attic /
|
|
|
|
|
|
+ $ borg create -e '/home/*/junk' backup /
|
|
|
|
|
|
# Exclude the contents of '/home/user/cache' but not the directory itself:
|
|
# Exclude the contents of '/home/user/cache' but not the directory itself:
|
|
- $ attic create -e /home/user/cache/ repo.attic /
|
|
|
|
|
|
+ $ borg create -e /home/user/cache/ backup /
|
|
|
|
|
|
# The file '/home/user/cache/important' is *not* backed up:
|
|
# The file '/home/user/cache/important' is *not* backed up:
|
|
- $ attic create -e /home/user/cache/ repo.attic / /home/user/cache/important
|
|
|
|
|
|
+ $ borg create -e /home/user/cache/ backup / /home/user/cache/important
|
|
'''
|
|
'''
|
|
|
|
|
|
def do_help(self, parser, commands, args):
|
|
def do_help(self, parser, commands, args):
|
|
@@ -420,7 +485,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
('--yearly', '--keep-yearly', 'Warning: "--yearly" has been deprecated. Use "--keep-yearly" instead.')
|
|
('--yearly', '--keep-yearly', 'Warning: "--yearly" has been deprecated. Use "--keep-yearly" instead.')
|
|
]
|
|
]
|
|
if args and args[0] == 'verify':
|
|
if args and args[0] == 'verify':
|
|
- print('Warning: "attic verify" has been deprecated. Use "attic extract --dry-run" instead.')
|
|
|
|
|
|
+ print('Warning: "borg verify" has been deprecated. Use "borg extract --dry-run" instead.')
|
|
args = ['extract', '--dry-run'] + args[1:]
|
|
args = ['extract', '--dry-run'] + args[1:]
|
|
for i, arg in enumerate(args[:]):
|
|
for i, arg in enumerate(args[:]):
|
|
for old_name, new_name, warning in deprecations:
|
|
for old_name, new_name, warning in deprecations:
|
|
@@ -442,24 +507,34 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
with open(os.path.join(cache_dir, 'CACHEDIR.TAG'), 'w') as fd:
|
|
with open(os.path.join(cache_dir, 'CACHEDIR.TAG'), 'w') as fd:
|
|
fd.write(textwrap.dedent("""
|
|
fd.write(textwrap.dedent("""
|
|
Signature: 8a477f597d28d172789f06886806bc55
|
|
Signature: 8a477f597d28d172789f06886806bc55
|
|
- # This file is a cache directory tag created by Attic.
|
|
|
|
|
|
+ # This file is a cache directory tag created by Borg.
|
|
# For information about cache directory tags, see:
|
|
# For information about cache directory tags, see:
|
|
# http://www.brynosaurus.com/cachedir/
|
|
# http://www.brynosaurus.com/cachedir/
|
|
""").lstrip())
|
|
""").lstrip())
|
|
common_parser = argparse.ArgumentParser(add_help=False)
|
|
common_parser = argparse.ArgumentParser(add_help=False)
|
|
common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
|
|
common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
|
|
- default=False,
|
|
|
|
- help='verbose output')
|
|
|
|
|
|
+ default=False,
|
|
|
|
+ help='verbose output')
|
|
|
|
+ common_parser.add_argument('--no-files-cache', dest='cache_files', action='store_false',
|
|
|
|
+ help='do not load/update the file metadata cache used to detect unchanged files')
|
|
|
|
+ common_parser.add_argument('--umask', dest='umask', type=lambda s: int(s, 8), default=0o077, metavar='M',
|
|
|
|
+ help='set umask to M (local and remote, default: 0o077)')
|
|
|
|
+ common_parser.add_argument('--remote-path', dest='remote_path', default='borg', metavar='PATH',
|
|
|
|
+ help='set remote path to executable (default: "borg")')
|
|
|
|
|
|
# We can't use argparse for "serve" since we don't want it to show up in "Available commands"
|
|
# We can't use argparse for "serve" since we don't want it to show up in "Available commands"
|
|
if args:
|
|
if args:
|
|
args = self.preprocess_args(args)
|
|
args = self.preprocess_args(args)
|
|
|
|
|
|
- parser = argparse.ArgumentParser(description='Attic %s - Deduplicated Backups' % __version__)
|
|
|
|
|
|
+ parser = argparse.ArgumentParser(description='Borg %s - Deduplicated Backups' % __version__)
|
|
subparsers = parser.add_subparsers(title='Available commands')
|
|
subparsers = parser.add_subparsers(title='Available commands')
|
|
|
|
|
|
|
|
+ serve_epilog = textwrap.dedent("""
|
|
|
|
+ This command starts a repository server process. This command is usually not used manually.
|
|
|
|
+ """)
|
|
subparser = subparsers.add_parser('serve', parents=[common_parser],
|
|
subparser = subparsers.add_parser('serve', parents=[common_parser],
|
|
- description=self.do_serve.__doc__)
|
|
|
|
|
|
+ description=self.do_serve.__doc__, epilog=serve_epilog,
|
|
|
|
+ formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
subparser.set_defaults(func=self.do_serve)
|
|
subparser.set_defaults(func=self.do_serve)
|
|
subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append',
|
|
subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append',
|
|
metavar='PATH', help='restrict repository access to PATH')
|
|
metavar='PATH', help='restrict repository access to PATH')
|
|
@@ -467,6 +542,8 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
This command initializes an empty repository. A repository is a filesystem
|
|
This command initializes an empty repository. A repository is a filesystem
|
|
directory containing the deduplicated data from zero or more archives.
|
|
directory containing the deduplicated data from zero or more archives.
|
|
Encryption can be enabled at repository init time.
|
|
Encryption can be enabled at repository init time.
|
|
|
|
+ Please note that the 'passphrase' encryption mode is DEPRECATED (instead of it,
|
|
|
|
+ consider using 'repokey').
|
|
""")
|
|
""")
|
|
subparser = subparsers.add_parser('init', parents=[common_parser],
|
|
subparser = subparsers.add_parser('init', parents=[common_parser],
|
|
description=self.do_init.__doc__, epilog=init_epilog,
|
|
description=self.do_init.__doc__, epilog=init_epilog,
|
|
@@ -476,27 +553,51 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
type=location_validator(archive=False),
|
|
type=location_validator(archive=False),
|
|
help='repository to create')
|
|
help='repository to create')
|
|
subparser.add_argument('-e', '--encryption', dest='encryption',
|
|
subparser.add_argument('-e', '--encryption', dest='encryption',
|
|
- choices=('none', 'passphrase', 'keyfile'), default='none',
|
|
|
|
- help='select encryption method')
|
|
|
|
|
|
+ choices=('none', 'keyfile', 'repokey', 'passphrase'), default='none',
|
|
|
|
+ help='select encryption key mode')
|
|
|
|
|
|
check_epilog = textwrap.dedent("""
|
|
check_epilog = textwrap.dedent("""
|
|
- The check command verifies the consistency of a repository and the corresponding
|
|
|
|
- archives. The underlying repository data files are first checked to detect bit rot
|
|
|
|
- and other types of damage. After that the consistency and correctness of the archive
|
|
|
|
- metadata is verified.
|
|
|
|
-
|
|
|
|
- The archive metadata checks can be time consuming and requires access to the key
|
|
|
|
- file and/or passphrase if encryption is enabled. These checks can be skipped using
|
|
|
|
- the --repository-only option.
|
|
|
|
|
|
+ The check command verifies the consistency of a repository and the corresponding archives.
|
|
|
|
+
|
|
|
|
+ First, the underlying repository data files are checked:
|
|
|
|
+ - For all segments the segment magic (header) is checked
|
|
|
|
+ - For all objects stored in the segments, all metadata (e.g. crc and size) and
|
|
|
|
+ all data is read. The read data is checked by size and CRC. Bit rot and other
|
|
|
|
+ types of accidental damage can be detected this way.
|
|
|
|
+ - If we are in repair mode and a integrity error is detected for a segment,
|
|
|
|
+ we try to recover as many objects from the segment as possible.
|
|
|
|
+ - In repair mode, it makes sure that the index is consistent with the data
|
|
|
|
+ stored in the segments.
|
|
|
|
+ - If you use a remote repo server via ssh:, the repo check is executed on the
|
|
|
|
+ repo server without causing significant network traffic.
|
|
|
|
+ - The repository check can be skipped using the --archives-only option.
|
|
|
|
+
|
|
|
|
+ Second, the consistency and correctness of the archive metadata is verified:
|
|
|
|
+ - Is the repo manifest present? If not, it is rebuilt from archive metadata
|
|
|
|
+ chunks (this requires reading and decrypting of all metadata and data).
|
|
|
|
+ - Check if archive metadata chunk is present. if not, remove archive from
|
|
|
|
+ manifest.
|
|
|
|
+ - For all files (items) in the archive, for all chunks referenced by these
|
|
|
|
+ files, check if chunk is present (if not and we are in repair mode, replace
|
|
|
|
+ it with a same-size chunk of zeros). This requires reading of archive and
|
|
|
|
+ file metadata, but not data.
|
|
|
|
+ - If we are in repair mode and we checked all the archives: delete orphaned
|
|
|
|
+ chunks from the repo.
|
|
|
|
+ - if you use a remote repo server via ssh:, the archive check is executed on
|
|
|
|
+ the client machine (because if encryption is enabled, the checks will require
|
|
|
|
+ decryption and this is always done client-side, because key access will be
|
|
|
|
+ required).
|
|
|
|
+ - The archive checks can be time consuming, they can be skipped using the
|
|
|
|
+ --repository-only option.
|
|
""")
|
|
""")
|
|
subparser = subparsers.add_parser('check', parents=[common_parser],
|
|
subparser = subparsers.add_parser('check', parents=[common_parser],
|
|
description=self.do_check.__doc__,
|
|
description=self.do_check.__doc__,
|
|
epilog=check_epilog,
|
|
epilog=check_epilog,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
subparser.set_defaults(func=self.do_check)
|
|
subparser.set_defaults(func=self.do_check)
|
|
- subparser.add_argument('repository', metavar='REPOSITORY',
|
|
|
|
- type=location_validator(archive=False),
|
|
|
|
- help='repository to check consistency of')
|
|
|
|
|
|
+ subparser.add_argument('repository', metavar='REPOSITORY_OR_ARCHIVE',
|
|
|
|
+ type=location_validator(),
|
|
|
|
+ help='repository or archive to check consistency of')
|
|
subparser.add_argument('--repository-only', dest='repo_only', action='store_true',
|
|
subparser.add_argument('--repository-only', dest='repo_only', action='store_true',
|
|
default=False,
|
|
default=False,
|
|
help='only perform repository checks')
|
|
help='only perform repository checks')
|
|
@@ -506,6 +607,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
subparser.add_argument('--repair', dest='repair', action='store_true',
|
|
subparser.add_argument('--repair', dest='repair', action='store_true',
|
|
default=False,
|
|
default=False,
|
|
help='attempt to repair any inconsistencies found')
|
|
help='attempt to repair any inconsistencies found')
|
|
|
|
+ subparser.add_argument('--last', dest='last',
|
|
|
|
+ type=int, default=None, metavar='N',
|
|
|
|
+ help='only check last N archives (Default: all)')
|
|
|
|
|
|
change_passphrase_epilog = textwrap.dedent("""
|
|
change_passphrase_epilog = textwrap.dedent("""
|
|
The key files used for repository encryption are optionally passphrase
|
|
The key files used for repository encryption are optionally passphrase
|
|
@@ -524,7 +628,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
traversing all paths specified. The archive will consume almost no disk space for
|
|
traversing all paths specified. The archive will consume almost no disk space for
|
|
files or parts of files that have already been stored in other archives.
|
|
files or parts of files that have already been stored in other archives.
|
|
|
|
|
|
- See "attic help patterns" for more help on exclude patterns.
|
|
|
|
|
|
+ See the output of the "borg help patterns" command for more help on exclude patterns.
|
|
""")
|
|
""")
|
|
|
|
|
|
subparser = subparsers.add_parser('create', parents=[common_parser],
|
|
subparser = subparsers.add_parser('create', parents=[common_parser],
|
|
@@ -535,6 +639,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
subparser.add_argument('-s', '--stats', dest='stats',
|
|
subparser.add_argument('-s', '--stats', dest='stats',
|
|
action='store_true', default=False,
|
|
action='store_true', default=False,
|
|
help='print statistics for the created archive')
|
|
help='print statistics for the created archive')
|
|
|
|
+ subparser.add_argument('-p', '--progress', dest='progress',
|
|
|
|
+ action='store_true', default=False,
|
|
|
|
+ help='print progress while creating the archive')
|
|
subparser.add_argument('-e', '--exclude', dest='excludes',
|
|
subparser.add_argument('-e', '--exclude', dest='excludes',
|
|
type=ExcludePattern, action='append',
|
|
type=ExcludePattern, action='append',
|
|
metavar="PATTERN", help='exclude paths matching PATTERN')
|
|
metavar="PATTERN", help='exclude paths matching PATTERN')
|
|
@@ -553,6 +660,19 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
subparser.add_argument('--numeric-owner', dest='numeric_owner',
|
|
subparser.add_argument('--numeric-owner', dest='numeric_owner',
|
|
action='store_true', default=False,
|
|
action='store_true', default=False,
|
|
help='only store numeric user and group identifiers')
|
|
help='only store numeric user and group identifiers')
|
|
|
|
+ subparser.add_argument('--timestamp', dest='timestamp',
|
|
|
|
+ type=timestamp, default=None,
|
|
|
|
+ metavar='yyyy-mm-ddThh:mm:ss',
|
|
|
|
+ help='manually specify the archive creation date/time (UTC). '
|
|
|
|
+ 'alternatively, give a reference file/directory.')
|
|
|
|
+ subparser.add_argument('--chunker-params', dest='chunker_params',
|
|
|
|
+ type=ChunkerParams, default=CHUNKER_PARAMS,
|
|
|
|
+ metavar='CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,HASH_WINDOW_SIZE',
|
|
|
|
+ help='specify the chunker parameters. default: %d,%d,%d,%d' % CHUNKER_PARAMS)
|
|
|
|
+ subparser.add_argument('-C', '--compression', dest='compression',
|
|
|
|
+ type=int, default=0, metavar='N',
|
|
|
|
+ help='select compression algorithm and level. 0..9 is supported and means zlib '
|
|
|
|
+ 'level 0 (no compression, fast, default) .. zlib level 9 (high compression, slow).')
|
|
subparser.add_argument('archive', metavar='ARCHIVE',
|
|
subparser.add_argument('archive', metavar='ARCHIVE',
|
|
type=location_validator(archive=True),
|
|
type=location_validator(archive=True),
|
|
help='archive to create')
|
|
help='archive to create')
|
|
@@ -565,7 +685,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
by passing a list of ``PATHs`` as arguments. The file selection can further
|
|
by passing a list of ``PATHs`` as arguments. The file selection can further
|
|
be restricted by using the ``--exclude`` option.
|
|
be restricted by using the ``--exclude`` option.
|
|
|
|
|
|
- See "attic help patterns" for more help on exclude patterns.
|
|
|
|
|
|
+ See the output of the "borg help patterns" command for more help on exclude patterns.
|
|
""")
|
|
""")
|
|
subparser = subparsers.add_parser('extract', parents=[common_parser],
|
|
subparser = subparsers.add_parser('extract', parents=[common_parser],
|
|
description=self.do_extract.__doc__,
|
|
description=self.do_extract.__doc__,
|
|
@@ -587,15 +707,36 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
subparser.add_argument('--strip-components', dest='strip_components',
|
|
subparser.add_argument('--strip-components', dest='strip_components',
|
|
type=int, default=0, metavar='NUMBER',
|
|
type=int, default=0, metavar='NUMBER',
|
|
help='Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.')
|
|
help='Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.')
|
|
|
|
+ subparser.add_argument('--stdout', dest='stdout',
|
|
|
|
+ action='store_true', default=False,
|
|
|
|
+ help='write all extracted data to stdout')
|
|
|
|
+ subparser.add_argument('--sparse', dest='sparse',
|
|
|
|
+ action='store_true', default=False,
|
|
|
|
+ help='create holes in output sparse file from all-zero chunks')
|
|
subparser.add_argument('archive', metavar='ARCHIVE',
|
|
subparser.add_argument('archive', metavar='ARCHIVE',
|
|
type=location_validator(archive=True),
|
|
type=location_validator(archive=True),
|
|
help='archive to extract')
|
|
help='archive to extract')
|
|
subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
|
|
subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
|
|
help='paths to extract')
|
|
help='paths to extract')
|
|
|
|
|
|
|
|
+ rename_epilog = textwrap.dedent("""
|
|
|
|
+ This command renames an archive in the repository.
|
|
|
|
+ """)
|
|
|
|
+ subparser = subparsers.add_parser('rename', parents=[common_parser],
|
|
|
|
+ description=self.do_rename.__doc__,
|
|
|
|
+ epilog=rename_epilog,
|
|
|
|
+ formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
|
|
+ subparser.set_defaults(func=self.do_rename)
|
|
|
|
+ subparser.add_argument('archive', metavar='ARCHIVE',
|
|
|
|
+ type=location_validator(archive=True),
|
|
|
|
+ help='archive to rename')
|
|
|
|
+ subparser.add_argument('name', metavar='NEWNAME', type=str,
|
|
|
|
+ help='the new archive name to use')
|
|
|
|
+
|
|
delete_epilog = textwrap.dedent("""
|
|
delete_epilog = textwrap.dedent("""
|
|
- This command deletes an archive from the repository. Any disk space not
|
|
|
|
- shared with any other existing archive is also reclaimed.
|
|
|
|
|
|
+ This command deletes an archive from the repository or the complete repository.
|
|
|
|
+ Disk space is reclaimed accordingly. If you delete the complete repository, the
|
|
|
|
+ local cache for it (if any) is also deleted.
|
|
""")
|
|
""")
|
|
subparser = subparsers.add_parser('delete', parents=[common_parser],
|
|
subparser = subparsers.add_parser('delete', parents=[common_parser],
|
|
description=self.do_delete.__doc__,
|
|
description=self.do_delete.__doc__,
|
|
@@ -605,9 +746,9 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
subparser.add_argument('-s', '--stats', dest='stats',
|
|
subparser.add_argument('-s', '--stats', dest='stats',
|
|
action='store_true', default=False,
|
|
action='store_true', default=False,
|
|
help='print statistics for the deleted archive')
|
|
help='print statistics for the deleted archive')
|
|
- subparser.add_argument('archive', metavar='ARCHIVE',
|
|
|
|
- type=location_validator(archive=True),
|
|
|
|
- help='archive to delete')
|
|
|
|
|
|
+ subparser.add_argument('target', metavar='TARGET',
|
|
|
|
+ type=location_validator(),
|
|
|
|
+ help='archive or repository to delete')
|
|
|
|
|
|
list_epilog = textwrap.dedent("""
|
|
list_epilog = textwrap.dedent("""
|
|
This command lists the contents of a repository or an archive.
|
|
This command lists the contents of a repository or an archive.
|
|
@@ -716,27 +857,69 @@ Type "Yes I am sure" if you understand this and want to continue.\n""")
|
|
|
|
|
|
args = parser.parse_args(args or ['-h'])
|
|
args = parser.parse_args(args or ['-h'])
|
|
self.verbose = args.verbose
|
|
self.verbose = args.verbose
|
|
|
|
+ os.umask(args.umask)
|
|
|
|
+ RemoteRepository.remote_path = args.remote_path
|
|
|
|
+ RemoteRepository.umask = args.umask
|
|
update_excludes(args)
|
|
update_excludes(args)
|
|
return args.func(args)
|
|
return args.func(args)
|
|
|
|
|
|
|
|
|
|
-def main():
|
|
|
|
|
|
+def sig_info_handler(signum, stack): # pragma: no cover
|
|
|
|
+ """search the stack for infos about the currently processed file and print them"""
|
|
|
|
+ for frame in inspect.getouterframes(stack):
|
|
|
|
+ func, loc = frame[3], frame[0].f_locals
|
|
|
|
+ if func in ('process_file', '_process', ): # create op
|
|
|
|
+ path = loc['path']
|
|
|
|
+ try:
|
|
|
|
+ pos = loc['fd'].tell()
|
|
|
|
+ total = loc['st'].st_size
|
|
|
|
+ except Exception:
|
|
|
|
+ pos, total = 0, 0
|
|
|
|
+ print("{0} {1}/{2}".format(path, format_file_size(pos), format_file_size(total)))
|
|
|
|
+ break
|
|
|
|
+ if func in ('extract_item', ): # extract op
|
|
|
|
+ path = loc['item'][b'path']
|
|
|
|
+ try:
|
|
|
|
+ pos = loc['fd'].tell()
|
|
|
|
+ except Exception:
|
|
|
|
+ pos = 0
|
|
|
|
+ print("{0} {1}/???".format(path, format_file_size(pos)))
|
|
|
|
+ break
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def setup_signal_handlers(): # pragma: no cover
|
|
|
|
+ sigs = []
|
|
|
|
+ if hasattr(signal, 'SIGUSR1'):
|
|
|
|
+ sigs.append(signal.SIGUSR1) # kill -USR1 pid
|
|
|
|
+ if hasattr(signal, 'SIGINFO'):
|
|
|
|
+ sigs.append(signal.SIGINFO) # kill -INFO pid (or ctrl-t)
|
|
|
|
+ for sig in sigs:
|
|
|
|
+ signal.signal(sig, sig_info_handler)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def main(): # pragma: no cover
|
|
# Make sure stdout and stderr have errors='replace') to avoid unicode
|
|
# Make sure stdout and stderr have errors='replace') to avoid unicode
|
|
# issues when print()-ing unicode file names
|
|
# issues when print()-ing unicode file names
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, 'replace', line_buffering=True)
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, sys.stdout.encoding, 'replace', line_buffering=True)
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, 'replace', line_buffering=True)
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, sys.stderr.encoding, 'replace', line_buffering=True)
|
|
|
|
+ setup_signal_handlers()
|
|
archiver = Archiver()
|
|
archiver = Archiver()
|
|
try:
|
|
try:
|
|
exit_code = archiver.run(sys.argv[1:])
|
|
exit_code = archiver.run(sys.argv[1:])
|
|
except Error as e:
|
|
except Error as e:
|
|
- archiver.print_error(e.get_message())
|
|
|
|
|
|
+ archiver.print_error(e.get_message() + "\n%s" % traceback.format_exc())
|
|
exit_code = e.exit_code
|
|
exit_code = e.exit_code
|
|
|
|
+ except RemoteRepository.RPCError as e:
|
|
|
|
+ archiver.print_error('Error: Remote Exception.\n%s' % str(e))
|
|
|
|
+ exit_code = 1
|
|
|
|
+ except Exception:
|
|
|
|
+ archiver.print_error('Error: Local Exception.\n%s' % traceback.format_exc())
|
|
|
|
+ exit_code = 1
|
|
except KeyboardInterrupt:
|
|
except KeyboardInterrupt:
|
|
- archiver.print_error('Error: Keyboard interrupt')
|
|
|
|
|
|
+ archiver.print_error('Error: Keyboard interrupt.\n%s' % traceback.format_exc())
|
|
exit_code = 1
|
|
exit_code = 1
|
|
- else:
|
|
|
|
- if exit_code:
|
|
|
|
- archiver.print_error('Exiting with failure status due to previous errors')
|
|
|
|
|
|
+ if exit_code:
|
|
|
|
+ archiver.print_error('Exiting with failure status due to previous errors')
|
|
sys.exit(exit_code)
|
|
sys.exit(exit_code)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if __name__ == '__main__':
|