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.
194 lines
6 KiB
Python
194 lines
6 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
|
|
|
|
"""Persistence for user-registered custom model scan folders.
|
|
|
|
Self-bootstrapping table inside the existing studio SQLite so the Hub module
|
|
doesn't have to modify upstream studio_db.py's schema init."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import platform
|
|
import sqlite3
|
|
import threading
|
|
from datetime import datetime, timezone
|
|
|
|
from storage.studio_db import get_connection
|
|
from hub.utils.paths import normalize_path
|
|
|
|
|
|
_schema_lock = threading.Lock()
|
|
_schema_ready = False
|
|
_SENSITIVE_PATH_COMPONENTS = {
|
|
".aws",
|
|
".azure",
|
|
".config",
|
|
".docker",
|
|
".gcloud",
|
|
".gnupg",
|
|
".huggingface",
|
|
".kaggle",
|
|
".kube",
|
|
".modelscope",
|
|
".ngc",
|
|
".local",
|
|
".mozilla",
|
|
".pki",
|
|
".thunderbird",
|
|
".ssh",
|
|
".1password",
|
|
".bitwarden",
|
|
".password-store",
|
|
"1password",
|
|
"bitwarden",
|
|
"keychains",
|
|
"keyrings",
|
|
"mozilla",
|
|
"thunderbird",
|
|
}
|
|
|
|
|
|
def _denied_path_prefixes() -> list[str]:
|
|
system = platform.system()
|
|
if system == "Linux":
|
|
return ["/proc", "/sys", "/dev", "/etc", "/boot", "/run"]
|
|
if system == "Darwin":
|
|
# realpath() resolves /etc -> /private/etc, /tmp -> /private/tmp on macOS,
|
|
# so include the /private variants to avoid bypasses.
|
|
return [
|
|
"/System",
|
|
"/Library",
|
|
"/dev",
|
|
"/etc",
|
|
"/private/etc",
|
|
"/tmp",
|
|
"/private/tmp",
|
|
"/var",
|
|
"/private/var",
|
|
]
|
|
if system == "Windows":
|
|
win = os.environ.get("SystemRoot", r"C:\Windows")
|
|
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
|
pf86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
|
return [os.path.normcase(p) for p in [win, pf, pf86]]
|
|
return []
|
|
|
|
|
|
def _contains_sensitive_path_component(path: str) -> bool:
|
|
parts = os.path.normpath(path).split(os.sep)
|
|
return any(part.lower() in _SENSITIVE_PATH_COMPONENTS for part in parts)
|
|
|
|
|
|
def contains_sensitive_path_component(path: str) -> bool:
|
|
"""Public predicate for the credential/config denylist (.ssh, .aws, ...).
|
|
|
|
Shared with the folder browser so browse and register enforce one policy."""
|
|
return _contains_sensitive_path_component(path)
|
|
|
|
|
|
def _ensure_schema(conn: sqlite3.Connection) -> None:
|
|
global _schema_ready
|
|
if _schema_ready:
|
|
return
|
|
with _schema_lock:
|
|
if _schema_ready:
|
|
return
|
|
collation = "COLLATE NOCASE" if platform.system() == "Windows" else ""
|
|
conn.execute(
|
|
f"""
|
|
CREATE TABLE IF NOT EXISTS scan_folders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
path TEXT NOT NULL UNIQUE {collation},
|
|
created_at TEXT NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
conn.commit()
|
|
_schema_ready = True
|
|
|
|
|
|
def list_scan_folders() -> list[dict]:
|
|
conn = get_connection()
|
|
try:
|
|
_ensure_schema(conn)
|
|
rows = conn.execute(
|
|
"SELECT id, path, created_at FROM scan_folders ORDER BY created_at"
|
|
).fetchall()
|
|
return [dict(row) for row in rows]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def add_scan_folder(path: str) -> dict:
|
|
"""Add a readable directory for the local OS user; not a multi-user sandbox."""
|
|
if not path or not path.strip():
|
|
raise ValueError("Path cannot be empty")
|
|
normalized = os.path.realpath(os.path.expanduser(normalize_path(path.strip())))
|
|
|
|
if not os.path.exists(normalized):
|
|
raise ValueError("Path does not exist")
|
|
if not os.path.isdir(normalized):
|
|
raise ValueError("Path must be a directory, not a file")
|
|
if not os.access(normalized, os.R_OK | os.X_OK):
|
|
raise ValueError("Path is not readable")
|
|
if os.path.dirname(normalized) == normalized:
|
|
# Registering a filesystem root would expose denied system dirs via browse.
|
|
raise ValueError("The filesystem root cannot be registered")
|
|
if _contains_sensitive_path_component(normalized):
|
|
raise ValueError("Credential or configuration directories are not allowed")
|
|
|
|
is_win = platform.system() == "Windows"
|
|
check = os.path.normcase(normalized) if is_win else normalized
|
|
for prefix in _denied_path_prefixes():
|
|
if check == prefix or check.startswith(prefix + os.sep):
|
|
raise ValueError(f"Path under {prefix} is not allowed")
|
|
|
|
conn = get_connection()
|
|
try:
|
|
_ensure_schema(conn)
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
if is_win:
|
|
existing = conn.execute(
|
|
"SELECT id, path, created_at FROM scan_folders WHERE path = ? COLLATE NOCASE",
|
|
(normalized,),
|
|
).fetchone()
|
|
else:
|
|
existing = conn.execute(
|
|
"SELECT id, path, created_at FROM scan_folders WHERE path = ?",
|
|
(normalized,),
|
|
).fetchone()
|
|
if existing is not None:
|
|
return dict(existing)
|
|
try:
|
|
conn.execute(
|
|
"INSERT INTO scan_folders (path, created_at) VALUES (?, ?)",
|
|
(normalized, now),
|
|
)
|
|
conn.commit()
|
|
except sqlite3.IntegrityError:
|
|
pass
|
|
fallback_sql = (
|
|
"SELECT id, path, created_at FROM scan_folders WHERE path = ? COLLATE NOCASE"
|
|
if is_win
|
|
else "SELECT id, path, created_at FROM scan_folders WHERE path = ?"
|
|
)
|
|
row = conn.execute(fallback_sql, (normalized,)).fetchone()
|
|
if row is None:
|
|
raise ValueError("Folder was concurrently removed")
|
|
return dict(row)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def remove_scan_folder(id: int) -> None:
|
|
# sqlite INTEGER is signed 64-bit; ids outside that range cannot exist.
|
|
if not -(2**63) <= id < 2**63:
|
|
return
|
|
conn = get_connection()
|
|
try:
|
|
_ensure_schema(conn)
|
|
conn.execute("DELETE FROM scan_folders WHERE id = ?", (id,))
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|