瀏覽代碼

Merge pull request #1846 from Abogical/master

Improve extract progress display, for #1721
enkore 8 年之前
父節點
當前提交
cf8f8fb746
共有 5 個文件被更改,包括 58 次插入17 次删除
  1. 4 8
      src/borg/archive.py
  2. 4 1
      src/borg/archiver.py
  3. 41 5
      src/borg/helpers.py
  4. 1 1
      src/borg/testsuite/archiver.py
  5. 8 2
      src/borg/testsuite/helpers.py

+ 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 decode_dict, StableDict
 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 consume, chunkit
 from .helpers import CompressionDecider1, CompressionDecider2, CompressionSpec
@@ -93,11 +93,7 @@ class Statistics:
                     msg = ''
                     space = columns - swidth(msg)
                 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:
                 msg = ' ' * columns
             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:
                 for _, data in self.pipeline.fetch_many([c.id for c in item.chunks], is_preloaded=True):
                     if pi:
-                        pi.show(increase=len(data))
+                        pi.show(increase=len(data), info=[remove_surrogates(item.path)])
                     if stdout:
                         sys.stdout.buffer.write(data)
                 if stdout:
@@ -501,7 +497,7 @@ Number of files: {0.stats.nfiles}'''.format(
                 ids = [c.id for c in item.chunks]
                 for _, data in self.pipeline.fetch_many(ids, is_preloaded=True):
                     if pi:
-                        pi.show(increase=len(data))
+                        pi.show(increase=len(data), info=[remove_surrogates(item.path)])
                     with backup_io():
                         if sparse and self.zeros.startswith(data):
                             # 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)
         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')
             extracted_size = sum(item.file_size(hardlink_masters) for item in archive.iter_items(filter))
             pi.total = extracted_size
@@ -546,6 +546,9 @@ class Archiver:
         for pattern in include_patterns:
             if pattern.match_count == 0:
                 self.print_warning("Include pattern '%s' never matched.", pattern)
+        if pi:
+            # clear progress output
+            pi.finish()
         return self.exit_code
 
     @with_repository()

+ 41 - 5
src/borg/helpers.py

@@ -26,6 +26,7 @@ from functools import wraps, partial, lru_cache
 from itertools import islice
 from operator import attrgetter
 from string import Formatter
+from shutil import get_terminal_size
 
 import msgpack
 import msgpack.fallback
@@ -1191,6 +1192,23 @@ def yes(msg=None, false_msg=None, true_msg=None, default_msg=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:
     LOGGER = 'borg.output.progress'
 
@@ -1208,7 +1226,6 @@ class ProgressIndicatorPercent:
         self.trigger_at = start  # output next percentage value when reaching (at least) this
         self.step = step
         self.msg = msg
-        self.output_len = len(self.msg % 100.0)
         self.handler = None
         self.logger = logging.getLogger(self.LOGGER)
 
@@ -1239,14 +1256,33 @@ class ProgressIndicatorPercent:
             self.trigger_at += self.step
             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)
         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)
 
-    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)
 
     def finish(self):

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

@@ -774,7 +774,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
         with changedir('output'):
             output = self.cmd('extract', self.repository_location + '::test', '--progress')
-            assert 'Extracting files' in output
+            assert 'Extracting:' in output
 
     def _create_test_caches(self):
         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
 
 
-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.logger.setLevel('INFO')
     pi.show(0)
@@ -921,7 +924,10 @@ def test_progress_percentage_sameline(capfd):
     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.logger.setLevel('INFO')
     pi.show()