| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- 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 }
- }
|