Skip to main content
Glama
MIT License
27,120
19,793
  • Linux
  • Apple
processZipFile.ts8.52 kB
import { randomUUID } from 'node:crypto'; import fs from 'node:fs/promises'; import path from 'node:path'; import { unzip } from 'fflate'; import { type CliOptions, runDefaultAction, setLogLevel } from 'repomix'; import type { PackOptions, PackResult } from '../../types.js'; import { AppError } from '../../utils/errorHandler.js'; import { logMemoryUsage } from '../../utils/logger.js'; import { generateCacheKey } from './utils/cache.js'; import { cleanupTempDirectory, copyOutputToCurrentDirectory, createTempDirectory } from './utils/fileUtils.js'; import { cache } from './utils/sharedInstance.js'; // Enhanced ZIP extraction limits const ZIP_SECURITY_LIMITS = { MAX_FILES: 10000, // Maximum number of files in the archive MAX_UNCOMPRESSED_SIZE: 100_000_000, // Maximum total uncompressed size (100MB) MAX_COMPRESSION_RATIO: 100, // Maximum compression ratio to prevent ZIP bombs MAX_PATH_LENGTH: 200, // Maximum file path length MAX_NESTING_LEVEL: 50, // Maximum directory nesting level }; /** * Process an uploaded ZIP file */ export async function processZipFile(file: File, format: string, options: PackOptions): Promise<PackResult> { if (!file) { throw new AppError('File is required for file processing', 400); } const cacheKey = generateCacheKey(`${file.name}-${file.size}-${file.lastModified}`, format, options, 'file'); // Check if the result is already cached const cachedResult = await cache.get(cacheKey); if (cachedResult) { return cachedResult; } const outputFilePath = `repomix-output-${randomUUID()}.txt`; // Create CLI options const cliOptions = { output: outputFilePath, style: format, parsableStyle: options.outputParsable, removeComments: options.removeComments, removeEmptyLines: options.removeEmptyLines, outputShowLineNumbers: options.showLineNumbers, fileSummary: options.fileSummary, directoryStructure: options.directoryStructure, compress: options.compress, securityCheck: false, topFilesLen: 10, include: options.includePatterns, ignore: options.ignorePatterns, quiet: true, // Enable quiet mode to suppress output } as CliOptions; setLogLevel(-1); const tempDirPath = await createTempDirectory(); try { // Log memory usage before processing logMemoryUsage('ZIP file processing started', { fileName: file.name, fileSize: file.size, format: format, }); // Extract the ZIP file to the temporary directory with enhanced security checks await extractZipWithSecurity(file, tempDirPath); // Execute default action on the extracted directory const result = await runDefaultAction([tempDirPath], tempDirPath, cliOptions); await copyOutputToCurrentDirectory(tempDirPath, process.cwd(), result.config.output.filePath); const { packResult } = result; // Read the generated file const content = await fs.readFile(outputFilePath, 'utf-8'); // Create pack result const packResultData: PackResult = { content, format, metadata: { repository: file.name, timestamp: new Date().toISOString(), summary: { totalFiles: packResult.totalFiles, totalCharacters: packResult.totalCharacters, totalTokens: packResult.totalTokens, }, topFiles: Object.entries(packResult.fileCharCounts) .map(([path, charCount]) => ({ path, charCount, tokenCount: packResult.fileTokenCounts[path] || 0, })) .sort((a, b) => b.charCount - a.charCount) .slice(0, cliOptions.topFilesLen), }, }; // Save the result to cache await cache.set(cacheKey, packResultData); // Log memory usage after processing logMemoryUsage('ZIP file processing completed', { fileName: file.name, totalFiles: packResult.totalFiles, totalCharacters: packResult.totalCharacters, totalTokens: packResult.totalTokens, }); return packResultData; } catch (error) { console.error('Error processing uploaded file:', error); if (error instanceof AppError) { throw error; } throw new AppError(`File processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 500); } finally { cleanupTempDirectory(tempDirPath); // Clean up the output file try { await fs.unlink(outputFilePath); } catch (err) { // Ignore file deletion errors console.warn('Failed to cleanup output file:', err); } } } /** * Enhanced ZIP extraction with security checks using fflate */ async function extractZipWithSecurity(file: File, destPath: string): Promise<void> { try { const arrayBuffer = await file.arrayBuffer(); const buffer = new Uint8Array(arrayBuffer); // Unzip using fflate with promise wrapper const files = await new Promise<Record<string, Uint8Array>>((resolve, reject) => { unzip(buffer, (err, data) => { if (err) reject(err); else resolve(data); }); }); const filePaths = Object.keys(files); // 1. Check number of files if (filePaths.length > ZIP_SECURITY_LIMITS.MAX_FILES) { throw new AppError( `ZIP contains too many files (${filePaths.length}). Maximum allowed: ${ZIP_SECURITY_LIMITS.MAX_FILES}`, 413, ); } // 2. Calculate total uncompressed size const totalUncompressedSize = Object.values(files).reduce((sum, data) => sum + data.length, 0); if (totalUncompressedSize > ZIP_SECURITY_LIMITS.MAX_UNCOMPRESSED_SIZE) { throw new AppError( `Uncompressed size (${(totalUncompressedSize / 1_000_000).toFixed(2)}MB) exceeds maximum limit of ${ ZIP_SECURITY_LIMITS.MAX_UNCOMPRESSED_SIZE / 1_000_000 }MB`, 413, ); } // 3. Check compression ratio (ZIP bomb detection) if (file.size > 0) { const compressionRatio = totalUncompressedSize / file.size; if (compressionRatio > ZIP_SECURITY_LIMITS.MAX_COMPRESSION_RATIO) { throw new AppError( `Suspicious compression ratio (${compressionRatio.toFixed(2)}:1). Maximum allowed: ${ZIP_SECURITY_LIMITS.MAX_COMPRESSION_RATIO}:1`, 400, ); } } // 4. Validate all entries for path traversal, file extensions, and nesting level const processedPaths = new Set<string>(); for (const entryPath of filePaths) { // Skip directories (fflate doesn't include directory entries, only files) if (entryPath.endsWith('/')) continue; // 4.1 Check for unsafe paths (directory traversal prevention) const normalizedPath = path.normalize(path.join(destPath, entryPath)); if (!normalizedPath.startsWith(destPath)) { throw new AppError( `Security violation: Potential directory traversal attack detected in path: ${entryPath}`, 400, ); } // 4.2 Check path length if (entryPath.length > ZIP_SECURITY_LIMITS.MAX_PATH_LENGTH) { throw new AppError( `File path exceeds maximum length: ${entryPath.length} > ${ZIP_SECURITY_LIMITS.MAX_PATH_LENGTH}`, 400, ); } // 4.3 Check nesting level const nestingLevel = entryPath.split('/').length - 1; if (nestingLevel > ZIP_SECURITY_LIMITS.MAX_NESTING_LEVEL) { throw new AppError( `Directory nesting level exceeds maximum: ${nestingLevel} > ${ZIP_SECURITY_LIMITS.MAX_NESTING_LEVEL}`, 400, ); } // 4.4 Check for duplicate paths (could indicate ZipSlip vulnerability attempts) if (processedPaths.has(normalizedPath)) { throw new AppError(`Duplicate file path detected: ${entryPath}. This could indicate a malicious archive.`, 400); } processedPaths.add(normalizedPath); } // If all checks pass, extract the files await fs.mkdir(destPath, { recursive: true }); for (const [filePath, data] of Object.entries(files)) { if (filePath.endsWith('/')) continue; // Skip directories const fullPath = path.join(destPath, filePath); const dirPath = path.dirname(fullPath); // Create directory if it doesn't exist await fs.mkdir(dirPath, { recursive: true }); // Write the file await fs.writeFile(fullPath, data); } } catch (error) { if (error instanceof AppError) { throw error; } throw new AppError(`Failed to extract ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yamadashy/repomix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server