|
| 1 | +import tempfile |
1 | 2 | import time |
| 3 | +import urllib.parse |
2 | 4 | import urllib.request |
3 | 5 | from collections.abc import Callable |
4 | 6 | from pathlib import Path |
@@ -28,6 +30,7 @@ def download_file(download_url: str, save_file_path: str, |
28 | 30 | last_log_time = time.time() |
29 | 31 | save_path = Path(save_file_path) |
30 | 32 | save_path.parent.mkdir(parents=True, exist_ok=True) |
| 33 | + temp_path: Path | None = None |
31 | 34 |
|
32 | 35 | def log_download_progress(downloaded_bytes: int, total_size: int) -> None: |
33 | 36 | nonlocal last_log_time |
@@ -55,43 +58,64 @@ def log_download_progress(downloaded_bytes: int, total_size: int) -> None: |
55 | 58 | if progress_callback is not None: |
56 | 59 | progress_callback(0, msg) |
57 | 60 |
|
| 61 | + url = urllib.parse.urlparse(download_url) |
| 62 | + if url.scheme not in ('http', 'https'): |
| 63 | + raise ValueError(f"不支持的下载协议:{download_url}") |
| 64 | + |
58 | 65 | request = urllib.request.Request(download_url) |
59 | | - with opener.open(request, timeout=60) as response, save_path.open('wb') as file: |
| 66 | + with opener.open(request, timeout=60) as response: |
60 | 67 | total_size = int(response.headers.get('Content-Length', '0') or 0) |
61 | 68 | downloaded_bytes = 0 |
62 | 69 | chunk_size = 1024 * 64 |
63 | 70 |
|
64 | | - while True: |
65 | | - if progress_signal is not None and progress_signal.get('signal') == 'cancel': |
66 | | - raise DownloadCancelledError("下载已取消") |
| 71 | + with tempfile.NamedTemporaryFile('wb', dir=save_path.parent, delete=False) as file: |
| 72 | + temp_path = Path(file.name) |
| 73 | + while True: |
| 74 | + if progress_signal is not None and progress_signal.get('signal') == 'cancel': |
| 75 | + raise DownloadCancelledError("下载已取消") |
| 76 | + |
| 77 | + chunk = response.read(chunk_size) |
| 78 | + if not chunk: |
| 79 | + break |
67 | 80 |
|
68 | | - chunk = response.read(chunk_size) |
69 | | - if not chunk: |
70 | | - break |
| 81 | + file.write(chunk) |
| 82 | + downloaded_bytes += len(chunk) |
| 83 | + log_download_progress(downloaded_bytes, total_size) |
71 | 84 |
|
72 | | - file.write(chunk) |
73 | | - downloaded_bytes += len(chunk) |
74 | | - log_download_progress(downloaded_bytes, total_size) |
| 85 | + if total_size > 0 and downloaded_bytes != total_size: |
| 86 | + raise DownloadIncompleteError( |
| 87 | + f"下载不完整:{downloaded_bytes}/{total_size} bytes" |
| 88 | + ) |
| 89 | + |
| 90 | + temp_path.replace(save_path) |
| 91 | + temp_path = None |
75 | 92 |
|
76 | 93 | msg = f"{gt('下载完成')} {save_file_path}" |
77 | 94 | log.info(msg) |
78 | 95 | if progress_callback is not None: |
79 | 96 | progress_callback(1, msg) |
80 | 97 | return True |
81 | 98 | except DownloadCancelledError: |
82 | | - save_path.unlink(missing_ok=True) |
| 99 | + if temp_path is not None: |
| 100 | + temp_path.unlink(missing_ok=True) |
83 | 101 | msg = f"{gt('下载已取消')}" |
84 | 102 | log.info(msg) |
85 | 103 | if progress_callback is not None: |
86 | 104 | progress_callback(0, msg) |
87 | 105 | return False |
88 | 106 | except Exception as e: |
89 | | - save_path.unlink(missing_ok=True) |
| 107 | + if temp_path is not None: |
| 108 | + temp_path.unlink(missing_ok=True) |
90 | 109 | msg = f"{gt('下载失败')} {e}" |
91 | 110 | if progress_callback is not None: |
92 | 111 | progress_callback(0, msg) |
93 | 112 | log.error(msg, exc_info=True) |
94 | 113 | return False |
95 | 114 |
|
| 115 | + |
96 | 116 | class DownloadCancelledError(Exception): |
97 | 117 | pass |
| 118 | + |
| 119 | + |
| 120 | +class DownloadIncompleteError(Exception): |
| 121 | + pass |
0 commit comments