api/src/services/pages-deployer.tsblame
View source
e5b523e1import { execFileSync } from "child_process";
e5b523e2import { mkdirSync, rmSync, existsSync, renameSync } from "fs";
e5b523e3import type Database from "better-sqlite3";
e5b523e4
e5b523e5export class PagesDeployer {
e5b523e6 constructor(
e5b523e7 private db: Database.Database,
e5b523e8 private bridgeUrl: string,
e5b523e9 private sitesDir: string,
e5b523e10 private logger: {
e5b523e11 info: (...args: any[]) => void;
e5b523e12 error: (...args: any[]) => void;
e5b523e13 }
e5b523e14 ) {
e5b523e15 mkdirSync(sitesDir, { recursive: true });
e5b523e16 }
e5b523e17
e5b523e18 /**
ff50d0319 * Get the deploy path for a repo's pages.
ff50d0320 * Custom domain: sitesDir/{domain}/ (Caddy serves via {host} matching)
ff50d0321 * Default: sitesDir/_pages/{owner}/{repo}/ (Caddy serves via path-based routing on pages.grove.host)
e5b523e22 */
ff50d0323 getDeployPath(repoName: string): { path: string; isCustomDomain: boolean } | null {
e5b523e24 const repo = this.db
e5b523e25 .prepare(
ff50d0326 `SELECT r.pages_domain, r.pages_enabled, rwo.owner_name, r.name
ff50d0327 FROM repos r
ff50d0328 JOIN repos_with_owner rwo ON rwo.id = r.id
ff50d0329 WHERE r.name = ? AND r.pages_enabled = 1`
e5b523e30 )
e5b523e31 .get(repoName) as any;
e5b523e32
ff50d0333 if (!repo) return null;
e5b523e34
ff50d0335 if (repo.pages_domain) {
ff50d0336 return { path: `${this.sitesDir}/${repo.pages_domain}`, isCustomDomain: true };
e5b523e37 }
e5b523e38
ff50d0339 return { path: `${this.sitesDir}/_pages/${repo.owner_name}/${repo.name}`, isCustomDomain: false };
ff50d0340 }
ff50d0341
ff50d0342 /**
ff50d0343 * Deploy a repo's pages content to disk.
ff50d0344 * Archives the repo from bridge and extracts to the deploy path.
ff50d0345 * Uses atomic rename so Caddy never sees a half-written site.
ff50d0346 */
ff50d0347 async deploy(repoName: string, ref: string): Promise<void> {
ff50d0348 const deployInfo = this.getDeployPath(repoName);
ff50d0349 if (!deployInfo) return;
ff50d0350
ff50d0351 const { path: destDir } = deployInfo;
ff50d0352
ff50d0353 this.logger.info({ repo: repoName, dest: destDir, ref }, "Deploying pages");
e5b523e54
ff50d0355 const tmpDir = `${destDir}.tmp-${Date.now()}`;
e5b523e56
e5b523e57 try {
e5b523e58 const url = `${this.bridgeUrl}/repos/${repoName}/archive/${ref}`;
e5b523e59 const res = await fetch(url);
e5b523e60 if (!res.ok) {
e5b523e61 throw new Error(
e5b523e62 `Archive fetch failed: ${res.status} ${res.statusText}`
e5b523e63 );
e5b523e64 }
e5b523e65
e5b523e66 mkdirSync(tmpDir, { recursive: true });
e5b523e67 const tarData = Buffer.from(await res.arrayBuffer());
e5b523e68 execFileSync("tar", ["xf", "-", "-C", tmpDir], { input: tarData });
e5b523e69
e5b523e70 // Atomic swap
e5b523e71 const oldDir = `${destDir}.old`;
e5b523e72 if (existsSync(destDir)) {
e5b523e73 renameSync(destDir, oldDir);
e5b523e74 }
ff50d0375 // Ensure parent directory exists for nested paths
ff50d0376 mkdirSync(destDir.substring(0, destDir.lastIndexOf("/")), { recursive: true });
e5b523e77 renameSync(tmpDir, destDir);
e5b523e78 if (existsSync(oldDir)) {
e5b523e79 rmSync(oldDir, { recursive: true, force: true });
e5b523e80 }
e5b523e81
e5b523e82 this.logger.info(
ff50d0383 { repo: repoName, dest: destDir },
e5b523e84 "Pages deployed successfully"
e5b523e85 );
e5b523e86 } catch (err) {
e5b523e87 if (existsSync(tmpDir)) {
e5b523e88 rmSync(tmpDir, { recursive: true, force: true });
e5b523e89 }
e5b523e90 throw err;
e5b523e91 }
e5b523e92 }
e5b523e93
ff50d0394 /** Remove a site's deployed files by deploy path. */
ff50d0395 undeploy(deployPath: string): void {
ff50d0396 if (existsSync(deployPath)) {
ff50d0397 rmSync(deployPath, { recursive: true, force: true });
ff50d0398 this.logger.info({ path: deployPath }, "Pages undeployed");
e5b523e99 }
e5b523e100 }
e5b523e101
ff50d03102 /** Check if a custom domain is configured for any repo (used by Caddy's on-demand TLS). */
e5b523e103 isDomainConfigured(domain: string): boolean {
e5b523e104 const row = this.db
e5b523e105 .prepare(
e5b523e106 `SELECT id FROM repos WHERE pages_domain = ? AND pages_enabled = 1`
e5b523e107 )
e5b523e108 .get(domain);
e5b523e109 return !!row;
e5b523e110 }
e5b523e111}