import { BlobReader, BlobWriter, TextWriter, ZipReader } from '@zip.js/zip.js' import { FileAction, type FileActionResult, type FileData } from './FileExplorerAndEditor.types' import { formatBytes } from '@/lib/helpers' // Configuration for zip file extraction export const ZIP_EXTRACTION_CONFIG = { // Maximum total extracted size: 50MB (reasonable for edge functions) MAX_TOTAL_EXTRACTED_SIZE: 50 * 1024 * 1024, // 50MB // Maximum individual file size: 10MB MAX_INDIVIDUAL_FILE_SIZE: 10 * 1024 * 1024, // 10MB } as const export const isBinaryFile = (fileName: string): boolean => { const extension = fileName.split('.').pop()?.toLowerCase() const binaryExtensions = [ 'wasm', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'svg', 'mp3', 'mp4', 'avi', 'mov', 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'pdf', ] return binaryExtensions.includes(extension || '') } export const getLanguageFromFileName = (fileName: string): string => { const extension = fileName.split('.').pop()?.toLowerCase() switch (extension) { case 'ts': case 'tsx': return 'typescript' case 'js': case 'jsx': return 'javascript' case 'json': return 'json' case 'html': return 'html' case 'css': return 'css' case 'md': return 'markdown' case 'csv': return 'csv' default: return 'plaintext' // Default to plaintext } } /** * Check if a file is a zip archive based on file extension */ export const isZipFile = (fileName: string): boolean => { const extension = fileName.split('.').pop()?.toLowerCase() return extension === 'zip' } /** * Extract files from a zip archive * Returns an array of extracted files with their full paths as names (flat structure) */ export const extractZipFile = async ( zipFile: File, onProgress?: (current: number, total: number) => void ): Promise<{ name: string; content: string; size: number }[]> => { const zipReader = new ZipReader(new BlobReader(zipFile)) const entries = await zipReader.getEntries() const extractedFiles: { name: string; content: string; size: number }[] = [] const skippedFiles: string[] = [] const oversizedFiles: string[] = [] const failedFiles: string[] = [] let totalExtractedSize = 0 // Filter out directories and process files const fileEntries = entries.filter((entry) => !entry.directory) for (let i = 0; i < fileEntries.length; i++) { const entry = fileEntries[i] const fileName = entry.filename // Report progress if (onProgress) { onProgress(i + 1, fileEntries.length) } // Skip hidden files and system files const pathParts = fileName.split('/') const hasHiddenFolder = pathParts.some((part) => part.startsWith('.') || part === '__MACOSX') if (hasHiddenFolder || fileName === '.DS_Store') { skippedFiles.push(fileName) continue } // Check individual file size const uncompressedSize = entry.uncompressedSize // Guard against undefined/NaN uncompressedSize to prevent bypass of size validation if (uncompressedSize === undefined || Number.isNaN(uncompressedSize)) { oversizedFiles.push(`${fileName} (unknown size - metadata unavailable)`) continue } if (uncompressedSize > ZIP_EXTRACTION_CONFIG.MAX_INDIVIDUAL_FILE_SIZE) { oversizedFiles.push(`${fileName} (${formatBytes(uncompressedSize)})`) continue } // Check total extracted size if (totalExtractedSize + uncompressedSize > ZIP_EXTRACTION_CONFIG.MAX_TOTAL_EXTRACTED_SIZE) { throw new Error( `Total extracted size would exceed ${formatBytes(ZIP_EXTRACTION_CONFIG.MAX_TOTAL_EXTRACTED_SIZE)}. ` + `Current: ${formatBytes(totalExtractedSize)}, ` + `Attempted to add: ${formatBytes(uncompressedSize)}` ) } // Extract file content try { // Skip if entry is a directory or doesn't have getData method if (entry.directory || !entry.getData) { console.warn(`Entry ${fileName} is a directory or has no getData method, skipping`) failedFiles.push(fileName) continue } let content: string if (isBinaryFile(fileName)) { // For binary files, read as blob and convert to binary string const blob = await entry.getData(new BlobWriter()) const arrayBuffer = await blob.arrayBuffer() const bytes = new Uint8Array(arrayBuffer) content = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('') } else { // For text files, read as text content = await entry.getData(new TextWriter()) } extractedFiles.push({ name: fileName, // Keep full path as file name (flat structure) content, size: uncompressedSize, }) totalExtractedSize += uncompressedSize } catch (error) { console.error(`Failed to extract file ${fileName}:`, error) failedFiles.push(fileName) } } await zipReader.close() // Throw error if no valid files found if (extractedFiles.length === 0) { const reasons: string[] = [] if (skippedFiles.length > 0) { reasons.push(`${skippedFiles.length} hidden/system files`) } if (oversizedFiles.length > 0) { reasons.push(`${oversizedFiles.length} oversized files`) } if (failedFiles.length > 0) { reasons.push(`${failedFiles.length} files failed to extract`) } throw new Error( `No valid files found in zip archive. ${reasons.length > 0 ? 'Skipped: ' + reasons.join(', ') : ''}` ) } return extractedFiles } export const getFileAction = ( fileName: string, existingFiles: FileData[], newFiles: FileData[] ): FileActionResult => { const existingIndex = existingFiles.findIndex((f) => f.name === fileName) if (existingIndex !== -1) { return { action: FileAction.REPLACE_EXISTING, index: existingIndex } } const newIndex = newFiles.findIndex((f) => f.name === fileName) if (newIndex !== -1) { return { action: FileAction.REPLACE_NEW, index: newIndex } } return { action: FileAction.CREATE_NEW } }