SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
2.9 KB · 72 lines javascript
Raw Blame History
1/**2 * ─────────────────────────────────────────────3 *  SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 *  Author  : Simon-Pierre Boucher6 *  Contact : contact@spboucher.ai7 *  File    : src/git/archive.mjs8 *  Purpose : zip / tar.gz snapshot generation with per-sha caching9 *  License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { spawn } from 'node:child_process';14import { createWriteStream, createReadStream, existsSync, renameSync, statSync, rmSync } from 'node:fs';15import { mkdirSync } from 'node:fs';16import { dirname } from 'node:path';17import { randomBytes } from 'node:crypto';1819export const ARCHIVE_FORMATS = Object.freeze({20  zip: { ext: 'zip', mime: 'application/zip', gitFormat: 'zip' },21  'tar.gz': { ext: 'tar.gz', mime: 'application/gzip', gitFormat: 'tar.gz' },22});2324/**25 * Produce (or reuse) a snapshot archive for `<repo>@<sha>` and stream it.26 * @param {{repos: object, cache: object}} ctx27 * @param {string} repo28 * @param {string} sha resolved commit sha29 * @param {'zip'|'tar.gz'} format30 * @param {import('fastify').FastifyReply} reply31 * @param {string} downloadName filename presented to the client (no extension)32 */33export async function sendArchive(ctx, repo, sha, format, reply, downloadName) {34  const spec = ARCHIVE_FORMATS[format];35  if (!spec) {36    return reply.code(400).send({ error: { code: 'bad_format', message: 'zip or tar.gz only' } });37  }38  const cachePath = ctx.cache.repoPath(repo, sha, `archive.${spec.ext}`);3940  if (!existsSync(cachePath)) {41    mkdirSync(dirname(cachePath), { recursive: true });42    const tmp = `${cachePath}.${randomBytes(4).toString('hex')}.tmp`;43    const ok = await new Promise((resolve) => {44      const child = spawn(45        'git',46        ['archive', `--format=${spec.gitFormat}`, `--prefix=${downloadName}/`, sha],47        { cwd: ctx.repos.dir(repo) },48      );49      const out = createWriteStream(tmp);50      child.stdout.pipe(out);51      child.on('error', () => resolve(false));52      child.on('close', (code) => {53        out.close(() => resolve(code === 0));54      });55    });56    if (!ok) {57      rmSync(tmp, { force: true });58      return reply.code(500).send({ error: { code: 'archive_failed', message: 'could not create archive' } });59    }60    renameSync(tmp, cachePath);61  }6263  const size = statSync(cachePath).size;64  reply65    .header('Content-Type', spec.mime)66    .header('Content-Length', size)67    .header('Content-Disposition', `attachment; filename="${downloadName}.${spec.ext}"`)68    .header('Cache-Control', 'public, max-age=31536000, immutable')69    .header('X-Content-Type-Options', 'nosniff');70  return reply.send(createReadStream(cachePath));71}72