|
13 | 13 | import shutil |
14 | 14 | import shlex |
15 | 15 | import re |
| 16 | +import threading |
16 | 17 |
|
17 | 18 | # Python 2/3 compatibility for urllib and input |
18 | 19 | try: |
@@ -193,10 +194,129 @@ def fetch_url_content(url): |
193 | 194 | time.sleep(3) |
194 | 195 | return None |
195 | 196 |
|
| 197 | +def get_remote_file_info(url): |
| 198 | + req = Request(url) |
| 199 | + req.add_header('User-Agent', 'python-qemu-script') |
| 200 | + if hasattr(req, 'method'): |
| 201 | + req.method = 'HEAD' |
| 202 | + else: |
| 203 | + try: |
| 204 | + req.get_method = lambda: 'HEAD' |
| 205 | + except Exception: |
| 206 | + pass |
| 207 | + try: |
| 208 | + resp = urlopen(req) |
| 209 | + length = int(resp.headers.get('Content-Length', '0')) |
| 210 | + accept_ranges = resp.headers.get('Accept-Ranges', '').lower() == 'bytes' |
| 211 | + try: |
| 212 | + resp.close() |
| 213 | + except Exception: |
| 214 | + pass |
| 215 | + return length, accept_ranges |
| 216 | + except Exception: |
| 217 | + return 0, False |
| 218 | + |
| 219 | +def download_file_multithread(url, dest, total_size, show_progress): |
| 220 | + tmp_dest = dest + ".part" |
| 221 | + try: |
| 222 | + with open(tmp_dest, 'wb') as f: |
| 223 | + f.truncate(total_size) |
| 224 | + except IOError: |
| 225 | + return False |
| 226 | + |
| 227 | + num_threads = min(4, max(1, total_size // (8 * 1024 * 1024))) |
| 228 | + chunk_size = (total_size + num_threads - 1) // num_threads |
| 229 | + progress_lock = threading.Lock() |
| 230 | + downloaded = [0] |
| 231 | + last_percent = [-1] |
| 232 | + errors = [] |
| 233 | + stop_event = threading.Event() |
| 234 | + |
| 235 | + def update_progress(): |
| 236 | + if not show_progress: |
| 237 | + return |
| 238 | + percent = int(downloaded[0] * 100 / total_size) |
| 239 | + if percent != last_percent[0]: |
| 240 | + last_percent[0] = percent |
| 241 | + sys.stdout.write("\r {:3d}% ({:.1f}/{:.1f} MB)".format( |
| 242 | + percent, |
| 243 | + downloaded[0] / (1024 * 1024.0), |
| 244 | + total_size / (1024 * 1024.0) |
| 245 | + )) |
| 246 | + sys.stdout.flush() |
| 247 | + |
| 248 | + def worker(start, end): |
| 249 | + if stop_event.is_set(): |
| 250 | + return |
| 251 | + req = Request(url) |
| 252 | + req.add_header('User-Agent', 'python-qemu-script') |
| 253 | + req.add_header('Range', 'bytes={}-{}'.format(start, end)) |
| 254 | + try: |
| 255 | + resp = urlopen(req) |
| 256 | + with open(tmp_dest, 'r+b') as f: |
| 257 | + f.seek(start) |
| 258 | + while not stop_event.is_set(): |
| 259 | + chunk = resp.read(128 * 1024) |
| 260 | + if not chunk: |
| 261 | + break |
| 262 | + f.write(chunk) |
| 263 | + if show_progress: |
| 264 | + with progress_lock: |
| 265 | + downloaded[0] += len(chunk) |
| 266 | + update_progress() |
| 267 | + try: |
| 268 | + resp.close() |
| 269 | + except Exception: |
| 270 | + pass |
| 271 | + except Exception as e: |
| 272 | + stop_event.set() |
| 273 | + with progress_lock: |
| 274 | + errors.append(e) |
| 275 | + |
| 276 | + threads = [] |
| 277 | + for index in range(num_threads): |
| 278 | + start = index * chunk_size |
| 279 | + end = min(total_size - 1, start + chunk_size - 1) |
| 280 | + if start > end: |
| 281 | + break |
| 282 | + t = threading.Thread(target=worker, args=(start, end)) |
| 283 | + t.daemon = True |
| 284 | + t.start() |
| 285 | + threads.append(t) |
| 286 | + |
| 287 | + for t in threads: |
| 288 | + t.join() |
| 289 | + |
| 290 | + if show_progress: |
| 291 | + sys.stdout.write("\n") |
| 292 | + sys.stdout.flush() |
| 293 | + |
| 294 | + if errors or stop_event.is_set(): |
| 295 | + try: |
| 296 | + os.remove(tmp_dest) |
| 297 | + except OSError: |
| 298 | + pass |
| 299 | + return False |
| 300 | + |
| 301 | + try: |
| 302 | + if hasattr(os, 'replace'): |
| 303 | + os.replace(tmp_dest, dest) |
| 304 | + else: |
| 305 | + shutil.move(tmp_dest, dest) |
| 306 | + except Exception: |
| 307 | + return False |
| 308 | + return True |
| 309 | + |
196 | 310 | def download_file(url, dest): |
197 | 311 | log("Downloading " + url) |
198 | 312 | show_progress = sys.stdout.isatty() |
199 | 313 |
|
| 314 | + size, can_range = get_remote_file_info(url) |
| 315 | + if can_range and size > 0: |
| 316 | + if download_file_multithread(url, dest, size, show_progress): |
| 317 | + return True |
| 318 | + log("Falling back to single-thread download...") |
| 319 | + |
200 | 320 | def make_progress_hook(): |
201 | 321 | if not show_progress: |
202 | 322 | return None |
|
0 commit comments