2025-12-12 21:55:38 -08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
2025-12-22 02:11:53 -08:00
|
|
|
from typing import Callable, Optional
|
2025-12-20 23:57:44 -08:00
|
|
|
import sys
|
2025-12-12 21:55:38 -08:00
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
2025-12-20 23:57:44 -08:00
|
|
|
from models import ProgressBar
|
|
|
|
|
|
2025-12-12 21:55:38 -08:00
|
|
|
|
|
|
|
|
def sanitize_filename(name: str, *, max_len: int = 150) -> str:
|
|
|
|
|
text = str(name or "").strip()
|
|
|
|
|
if not text:
|
|
|
|
|
return "download"
|
|
|
|
|
|
|
|
|
|
forbidden = set('<>:"/\\|?*')
|
|
|
|
|
cleaned = "".join("_" if c in forbidden else c for c in text)
|
|
|
|
|
cleaned = " ".join(cleaned.split()).strip().strip(".")
|
|
|
|
|
if not cleaned:
|
|
|
|
|
cleaned = "download"
|
|
|
|
|
return cleaned[:max_len]
|
|
|
|
|
|
|
|
|
|
|
2025-12-22 02:11:53 -08:00
|
|
|
def download_file(
|
|
|
|
|
url: str,
|
|
|
|
|
output_path: Path,
|
|
|
|
|
*,
|
|
|
|
|
session: Optional[requests.Session] = None,
|
|
|
|
|
timeout_s: float = 30.0,
|
2025-12-29 18:42:02 -08:00
|
|
|
progress_callback: Optional[Callable[[int,
|
|
|
|
|
Optional[int],
|
|
|
|
|
str],
|
|
|
|
|
None]] = None,
|
2025-12-22 02:11:53 -08:00
|
|
|
) -> bool:
|
2025-12-12 21:55:38 -08:00
|
|
|
output_path = Path(output_path)
|
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
s = session or requests.Session()
|
|
|
|
|
|
2025-12-22 02:11:53 -08:00
|
|
|
bar = ProgressBar() if progress_callback is None else None
|
2025-12-20 23:57:44 -08:00
|
|
|
downloaded = 0
|
|
|
|
|
total = None
|
|
|
|
|
|
2025-12-12 21:55:38 -08:00
|
|
|
try:
|
|
|
|
|
with s.get(url, stream=True, timeout=timeout_s) as resp:
|
|
|
|
|
resp.raise_for_status()
|
2025-12-20 23:57:44 -08:00
|
|
|
try:
|
|
|
|
|
total_val = int(resp.headers.get("content-length") or 0)
|
|
|
|
|
total = total_val if total_val > 0 else None
|
|
|
|
|
except Exception:
|
|
|
|
|
total = None
|
|
|
|
|
|
2025-12-22 02:11:53 -08:00
|
|
|
label = str(output_path.name or "download")
|
|
|
|
|
|
2025-12-20 23:57:44 -08:00
|
|
|
# Render once immediately so fast downloads still show something.
|
|
|
|
|
try:
|
2025-12-22 02:11:53 -08:00
|
|
|
if progress_callback is not None:
|
|
|
|
|
progress_callback(0, total, label)
|
|
|
|
|
elif bar is not None:
|
|
|
|
|
bar.update(downloaded=0, total=total, label=label, file=sys.stderr)
|
2025-12-20 23:57:44 -08:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
2025-12-12 21:55:38 -08:00
|
|
|
with open(output_path, "wb") as f:
|
|
|
|
|
for chunk in resp.iter_content(chunk_size=1024 * 256):
|
|
|
|
|
if chunk:
|
|
|
|
|
f.write(chunk)
|
2025-12-20 23:57:44 -08:00
|
|
|
downloaded += len(chunk)
|
|
|
|
|
try:
|
2025-12-22 02:11:53 -08:00
|
|
|
if progress_callback is not None:
|
|
|
|
|
progress_callback(downloaded, total, label)
|
|
|
|
|
elif bar is not None:
|
2025-12-29 17:05:03 -08:00
|
|
|
bar.update(
|
2025-12-29 18:42:02 -08:00
|
|
|
downloaded=downloaded,
|
|
|
|
|
total=total,
|
|
|
|
|
label=label,
|
|
|
|
|
file=sys.stderr
|
2025-12-29 17:05:03 -08:00
|
|
|
)
|
2025-12-20 23:57:44 -08:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
2025-12-22 02:11:53 -08:00
|
|
|
if bar is not None:
|
|
|
|
|
bar.finish()
|
2025-12-20 23:57:44 -08:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
2025-12-12 21:55:38 -08:00
|
|
|
return output_path.exists() and output_path.stat().st_size > 0
|
|
|
|
|
except Exception:
|
2025-12-20 23:57:44 -08:00
|
|
|
try:
|
2025-12-22 02:11:53 -08:00
|
|
|
if bar is not None:
|
|
|
|
|
bar.finish()
|
2025-12-20 23:57:44 -08:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
2025-12-12 21:55:38 -08:00
|
|
|
try:
|
|
|
|
|
if output_path.exists():
|
|
|
|
|
output_path.unlink()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
return False
|