download_missing_models.sh: GITHUB_TOKEN auth + codeload tarball fallback for git clones (datacenter IPs get 403 from github.com)
Browse files- download_missing_models.sh +77 -3
download_missing_models.sh
CHANGED
|
@@ -20,6 +20,12 @@
|
|
| 20 |
#
|
| 21 |
# Auth: uses the HUGGING_FACE_ACCESS_TOKEN environment variable if set
|
| 22 |
# (falls back to HF_TOKEN), otherwise prompts for a token.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
set -euo pipefail
|
| 25 |
|
|
@@ -312,14 +318,82 @@ def resolve_repo_url(dep):
|
|
| 312 |
return None, None
|
| 313 |
|
| 314 |
GIT_ENV = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
def install_pack(name, url, ver):
|
| 317 |
dest = os.path.join(CUSTOM_NODES_DIR, url.rstrip("/").split("/")[-1].removesuffix(".git"))
|
| 318 |
print(f"\n{C_CYAN}Installing {name} from {url} ...{C_RESET}")
|
| 319 |
if not os.path.isdir(dest):
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
r = subprocess.run(["git", "-C", dest, "checkout", ver], env=GIT_ENV, timeout=120,
|
| 324 |
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 325 |
print(f" pinned to commit {ver[:12]}" if r.returncode == 0
|
|
|
|
| 20 |
#
|
| 21 |
# Auth: uses the HUGGING_FACE_ACCESS_TOKEN environment variable if set
|
| 22 |
# (falls back to HF_TOKEN), otherwise prompts for a token.
|
| 23 |
+
# If GITHUB_TOKEN (or GH_TOKEN) is set, git clones of github.com repos are
|
| 24 |
+
# authenticated with it — datacenter IPs (e.g. RunPod) often get 403s on
|
| 25 |
+
# anonymous github.com traffic. Use a fine-grained PAT with no repo
|
| 26 |
+
# permissions; the token is sent as a one-shot header, never written to disk.
|
| 27 |
+
# With or without a token, a failed clone falls back to a repo tarball from
|
| 28 |
+
# codeload.github.com, which is typically not IP-blocked.
|
| 29 |
|
| 30 |
set -euo pipefail
|
| 31 |
|
|
|
|
| 318 |
return None, None
|
| 319 |
|
| 320 |
GIT_ENV = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
|
| 321 |
+
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
| 322 |
+
|
| 323 |
+
def github_owner_repo(url):
|
| 324 |
+
m = re.match(r"https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$", url)
|
| 325 |
+
return (m.group(1), m.group(2)) if m else None
|
| 326 |
+
|
| 327 |
+
def gh_headers():
|
| 328 |
+
h = {"User-Agent": "comfy-dep-installer"}
|
| 329 |
+
if GITHUB_TOKEN:
|
| 330 |
+
h["Authorization"] = f"Bearer {GITHUB_TOKEN}"
|
| 331 |
+
return h
|
| 332 |
+
|
| 333 |
+
def git_clone(url, dest):
|
| 334 |
+
"""Clone, authenticating github.com with GITHUB_TOKEN when available.
|
| 335 |
+
The token goes in a per-invocation header (git -c), so it is never
|
| 336 |
+
persisted in the clone's .git/config."""
|
| 337 |
+
cmd = ["git"]
|
| 338 |
+
if GITHUB_TOKEN and github_owner_repo(url):
|
| 339 |
+
import base64
|
| 340 |
+
basic = base64.b64encode(f"x-access-token:{GITHUB_TOKEN}".encode()).decode()
|
| 341 |
+
cmd += ["-c", f"http.https://github.com/.extraheader=Authorization: basic {basic}"]
|
| 342 |
+
cmd += ["clone", "--recursive", "--progress", url, dest]
|
| 343 |
+
subprocess.run(cmd, check=True, env=GIT_ENV, timeout=900, stdin=subprocess.DEVNULL)
|
| 344 |
+
|
| 345 |
+
def install_from_tarball(url, ver, dest):
|
| 346 |
+
"""Fallback when git clone is refused (github.com 403s anonymous requests
|
| 347 |
+
from many datacenter IPs): fetch the repo tarball from codeload.github.com,
|
| 348 |
+
which sits on separate infrastructure and is typically not blocked.
|
| 349 |
+
Caveats vs a real clone: no .git (ComfyUI-Manager can't update the pack)
|
| 350 |
+
and no submodules."""
|
| 351 |
+
owner, repo = github_owner_repo(url)
|
| 352 |
+
if ver and re.fullmatch(r"[0-9a-f]{40}", str(ver), re.I):
|
| 353 |
+
ref = str(ver)
|
| 354 |
+
else:
|
| 355 |
+
info = fetch_json_gh(f"https://api.github.com/repos/{owner}/{repo}")
|
| 356 |
+
ref = info.get("default_branch") or "main"
|
| 357 |
+
print(f" {C_YELLOW}falling back to tarball of {owner}/{repo}@{ref[:12]} via codeload.github.com ...{C_RESET}")
|
| 358 |
+
import shutil, tarfile, tempfile
|
| 359 |
+
req = urllib.request.Request(
|
| 360 |
+
f"https://codeload.github.com/{owner}/{repo}/tar.gz/{ref}", headers=gh_headers())
|
| 361 |
+
os.makedirs(CUSTOM_NODES_DIR, exist_ok=True)
|
| 362 |
+
# tmp dir lives next to dest so the final rename stays on one filesystem;
|
| 363 |
+
# "." prefix keeps installed_packs() from ever seeing it
|
| 364 |
+
with tempfile.TemporaryDirectory(dir=CUSTOM_NODES_DIR, prefix=".tarball-") as tmp:
|
| 365 |
+
tar_path = os.path.join(tmp, "repo.tar.gz")
|
| 366 |
+
with urllib.request.urlopen(req, timeout=120) as r, open(tar_path, "wb") as fh:
|
| 367 |
+
shutil.copyfileobj(r, fh)
|
| 368 |
+
with tarfile.open(tar_path) as tar:
|
| 369 |
+
try:
|
| 370 |
+
tar.extractall(tmp, filter="data")
|
| 371 |
+
except TypeError: # Python < 3.12 has no filter=
|
| 372 |
+
tar.extractall(tmp)
|
| 373 |
+
tops = [e for e in os.listdir(tmp)
|
| 374 |
+
if os.path.isdir(os.path.join(tmp, e))]
|
| 375 |
+
if len(tops) != 1:
|
| 376 |
+
raise RuntimeError(f"unexpected tarball layout: {tops}")
|
| 377 |
+
os.rename(os.path.join(tmp, tops[0]), dest)
|
| 378 |
+
|
| 379 |
+
def fetch_json_gh(url):
|
| 380 |
+
req = urllib.request.Request(url, headers=gh_headers())
|
| 381 |
+
with urllib.request.urlopen(req, timeout=30) as r:
|
| 382 |
+
return json.load(r)
|
| 383 |
|
| 384 |
def install_pack(name, url, ver):
|
| 385 |
dest = os.path.join(CUSTOM_NODES_DIR, url.rstrip("/").split("/")[-1].removesuffix(".git"))
|
| 386 |
print(f"\n{C_CYAN}Installing {name} from {url} ...{C_RESET}")
|
| 387 |
if not os.path.isdir(dest):
|
| 388 |
+
try:
|
| 389 |
+
git_clone(url, dest)
|
| 390 |
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
| 391 |
+
if not github_owner_repo(url):
|
| 392 |
+
raise
|
| 393 |
+
print(f" {C_YELLOW}⚠ git clone failed ({e}){C_RESET}")
|
| 394 |
+
install_from_tarball(url, ver, dest)
|
| 395 |
+
if ver and re.fullmatch(r"[0-9a-f]{40}", str(ver), re.I) \
|
| 396 |
+
and os.path.isdir(os.path.join(dest, ".git")):
|
| 397 |
r = subprocess.run(["git", "-C", dest, "checkout", ver], env=GIT_ENV, timeout=120,
|
| 398 |
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 399 |
print(f" pinned to commit {ver[:12]}" if r.returncode == 0
|