commit ee4ad81d092145610d88acf0693482704f9e824d
parent a8d91c07d1d4baa1bcb92defcbb6c995542c5892
Author: gracefu <81774659+gracefuu@users.noreply.github.com>
Date: Tue, 22 Apr 2025 22:22:14 +0800
Move git part of blog site to new site
Diffstat:
6 files changed, 373 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+__pycache__/
+git/
+public/
diff --git a/.gitmodules b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "stagit"]
+ path = stagit
+ url = git://git.codemadness.org/stagit
diff --git a/404.html b/404.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+404 not found
diff --git a/make.py b/make.py
@@ -0,0 +1,223 @@
+from make_utils import *
+import hashlib
+import json
+import os
+
+
+async def main():
+ STORAGENAME = "git-grace-moe"
+ STORAGEPASSWORD = (
+ await shell("secret-tool lookup owner git-grace-moe.b-cdn.net")
+ ).utf8stdout
+
+ PULLZONEID = "3659642"
+ APIPASSWORD = (await shell("secret-tool lookup owner grace@bunny.net")).utf8stdout
+ LOCALPATH = "public"
+
+ STORAGEURL = f"https://sg.storage.bunnycdn.com/{STORAGENAME}"
+ STORAGECMD = f"curl -H 'AccessKey: {STORAGEPASSWORD}' -s"
+ PURGEURL = f"https://api.bunny.net/pullzone/{PULLZONEID}/purgeCache"
+ APICMD = f"curl -H 'AccessKey: {APIPASSWORD}' -s"
+
+ @once()
+ def build_git_repo(
+ stagit_path: str,
+ source_git_path: str,
+ output_git_path: str,
+ output_url: str,
+ description: str,
+ ):
+ return shell(
+ f"""
+ set -eou pipefail
+
+ SRC_PATH="$(realpath {source_git_path})"
+
+ [ -d {output_git_path} ] || git init --bare {output_git_path}
+ GIT_PATH="$(realpath {output_git_path})"
+
+ mkdir -p public/{output_url}
+ PUBLIC_PATH="$(realpath public/{output_url})"
+
+ STAGIT_PATH="$(realpath {stagit_path})"
+
+ rm -rf "$GIT_PATH"/hooks
+
+ git -C "$SRC_PATH" push --no-recurse-submodules --mirror --force "$GIT_PATH"
+ # HEAD is not updated by --mirror, because HEAD is not a ref.
+ # Update it by hand
+ cp "$(git -C "$SRC_PATH" rev-parse --path-format=absolute --git-path HEAD)" "$GIT_PATH"/HEAD
+
+ git -C "$GIT_PATH" gc --no-detach --aggressive
+ git -C "$GIT_PATH" update-server-info
+
+ echo '{description}' > "$GIT_PATH"/description
+ echo 'https://git.grace.moe/{output_url}' > "$GIT_PATH"/url
+
+ cp -a "$GIT_PATH" -T "$PUBLIC_PATH"
+
+ echo + stagit {output_url}
+ ( cd "$PUBLIC_PATH" &&
+ "$STAGIT_PATH" \
+ -u https://blog.grace.moe/{output_url} \
+ "$GIT_PATH" &&
+ echo '<meta http-equiv="refresh" content="0; url=log.html" />' > index.html
+ )
+ echo - stagit {output_url}
+ """,
+ echo=EchoAll,
+ )
+
+ @once()
+ async def rebuild():
+ await shell(
+ """
+ rm -rf public
+ """
+ )
+ await asyncio.gather(
+ shell(
+ """
+ mkdir -p public
+ cp 404.html public
+ """
+ ),
+ build_git_repo(
+ "stagit/stagit",
+ "~/Documents/web/blog.grace.moe",
+ "git/blog.grace.moe",
+ "blog.grace.moe",
+ "Source for the blog blog.grace.moe",
+ ),
+ build_git_repo(
+ "stagit/stagit",
+ ".",
+ "git/git.grace.moe",
+ "git.grace.moe",
+ "Source for the git site git.grace.moe",
+ ),
+ build_git_repo(
+ "stagit/stagit",
+ "~/Documents/src/pymake",
+ "git/pymake",
+ "pymake",
+ "A build system based on Build Systems à la Carte",
+ ),
+ build_git_repo(
+ "stagit/stagit",
+ ".git/modules/stagit",
+ "git/stagit",
+ "stagit",
+ "My personal fork of stagit https://codemadness.org/stagit.html",
+ ),
+ )
+ await shell(
+ "cd public && ../stagit/stagit-index blog.grace.moe git.grace.moe pymake stagit > index.html",
+ echo=EchoAll,
+ )
+
+ @once()
+ @in_executor()
+ def hash_file(path):
+ with open(path, "rb") as f:
+ # print("+ hashing", path)
+ h = hashlib.sha256()
+ for chunk in f:
+ h.update(chunk)
+ # print("- hashing", path)
+ return h.hexdigest().upper()
+
+ bunny_sem = asyncio.Semaphore(80)
+
+ @once()
+ async def contents(path: str):
+ async with bunny_sem:
+ print("+++ download", path)
+ path_json = await shell(f"{STORAGECMD} '{STORAGEURL}/{path}/'")
+ print("--- download", path)
+ return json.loads(path_json.utf8stdout)
+
+ @once()
+ async def cleanfile(path: str):
+ if not os.path.isfile(f"{LOCALPATH}/{path}"):
+ async with bunny_sem:
+ print("+++ cleanfile", path)
+ await shell(f"{STORAGECMD} -XDELETE '{STORAGEURL}/{path}'")
+ print("--- cleanfile", path)
+
+ @once()
+ async def cleandir(path: str):
+ if not os.path.isdir(f"{LOCALPATH}/{path}"):
+ async with bunny_sem:
+ print("+++ cleandir", path)
+ await shell(f"{STORAGECMD} -XDELETE '{STORAGEURL}/{path}/'")
+ print("--- cleandir", path)
+
+ @once()
+ async def clean(path: str):
+ path_contents = await contents(path)
+ await asyncio.gather(
+ *(
+ (
+ (cleandir(path + "/" + ent["ObjectName"]))
+ if ent["IsDirectory"]
+ else (cleanfile(path + "/" + ent["ObjectName"]))
+ )
+ for ent in path_contents
+ if isinstance(ent, dict)
+ )
+ )
+ # print("- clean", path)
+
+ @once()
+ async def upload(path: str):
+ path_contents = await contents(path[: path.rfind("/")])
+
+ bunny_checksum = None
+ if isinstance(path_contents, list):
+ try:
+ bunny_checksum = next(
+ (
+ ent["Checksum"]
+ for ent in path_contents
+ if ent["ObjectName"] == path[path.rfind("/") + 1 :]
+ )
+ )
+ except StopIteration:
+ pass
+
+ our_checksum = await hash_file(f"{LOCALPATH}/{path}")
+
+ if bunny_checksum != our_checksum:
+ async with bunny_sem:
+ print("+++ uploading", path)
+ await shell(
+ f"{STORAGECMD} -T'{LOCALPATH}/{path}' '{STORAGEURL}/{path}'"
+ )
+ print("--- uploading", path)
+ # print("- upload", path)
+
+ @once()
+ async def purge():
+ async with bunny_sem:
+ print("+++ purge")
+ await shell(f"{APICMD} -XPOST '{PURGEURL}'")
+ print("--- purge")
+
+ @once()
+ async def all():
+ await rebuild()
+ UPLOAD = (await shell(f"cd '{LOCALPATH}' && find . -type f")).utf8stdout
+ CLEAN = (await shell(f"cd '{LOCALPATH}' && find . -type d")).utf8stdout
+ await asyncio.gather(
+ *((upload(path)) for path in UPLOAD.strip().split("\n")),
+ *((clean(path)) for path in CLEAN.strip().split("\n")),
+ )
+ await purge()
+
+ _ = all
+ return await make_main(locals())
+
+
+if __name__ == "__main__":
+ exit(asyncio.run(main()))
diff --git a/make_utils.py b/make_utils.py
@@ -0,0 +1,141 @@
+from concurrent.futures import Executor
+from typing import Any, Awaitable, Callable, ParamSpec, TypeVar
+import asyncio
+import collections
+import functools
+import inspect
+import subprocess
+import sys
+import traceback
+
+
+def once():
+ def decorator(f):
+ futs: dict[tuple[Any, ...], asyncio.Future] = {}
+ sig = inspect.signature(f)
+
+ @functools.wraps(f)
+ async def wrapped(*args, **kwargs):
+ bound_args = sig.bind(*args, **kwargs)
+ bound_args.apply_defaults()
+ key = tuple(bound_args.arguments.values())
+ if key in futs:
+ return await futs[key]
+ futs[key] = asyncio.Future()
+ try:
+ res = await f(*args, **kwargs)
+ futs[key].set_result(res)
+ return res
+ except BaseException as e:
+ traceback.print_exc()
+ futs[key].set_exception(e)
+ raise
+
+ return wrapped
+
+ return decorator
+
+
+def in_executor(executor: Executor | None = None):
+ Args = ParamSpec("Args")
+ T = TypeVar("T")
+
+ def decorator(f: Callable[Args, T]) -> Callable[Args, Awaitable[T]]:
+ @functools.wraps(f)
+ def wrapped(*args, **kwargs):
+ if kwargs:
+ return asyncio.get_event_loop().run_in_executor(
+ executor, functools.partial(f, **kwargs), *args
+ )
+ else:
+ return asyncio.get_event_loop().run_in_executor(executor, f, *args)
+
+ return wrapped
+
+ return decorator
+
+
+class ShellResult(collections.namedtuple("ShellResult", "stdout stderr returncode")):
+ __slots__ = ()
+
+ @property
+ def utf8stdout(self):
+ return self.stdout.decode("utf-8")
+
+ @property
+ def utf8stderr(self):
+ return self.stderr.decode("utf-8")
+
+
+EchoNothing = 0
+EchoStdout = 1
+EchoStderr = 2
+EchoAll = 3
+
+
+async def _exec_reader(istream, ostream, echo: Any = False):
+ contents = b""
+ async for chunk in istream:
+ contents += chunk
+ if echo:
+ ostream.write(chunk)
+ ostream.flush()
+ return contents
+
+
+async def exec(
+ program,
+ *args,
+ input: bytes | bytearray | memoryview | None = None,
+ echo: int = EchoNothing,
+) -> ShellResult:
+
+ proc = await asyncio.create_subprocess_exec(
+ program,
+ *args,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+
+ if input is not None:
+ proc.stdin.write(input) # type: ignore
+ _, stdout, stderr, returncode = await asyncio.gather(
+ proc.stdin.drain(), # type: ignore
+ _exec_reader(proc.stdout, sys.stdout.buffer, echo=echo & EchoStdout),
+ _exec_reader(proc.stderr, sys.stderr.buffer, echo=echo & EchoStderr),
+ proc.wait(),
+ )
+ else:
+ stdout, stderr, returncode = await asyncio.gather(
+ _exec_reader(proc.stdout, sys.stdout.buffer, echo=echo & EchoStdout),
+ _exec_reader(proc.stderr, sys.stderr.buffer, echo=echo & EchoStderr),
+ proc.wait(),
+ )
+
+ return ShellResult(stdout, stderr, returncode)
+
+
+async def shell(
+ cmd,
+ input: bytes | bytearray | memoryview | None = None,
+ echo: int = EchoNothing,
+) -> ShellResult:
+ proc = await asyncio.create_subprocess_shell(
+ cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+ stdout, stderr = await proc.communicate(input)
+ if echo & EchoStdout:
+ sys.stdout.buffer.write(stdout)
+ sys.stdout.buffer.flush()
+ if echo & EchoStderr:
+ sys.stderr.buffer.write(stderr)
+ sys.stderr.buffer.flush()
+ return ShellResult(stdout, stderr, proc.returncode)
+
+
+async def make_main(globals, default_target="all()"):
+ targets = sys.argv[1:]
+ if not targets:
+ targets.append(default_target)
+ await asyncio.gather(*(eval(target, globals=globals) for target in targets))
diff --git a/stagit b/stagit
@@ -0,0 +1 @@
+Subproject commit 96cbcdf6e6f2e5e572c8b01a6990151462cb4b00