diff options
author | André Erdmann <dywi@mailerd.de> | 2014-02-22 15:49:44 +0100 |
---|---|---|
committer | André Erdmann <dywi@mailerd.de> | 2014-02-22 15:49:44 +0100 |
commit | af8169aca4833ac4b75c9cb109adddd24c910c78 (patch) | |
tree | 5d2fb0de85f2278b4caeb0663c49e88e4588feb9 | |
parent | roverlay-9999.ebuild: compression USE flags (diff) | |
parent | roverlay/remote/websync: show download status (diff) | |
download | R_overlay-af8169aca4833ac4b75c9cb109adddd24c910c78.tar.gz R_overlay-af8169aca4833ac4b75c9cb109adddd24c910c78.tar.bz2 R_overlay-af8169aca4833ac4b75c9cb109adddd24c910c78.zip |
Merge branch 'feature/websync-pbar'
-rw-r--r-- | roverlay/remote/websync.py | 39 | ||||
-rw-r--r-- | roverlay/util/progressbar.py | 210 |
2 files changed, 242 insertions, 7 deletions
diff --git a/roverlay/remote/websync.py b/roverlay/remote/websync.py index 24f5cd5..06689ad 100644 --- a/roverlay/remote/websync.py +++ b/roverlay/remote/websync.py @@ -4,6 +4,7 @@ # Distributed under the terms of the GNU General Public License; # either version 2 of the License, or (at your option) any later version. +from __future__ import division from __future__ import print_function """websync, sync packages via http""" @@ -31,6 +32,7 @@ HTTPError = _urllib_error.HTTPError from roverlay import config, digest, util from roverlay.remote.basicrepo import BasicRepo +from roverlay.util.progressbar import DownloadProgressBar, NullProgressBar # number of sync retries # changed 2014-02-15: does no longer include the first run @@ -50,6 +52,15 @@ class WebsyncBase ( BasicRepo ): HTTP_ERROR_RETRY_CODES = frozenset ({ 404, 410, 500, 503 }) URL_ERROR_RETRY_CODES = frozenset ({ errno.ETIMEDOUT, }) RETRY_ON_TIMEOUT = True + PROGRESS_BAR_CLS = None + + def __new__ ( cls, *args, **kwargs ): + if cls.PROGRESS_BAR_CLS is None: + cls.PROGRESS_BAR_CLS = ( + DownloadProgressBar if VERBOSE else NullProgressBar + ) + return super ( WebsyncBase, cls ).__new__ ( cls ) + # --- end of __new__ (...) --- def __init__ ( self, name, @@ -170,27 +181,41 @@ class WebsyncBase ( BasicRepo ): bytes_fetched = 0 assert blocksize - # FIXME: debug print (?) - if VERBOSE: - print ( - "Fetching {f} from {u} ...".format ( f=package_file, u=src_uri ) - ) - # unlink the existing file first (if it exists) # this is necessary for keeping hardlinks intact (-> package mirror) util.try_unlink ( distfile ) - with open ( distfile, mode='wb' ) as fh: + with \ + open ( distfile, mode='wb' ) as fh, \ + self.PROGRESS_BAR_CLS ( + package_file.ljust(50), expected_filesize + ) as progress_bar: + + progress_bar.update ( 0 ) block = webh.read ( blocksize ) + while block: # write block to file fh.write ( block ) # ? bytelen bytes_fetched += len ( block ) + # update progress bar on every 4th block + # blocks_fetched := math.ceil ( bytes_fetched / blocksize ) + # + # Usually, only the last block's size is <= blocksize, + # so floordiv is sufficient here + # (the progress bar gets updated for the last block anyway) + # + if 0 == ( bytes_fetched // blocksize ) % 4: + progress_bar.update ( bytes_fetched ) + # get the next block block = webh.read ( blocksize ) # -- end while + + # final progress bar update (before closing the file) + progress_bar.update ( bytes_fetched ) # -- with if bytes_fetched == expected_filesize: diff --git a/roverlay/util/progressbar.py b/roverlay/util/progressbar.py new file mode 100644 index 0000000..24c934e --- /dev/null +++ b/roverlay/util/progressbar.py @@ -0,0 +1,210 @@ +# R overlay -- util, progressbar +# -*- coding: utf-8 -*- +# Copyright (C) 2014 André Erdmann <dywi@mailerd.de> +# Distributed under the terms of the GNU General Public License; +# either version 2 of the License, or (at your option) any later version. +from __future__ import division + +import abc +import sys + +import roverlay.util.objects + + +class AbstractProgressBarBase ( roverlay.util.objects.AbstractObject ): + """Abstract base class for progress bars.""" + + @abc.abstractmethod + def setup ( self, *args, **kwargs ): + """Initialization code for __init__() and reset(). + + Returns: None + + arguments: + * *args, **kwargs -- progress bar data + """ + pass + # --- end of setup (...) --- + + def reset ( self, *args, **kwargs ): + """Finalizes the current progress bar and resets it afterwards. + + Returns: None + + arguments: + * *args, **kwargs -- passed to setup() + """ + self.print_newline() + self.setup ( *args, **kwargs ) + # --- end of reset (...) --- + + def __init__ ( self, *args, **kwargs ): + """Initializes a progress bar instance by calling its setup() method. + + arguments: + * *args, **kwargs -- passed to setup() + """ + super ( AbstractProgressBarBase, self ).__init__() + self.setup ( *args, **kwargs ) + # --- end of __init__ (...) --- + + @abc.abstractmethod + def write ( self, message ): + """(Over-)writes the progress bar, using the given message. + + Note: message should not contain newline chars. + + Returns: None + + arguments: + * message -- + """ + raise NotImplementedError() + # --- end of write (...) --- + + @abc.abstractmethod + def print_newline ( self ): + """ + Finalizes the current progress bar, usually by printing a newline char. + + Returns: None + """ + raise NotImplementedError() + # --- end of print_newline (...) --- + + @abc.abstractmethod + def update ( self, *args, **kwargs ): + """Updates the progress bar using the given data. + + Returns: None + + arguments: + * *args, **kwargs -- not specified by this class + """ + raise NotImplementedError() + # --- end of update (...) --- + + def __enter__ ( self ): + # "with"-statement, setup code + return self + + def __exit__ ( self, _type, value, traceback ): + # "with"-statement, teardown code + self.print_newline() + +# --- end of AbstractProgressBarBase --- + + +class AbstractProgressBar ( AbstractProgressBarBase ): + """ + Abstract base class for progress bars that write to a stream, e.g. stdout. + """ + + CARRIAGE_RET_CHR = chr(13) + #BACKSPACE_CHR = chr(8) + + def setup ( self, stream=None ): + self.stream = ( sys.stdout if stream is None else stream ) + # --- end of __init__ (...) --- + + def write ( self, message ): + self.stream.write ( self.CARRIAGE_RET_CHR + message ) + self.stream.flush() + # --- end of write (...) --- + + def print_newline ( self ): + self.stream.write ( "\n" ) + self.stream.flush() + # --- end of print_newline (...) --- + +# --- end of AbstractProgressBar --- + + +class AbstractPercentageProgressBar ( AbstractProgressBar ): + """Base class for displaying progress as percentage 0.00%..100.00%.""" + # not a real progress bar, just a progress indicator + + # str for formatting the percentage + # by default, reserve space for 7 chars ("ddd.dd%") + # might be set by derived classes and/or instances + PERCENTAGE_FMT = "{:>7.2%}" + + def setup ( self, message_header=None, stream=None ): + super ( AbstractPercentageProgressBar, self ).setup ( stream=stream ) + self.message_header = message_header + # --- end of setup (...) --- + + @abc.abstractmethod + def get_percentage ( self, *args, **kwargs ): + """Returns a float or int expressing a percentage. + + Any value < 0 is interpreted as "UNKNOWN". + + arguments: + * *args, **kwargs -- progress information (from update()) + """ + raise NotImplementedError() + # --- end of get_percentage (...) --- + + def _update ( self, percentage ): + if self.message_header: + message = str(self.message_header) + " " + else: + message = "" + + if percentage < 0: + message += "UNKNOWN" + else: + message += self.PERCENTAGE_FMT.format ( percentage ) + + self.write ( message ) + # --- end of _update (...) --- + + def update ( self, *args, **kwargs ): + self._update ( self.get_percentage ( *args, **kwargs ) ) + # --- end of update (...) --- + +# --- end of AbstractPercentageProgressBar --- + + +class NullProgressBar ( AbstractProgressBarBase ): + """A progress bar that discards any information.""" + + def setup ( self, *args, **kwargs ): + pass + + def write ( self, *args, **kwargs ): + pass + + def print_newline ( self, *args, **kwargs ): + pass + + def update ( self, *args, **kwargs ): + pass + +# --- end of NullProgressBar --- + + +class DownloadProgressBar ( AbstractPercentageProgressBar ): + """A progress bar for file transfers, + expressing a percentage "bytes transferred / total size". + + Note: + update() shouldn't be called too often as writing to console is rather slow + """ + + def setup ( self, filename=None, filesize=None, stream=None ): + super ( DownloadProgressBar, self ).setup ( + message_header = ( + ( "Fetching " + str(filename) ) if filename else None + ), + stream = stream + ) + self.filesize = filesize + # --- end of setup (...) --- + + def get_percentage ( self, current_filesize ): + return ( current_filesize / self.filesize ) if self.filesize else -1.0 + # --- end of get_percentage (...) --- + +# --- end of DownloadProgressBar --- |