Merge pull request #1846 from Abogical/master

Improve extract progress display, for #1721
This commit is contained in:
enkore 2016-11-14 21:43:35 +01:00 committed by GitHub
commit cf8f8fb746
5 changed files with 58 additions and 17 deletions

View File

@ -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

View 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()

View File

@ -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):

View File

@ -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)

View File

@ -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()