unsloth/studio/backend/hub/utils/state_dir.py
Michael Han 6d8c18cd1a
Replace standalone Studio wording with Unsloth (#7221)
* Replace standalone Studio wording with Unsloth

Replace the single word Studio with Unsloth wherever it is used as
shorthand for Unsloth Studio in docs, CLI output, UI strings, i18n
locales, workflow display names, comments and docstrings.

Kept unchanged: the full name Unsloth Studio, third party product
names (LM Studio, Visual Studio, Mac Studio), feature names
(Recipe Studio, Fine-tuning Studio and its translations), and all
identifiers such as env vars, commands, paths and filenames.

* Address review feedback on the Studio wording rename

Use "an" before Unsloth where the rename left the article as "a".
Restore the split brand where Unsloth and Studio render as two halves
of the full product name: the onboarding sidebar subtitle and the
IPv6 localhost warning. Scope two messages to the full name Unsloth
Studio where plain Unsloth was misleading: the AMD README bullet and
the CLI studio setup error.
2026-07-19 00:47:04 -07:00

179 lines
6.3 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
"""Filesystem layout for Hub download state.
State directory sits beside HF's cache (under Unsloth's own cache root)
so it survives ``huggingface-cli delete-cache`` and any other HF-side
cache lifecycle. Two subdirectories:
<studio cache>/hub-state/
manifests/ <key>.json per-download expected-files manifest
cancelled/ <key>.json per-download cancel marker
The ``<key>`` mirrors HF's cache dir naming while the resulting manifest,
cancel-marker, and atomic-write temp filenames fit common filesystem basename
limits. Very long repo IDs use a stable hash in the state key:
models--<owner>--<name> full snapshot
models--<owner>--<name>--variant--<variant> GGUF variant
datasets--<owner>--<name> dataset snapshot
All path accessors return ``Optional[Path]`` and yield ``None`` when
the directory can't be created (read-only FS, permission error).
Callers must treat ``None`` as "no state available" and fall through
to existing on-disk-only behavior; this module never raises on a
configuration failure.
"""
from __future__ import annotations
import hashlib
import re
from pathlib import Path
from typing import Literal, Optional, get_args
from loggers import get_logger
from hub.utils.paths import cache_root
logger = get_logger(__name__)
RepoType = Literal["model", "dataset"]
_VALID_REPO_TYPES: tuple[RepoType, ...] = get_args(RepoType)
_HUB_STATE_DIRNAME = "hub-state"
_MANIFESTS_SUBDIR = "manifests"
_CANCELLED_SUBDIR = "cancelled"
_WORKERS_SUBDIR = "workers"
_SAFE_VARIANT_FRAGMENT = re.compile(r"^[a-z0-9._-]{1,64}$")
_MAX_STATE_BASENAME_BYTES = 255
_STATE_EXTENSION = ".json"
# _atomic_write_json writes ".<target>.tmp-<8hex>" beside the final file.
_ATOMIC_WRITE_TMP_OVERHEAD = len(".") + len(".tmp-") + 8
_MAX_VARIANT_FRAGMENT_LENGTH = 64
def state_root() -> Optional[Path]:
"""Return the Hub state root, creating it if needed. ``None`` on failure."""
root = cache_root() / _HUB_STATE_DIRNAME
try:
root.mkdir(parents = True, exist_ok = True)
except OSError as exc:
logger.debug("Could not create hub state root %s: %s", root, exc)
return None
return root
def _subdir(name: str) -> Optional[Path]:
root = state_root()
if root is None:
return None
path = root / name
try:
path.mkdir(parents = True, exist_ok = True)
except OSError as exc:
logger.debug("Could not create hub state subdir %s: %s", path, exc)
return None
return path
def repo_cache_basename(repo_type: RepoType, repo_id: str) -> str:
# Reject a bad repo_type at runtime: a wrong value would silently produce a
# wrong filename and a misclassified scanner row (the Literal only guards
# statically; dynamic/JSON-sourced values slip past it).
if repo_type not in _VALID_REPO_TYPES:
raise ValueError(f"repo_type must be one of {_VALID_REPO_TYPES}, got {repo_type!r}")
return f"{repo_type}s--{repo_id.replace('/', '--')}".lower()
def _filename_bytes(name: str) -> int:
return len(name.encode("utf-8"))
def _state_filename_fits(entry_key: str) -> bool:
filename = f"{entry_key}{_STATE_EXTENSION}"
return _filename_bytes(filename) + _ATOMIC_WRITE_TMP_OVERHEAD <= _MAX_STATE_BASENAME_BYTES
def _state_repo_key(repo_type: RepoType, repo_id: str) -> str:
base = repo_cache_basename(repo_type, repo_id)
variant_prefix = f"{base}--variant--"
longest_variant_key = f"{variant_prefix}{'x' * _MAX_VARIANT_FRAGMENT_LENGTH}"
if _state_filename_fits(longest_variant_key):
return base
digest = hashlib.sha256(base.encode("utf-8")).hexdigest()[:32]
return f"{repo_type}s--sha256-{digest}"
def variant_filename_prefix(repo_type: RepoType, repo_id: str) -> str:
"""Lowercased prefix every variant-keyed state file for this repo shares.
The single source the download_manifest enumerators match against, so the
scheme in :func:`_entry_key` cannot drift from them silently."""
return f"{_state_repo_key(repo_type, repo_id)}--variant--"
def _entry_key(repo_type: RepoType, repo_id: str, variant: Optional[str]) -> str:
base = _state_repo_key(repo_type, repo_id)
if not variant:
return base
normalized_variant = variant.strip().lower()
if _SAFE_VARIANT_FRAGMENT.fullmatch(normalized_variant):
variant_fragment = normalized_variant
else:
digest = hashlib.sha256(normalized_variant.encode("utf-8")).hexdigest()[:32]
variant_fragment = f"sha256-{digest}"
return f"{variant_filename_prefix(repo_type, repo_id)}{variant_fragment}"
def manifest_path(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
) -> Optional[Path]:
"""Path to the manifest file for this triple. May or may not exist."""
parent = _subdir(_MANIFESTS_SUBDIR)
if parent is None:
return None
return parent / f"{_entry_key(repo_type, repo_id, variant)}.json"
def marker_path(
repo_type: RepoType,
repo_id: str,
variant: Optional[str] = None,
) -> Optional[Path]:
"""Path to the cancel-marker file for this triple. May or may not exist."""
parent = _subdir(_CANCELLED_SUBDIR)
if parent is None:
return None
return parent / f"{_entry_key(repo_type, repo_id, variant)}.json"
def manifests_dir() -> Optional[Path]:
"""Manifests subdirectory, created on demand. ``None`` on failure.
Exposed for iter_variant_manifests, which enumerates the directory to find
every variant-keyed manifest for a repo (the path helpers above answer
"where would key X go" but not "what keys exist")."""
return _subdir(_MANIFESTS_SUBDIR)
def cancelled_dir() -> Optional[Path]:
"""Cancel-marker subdirectory, created on demand. ``None`` on failure.
See manifests_dir for why this iteration entry point is needed."""
return _subdir(_CANCELLED_SUBDIR)
def workers_dir() -> Optional[Path]:
"""Worker PID-breadcrumb subdirectory, created on demand. ``None`` on failure.
Each live download worker drops one breadcrumb here so a backend that
restarts after a hard crash can reap workers it can no longer reach through
its in-memory registry."""
return _subdir(_WORKERS_SUBDIR)