#!/usr/bin/env python3
import os
import subprocess
import sys
from pathlib import Path

UPLOAD_URL = "https://st.ht/api/upload"
SITE_DIR = Path("_site")
ARCHIVE_PATH = Path(".stht-upload/site.tar.gz")
MAX_ARCHIVE_BYTES = 32 * 1024 * 1024


def fail(message: str):
    print(message, file=sys.stderr)
    sys.exit(1)


def run(*args: str):
    print("$", " ".join(args))
    result = subprocess.run(args)
    if result.returncode != 0:
        fail(f"Command failed with exit code {result.returncode}: {args[0]}")


def fmt_mib(n: int) -> str:
    return f"{n / 1024 / 1024:.2f} MiB"


token = os.environ.get("STHT_TOKEN")
if not token:
    fail("Missing STHT_TOKEN. Export it before running.")

run("bun", "run", "build")

if not (SITE_DIR / "index.html").exists():
    fail(f"Missing {SITE_DIR}/index.html. st.ht requires index.html at the archive root.")

ARCHIVE_PATH.parent.mkdir(parents=True, exist_ok=True)
ARCHIVE_PATH.unlink(missing_ok=True)
run("tar", "-czf", str(ARCHIVE_PATH), "-C", str(SITE_DIR), ".")

size = ARCHIVE_PATH.stat().st_size
if size > MAX_ARCHIVE_BYTES:
    fail(f"Archive is too large: {fmt_mib(size)}. Maximum is {fmt_mib(MAX_ARCHIVE_BYTES)}.")

print(f"Uploading {ARCHIVE_PATH} ({fmt_mib(size)}) to st.ht...")

import urllib.request

with open(ARCHIVE_PATH, "rb") as f:
    data = f.read()

boundary = "----PythonFormBoundary"
body = (
    f"--{boundary}\r\n"
    f'Content-Disposition: form-data; name="file"; filename="site.tar.gz"\r\n'
    f"Content-Type: application/gzip\r\n\r\n"
).encode() + data + f"\r\n--{boundary}--\r\n".encode()

req = urllib.request.Request(
    UPLOAD_URL,
    data=body,
    headers={
        "Authorization": f"Bearer {token}",
        "Content-Type": f"multipart/form-data; boundary={boundary}",
    },
    method="POST",
)

try:
    with urllib.request.urlopen(req) as resp:
        print(resp.read().decode() or "Upload complete.")
except urllib.error.HTTPError as e:
    fail(f"Upload failed: HTTP {e.code} {e.reason}\n{e.read().decode()}")

ARCHIVE_PATH.unlink(missing_ok=True)
Edit

Pub: 05 May 2026 05:10 UTC

Views: 7