diff --git a/.gitignore b/.gitignore index 2ac9fb0..6fc3840 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ .venv out +prod node_modules __pycache__/ package-lock.json -package.json - -cheatsheet_inventory.json \ No newline at end of file +package.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index faf1d83..70b3546 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,10 @@ -FROM python:3.11 AS build +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt -WORKDIR /workdir COPY . . -RUN pip install --no-cache-dir -r ./requirements.txt -RUN python3 src/build.py - -FROM caddy:latest AS serve - -COPY --from=build /workdir/out/ /www/ -COPY --from=build /workdir/Caddyfile /etc/caddy/Caddyfile - +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0b56b46..a3b9a15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,5 @@ typing-inspection==0.4.2 typing_extensions==4.15.0 urllib3==2.6.3 Werkzeug==3.1.5 +fastapi==0.115.0 +uvicorn[standard]==0.30.1 diff --git a/src/build.py b/src/build.py index f5d25ed..3b3757d 100644 --- a/src/build.py +++ b/src/build.py @@ -1,3 +1,4 @@ +import asyncio from jinja2 import Environment, FileSystemLoader, select_autoescape import shutil @@ -11,35 +12,46 @@ INVENTORY_FILE = "cheatsheet_inventory.json" STATIC_DIR = "static" TEMPLATES_DIR = "templates" OUTPUT_DIR = "out" +PROD_DIR = "prod" -inv_raw = load_cheatsheet_inventory(INVENTORY_FILE) +async def build(trigger_list: list[str] | None = None): + inv_raw = load_cheatsheet_inventory(INVENTORY_FILE) -# Clear output directory -shutil.rmtree(OUTPUT_DIR, ignore_errors=True) -shutil.copytree(STATIC_DIR, OUTPUT_DIR) + # Clear output directory + shutil.rmtree(OUTPUT_DIR, ignore_errors=True) + shutil.copytree(STATIC_DIR, OUTPUT_DIR) -inv: list[CSItem] = prepare_cheatsheets(inv_raw, OUTPUT_DIR) + inv: list[CSItem] = await prepare_cheatsheets(inv_raw, OUTPUT_DIR) + + if not os.path.exists(PROD_DIR): + os.mkdir(PROD_DIR) + + env = Environment( + loader=FileSystemLoader(TEMPLATES_DIR), + autoescape=select_autoescape() + ) + + index = env.get_template("index.html.j2") + + print(f"{len(inv)} Cheatsheets") + for i in inv: + print("-", i) + + thisYear = datetime.datetime.now().year + + with open(f"{OUTPUT_DIR}/index.html", "w", encoding="utf-8") as f: + f.write(index.render(items=inv, thisYear=thisYear)) + + with open(f"{OUTPUT_DIR}/impressum.html", "w", encoding="utf-8") as f: + f.write(env.get_template("impressum.html.j2").render(thisYear=thisYear)) + + with open(f"{OUTPUT_DIR}/license.html", "w", encoding="utf-8") as f: + f.write(env.get_template("license.html.j2").render(thisYear=thisYear)) + + # Copy to prod + print("Copying to prod directory...") + shutil.copytree(OUTPUT_DIR, PROD_DIR, dirs_exist_ok=True) -env = Environment( - loader=FileSystemLoader(TEMPLATES_DIR), - autoescape=select_autoescape() -) - -index = env.get_template("index.html.j2") - -print(f"{len(inv)} Cheatsheets") -for i in inv: - print("-", i) - -thisYear = datetime.datetime.now().year - -with open(f"{OUTPUT_DIR}/index.html", "w", encoding="utf-8") as f: - f.write(index.render(items=inv, thisYear=thisYear)) - -with open(f"{OUTPUT_DIR}/impressum.html", "w", encoding="utf-8") as f: - f.write(env.get_template("impressum.html.j2").render(thisYear=thisYear)) - -with open(f"{OUTPUT_DIR}/license.html", "w", encoding="utf-8") as f: - f.write(env.get_template("license.html.j2").render(thisYear=thisYear)) - +if __name__ == "__main__": + asyncio.run(build()) \ No newline at end of file diff --git a/src/inventory.py b/src/inventory.py index 6b69040..f1b9364 100644 --- a/src/inventory.py +++ b/src/inventory.py @@ -20,7 +20,7 @@ def load_cheatsheet_inventory(file: str) -> CSInventoryConfig: return res -def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: +async def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: res: list[CSItem] = [] for item in config.items: @@ -29,10 +29,10 @@ def prepare_cheatsheets(config: CSInventoryConfig, outdir: str) -> list[CSItem]: try: match item.source.type: case CheatsheetSourceType.GITEA_SOURCE: - new_items += process_gitea(item, outdir) + new_items += await process_gitea(item, outdir) case CheatsheetSourceType.PLAIN_URL: - new_items.append(process_plain_url(item, outdir)) + new_items.append(await process_plain_url(item, outdir)) case _: print("Unknow Source Type:", item.source.type) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..0f075a7 --- /dev/null +++ b/src/main.py @@ -0,0 +1,59 @@ +from fastapi import FastAPI, HTTPException, Depends +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from pydantic import BaseModel +import queue +import asyncio +from contextlib import asynccontextmanager +import os + +from build import build as run_build + + +class TriggerRequest(BaseModel): + items: list[str] + + +build_queue: asyncio.Queue = None + +async def worker(): + print("Build queue thread started") + while True: + selected = await build_queue.get() + print("Processing build request for:", selected) + await run_build(trigger_list=selected) + +@asynccontextmanager +async def lifespan(app: FastAPI): + global build_queue + build_queue = asyncio.Queue() + task = asyncio.create_task(worker()) + + try: + yield + finally: + task.cancel() + + try: + await task + except asyncio.CancelledError: + pass + +app = FastAPI(title="FSSquared Trigger API", lifespan=lifespan) + + +@app.post("/trigger") +async def trigger(payload: TriggerRequest): + build_queue.put(payload.items) + return {"status": "ok", "requested": payload.items} + + +@app.post("/trigger/all") +async def trigger_all(): + await build_queue.put(None) + return {"status": "ok", "requested": "all"} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000) + diff --git a/src/sources/gitea.py b/src/sources/gitea.py index f6c010f..15df9c3 100644 --- a/src/sources/gitea.py +++ b/src/sources/gitea.py @@ -5,7 +5,7 @@ import requests from pathlib import Path -def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: +async def process_gitea(item: CSInventoryItem, outdir: str) -> list[CSItem] | None: source: CSSourceGitea = item.source commit_hash = get_release_commit_sha(source.base_url, source.owner, source.repo, source.tag) asserts = list_release_assets(source.base_url, source.owner, source.repo, source.tag) diff --git a/src/sources/plain.py b/src/sources/plain.py index f50d1ba..0da025e 100644 --- a/src/sources/plain.py +++ b/src/sources/plain.py @@ -1,7 +1,7 @@ from sources import CSInventoryItem, CSSourcePlainURL, CSItem from sources.util import cache_cheatsheet, get_datestring -def process_plain_url(item: CSInventoryItem, outdir: str) -> CSItem | None: +async def process_plain_url(item: CSInventoryItem, outdir: str) -> CSItem | None: source: CSSourcePlainURL = item.source res_url = source.url