twanghcmut's picture
download
raw
5.85 kB
"""Filesystem and HTTP download helpers shared across the data layer.
Every write to disk goes through :func:`atomic_write` or
:func:`download_with_resume`'s temp-file-then-rename pattern: a process killed
mid-download must never leave a truncated file sitting at the *final* path,
because that file's mere existence is what every later "already cached, skip
it" check relies on.
"""
from __future__ import annotations
import os
import tempfile
from pathlib import Path
import requests
from requests.adapters import HTTPAdapter
from tqdm import tqdm
from urllib3.util.retry import Retry
from fpgm.utils.logging import get_logger
logger = get_logger(__name__)
_CHUNK_SIZE = 1024 * 1024 # 1 MiB
def ensure_dir(path: Path) -> Path:
"""Create ``path`` as a directory (parents included) if it doesn't exist."""
path = Path(path)
path.mkdir(parents=True, exist_ok=True)
return path
def human_bytes(n: int | float) -> str:
"""Render a byte count as e.g. ``"3.5 GB"`` (decimal units, matching GCS/HF listings)."""
n = float(n)
for unit in ("B", "KB", "MB", "GB"):
if n < 1000.0:
return f"{int(n)} {unit}" if unit == "B" else f"{n:.1f} {unit}"
n /= 1000.0
return f"{n:.1f} TB"
def atomic_write(path: Path, data: bytes) -> None:
"""Write ``data`` to ``path`` without ever exposing a partially-written file."""
path = Path(path)
ensure_dir(path.parent)
fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
try:
with os.fdopen(fd, "wb") as fh:
fh.write(data)
os.replace(tmp_name, path)
except BaseException:
Path(tmp_name).unlink(missing_ok=True)
raise
def build_retrying_session(max_retries: int = 5, backoff_factor: float = 0.5) -> requests.Session:
"""A :class:`requests.Session` that retries transient failures with backoff."""
session = requests.Session()
retry = Retry(
total=max_retries,
backoff_factor=backoff_factor,
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=("GET", "HEAD"),
)
adapter = HTTPAdapter(max_retries=retry)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
def remote_content_length(
url: str, session: requests.Session, timeout: float = 30.0
) -> int | None:
"""``Content-Length`` of ``url`` via ``HEAD``, or ``None`` if unreported.
Returns ``None`` when the response is content-encoded (gzip/br/deflate). In
that case ``Content-Length`` describes the *compressed* transfer, while
``requests`` transparently decompresses the body before it reaches disk, so
the two are not comparable. GitHub's raw CDN does exactly this for
``text/plain`` (our ``.dae`` and ``.urdf`` files, though not the
``application/octet-stream`` ``.stl``s), and comparing the two silently
re-downloaded every text asset on every run.
"""
resp = session.head(url, timeout=timeout, allow_redirects=True)
resp.raise_for_status()
encoding = resp.headers.get("Content-Encoding", "").strip().lower()
if encoding and encoding != "identity":
return None
length = resp.headers.get("Content-Length")
return int(length) if length is not None else None
def download_with_resume(
url: str,
dest: Path,
*,
session: requests.Session | None = None,
timeout: float = 60.0,
progress: bool = True,
) -> Path:
"""Stream ``url`` to ``dest``, skipping the download if it is already cached.
"Resume" here is the pragmatic version that matters for this pipeline: a
previously-completed download is recognised by comparing its size against
the server's ``Content-Length`` and is never re-fetched, and an
in-progress download is written to a sibling ``.part`` file so a crash
never leaves a corrupt file at ``dest`` masquerading as a finished one.
True byte-range resume is not implemented -- it isn't needed at these file
sizes (mp4s ~5 MB, trajectory.h5 ~888 KB, metadata.json ~1.7 KB).
Returns:
``dest``.
"""
dest = Path(dest)
session = session or build_retrying_session()
ensure_dir(dest.parent)
expected_size = remote_content_length(url, session, timeout=timeout)
if dest.exists():
actual_size = dest.stat().st_size
if expected_size is not None and actual_size == expected_size:
logger.debug("cached, skipping download: %s", dest)
return dest
if expected_size is None and actual_size > 0:
# Size is unverifiable (compressed transfer or no Content-Length).
# A complete file is still recognisable: partial downloads live in a
# sibling ``.part`` and are only renamed once finished, so anything
# sitting at ``dest`` with content is by construction complete.
logger.debug("cached (size unverifiable), skipping download: %s", dest)
return dest
tmp_path = dest.with_name(dest.name + ".part")
with session.get(url, stream=True, timeout=timeout) as resp:
resp.raise_for_status()
total = int(resp.headers.get("Content-Length", 0)) or expected_size or 0
with (
open(tmp_path, "wb") as fh,
tqdm(
total=total or None,
unit="B",
unit_scale=True,
desc=dest.name,
disable=not progress,
leave=False,
) as bar,
):
for chunk in resp.iter_content(chunk_size=_CHUNK_SIZE):
if not chunk:
continue
fh.write(chunk)
bar.update(len(chunk))
os.replace(tmp_path, dest)
logger.info("downloaded %s (%s)", dest, human_bytes(dest.stat().st_size))
return dest

Xet Storage Details

Size:
5.85 kB
·
Xet hash:
886c5f6eaef6e1144fc2cc7608a674d9706a9c731dcdb460071fd91fc5568898

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.