make.py (9887B)
1 #!/usr/bin/env -S uv run 2 from make3 import EchoAll, file_hash, make_main, once, shell 3 import asyncio 4 import http.client 5 import json 6 import os 7 8 9 async def main(): 10 STORAGENAME = "git-grace-moe" 11 STORAGEPASSWORD = ( 12 await shell("secret-tool lookup owner git-grace-moe.b-cdn.net") 13 ).utf8stdout 14 15 PULLZONEID = "3659642" 16 APIPASSWORD = (await shell("secret-tool lookup owner grace@bunny.net")).utf8stdout 17 LOCALPATH = "public" 18 19 storage_conn_ = [ 20 http.client.HTTPSConnection("sg.storage.bunnycdn.com") for _ in range(32) 21 ] 22 api_conn = http.client.HTTPSConnection("api.bunny.net") 23 conn_ready = asyncio.gather( 24 *( 25 asyncio.get_event_loop().run_in_executor(None, c.connect) 26 for c in storage_conn_ 27 ), 28 asyncio.get_event_loop().run_in_executor(None, api_conn.connect), 29 ) 30 31 storage_conn = asyncio.Queue() 32 for c in storage_conn_: 33 storage_conn.put_nowait(c) 34 35 @once() 36 def build_git_repo( 37 stagit_path: str, 38 source_git_path: str, 39 output_git_path: str, 40 output_url: str, 41 description: str, 42 ): 43 return shell( 44 f""" 45 set -eou pipefail 46 47 SRC_PATH="$(realpath {source_git_path})" 48 49 [ -d {output_git_path} ] || git init --bare {output_git_path} 50 GIT_PATH="$(realpath {output_git_path})" 51 52 mkdir -p public/{output_url} 53 PUBLIC_PATH="$(realpath public/{output_url})" 54 55 STAGIT_PATH="$(realpath {stagit_path})" 56 STYLE_PATH="$(realpath style.css)" 57 58 rm -rf "$GIT_PATH"/hooks 59 60 git -C "$SRC_PATH" push --no-recurse-submodules --mirror --force "$GIT_PATH" 61 # HEAD is not updated by --mirror, because HEAD is not a ref. 62 # Update it by hand 63 cp "$(git -C "$SRC_PATH" rev-parse --path-format=absolute --git-path HEAD)" "$GIT_PATH"/HEAD 64 65 git -C "$GIT_PATH" gc --no-detach --aggressive 66 git -C "$GIT_PATH" update-server-info 67 68 echo '{description}' > "$GIT_PATH"/description 69 echo 'https://git.grace.moe/{output_url}' > "$GIT_PATH"/url 70 71 cp -a "$GIT_PATH" -T "$PUBLIC_PATH" 72 73 echo + stagit {output_url} 74 ( cd "$PUBLIC_PATH" && 75 "$STAGIT_PATH" \ 76 -s "$STYLE_PATH" \ 77 -u https://blog.grace.moe/{output_url} \ 78 "$GIT_PATH" && 79 echo '<meta http-equiv="refresh" content="0; url=log.html" />' > index.html 80 ) 81 echo - stagit {output_url} 82 """, 83 echo=EchoAll, 84 ) 85 86 @once() 87 async def rebuild(): 88 await shell( 89 """ 90 rm -rf public 91 mkdir -p public 92 ( cd stagit && make ) 93 """, 94 echo=EchoAll, 95 ) 96 await asyncio.gather( 97 shell( 98 """ 99 cp 404.html public 100 cp -a icons public 101 """ 102 ), 103 build_git_repo( 104 "stagit/stagit", 105 "~/Documents/web/blog.grace.moe", 106 "git/blog.grace.moe", 107 "blog.grace.moe", 108 "Source for the blog blog.grace.moe", 109 ), 110 build_git_repo( 111 "stagit/stagit", 112 ".", 113 "git/git.grace.moe", 114 "git.grace.moe", 115 "Source for the git site git.grace.moe", 116 ), 117 build_git_repo( 118 "stagit/stagit", 119 "~/Documents/src/pymake", 120 "git/pymake", 121 "pymake", 122 "A build system based on Build Systems à la Carte", 123 ), 124 build_git_repo( 125 "stagit/stagit", 126 "~/Documents/src/mymarkdown", 127 "git/mymarkdown", 128 "mymarkdown", 129 "My markdown", 130 ), 131 build_git_repo( 132 "stagit/stagit", 133 ".git/modules/stagit", 134 "git/stagit", 135 "stagit", 136 "My personal fork of stagit https://codemadness.org/stagit.html", 137 ), 138 ) 139 await shell( 140 "cd public && ../stagit/stagit-index -s ../index-style.css blog.grace.moe git.grace.moe pymake mymarkdown stagit > index.html", 141 echo=EchoAll, 142 ) 143 144 @once() 145 async def contents(path: str): 146 c: http.client.HTTPSConnection = await storage_conn.get() 147 print("+++ download", path) 148 149 c.request( 150 "GET", 151 f"/{STORAGENAME}/{path}/", 152 headers={"AccessKey": STORAGEPASSWORD}, 153 ) 154 loop = asyncio.get_event_loop() 155 f = loop.create_future() 156 loop.add_reader(c.sock.fileno(), f.set_result, None) 157 await f 158 loop.remove_reader(c.sock.fileno()) 159 resp = c.getresponse() 160 resp_body = resp.read() 161 if resp.status != 200: 162 print("!!! download", resp.status, resp.reason, resp_body) 163 path_json = resp_body.decode("utf-8") 164 165 storage_conn.put_nowait(c) 166 print("--- download", path) 167 return json.loads(path_json) 168 169 @once() 170 async def cleanfile(path: str): 171 if not os.path.isfile(f"{LOCALPATH}/{path}"): 172 c = await storage_conn.get() 173 print("+++ cleanfile", path) 174 175 c.request( 176 "DELETE", 177 f"/{STORAGENAME}/{path}", 178 headers={"AccessKey": STORAGEPASSWORD}, 179 ) 180 loop = asyncio.get_event_loop() 181 f = loop.create_future() 182 loop.add_reader(c.sock.fileno(), f.set_result, None) 183 await f 184 loop.remove_reader(c.sock.fileno()) 185 resp = c.getresponse() 186 resp_body = resp.read() 187 if resp.status != 200: 188 print("!!! cleanfile", resp.status, resp.reason, resp_body) 189 190 storage_conn.put_nowait(c) 191 print("--- cleanfile", path) 192 193 @once() 194 async def cleandir(path: str): 195 if not os.path.isdir(f"{LOCALPATH}/{path}"): 196 c = await storage_conn.get() 197 print("+++ cleandir", path) 198 199 c.request( 200 "DELETE", 201 f"/{STORAGENAME}/{path}/", 202 headers={"AccessKey": STORAGEPASSWORD}, 203 ) 204 loop = asyncio.get_event_loop() 205 f = loop.create_future() 206 loop.add_reader(c.sock.fileno(), f.set_result, None) 207 await f 208 loop.remove_reader(c.sock.fileno()) 209 resp = c.getresponse() 210 resp_body = resp.read() 211 if resp.status != 200: 212 print("!!! cleandir", resp.status, resp.reason, resp_body) 213 214 storage_conn.put_nowait(c) 215 print("--- cleandir", path) 216 217 @once() 218 async def clean(path: str): 219 path_contents = await contents(path) 220 await asyncio.gather( 221 *( 222 ( 223 (cleandir(path + "/" + ent["ObjectName"])) 224 if ent["IsDirectory"] 225 else (cleanfile(path + "/" + ent["ObjectName"])) 226 ) 227 for ent in path_contents 228 if isinstance(ent, dict) 229 ) 230 ) 231 # print("- clean", path) 232 233 @once() 234 async def upload(path: str): 235 path_contents = await contents(path[: path.rfind("/")]) 236 237 bunny_checksum = None 238 if isinstance(path_contents, list): 239 try: 240 bunny_checksum = next( 241 ( 242 ent["Checksum"] 243 for ent in path_contents 244 if ent["ObjectName"] == path[path.rfind("/") + 1 :] 245 ) 246 ) 247 except StopIteration: 248 pass 249 250 our_checksum = (await file_hash(f"{LOCALPATH}/{path}")).upper() 251 252 if bunny_checksum != our_checksum: 253 c = await storage_conn.get() 254 print("+++ uploading", path) 255 256 with open(f"{LOCALPATH}/{path}", "rb") as f: 257 c.request( 258 "PUT", 259 f"/{STORAGENAME}/{path}", 260 body=f, 261 headers={"AccessKey": STORAGEPASSWORD}, 262 ) 263 loop = asyncio.get_event_loop() 264 f = loop.create_future() 265 loop.add_reader(c.sock.fileno(), f.set_result, None) 266 await f 267 loop.remove_reader(c.sock.fileno()) 268 resp = c.getresponse() 269 resp_body = resp.read() 270 if resp.status != 201: 271 print("!!! uploading", resp.status, resp.reason, resp_body) 272 273 storage_conn.put_nowait(c) 274 print("--- uploading", path) 275 # print("- upload", path) 276 277 @once() 278 async def purge(): 279 print("+++ purge") 280 281 api_conn.request( 282 "POST", 283 f"/pullzone/{PULLZONEID}/purgeCache", 284 headers={"AccessKey": APIPASSWORD}, 285 ) 286 resp = api_conn.getresponse() 287 resp_body = resp.read() 288 if resp.status != 204: 289 print("!!! purge", resp.status, resp.reason, resp_body) 290 291 print("--- purge") 292 293 @once() 294 async def all(): 295 await rebuild() 296 UPLOAD = (await shell(f"cd '{LOCALPATH}' && find . -type f")).utf8stdout 297 CLEAN = (await shell(f"cd '{LOCALPATH}' && find . -type d")).utf8stdout 298 await conn_ready 299 await asyncio.gather( 300 *((upload(path)) for path in UPLOAD.strip().split("\n") if path), 301 *((clean(path)) for path in CLEAN.strip().split("\n") if path), 302 ) 303 await purge() 304 305 _ = all 306 return await make_main(locals()) 307 308 309 if __name__ == "__main__": 310 exit(asyncio.run(main()))