pages-deployer: use archive endpoint instead of per-file blob fetching

Fixes binary file corruption (e.g. PNG images) caused by the blob
endpoint using String::from_utf8_lossy which replaces non-UTF8 bytes.
The archive endpoint returns a proper tar stream, preserving binary
content.
Anton Kaminsky23d ago50659959b996parent 27902ea
1 file changed+12-14
api/src/services/pages-deployer.ts
@@ -1,5 +1,5 @@
1import { execFileSync } from "child_process";
21import { mkdirSync, rmSync, existsSync, renameSync } from "fs";
2import { execFileSync } from "child_process";
33import type Database from "better-sqlite3";
44
55export class PagesDeployer {
@@ -20,15 +20,15 @@
2020 * Custom domain: sitesDir/{domain}/ (Caddy serves via {host} matching)
2121 * Default: sitesDir/_pages/{owner}/{repo}/ (Caddy serves via path-based routing on pages.grove.host)
2222 */
23 getDeployPath(repoName: string): { path: string; isCustomDomain: boolean } | null {
23 getDeployPath(ownerName: string, repoName: string): { path: string; isCustomDomain: boolean } | null {
2424 const repo = this.db
2525 .prepare(
2626 `SELECT r.pages_domain, r.pages_enabled, rwo.owner_name, r.name
2727 FROM repos r
2828 JOIN repos_with_owner rwo ON rwo.id = r.id
29 WHERE r.name = ? AND r.pages_enabled = 1`
29 WHERE rwo.owner_name = ? AND r.name = ? AND r.pages_enabled = 1`
3030 )
31 .get(repoName) as any;
31 .get(ownerName, repoName) as any;
3232
3333 if (!repo) return null;
3434
@@ -41,29 +41,27 @@
4141
4242 /**
4343 * Deploy a repo's pages content to disk.
44 * Archives the repo from bridge and extracts to the deploy path.
44 * Fetches a tar archive from the bridge and extracts to the deploy path.
4545 * Uses atomic rename so Caddy never sees a half-written site.
4646 */
47 async deploy(repoName: string, ref: string): Promise<void> {
48 const deployInfo = this.getDeployPath(repoName);
47 async deploy(ownerName: string, repoName: string, ref: string): Promise<void> {
48 const deployInfo = this.getDeployPath(ownerName, repoName);
4949 if (!deployInfo) return;
5050
5151 const { path: destDir } = deployInfo;
5252
53 this.logger.info({ repo: repoName, dest: destDir, ref }, "Deploying pages");
53 this.logger.info({ owner: ownerName, repo: repoName, dest: destDir, ref }, "Deploying pages");
5454
5555 const tmpDir = `${destDir}.tmp-${Date.now()}`;
5656
5757 try {
58 mkdirSync(tmpDir, { recursive: true });
59
5860 const url = `${this.bridgeUrl}/repos/${repoName}/archive/${ref}`;
5961 const res = await fetch(url);
6062 if (!res.ok) {
61 throw new Error(
62 `Archive fetch failed: ${res.status} ${res.statusText}`
63 );
63 throw new Error(`Archive fetch failed: ${res.status} ${res.statusText}`);
6464 }
65
66 mkdirSync(tmpDir, { recursive: true });
6765 const tarData = Buffer.from(await res.arrayBuffer());
6866 execFileSync("tar", ["xf", "-", "-C", tmpDir], { input: tarData });
6967
@@ -80,7 +78,7 @@
8078 }
8179
8280 this.logger.info(
83 { repo: repoName, dest: destDir },
81 { owner: ownerName, repo: repoName, dest: destDir },
8482 "Pages deployed successfully"
8583 );
8684 } catch (err) {
8785