From efb9f62a374766d86e2fac09affe7edfae4f58e7 Mon Sep 17 00:00:00 2001 From: Tilman Date: Tue, 2 Dec 2025 23:47:07 +0100 Subject: [PATCH] Replace zip with JSZip --- deno.json | 1 + routes/api/files/download-directory.tsx | 88 +++++++++++++------------ 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/deno.json b/deno.json index 60e9c90..17157e2 100644 --- a/deno.json +++ b/deno.json @@ -50,6 +50,7 @@ "@std/path": "jsr:@std/path@1.1.2", "chart.js": "npm:chart.js@4.5.0", + "jszip": "npm:jszip@3.10.1", "mrmime": "npm:mrmime@2.0.1", "nodemailer": "npm:nodemailer@7.0.6", "openid-client": "npm:openid-client@6.8.0", diff --git a/routes/api/files/download-directory.tsx b/routes/api/files/download-directory.tsx index a5494e6..0f3ce43 100644 --- a/routes/api/files/download-directory.tsx +++ b/routes/api/files/download-directory.tsx @@ -1,5 +1,6 @@ import { Handlers } from 'fresh/server.ts'; import { join } from '@std/path'; +import JSZip from 'jszip'; import { FreshContextState } from '/lib/types.ts'; import { AppConfig } from '/lib/config.ts'; @@ -39,55 +40,58 @@ export const handler: Handlers = { const userRootPath = join(filesRootPath, context.state.user.id); const fullDirectoryPath = join(userRootPath, directoryPath); - // Use the zip command to create the archive with streaming - const zipProcess = new Deno.Command('zip', { - args: ['-r', '-', '.'], - cwd: fullDirectoryPath, - stdout: 'piped', - stderr: 'piped', - }).spawn(); + // Create a JSZip instance + const zip = new JSZip(); - // Get the zip stream from the process stdout - const zipStream = zipProcess.stdout; + // Recursively add files to the zip + async function addFilesToZip(currentPath: string, zipFolder: JSZip) { + for await (const entry of Deno.readDir(currentPath)) { + const entryPath = join(currentPath, entry.name); - // Monitor process errors and log them (stream will end on error) - zipProcess.status.then(async (status) => { - if (status.code !== 0) { - console.error('Zip command failed with code:', status.code); - // Read and log stderr for error details - try { - const stderrReader = zipProcess.stderr.getReader(); - try { - const chunks: Uint8Array[] = []; - while (true) { - const { done, value } = await stderrReader.read(); - if (done) break; - if (value) chunks.push(value); - } - if (chunks.length > 0) { - // Concatenate chunks - const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); - const combined = new Uint8Array(totalLength); - let offset = 0; - for (const chunk of chunks) { - combined.set(chunk, offset); - offset += chunk.length; - } - const errorText = new TextDecoder().decode(combined); - console.error('Zip stderr:', errorText); - } - } finally { - stderrReader.releaseLock(); + if (entry.isFile) { + // Read file and add to zip + const fileContent = await Deno.readFile(entryPath); + zipFolder.file(entry.name, fileContent); + } else if (entry.isDirectory) { + // Create a folder in the zip and recursively add its contents + const subFolder = zipFolder.folder(entry.name); + if (subFolder) { + await addFilesToZip(entryPath, subFolder); } - } catch (error) { - console.error('Error reading stderr:', error); } } - }).catch((error) => { - console.error('Zip process error:', error); + } + + // Add all files from the directory + await addFilesToZip(fullDirectoryPath, zip); + + // Generate the zip file as a stream + const zipStream = zip.generateNodeStream({ + type: 'nodebuffer', + streamFiles: true, + compression: 'DEFLATE', + compressionOptions: { level: 6 }, }); - return new Response(zipStream, { + // Convert Node.js stream to Web ReadableStream + const readableStream = new ReadableStream({ + async start(controller) { + zipStream.on('data', (chunk: Buffer) => { + controller.enqueue(new Uint8Array(chunk)); + }); + + zipStream.on('end', () => { + controller.close(); + }); + + zipStream.on('error', (error: Error) => { + console.error('Zip stream error:', error); + controller.error(error); + }); + }, + }); + + return new Response(readableStream, { status: 200, headers: { 'content-type': 'application/zip',