|
10 | 10 | import random |
11 | 11 | import string |
12 | 12 | import sys |
| 13 | +import tempfile |
13 | 14 | import time |
14 | 15 | import traceback |
15 | 16 | from datetime import datetime |
16 | 17 | from enum import Enum |
17 | 18 | from functools import lru_cache |
| 19 | +from pathlib import Path |
18 | 20 | from uuid import UUID |
19 | 21 |
|
20 | 22 | import requests |
@@ -319,13 +321,30 @@ def strtobool(val: str | None) -> bool: |
319 | 321 | raise ValueError("invalid truth value '{}'".format(val)) |
320 | 322 |
|
321 | 323 | def url_download(url: str, filename: str) -> None: |
322 | | - r = requests.get(url, stream=True) |
323 | | - r.raise_for_status() |
324 | | - tempfilename = filename + ".part" |
325 | | - with open(tempfilename, 'wb') as fd: |
326 | | - for chunk in r.iter_content(chunk_size=128): |
327 | | - fd.write(chunk) |
328 | | - os.rename(tempfilename, filename) |
| 324 | + """ |
| 325 | + Download the content of `url` to the `filename` destination. |
| 326 | +
|
| 327 | + A randomized filename is used during download to prevent file corruption on |
| 328 | + concurrent use. If the download fails then the temporary file is removed. |
| 329 | + """ |
| 330 | + destination = Path(filename) |
| 331 | + destination.parent.mkdir(parents=True, exist_ok=True) |
| 332 | + with requests.get(url, stream=True) as r: |
| 333 | + r.raise_for_status() |
| 334 | + temp_name: str | None = None |
| 335 | + try: |
| 336 | + with tempfile.NamedTemporaryFile( |
| 337 | + dir=destination.parent, prefix=f"{destination.name}.", suffix=".part", delete=False |
| 338 | + ) as fd: |
| 339 | + temp_name = fd.name |
| 340 | + for chunk in r.iter_content(chunk_size=64 * 1024): |
| 341 | + fd.write(chunk) |
| 342 | + except BaseException: |
| 343 | + if temp_name is not None: |
| 344 | + os.unlink(temp_name) |
| 345 | + raise |
| 346 | + else: |
| 347 | + os.rename(temp_name, filename) |
329 | 348 |
|
330 | 349 | def randid(length: int = 6) -> str: |
331 | 350 | """ |
|
0 commit comments