Replace zip with JSZip

This commit is contained in:
Tilman 2025-12-02 23:47:07 +01:00
parent ed88b927d3
commit efb9f62a37
2 changed files with 47 additions and 42 deletions

View file

@ -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",

View file

@ -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<Data, FreshContextState> = {
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',