git.grace.moe

Source for the git site git.grace.moe
git clone https://git.grace.moe/git.grace.moe
Log | Files | Refs | Submodules

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()))