unsloth/studio/backend/hub/utils/download_manifest.py
Eyera aec41d17ed
feat(studio): Hub + Download Manager (#5916)
Adds the Studio Hub and download manager: browse Hugging Face models and datasets, download GGUF and safetensors with live progress and cancellation, and manage on-device inventory. The Hub does not require a GPU, so it is available on chat-only hosts.

CI: all substantive checks pass, including the three Core jobs after unsloth-zoo#736. The two red checks are non-code flakes, a transient npm-registry DNS resolution failure in the package scan and one quantized vision-model output assertion whose sibling shards passed.
2026-06-09 04:11:24 -07:00

487 lines
17 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Hub download manifest + cancel-marker primitives.
Manifests record what a download was supposed to fetch (path + declared
size per expected file). Consumed by:
- the worker post-download, to verify on-disk sizes match what HF
declared, so a resume that no-ops doesn't get classified as success;
- the inventory scanner, to mark a row partial when expected files
are absent or undersized, so a half-finished GGUF/dataset doesn't
masquerade as a complete on-device row.
Cancel markers record that a user-initiated cancel landed for a
(repo_type, repo_id, variant) triple. *Existence* is the signal the
scanner reads; the body carries debuggability metadata. Markers are
cleared at the start of a new download attempt (supersedes prior cancel)
and on successful completion (defensive, in case the start clear failed).
I/O contracts:
- Writes are atomic via ``tmp + os.replace``: a SIGKILL mid-write
cannot leave a half-written file readable to the next reader.
- Manifest reads fail *open*: missing/corrupt/schema-mismatched
manifests return ``None`` and the scanner falls through to the
legacy on-disk-only check (matches HF-cache imports and pre-fix
downloads that never wrote a manifest).
- Cancel-marker reads fail *closed*: file existence is the signal
regardless of body parseability, so a corrupt marker still
suppresses the "on device" classification.
"""
from __future__ import annotations
import json
import os
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterator, Optional, Sequence
from loggers import get_logger
from hub.utils.state_dir import (
RepoType,
cancelled_dir,
manifest_path,
manifests_dir,
marker_path,
variant_filename_prefix,
)
logger = get_logger(__name__)
_MANIFEST_VERSION = 1
_MARKER_VERSION = 2
_LEGACY_MARKER_VERSION = 1
# Verbatim phrase the worker emits on a degraded completion and the download
# lifecycle escalates to a warning log. Shared so the emit and match stay coupled.
MANIFEST_DEGRADED_MARKER = "completed without a manifest so partial detection is degraded"
@dataclass(frozen = True)
class ExpectedFile:
path: str
size: int
sha256: Optional[str] = None
@dataclass(frozen = True)
class Manifest:
repo_type: RepoType
repo_id: str
variant: Optional[str]
started_at: str
expected_files: tuple[ExpectedFile, ...]
transport: Optional[str] = None
@dataclass(frozen = True)
class VerifyResult:
ok: bool
missing: tuple[str, ...]
size_mismatched: tuple[str, ...]
def _atomic_write_json(path: Path, payload: dict) -> bool:
# Per-write uuid suffix so a concurrent caller or a stale tmp from a
# previous crash cannot collide with the in-flight write.
tmp = path.with_name(f".{path.name}.tmp-{uuid.uuid4().hex[:8]}")
try:
with tmp.open("w", encoding = "utf-8") as handle:
handle.write(json.dumps(payload, indent = 2))
handle.flush()
os.fsync(handle.fileno())
os.replace(tmp, path)
except OSError as exc:
logger.debug("Atomic write failed for %s: %s", path, exc)
try:
tmp.unlink(missing_ok = True)
except OSError:
pass
return False
if os.name != "nt":
try:
flags = os.O_RDONLY
if hasattr(os, "O_DIRECTORY"):
flags |= os.O_DIRECTORY
parent_fd = os.open(path.parent, flags)
try:
os.fsync(parent_fd)
finally:
os.close(parent_fd)
except OSError as exc:
logger.debug("Parent dir fsync failed for %s: %s", path, exc)
return True
def write_manifest(
repo_type: RepoType,
repo_id: str,
variant: Optional[str],
expected_files: Sequence[ExpectedFile],
transport: Optional[str] = None,
) -> bool:
"""Write/overwrite the manifest for this triple. Best-effort.
``False`` on write failure must not be treated as fatal: the
worst-case fallback is the pre-fix scanner behavior (one missed
partial detection), which is no regression.
"""
path = manifest_path(repo_type, repo_id, variant)
if path is None:
return False
payload = {
"version": _MANIFEST_VERSION,
"repo_type": repo_type,
"repo_id": repo_id,
"variant": variant,
"started_at": datetime.now(timezone.utc).isoformat(),
"expected_files": [
{
"path": f.path,
"size": int(f.size),
**({"sha256": f.sha256} if f.sha256 else {}),
}
for f in expected_files
],
"transport": transport,
}
return _atomic_write_json(path, payload)
def read_manifest(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
) -> Optional[Manifest]:
"""Return the manifest if present and parseable; ``None`` otherwise.
Treats missing-file, parse-error, and any schema mismatch all as
``None`` (fail-open). Scanner callers fall through to on-disk-only
behavior on ``None`` so this never regresses legacy/imported repos
that have no manifest.
Forward-compat: accepts only ``version == 1``; an unknown version is
treated as no manifest. A future v2 schema MUST either keep v1's
``expected_files`` shape on the same filename (bump
``_MANIFEST_VERSION`` and widen this check) or live under a different
filename, so an incompatible payload can never mis-classify rows.
"""
path = manifest_path(repo_type, repo_id, variant)
if path is None or not path.is_file():
return None
try:
data = json.loads(path.read_text(encoding = "utf-8"))
except (OSError, ValueError) as exc:
logger.debug("Could not read manifest %s: %s", path, exc)
return None
if not isinstance(data, dict):
return None
if data.get("version") != _MANIFEST_VERSION:
logger.debug(
"Manifest %s has unknown version %r; ignoring.",
path,
data.get("version"),
)
return None
raw_files = data.get("expected_files")
if not isinstance(raw_files, list):
return None
expected: list[ExpectedFile] = []
for item in raw_files:
if not isinstance(item, dict):
return None
file_path = item.get("path")
size = item.get("size")
if not isinstance(file_path, str) or not isinstance(size, int):
return None
sha256 = item.get("sha256")
expected.append(
ExpectedFile(
path = file_path,
size = size,
sha256 = sha256 if isinstance(sha256, str) and sha256 else None,
)
)
raw_variant = data.get("variant")
transport = data.get("transport")
return Manifest(
repo_type = repo_type,
repo_id = str(data.get("repo_id", repo_id)),
variant = raw_variant if raw_variant else None,
started_at = str(data.get("started_at", "")),
expected_files = tuple(expected),
transport = transport if transport in ("http", "xet") else None,
)
def verify_against_disk(manifest: Manifest, snapshot_dir: Path) -> VerifyResult:
"""Check every expected file is present in *snapshot_dir* at its declared size.
Presence + size only, not content integrity: it converts a
no-op-on-cached ``snapshot_download`` into a clear error when shards are
missing or truncated, and marks a scanner row partial when expected bytes
aren't on disk. Byte-level integrity is already covered upstream by
``huggingface_hub`` (size check on HTTP, content-addressed chunk hashes on
XET), so re-hashing finalized multi-GB weights here would only duplicate
that at a large cost. ``Path.stat()`` follows symlinks, so HF's symlink and
Windows copy cache layouts both verify correctly.
"""
missing: list[str] = []
mismatched: list[str] = []
for expected in manifest.expected_files:
target = snapshot_dir / expected.path
try:
actual_size = target.stat().st_size
except OSError:
missing.append(expected.path)
continue
# expected.size == 0 means HF metadata had no declared size: verify
# existence only rather than flagging every such file as mismatched.
if expected.size > 0 and actual_size != expected.size:
mismatched.append(expected.path)
return VerifyResult(
ok = not missing and not mismatched,
missing = tuple(missing),
size_mismatched = tuple(mismatched),
)
def expected_files_from_snapshot_dir(snapshot_dir: Path) -> list[ExpectedFile]:
"""Derive expected-file entries from a completed snapshot directory.
Last-resort manifest source for when HF metadata was unreachable for the
whole download. ``snapshot_download`` has already exited cleanly, so every
regular file is a finished, correctly-sized blob; recording them keeps the
scanner's completion check in agreement with the worker's exit-0 success
instead of leaving a finished repo perpetually partial. ``stat()`` follows
HF's symlink layout and Windows copies, so the recorded sizes match what
``verify_against_disk`` later reads.
"""
out: list[ExpectedFile] = []
try:
entries = sorted(snapshot_dir.rglob("*"))
except OSError:
return out
for path in entries:
try:
if not path.is_file():
continue
relative = path.relative_to(snapshot_dir).as_posix()
out.append(
ExpectedFile(
path = relative,
size = path.stat().st_size,
sha256 = None,
)
)
except OSError:
continue
return out
def write_cancel_marker(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
transport: Optional[str] = None,
) -> bool:
"""Record that this triple was cancelled. Idempotent across repeated cancels.
``transport`` ("http"/"xet") is surfaced via partial_transport on
inventory rows so the UI labels HTTP retries as continuable and XET
retries as full redownloads. None is accepted for forward-compat.
"""
path = marker_path(repo_type, repo_id, variant)
if path is None:
return False
payload = {
"version": _MARKER_VERSION,
"repo_type": repo_type,
"repo_id": repo_id,
"variant": variant,
"transport": transport,
"cancelled_at": datetime.now(timezone.utc).isoformat(),
}
return _atomic_write_json(path, payload)
def read_cancel_marker_transport(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
) -> Optional[str]:
"""Return the transport recorded in the cancel marker, or ``None`` if no
marker exists or it is unreadable.
Cases:
* No marker on disk → ``None``.
* Legacy v1 marker → ``"http"``: v1 markers were only written by the
HTTP path, so the transport is unambiguous despite the absent field.
* v2 marker with a valid ``"http"`` / ``"xet"`` transport → that value.
* Corrupt, non-dict, or v2-with-missing-transport marker → ``None``.
Defaulting these to ``"http"`` misled the UI into showing a
byte-resume "Continue" label for what may have been an XET cancel;
``None`` keeps the neutral "Retry" label.
* Unknown future versions → ``None`` (unknown layout, unknown transport).
"""
path = marker_path(repo_type, repo_id, variant)
if path is None or not path.is_file():
return None
try:
data = json.loads(path.read_text(encoding = "utf-8"))
except (OSError, ValueError) as exc:
logger.debug("Could not read cancel marker %s: %s", path, exc)
return None
if not isinstance(data, dict):
return None
version = data.get("version")
if version == _LEGACY_MARKER_VERSION:
return "http"
if version != _MARKER_VERSION:
return None
transport = data.get("transport")
if isinstance(transport, str) and transport in ("http", "xet"):
return transport
return None
def clear_cancel_marker(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
) -> None:
"""Remove the cancel marker for this triple if present.
Idempotent: a missing marker is not an error. Called at
download-start (a fresh attempt supersedes prior cancel state) and
again at successful completion (cleans up if the start clear failed).
"""
path = marker_path(repo_type, repo_id, variant)
if path is None:
return
try:
path.unlink(missing_ok = True)
except OSError as exc:
logger.debug("Could not clear cancel marker %s: %s", path, exc)
def has_cancel_marker(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
) -> bool:
"""File-existence check only. Body is never read.
Fail-closed: a corrupt marker still returns ``True`` because the
file's existence is the signal (the user once cancelled this
triple, even if the body is unreadable).
"""
path = marker_path(repo_type, repo_id, variant)
if path is None:
return False
try:
return path.is_file()
except OSError:
return False
def delete_manifest(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
) -> bool:
path = manifest_path(repo_type, repo_id, variant)
if path is None:
return False
try:
if not path.is_file():
return False
path.unlink()
return True
except OSError as exc:
logger.debug("Could not delete manifest %s: %s", path, exc)
return False
def purge_state(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
) -> bool:
"""Remove manifest + cancel marker for this triple. Returns ``True``
when anything was present on disk before the call. Idempotent."""
marker_existed = has_cancel_marker(repo_type, repo_id, variant)
manifest_removed = delete_manifest(repo_type, repo_id, variant)
clear_cancel_marker(repo_type, repo_id, variant)
return marker_existed or manifest_removed
def purge_all_state_for_repo(repo_type: RepoType, repo_id: str) -> int:
"""Remove the snapshot-level manifest + marker AND every variant-keyed
manifest + marker for this repo. Used by the route delete handlers so
scanner state never outlives the cache it described. Returns the count
of (repo, variant) triples that had any state on disk."""
removed = 0
if purge_state(repo_type, repo_id, None):
removed += 1
variants: set[str] = set()
for variant, _ in iter_variant_manifests(repo_type, repo_id):
variants.add(variant)
for variant, _ in iter_variant_markers(repo_type, repo_id):
variants.add(variant)
for variant in variants:
if purge_state(repo_type, repo_id, variant):
removed += 1
return removed
def _variant_from_state_file(path: Path, fallback: str) -> str:
try:
data = json.loads(path.read_text(encoding = "utf-8"))
except (OSError, ValueError):
return fallback
if not isinstance(data, dict):
return fallback
variant = data.get("variant")
return variant if isinstance(variant, str) and variant else fallback
def _iter_variant_state_files(
parent: Optional[Path], repo_type: RepoType, repo_id: str
) -> Iterator[tuple[str, Path]]:
if parent is None:
return
prefix = variant_filename_prefix(repo_type, repo_id)
try:
entries = list(parent.iterdir())
except OSError:
return
for entry in entries:
if not entry.is_file() or not entry.name.endswith(".json"):
continue
stem = entry.name[: -len(".json")]
if not stem.lower().startswith(prefix):
continue
variant = stem[len(prefix) :]
if variant:
yield _variant_from_state_file(entry, variant), entry
def iter_variant_manifests(repo_type: RepoType, repo_id: str) -> Iterator[tuple[str, Path]]:
"""Yield (variant, manifest_path) for every variant-keyed manifest
written for this repo. Used by is_gguf_repo_partial to enumerate all
variants present on disk so the all-variants-broken gate can run."""
yield from _iter_variant_state_files(manifests_dir(), repo_type, repo_id)
def iter_variant_markers(repo_type: RepoType, repo_id: str) -> Iterator[tuple[str, Path]]:
"""Yield (variant, marker_path) for every variant-keyed cancel marker.
Companion to iter_variant_manifests: catches variants cancelled
before download-start ever wrote a manifest (very early failures)."""
yield from _iter_variant_state_files(cancelled_dir(), repo_type, repo_id)