feat: replace JSZip with archiver for ZIP file creation and streaming to disk

- Added archiver package to package.json
- Updated download-filtered-entries API to use archiver for creating ZIP files
- Streamlined ZIP file creation process and ensured proper error handling
- Improved logging for added audio files and ZIP file saving
This commit is contained in:
averel10
2026-03-20 08:06:48 +01:00
parent 815a509f4f
commit 0f3b88be91
3 changed files with 1126 additions and 58 deletions

1060
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@
"@better-auth/drizzle-adapter": "^1.5.4", "@better-auth/drizzle-adapter": "^1.5.4",
"@libsql/client": "^0.17.0", "@libsql/client": "^0.17.0",
"@tailwindcss/postcss": "^4.2.1", "@tailwindcss/postcss": "^4.2.1",
"archiver": "^7.0.0",
"auth": "^1.5.3", "auth": "^1.5.3",
"better-auth": "^1.4.19", "better-auth": "^1.4.19",
"better-auth-localization": "^2.3.1", "better-auth-localization": "^2.3.1",

View File

@@ -1,13 +1,10 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { downloadFilteredEntries } from '@/app/actions/download-filtered-entries'; import { downloadFilteredEntries } from '@/app/actions/download-filtered-entries';
import JSZip from 'jszip'; import archiver from 'archiver';
import { join } from 'path'; import { join } from 'path';
import { readFile, existsSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync } from 'fs'; import { existsSync, mkdirSync, readdirSync, unlinkSync, statSync, createWriteStream } from 'fs';
import { promisify } from 'util';
import { requireAdmin } from '@/lib/auth'; import { requireAdmin } from '@/lib/auth';
const readFileAsync = promisify(readFile);
// Clean up old ZIP files older than 24 hours // Clean up old ZIP files older than 24 hours
async function cleanupOldZips(downloadsDir: string, maxAgeHours: number = 24) { async function cleanupOldZips(downloadsDir: string, maxAgeHours: number = 24) {
try { try {
@@ -72,19 +69,36 @@ export async function POST(request: NextRequest) {
// Get filtered entries data from server action // Get filtered entries data from server action
const result = await downloadFilteredEntries(datasetId, filters || {}); const result = await downloadFilteredEntries(datasetId, filters || {});
// Create ZIP on server // Create filename with timestamp for uniqueness
const zip = new JSZip(); const timestamp = Date.now();
const fileName = `dataset-${datasetId}-export-${new Date().toISOString().split('T')[0]}-${timestamp}.zip`;
const downloadsDir = join(process.cwd(), 'public', 'downloads');
// Ensure downloads directory exists
try {
mkdirSync(downloadsDir, { recursive: true });
} catch (error) {
console.error('Error creating downloads directory:', error);
}
const filePath = join(downloadsDir, fileName);
// Create ZIP archive with streaming to disk
const output = createWriteStream(filePath);
const zip = archiver('zip', { zlib: { level: 9 } });
// Pipe archive to file
zip.pipe(output);
// Add CSV file // Add CSV file
zip.file('metadata.csv', result.csvContent); zip.append(result.csvContent, { name: 'metadata.csv' });
// Add metadata JSON // Add metadata JSON
zip.file('filter-metadata.json', result.metadataContent); zip.append(result.metadataContent, { name: 'filter-metadata.json' });
// Add audio files to 'audio' folder // Add audio files to 'wavs' folder
if (result.entries && result.entries.length > 0) { if (result.entries && result.entries.length > 0) {
const audioFolder = zip.folder('wavs');
if (audioFolder) {
let filesAdded = 0; let filesAdded = 0;
for (const entry of result.entries) { for (const entry of result.entries) {
try { try {
@@ -98,8 +112,7 @@ export async function POST(request: NextRequest) {
); );
if (existsSync(audioPath)) { if (existsSync(audioPath)) {
const fileBuffer = await readFileAsync(audioPath); zip.file(audioPath, { name: `wavs/${entry.fileName}` });
zip.file(entry.fileName, fileBuffer);
filesAdded++; filesAdded++;
} else { } else {
console.warn(`Audio file not found: ${audioPath}`); console.warn(`Audio file not found: ${audioPath}`);
@@ -110,32 +123,26 @@ export async function POST(request: NextRequest) {
} }
} }
console.log(`Added ${filesAdded}/${result.entries.length} audio files to ZIP`); console.log(`Added ${filesAdded}/${result.entries.length} audio files to ZIP`);
}
} else { } else {
console.warn('No entries to add audio files for'); console.warn('No entries to add audio files for');
} }
// Generate ZIP blob // Finalize and wait for the archive to finish writing to disk
const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' }); await new Promise<void>((resolve, reject) => {
output.on('close', () => {
// Save ZIP to public folder
const downloadsDir = join(process.cwd(), 'public', 'downloads');
// Ensure downloads directory exists
try {
mkdirSync(downloadsDir, { recursive: true });
} catch (error) {
console.error('Error creating downloads directory:', error);
}
// Create filename with timestamp for uniqueness
const timestamp = Date.now();
const fileName = `dataset-${datasetId}-export-${new Date().toISOString().split('T')[0]}-${timestamp}.zip`;
const filePath = join(downloadsDir, fileName);
// Write ZIP file to disk
writeFileSync(filePath, zipBuffer);
console.log(`ZIP file saved to: ${filePath}`); console.log(`ZIP file saved to: ${filePath}`);
resolve();
});
zip.on('error', (error) => {
console.error('Archive error:', error);
reject(error);
});
output.on('error', (error) => {
console.error('Output stream error:', error);
reject(error);
});
zip.finalize();
});
// Clean up old ZIP files in the background // Clean up old ZIP files in the background
cleanupOldZips(downloadsDir).catch(error => { cleanupOldZips(downloadsDir).catch(error => {