Browse Source

Merge pull request #1846 from Abogical/master

Improve extract progress display, for #1721
enkore 8 years ago
parent
commit
cf8f8fb746

+ 4 - 8
src/borg/archive.py

@@ -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):
-                        path = '%s...%s' % (swidth_slice(path, space // 2 - swidth('...')),
-                                            swidth_slice(path, -space // 2))
-                    space -= swidth(path)
-                    msg += path + ' ' * space
+                    msg += ellipsis_truncate(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

+ 4 - 1
src/borg/archiver.py

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

+ 41 - 5
src/borg/helpers.py

@@ -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):
-        self.output_len = max(len(message), self.output_len)
-        message = message.ljust(self.output_len)
+    def output(self, message, justify=True):
+        if justify:
+            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):

+ 1 - 1
src/borg/testsuite/archiver.py

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

+ 8 - 2
src/borg/testsuite/helpers.py

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