| 1 | #!/usr/bin/env python3 |
| 2 | """Download pinned model files once, with resumable transfers and SHA-256 checks.""" |
| 3 | import argparse |
| 4 | import fcntl |
| 5 | import hashlib |
| 6 | import json |
| 7 | import os |
| 8 | from pathlib import Path |
| 9 | import subprocess |
| 10 | |
| 11 | |
| 12 | def checksum(path): |
| 13 | with path.open("rb") as source: |
| 14 | return hashlib.file_digest(source, "sha256").hexdigest() |
| 15 | |
| 16 | |
| 17 | def download(root): |
| 18 | manifest = json.loads(Path(__file__).with_name("models.json").read_text()) |
| 19 | directory = root / manifest["directory"] |
| 20 | directory.mkdir(parents=True, exist_ok=True) |
| 21 | with (directory / ".download.lock").open("a") as lock: |
| 22 | fcntl.flock(lock, fcntl.LOCK_EX) |
| 23 | for spec in manifest["files"]: |
| 24 | target = directory / spec["name"] |
| 25 | if target.exists(): |
| 26 | if target.stat().st_size == spec["bytes"] and checksum(target) == spec["sha256"]: |
| 27 | print(f"Verified {target.name}", flush=True) |
| 28 | continue |
| 29 | raise ValueError(f"Checksum mismatch: {target}. Move this file aside and retry.") |
| 30 | pending = target.with_suffix(target.suffix + ".part") |
| 31 | url = f"https://huggingface.co/{manifest['repository']}/resolve/{manifest['revision']}/{spec['name']}" |
| 32 | print(f"Downloading {target.name}", flush=True) |
| 33 | subprocess.run([ |
| 34 | "curl", "--fail", "--location", "--silent", "--show-error", |
| 35 | "--continue-at", "-", "--retry", "3", "--retry-delay", "3", |
| 36 | "--connect-timeout", "15", "--max-time", "28800", |
| 37 | "--output", str(pending), url, |
| 38 | ], check=True) |
| 39 | if pending.stat().st_size != spec["bytes"] or checksum(pending) != spec["sha256"]: |
| 40 | raise ValueError(f"Checksum mismatch: {pending}. Move this file aside and retry.") |
| 41 | pending.chmod(0o640) |
| 42 | pending.replace(target) |
| 43 | print(f"Verified {target.name}", flush=True) |
| 44 | (directory / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") |
| 45 | |
| 46 | |
| 47 | if __name__ == "__main__": |
| 48 | parser = argparse.ArgumentParser(description="Download local AI models") |
| 49 | parser.add_argument("--root", type=Path, default=Path("/srv/clover/Media/AI/LLM")) |
| 50 | args = parser.parse_args() |
| 51 | os.umask(0o027) |
| 52 | download(args.root) |