1 |
commit: ca1020e87ff4421499df80c1d1af9e6c33f9afe2 |
2 |
Author: André Erdmann <dywi <AT> mailerd <DOT> de> |
3 |
AuthorDate: Sun Feb 16 16:44:15 2014 +0000 |
4 |
Commit: André Erdmann <dywi <AT> mailerd <DOT> de> |
5 |
CommitDate: Sun Feb 16 16:44:15 2014 +0000 |
6 |
URL: http://git.overlays.gentoo.org/gitweb/?p=proj/R_overlay.git;a=commit;h=ca1020e8 |
7 |
|
8 |
roverlay/remote/websync: show download status |
9 |
|
10 |
--- |
11 |
roverlay/remote/websync.py | 39 ++++++-- |
12 |
roverlay/util/progressbar.py | 210 +++++++++++++++++++++++++++++++++++++++++++ |
13 |
2 files changed, 242 insertions(+), 7 deletions(-) |
14 |
|
15 |
diff --git a/roverlay/remote/websync.py b/roverlay/remote/websync.py |
16 |
index 24f5cd5..06689ad 100644 |
17 |
--- a/roverlay/remote/websync.py |
18 |
+++ b/roverlay/remote/websync.py |
19 |
@@ -4,6 +4,7 @@ |
20 |
# Distributed under the terms of the GNU General Public License; |
21 |
# either version 2 of the License, or (at your option) any later version. |
22 |
|
23 |
+from __future__ import division |
24 |
from __future__ import print_function |
25 |
|
26 |
"""websync, sync packages via http""" |
27 |
@@ -31,6 +32,7 @@ HTTPError = _urllib_error.HTTPError |
28 |
|
29 |
from roverlay import config, digest, util |
30 |
from roverlay.remote.basicrepo import BasicRepo |
31 |
+from roverlay.util.progressbar import DownloadProgressBar, NullProgressBar |
32 |
|
33 |
# number of sync retries |
34 |
# changed 2014-02-15: does no longer include the first run |
35 |
@@ -50,6 +52,15 @@ class WebsyncBase ( BasicRepo ): |
36 |
HTTP_ERROR_RETRY_CODES = frozenset ({ 404, 410, 500, 503 }) |
37 |
URL_ERROR_RETRY_CODES = frozenset ({ errno.ETIMEDOUT, }) |
38 |
RETRY_ON_TIMEOUT = True |
39 |
+ PROGRESS_BAR_CLS = None |
40 |
+ |
41 |
+ def __new__ ( cls, *args, **kwargs ): |
42 |
+ if cls.PROGRESS_BAR_CLS is None: |
43 |
+ cls.PROGRESS_BAR_CLS = ( |
44 |
+ DownloadProgressBar if VERBOSE else NullProgressBar |
45 |
+ ) |
46 |
+ return super ( WebsyncBase, cls ).__new__ ( cls ) |
47 |
+ # --- end of __new__ (...) --- |
48 |
|
49 |
def __init__ ( self, |
50 |
name, |
51 |
@@ -170,27 +181,41 @@ class WebsyncBase ( BasicRepo ): |
52 |
bytes_fetched = 0 |
53 |
assert blocksize |
54 |
|
55 |
- # FIXME: debug print (?) |
56 |
- if VERBOSE: |
57 |
- print ( |
58 |
- "Fetching {f} from {u} ...".format ( f=package_file, u=src_uri ) |
59 |
- ) |
60 |
- |
61 |
# unlink the existing file first (if it exists) |
62 |
# this is necessary for keeping hardlinks intact (-> package mirror) |
63 |
util.try_unlink ( distfile ) |
64 |
|
65 |
- with open ( distfile, mode='wb' ) as fh: |
66 |
+ with \ |
67 |
+ open ( distfile, mode='wb' ) as fh, \ |
68 |
+ self.PROGRESS_BAR_CLS ( |
69 |
+ package_file.ljust(50), expected_filesize |
70 |
+ ) as progress_bar: |
71 |
+ |
72 |
+ progress_bar.update ( 0 ) |
73 |
block = webh.read ( blocksize ) |
74 |
+ |
75 |
while block: |
76 |
# write block to file |
77 |
fh.write ( block ) |
78 |
# ? bytelen |
79 |
bytes_fetched += len ( block ) |
80 |
|
81 |
+ # update progress bar on every 4th block |
82 |
+ # blocks_fetched := math.ceil ( bytes_fetched / blocksize ) |
83 |
+ # |
84 |
+ # Usually, only the last block's size is <= blocksize, |
85 |
+ # so floordiv is sufficient here |
86 |
+ # (the progress bar gets updated for the last block anyway) |
87 |
+ # |
88 |
+ if 0 == ( bytes_fetched // blocksize ) % 4: |
89 |
+ progress_bar.update ( bytes_fetched ) |
90 |
+ |
91 |
# get the next block |
92 |
block = webh.read ( blocksize ) |
93 |
# -- end while |
94 |
+ |
95 |
+ # final progress bar update (before closing the file) |
96 |
+ progress_bar.update ( bytes_fetched ) |
97 |
# -- with |
98 |
|
99 |
if bytes_fetched == expected_filesize: |
100 |
|
101 |
diff --git a/roverlay/util/progressbar.py b/roverlay/util/progressbar.py |
102 |
new file mode 100644 |
103 |
index 0000000..24c934e |
104 |
--- /dev/null |
105 |
+++ b/roverlay/util/progressbar.py |
106 |
@@ -0,0 +1,210 @@ |
107 |
+# R overlay -- util, progressbar |
108 |
+# -*- coding: utf-8 -*- |
109 |
+# Copyright (C) 2014 André Erdmann <dywi@×××××××.de> |
110 |
+# Distributed under the terms of the GNU General Public License; |
111 |
+# either version 2 of the License, or (at your option) any later version. |
112 |
+from __future__ import division |
113 |
+ |
114 |
+import abc |
115 |
+import sys |
116 |
+ |
117 |
+import roverlay.util.objects |
118 |
+ |
119 |
+ |
120 |
+class AbstractProgressBarBase ( roverlay.util.objects.AbstractObject ): |
121 |
+ """Abstract base class for progress bars.""" |
122 |
+ |
123 |
+ @abc.abstractmethod |
124 |
+ def setup ( self, *args, **kwargs ): |
125 |
+ """Initialization code for __init__() and reset(). |
126 |
+ |
127 |
+ Returns: None |
128 |
+ |
129 |
+ arguments: |
130 |
+ * *args, **kwargs -- progress bar data |
131 |
+ """ |
132 |
+ pass |
133 |
+ # --- end of setup (...) --- |
134 |
+ |
135 |
+ def reset ( self, *args, **kwargs ): |
136 |
+ """Finalizes the current progress bar and resets it afterwards. |
137 |
+ |
138 |
+ Returns: None |
139 |
+ |
140 |
+ arguments: |
141 |
+ * *args, **kwargs -- passed to setup() |
142 |
+ """ |
143 |
+ self.print_newline() |
144 |
+ self.setup ( *args, **kwargs ) |
145 |
+ # --- end of reset (...) --- |
146 |
+ |
147 |
+ def __init__ ( self, *args, **kwargs ): |
148 |
+ """Initializes a progress bar instance by calling its setup() method. |
149 |
+ |
150 |
+ arguments: |
151 |
+ * *args, **kwargs -- passed to setup() |
152 |
+ """ |
153 |
+ super ( AbstractProgressBarBase, self ).__init__() |
154 |
+ self.setup ( *args, **kwargs ) |
155 |
+ # --- end of __init__ (...) --- |
156 |
+ |
157 |
+ @abc.abstractmethod |
158 |
+ def write ( self, message ): |
159 |
+ """(Over-)writes the progress bar, using the given message. |
160 |
+ |
161 |
+ Note: message should not contain newline chars. |
162 |
+ |
163 |
+ Returns: None |
164 |
+ |
165 |
+ arguments: |
166 |
+ * message -- |
167 |
+ """ |
168 |
+ raise NotImplementedError() |
169 |
+ # --- end of write (...) --- |
170 |
+ |
171 |
+ @abc.abstractmethod |
172 |
+ def print_newline ( self ): |
173 |
+ """ |
174 |
+ Finalizes the current progress bar, usually by printing a newline char. |
175 |
+ |
176 |
+ Returns: None |
177 |
+ """ |
178 |
+ raise NotImplementedError() |
179 |
+ # --- end of print_newline (...) --- |
180 |
+ |
181 |
+ @abc.abstractmethod |
182 |
+ def update ( self, *args, **kwargs ): |
183 |
+ """Updates the progress bar using the given data. |
184 |
+ |
185 |
+ Returns: None |
186 |
+ |
187 |
+ arguments: |
188 |
+ * *args, **kwargs -- not specified by this class |
189 |
+ """ |
190 |
+ raise NotImplementedError() |
191 |
+ # --- end of update (...) --- |
192 |
+ |
193 |
+ def __enter__ ( self ): |
194 |
+ # "with"-statement, setup code |
195 |
+ return self |
196 |
+ |
197 |
+ def __exit__ ( self, _type, value, traceback ): |
198 |
+ # "with"-statement, teardown code |
199 |
+ self.print_newline() |
200 |
+ |
201 |
+# --- end of AbstractProgressBarBase --- |
202 |
+ |
203 |
+ |
204 |
+class AbstractProgressBar ( AbstractProgressBarBase ): |
205 |
+ """ |
206 |
+ Abstract base class for progress bars that write to a stream, e.g. stdout. |
207 |
+ """ |
208 |
+ |
209 |
+ CARRIAGE_RET_CHR = chr(13) |
210 |
+ #BACKSPACE_CHR = chr(8) |
211 |
+ |
212 |
+ def setup ( self, stream=None ): |
213 |
+ self.stream = ( sys.stdout if stream is None else stream ) |
214 |
+ # --- end of __init__ (...) --- |
215 |
+ |
216 |
+ def write ( self, message ): |
217 |
+ self.stream.write ( self.CARRIAGE_RET_CHR + message ) |
218 |
+ self.stream.flush() |
219 |
+ # --- end of write (...) --- |
220 |
+ |
221 |
+ def print_newline ( self ): |
222 |
+ self.stream.write ( "\n" ) |
223 |
+ self.stream.flush() |
224 |
+ # --- end of print_newline (...) --- |
225 |
+ |
226 |
+# --- end of AbstractProgressBar --- |
227 |
+ |
228 |
+ |
229 |
+class AbstractPercentageProgressBar ( AbstractProgressBar ): |
230 |
+ """Base class for displaying progress as percentage 0.00%..100.00%.""" |
231 |
+ # not a real progress bar, just a progress indicator |
232 |
+ |
233 |
+ # str for formatting the percentage |
234 |
+ # by default, reserve space for 7 chars ("ddd.dd%") |
235 |
+ # might be set by derived classes and/or instances |
236 |
+ PERCENTAGE_FMT = "{:>7.2%}" |
237 |
+ |
238 |
+ def setup ( self, message_header=None, stream=None ): |
239 |
+ super ( AbstractPercentageProgressBar, self ).setup ( stream=stream ) |
240 |
+ self.message_header = message_header |
241 |
+ # --- end of setup (...) --- |
242 |
+ |
243 |
+ @abc.abstractmethod |
244 |
+ def get_percentage ( self, *args, **kwargs ): |
245 |
+ """Returns a float or int expressing a percentage. |
246 |
+ |
247 |
+ Any value < 0 is interpreted as "UNKNOWN". |
248 |
+ |
249 |
+ arguments: |
250 |
+ * *args, **kwargs -- progress information (from update()) |
251 |
+ """ |
252 |
+ raise NotImplementedError() |
253 |
+ # --- end of get_percentage (...) --- |
254 |
+ |
255 |
+ def _update ( self, percentage ): |
256 |
+ if self.message_header: |
257 |
+ message = str(self.message_header) + " " |
258 |
+ else: |
259 |
+ message = "" |
260 |
+ |
261 |
+ if percentage < 0: |
262 |
+ message += "UNKNOWN" |
263 |
+ else: |
264 |
+ message += self.PERCENTAGE_FMT.format ( percentage ) |
265 |
+ |
266 |
+ self.write ( message ) |
267 |
+ # --- end of _update (...) --- |
268 |
+ |
269 |
+ def update ( self, *args, **kwargs ): |
270 |
+ self._update ( self.get_percentage ( *args, **kwargs ) ) |
271 |
+ # --- end of update (...) --- |
272 |
+ |
273 |
+# --- end of AbstractPercentageProgressBar --- |
274 |
+ |
275 |
+ |
276 |
+class NullProgressBar ( AbstractProgressBarBase ): |
277 |
+ """A progress bar that discards any information.""" |
278 |
+ |
279 |
+ def setup ( self, *args, **kwargs ): |
280 |
+ pass |
281 |
+ |
282 |
+ def write ( self, *args, **kwargs ): |
283 |
+ pass |
284 |
+ |
285 |
+ def print_newline ( self, *args, **kwargs ): |
286 |
+ pass |
287 |
+ |
288 |
+ def update ( self, *args, **kwargs ): |
289 |
+ pass |
290 |
+ |
291 |
+# --- end of NullProgressBar --- |
292 |
+ |
293 |
+ |
294 |
+class DownloadProgressBar ( AbstractPercentageProgressBar ): |
295 |
+ """A progress bar for file transfers, |
296 |
+ expressing a percentage "bytes transferred / total size". |
297 |
+ |
298 |
+ Note: |
299 |
+ update() shouldn't be called too often as writing to console is rather slow |
300 |
+ """ |
301 |
+ |
302 |
+ def setup ( self, filename=None, filesize=None, stream=None ): |
303 |
+ super ( DownloadProgressBar, self ).setup ( |
304 |
+ message_header = ( |
305 |
+ ( "Fetching " + str(filename) ) if filename else None |
306 |
+ ), |
307 |
+ stream = stream |
308 |
+ ) |
309 |
+ self.filesize = filesize |
310 |
+ # --- end of setup (...) --- |
311 |
+ |
312 |
+ def get_percentage ( self, current_filesize ): |
313 |
+ return ( current_filesize / self.filesize ) if self.filesize else -1.0 |
314 |
+ # --- end of get_percentage (...) --- |
315 |
+ |
316 |
+# --- end of DownloadProgressBar --- |