269 lines
8.1 KiB
TypeScript
269 lines
8.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { FileExampleManager } from '../../../src/data/fileExamples';
|
|
|
|
// Simple ZIP creation without external dependencies
|
|
class SimpleZipCreator {
|
|
private files: Array<{ filename: string; content: string }> = [];
|
|
|
|
addFile(filename: string, content: string) {
|
|
this.files.push({ filename, content });
|
|
}
|
|
|
|
// Create a basic ZIP file structure
|
|
create(): number[] {
|
|
const encoder = new TextEncoder();
|
|
const chunks: number[][] = [];
|
|
|
|
let offset = 0;
|
|
const centralDirectory: Array<{
|
|
name: string;
|
|
offset: number;
|
|
size: number;
|
|
compressedSize: number;
|
|
}> = [];
|
|
|
|
// Process each file
|
|
for (const file of this.files) {
|
|
const contentBytes = Array.from(encoder.encode(file.content));
|
|
const filenameBytes = Array.from(encoder.encode(file.filename));
|
|
|
|
// Local file header
|
|
const localHeader: number[] = [];
|
|
|
|
// Local file header signature (little endian)
|
|
localHeader.push(0x50, 0x4b, 0x03, 0x04);
|
|
// Version needed to extract
|
|
localHeader.push(20, 0);
|
|
// General purpose bit flag
|
|
localHeader.push(0, 0);
|
|
// Compression method (0 = store)
|
|
localHeader.push(0, 0);
|
|
// Last modified time/date
|
|
localHeader.push(0, 0, 0, 0);
|
|
// CRC32 (0 for simplicity)
|
|
localHeader.push(0, 0, 0, 0);
|
|
// Compressed size
|
|
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
|
|
// Uncompressed size
|
|
localHeader.push(...intToLittleEndian(contentBytes.length, 4));
|
|
// Filename length
|
|
localHeader.push(...intToLittleEndian(filenameBytes.length, 2));
|
|
// Extra field length
|
|
localHeader.push(0, 0);
|
|
|
|
// Add filename
|
|
localHeader.push(...filenameBytes);
|
|
|
|
chunks.push(localHeader);
|
|
chunks.push(contentBytes);
|
|
|
|
// Store info for central directory
|
|
centralDirectory.push({
|
|
name: file.filename,
|
|
offset: offset,
|
|
size: contentBytes.length,
|
|
compressedSize: contentBytes.length
|
|
});
|
|
|
|
offset += localHeader.length + contentBytes.length;
|
|
}
|
|
|
|
// Central directory
|
|
const centralDirectoryChunks: number[][] = [];
|
|
let centralDirectoryOffset = offset;
|
|
|
|
for (const entry of centralDirectory) {
|
|
const filenameBytes = Array.from(encoder.encode(entry.name));
|
|
const centralHeader: number[] = [];
|
|
|
|
// Central directory header signature
|
|
centralHeader.push(0x50, 0x4b, 0x01, 0x02);
|
|
// Version made by
|
|
centralHeader.push(20, 0);
|
|
// Version needed to extract
|
|
centralHeader.push(20, 0);
|
|
// General purpose bit flag
|
|
centralHeader.push(0, 0);
|
|
// Compression method
|
|
centralHeader.push(0, 0);
|
|
// Last modified time/date
|
|
centralHeader.push(0, 0, 0, 0);
|
|
// CRC32
|
|
centralHeader.push(0, 0, 0, 0);
|
|
// Compressed size
|
|
centralHeader.push(...intToLittleEndian(entry.compressedSize, 4));
|
|
// Uncompressed size
|
|
centralHeader.push(...intToLittleEndian(entry.size, 4));
|
|
// Filename length
|
|
centralHeader.push(...intToLittleEndian(filenameBytes.length, 2));
|
|
// Extra field length
|
|
centralHeader.push(0, 0);
|
|
// File comment length
|
|
centralHeader.push(0, 0);
|
|
// Disk number start
|
|
centralHeader.push(0, 0);
|
|
// Internal file attributes
|
|
centralHeader.push(0, 0);
|
|
// External file attributes
|
|
centralHeader.push(0, 0, 0, 0);
|
|
// Relative offset of local header
|
|
centralHeader.push(...intToLittleEndian(entry.offset, 4));
|
|
|
|
// Add filename
|
|
centralHeader.push(...filenameBytes);
|
|
|
|
centralDirectoryChunks.push(centralHeader);
|
|
}
|
|
|
|
const centralDirectorySize = centralDirectoryChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
|
|
// End of central directory
|
|
const endOfCentralDirectory: number[] = [];
|
|
|
|
// End of central directory signature
|
|
endOfCentralDirectory.push(0x50, 0x4b, 0x05, 0x06);
|
|
// Number of this disk
|
|
endOfCentralDirectory.push(0, 0);
|
|
// Number of the disk with the start of the central directory
|
|
endOfCentralDirectory.push(0, 0);
|
|
// Total number of entries on this disk
|
|
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
|
|
// Total number of entries
|
|
endOfCentralDirectory.push(...intToLittleEndian(centralDirectory.length, 2));
|
|
// Size of the central directory
|
|
endOfCentralDirectory.push(...intToLittleEndian(centralDirectorySize, 4));
|
|
// Offset of start of central directory
|
|
endOfCentralDirectory.push(...intToLittleEndian(centralDirectoryOffset, 4));
|
|
// ZIP file comment length
|
|
endOfCentralDirectory.push(0, 0);
|
|
|
|
// Combine all chunks
|
|
const result: number[] = [];
|
|
chunks.forEach(chunk => result.push(...chunk));
|
|
centralDirectoryChunks.forEach(chunk => result.push(...chunk));
|
|
result.push(...endOfCentralDirectory);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Helper function to convert integer to little endian bytes
|
|
function intToLittleEndian(value: number, bytes: number): number[] {
|
|
const result: number[] = [];
|
|
for (let i = 0; i < bytes; i++) {
|
|
result.push((value >> (i * 8)) & 0xff);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json();
|
|
const { fileIds } = body;
|
|
|
|
if (!Array.isArray(fileIds) || fileIds.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'fileIds array is required and must not be empty' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Get file contents
|
|
const files = await Promise.all(
|
|
fileIds.map(async (id) => {
|
|
const file = await FileExampleManager.getFileExample(id);
|
|
if (!file) {
|
|
throw new Error(`File with id ${id} not found`);
|
|
}
|
|
return file;
|
|
})
|
|
);
|
|
|
|
// Create ZIP
|
|
const zipCreator = new SimpleZipCreator();
|
|
files.forEach(file => {
|
|
zipCreator.addFile(file.filename, file.content);
|
|
});
|
|
|
|
const zipData = zipCreator.create();
|
|
const buffer = Buffer.from(new Uint8Array(zipData));
|
|
|
|
// Return ZIP file
|
|
return new Response(buffer, {
|
|
headers: {
|
|
'Content-Type': 'application/zip',
|
|
'Content-Disposition': `attachment; filename="code-examples-${Date.now()}.zip"`,
|
|
'Cache-Control': 'no-cache',
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('ZIP download error:', error);
|
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
return NextResponse.json(
|
|
{ error: 'Failed to create zip file', details: errorMessage },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const { searchParams } = new URL(request.url);
|
|
const fileId = searchParams.get('id');
|
|
|
|
if (!fileId) {
|
|
return NextResponse.json(
|
|
{ error: 'id parameter is required' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const file = await FileExampleManager.getFileExample(fileId);
|
|
|
|
if (!file) {
|
|
return NextResponse.json(
|
|
{ error: 'File not found' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
const encoder = new TextEncoder();
|
|
const content = encoder.encode(file.content);
|
|
const buffer = Buffer.from(content);
|
|
|
|
return new Response(buffer, {
|
|
headers: {
|
|
'Content-Type': getMimeType(file.language),
|
|
'Content-Disposition': `attachment; filename="${file.filename}"`,
|
|
'Cache-Control': 'no-cache',
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('File download error:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to download file' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Helper function to get MIME type
|
|
function getMimeType(language: string): string {
|
|
const mimeTypes: Record<string, string> = {
|
|
'python': 'text/x-python',
|
|
'typescript': 'text/x-typescript',
|
|
'javascript': 'text/javascript',
|
|
'dockerfile': 'text/x-dockerfile',
|
|
'yaml': 'text/yaml',
|
|
'json': 'application/json',
|
|
'html': 'text/html',
|
|
'css': 'text/css',
|
|
'sql': 'text/x-sql',
|
|
'bash': 'text/x-shellscript',
|
|
'text': 'text/plain'
|
|
};
|
|
return mimeTypes[language] || 'text/plain';
|
|
}
|