/" folder so
* the archive expands into a single tidy directory.
*/
function render_archive(array $repo): void
{
if (!class_exists('ZipArchive')) {
archive_error(500, 'Zip archives are unavailable on this server (the PHP zip extension is not installed).');
return;
}
$files = get_files_by_repo((int) $repo['id']);
if (empty($files)) {
render_404();
return;
}
// ZipArchive writes to a real file, so build it in a temp file, stream it,
// then remove it. Building incrementally keeps memory use flat.
$tmp = tempnam(sys_get_temp_dir(), 'repozip_');
if ($tmp === false) {
archive_error(500, 'Unable to create the archive.');
return;
}
$zip = new ZipArchive();
if ($zip->open($tmp, ZipArchive::OVERWRITE) !== true) {
@unlink($tmp);
archive_error(500, 'Unable to create the archive.');
return;
}
$root = $repo['slug']; // top-level folder inside the zip
$added = 0;
// Preserve empty folders (they exist as folder rows but hold no files).
foreach (get_folders_by_repo((int) $repo['id']) as $folder) {
$zip->addEmptyDir($root . '/' . $folder['path']);
}
foreach ($files as $file) {
$full = resolve_repo_file($repo, $file['filename']);
if ($full === null || !is_file($full)) {
continue; // skip records whose file is missing on disk
}
if ($zip->addFile($full, $root . '/' . $file['filename'])) {
$added++;
}
}
$zip->close();
if ($added === 0) {
@unlink($tmp);
render_404();
return;
}
$size = filesize($tmp);
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $repo['slug'] . '.zip"');
header('X-Content-Type-Options: nosniff');
if ($size !== false) {
header('Content-Length: ' . $size);
}
readfile($tmp);
@unlink($tmp);
}
/** Emit a plain-text error for the archive endpoint (no HTML chrome yet sent). */
function archive_error(int $code, string $message): void
{
http_response_code($code);
header('Content-Type: text/plain; charset=utf-8');
echo $message;
}