chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
/**
* Getting a working tree for a run. The PDS serves these repositories over
* read-only smart HTTP, so this is a plain clone with a stock git binary and
* needs neither the remote helper nor credentials.
*/
import { spawn } from 'node:child_process';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
/**
* @param {string[]} args
* @param {(chunk: string) => void} onLog
* @param {Record<string, string>} [env] - added to the environment
* @returns {Promise<void>}
*/
function git(args, onLog, env) {
return new Promise((resolve, reject) => {
const child = spawn('git', args, {
stdio: ['ignore', 'pipe', 'pipe'],
env: env ? { ...process.env, ...env } : process.env,
});
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', onLog);
child.stderr.on('data', (chunk) => {
stderr += chunk;
onLog(chunk);
});
child.on('error', reject);
child.on('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`git ${args[0]} exited ${code}: ${stderr.trim()}`));
});
});
}
/**
* The clone URL the PDS serves this repository at.
* @param {string} service - PDS base URL
* @param {string} did
* @param {string} repoName
* @returns {string}
*/
export function cloneUrl(service, did, repoName) {
return `${service.replace(/\/+$/, '')}/git/${did}/${encodeURIComponent(repoName)}`;
}
/**
* The remote helper's URL for the same repository.
* @param {string} did
* @param {string} repoName
* @returns {string}
*/
export function helperUrl(did, repoName) {
return `atproto://${did}/${encodeURIComponent(repoName)}`;
}
/**
* Clone the repository and detach at one commit. The caller owns the
* directory and passes it to `discardCheckout` when the run finishes.
*
* Two ways in. Smart HTTP needs no helper on the PATH, so it goes first. It
* serves a repository by merging the bundle chain's packfiles, which git
* rejects when a chain repeats an object, so a chain long enough to repeat
* one falls back to the helper. The helper unbundles each bundle on its own
* and does not care.
* @param {Object} options
* @param {string} options.service
* @param {string} options.did
* @param {string} options.repoName
* @param {string} options.sha
* @param {(chunk: string) => void} options.onLog
* @returns {Promise<string>} the working tree path
*/
export async function checkout(options) {
const { service, did, repoName, sha, onLog } = options;
const dir = await mkdtemp(join(tmpdir(), 'pdsjs-git-ci-'));
try {
try {
await git(
['clone', '--quiet', cloneUrl(service, did, repoName), dir],
onLog,
);
} catch (httpErr) {
onLog(
`\n[smart HTTP clone failed: ${httpErr instanceof Error ? httpErr.message : httpErr}]\n[retrying with git-remote-atproto]\n`,
);
await rm(dir, { recursive: true, force: true });
await git(['clone', '--quiet', helperUrl(did, repoName), dir], onLog, {
// The helper resolves the authority through PLC without this.
ATPROTO_GIT_SERVICE: service,
});
}
await git(['-C', dir, 'checkout', '--quiet', '--detach', sha], onLog);
return dir;
} catch (err) {
await discardCheckout(dir);
throw err;
}
}
/**
* @param {string} dir
* @returns {Promise<void>}
*/
export async function discardCheckout(dir) {
await rm(dir, { recursive: true, force: true });
}