mirror of https://github.com/borgbackup/borg.git
Merge pull request #1846 from Abogical/master
Improve extract progress display, for #1721
This commit is contained in:
commit
cf8f8fb746
|
@ -31,7 +31,7 @@ from .helpers import format_time, format_timedelta, format_file_size, file_statu
|
||||||
from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates, swidth_slice
|
from .helpers import safe_encode, safe_decode, make_path_safe, remove_surrogates, swidth_slice
|
||||||
from .helpers import decode_dict, StableDict
|
from .helpers import decode_dict, StableDict
|
||||||
from .helpers import int_to_bigint, bigint_to_int, bin_to_hex
|
from .helpers import int_to_bigint, bigint_to_int, bin_to_hex
|
||||||
from .helpers import ProgressIndicatorPercent, log_multi
|
from .helpers import ellipsis_truncate, ProgressIndicatorPercent, log_multi
|
||||||
from .helpers import PathPrefixPattern, FnmatchPattern
|
from .helpers import PathPrefixPattern, FnmatchPattern
|
||||||
from .helpers import consume, chunkit
|
from .helpers import consume, chunkit
|
||||||
from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec
|
from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec
|
||||||
|
@ -93,11 +93,7 @@ class Statistics:
|
||||||
msg = ''
|
msg = ''
|
||||||
space = columns - swidth(msg)
|
space = columns - swidth(msg)
|
||||||
if space >= 8:
|
if space >= 8:
|
||||||
if space < swidth('...') + swidth(path):
|
msg += ellipsis_truncate(path, space)
|
||||||
path = '%s...%s' % (swidth_slice(path, space // 2 - swidth('...')),
|
|
||||||
swidth_slice(path, -space // 2))
|
|
||||||
space -= swidth(path)
|
|
||||||
msg += path + ' ' * space
|
|
||||||
else:
|
else:
|
||||||
msg = ' ' * columns
|
msg = ' ' * columns
|
||||||
print(msg, file=stream or sys.stderr, end="\r", flush=True)
|
print(msg, file=stream or sys.stderr, end="\r", flush=True)
|
||||||
|
@ -448,7 +444,7 @@ Number of files: {0.stats.nfiles}'''.format(
|
||||||
if 'chunks' in item:
|
if 'chunks' in item:
|
||||||
for _, data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True):
|
for _, data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True):
|
||||||
if pi:
|
if pi:
|
||||||
pi.show(increase=len(data))
|
pi.show(increase=len(data), info=[remove_surrogates(item.path)])
|
||||||
if stdout:
|
if stdout:
|
||||||
sys.stdout.buffer.write(data)
|
sys.stdout.buffer.write(data)
|
||||||
if stdout:
|
if stdout:
|
||||||
|
@ -501,7 +497,7 @@ Number of files: {0.stats.nfiles}'''.format(
|
||||||
ids = [c.id for c in item.chunks]
|
ids = [c.id for c in item.chunks]
|
||||||
for _, data in self.pipeline.fetch_many(ids, is_preloaded=True):
|
for _, data in self.pipeline.fetch_many(ids, is_preloaded=True):
|
||||||
if pi:
|
if pi:
|
||||||
pi.show(increase=len(data))
|
pi.show(increase=len(data), info=[remove_surrogates(item.path)])
|
||||||
with backup_io():
|
with backup_io():
|
||||||
if sparse and self.zeros.startswith(data):
|
if sparse and self.zeros.startswith(data):
|
||||||
# all-zero chunk: create a hole in a sparse file
|
# all-zero chunk: create a hole in a sparse file
|
||||||
|
|
|
@ -501,7 +501,7 @@ class Archiver:
|
||||||
|
|
||||||
filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components)
|
filter = self.build_filter(matcher, peek_and_store_hardlink_masters, strip_components)
|
||||||
if progress:
|
if progress:
|
||||||
pi = ProgressIndicatorPercent(msg='Extracting files %5.1f%%', step=0.1)
|
pi = ProgressIndicatorPercent(msg='%5.1f%% Extracting: %s', step=0.1)
|
||||||
pi.output('Calculating size')
|
pi.output('Calculating size')
|
||||||
extracted_size = sum(item.file_size(hardlink_masters) for item in archive.iter_items(filter))
|
extracted_size = sum(item.file_size(hardlink_masters) for item in archive.iter_items(filter))
|
||||||
pi.total = extracted_size
|
pi.total = extracted_size
|
||||||
|
@ -546,6 +546,9 @@ class Archiver:
|
||||||
for pattern in include_patterns:
|
for pattern in include_patterns:
|
||||||
if pattern.match_count == 0:
|
if pattern.match_count == 0:
|
||||||
self.print_warning("Include pattern '%s' never matched.", pattern)
|
self.print_warning("Include pattern '%s' never matched.", pattern)
|
||||||
|
if pi:
|
||||||
|
# clear progress output
|
||||||
|
pi.finish()
|
||||||
return self.exit_code
|
return self.exit_code
|
||||||
|
|
||||||
@with_repository()
|
@with_repository()
|
||||||
|
|
|
@ -26,6 +26,7 @@ from functools import wraps, partial, lru_cache
|
||||||
from itertools import islice
|
from itertools import islice
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
|
from shutil import get_terminal_size
|
||||||
|
|
||||||
import msgpack
|
import msgpack
|
||||||
import msgpack.fallback
|
import msgpack.fallback
|
||||||
|
@ -1191,6 +1192,23 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=None,
|
||||||
env_var_override = None
|
env_var_override = None
|
||||||
|
|
||||||
|
|
||||||
|
def ellipsis_truncate(msg, space):
|
||||||
|
"""
|
||||||
|
shorten a long string by adding ellipsis between it and return it, example:
|
||||||
|
this_is_a_very_long_string -------> this_is..._string
|
||||||
|
"""
|
||||||
|
from .platform import swidth
|
||||||
|
ellipsis_width = swidth('...')
|
||||||
|
msg_width = swidth(msg)
|
||||||
|
if space < 8:
|
||||||
|
# if there is very little space, just show ...
|
||||||
|
return '...' + ' ' * (space - ellipsis_width)
|
||||||
|
if space < ellipsis_width + msg_width:
|
||||||
|
return '%s...%s' % (swidth_slice(msg, space // 2 - ellipsis_width),
|
||||||
|
swidth_slice(msg, -space // 2))
|
||||||
|
return msg + ' ' * (space - msg_width)
|
||||||
|
|
||||||
|
|
||||||
class ProgressIndicatorPercent:
|
class ProgressIndicatorPercent:
|
||||||
LOGGER = 'borg.output.progress'
|
LOGGER = 'borg.output.progress'
|
||||||
|
|
||||||
|
@ -1208,7 +1226,6 @@ class ProgressIndicatorPercent:
|
||||||
self.trigger_at = start # output next percentage value when reaching (at least) this
|
self.trigger_at = start # output next percentage value when reaching (at least) this
|
||||||
self.step = step
|
self.step = step
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.output_len = len(self.msg % 100.0)
|
|
||||||
self.handler = None
|
self.handler = None
|
||||||
self.logger = logging.getLogger(self.LOGGER)
|
self.logger = logging.getLogger(self.LOGGER)
|
||||||
|
|
||||||
|
@ -1239,14 +1256,33 @@ class ProgressIndicatorPercent:
|
||||||
self.trigger_at += self.step
|
self.trigger_at += self.step
|
||||||
return pct
|
return pct
|
||||||
|
|
||||||
def show(self, current=None, increase=1):
|
def show(self, current=None, increase=1, info=None):
|
||||||
|
"""
|
||||||
|
Show and output the progress message
|
||||||
|
|
||||||
|
:param current: set the current percentage [None]
|
||||||
|
:param increase: increase the current percentage [None]
|
||||||
|
:param info: array of strings to be formatted with msg [None]
|
||||||
|
"""
|
||||||
pct = self.progress(current, increase)
|
pct = self.progress(current, increase)
|
||||||
if pct is not None:
|
if pct is not None:
|
||||||
|
# truncate the last argument, if no space is available
|
||||||
|
if info is not None:
|
||||||
|
# no need to truncate if we're not outputing to a terminal
|
||||||
|
terminal_space = get_terminal_size(fallback=(-1, -1))[0]
|
||||||
|
if terminal_space != -1:
|
||||||
|
space = terminal_space - len(self.msg % tuple([pct] + info[:-1] + ['']))
|
||||||
|
info[-1] = ellipsis_truncate(info[-1], space)
|
||||||
|
return self.output(self.msg % tuple([pct] + info), justify=False)
|
||||||
|
|
||||||
return self.output(self.msg % pct)
|
return self.output(self.msg % pct)
|
||||||
|
|
||||||
def output(self, message):
|
def output(self, message, justify=True):
|
||||||
self.output_len = max(len(message), self.output_len)
|
if justify:
|
||||||
message = message.ljust(self.output_len)
|
terminal_space = get_terminal_size(fallback=(-1, -1))[0]
|
||||||
|
# no need to ljust if we're not outputing to a terminal
|
||||||
|
if terminal_space != -1:
|
||||||
|
message = message.ljust(terminal_space)
|
||||||
self.logger.info(message)
|
self.logger.info(message)
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
|
|
|
@ -774,7 +774,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
|
|
||||||
with changedir('output'):
|
with changedir('output'):
|
||||||
output = self.cmd('extract', self.repository_location + '::test', '--progress')
|
output = self.cmd('extract', self.repository_location + '::test', '--progress')
|
||||||
assert 'Extracting files' in output
|
assert 'Extracting:' in output
|
||||||
|
|
||||||
def _create_test_caches(self):
|
def _create_test_caches(self):
|
||||||
self.cmd('init', self.repository_location)
|
self.cmd('init', self.repository_location)
|
||||||
|
|
|
@ -903,7 +903,10 @@ def test_yes_env_output(capfd, monkeypatch):
|
||||||
assert 'yes' in err
|
assert 'yes' in err
|
||||||
|
|
||||||
|
|
||||||
def test_progress_percentage_sameline(capfd):
|
def test_progress_percentage_sameline(capfd, monkeypatch):
|
||||||
|
# run the test as if it was in a 4x1 terminal
|
||||||
|
monkeypatch.setenv('COLUMNS', '4')
|
||||||
|
monkeypatch.setenv('LINES', '1')
|
||||||
pi = ProgressIndicatorPercent(1000, step=5, start=0, msg="%3.0f%%")
|
pi = ProgressIndicatorPercent(1000, step=5, start=0, msg="%3.0f%%")
|
||||||
pi.logger.setLevel('INFO')
|
pi.logger.setLevel('INFO')
|
||||||
pi.show(0)
|
pi.show(0)
|
||||||
|
@ -921,7 +924,10 @@ def test_progress_percentage_sameline(capfd):
|
||||||
assert err == ' ' * 4 + '\r'
|
assert err == ' ' * 4 + '\r'
|
||||||
|
|
||||||
|
|
||||||
def test_progress_percentage_step(capfd):
|
def test_progress_percentage_step(capfd, monkeypatch):
|
||||||
|
# run the test as if it was in a 4x1 terminal
|
||||||
|
monkeypatch.setenv('COLUMNS', '4')
|
||||||
|
monkeypatch.setenv('LINES', '1')
|
||||||
pi = ProgressIndicatorPercent(100, step=2, start=0, msg="%3.0f%%")
|
pi = ProgressIndicatorPercent(100, step=2, start=0, msg="%3.0f%%")
|
||||||
pi.logger.setLevel('INFO')
|
pi.logger.setLevel('INFO')
|
||||||
pi.show()
|
pi.show()
|
||||||
|
|
Loading…
Reference in New Issue