import { __logger } from "../logger/internal-logger"; import { CBFileSystem } from "../node/file-system"; import path from "path"; export interface FileSystemConfig { include: { directories: DirectoryConfig[]; } } // Directory configuration interface export interface DirectoryConfig { name: string; allowedExtensions?: string[]; excludeSubdirs?: string[]; includeFiles?: string[]; // Specific files to include (takes precedence over allowedExtensions) excludeFiles?: string[]; // Specific files to exclude (applied after inclusion rules) } /** * File management service for handling file operations */ export class FileManagementService { constructor(private fileSystem: CBFileSystem, private packageConfig: FileSystemConfig) { } /** * Collects essential files for the serverless application package * Only includes files and directories specified in packageConfig * @param {string} appDir - The application directory * @returns {string[]} Array of file paths to package */ collectFilesToPackage(appDir: string): string[] { const files: string[] = []; // Process directories specified in packageConfig this.packageConfig.include.directories.forEach(dirConfig => { if (dirConfig.name === '.') { // Handle root directory case - collect files directly from appDir const rootFiles = this.getFilesRecursively(appDir, dirConfig); files.push(...rootFiles); } else { // Handle subdirectory case const dirPath = path.join(appDir, dirConfig.name); if (this.fileSystem.existsSync(dirPath)) { const dirFiles = this.getFilesRecursively(dirPath, dirConfig); // Add directory files with directory prefix dirFiles.forEach((file: string) => { files.push(`${dirConfig.name}/${file}`); }); } else { throw new Error(`Directory ${dirPath} does not exist`); } } }); __logger.info(`Collected ${files.length} files to package`); __logger.info(` - Directories: ${this.packageConfig.include.directories.map(d => d.name).join(', ')}`); return files; } /** * Cleans all files and folders inside the dist directory * @param {string} appDir - The application directory */ cleanExistingPackages(appDir: string): void { const distDir = path.join(appDir, 'dist'); if (!this.fileSystem.existsSync(distDir)) { this.fileSystem.mkdirSync(distDir, { recursive: true }); return; } const files = this.fileSystem.readdirSync(distDir); if (files.length > 0) { __logger.info(`Cleaning ${files.length} item(s) in dist directory...`); files.forEach(item => { const itemPath = path.join(distDir, item); const stat = this.fileSystem.statSync(itemPath); if (stat.isDirectory()) { this.deleteDirectoryRecursively(itemPath); __logger.info(` - Removed directory: ${item}/`); } else { this.fileSystem.unlinkSync(itemPath); } }); } } /** * Determines the output path for the package * @param {string} appDir - The application directory * @param {string} appName - The application name * @param {PackageOptions} options - Package options * @returns {string} The output path for the package */ determineOutputPath(appDir: string, appName: string, packageName?: string): string { const distDir = path.join(appDir, 'dist'); packageName = packageName || appName; const zipFileName = `${packageName}.zip`; return path.join(distDir, zipFileName); } /** * Gets the file size in a human-readable format * @param {string} filePath - The file path * @returns {string} Human-readable file size */ getFileSize(filePath: string): string { const stats = this.fileSystem.statSync(filePath); const bytes = stats.size; if (bytes === 0) { return '0 Bytes'; } const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Collects files recursively from a directory based on directory-specific configurations * @param {string} dir - The directory to scan * @param {DirectoryConfig} dirConfig - Directory configuration with file inclusion/exclusion rules * @returns {string[]} Array of file paths relative to the scanned directory */ protected getFilesRecursively(dir: string, dirConfig: DirectoryConfig): string[] { const files: string[] = []; // Get configuration values const allowedExtensions = dirConfig.allowedExtensions || []; const excludeSubdirs = dirConfig.excludeSubdirs || []; const includeFiles = dirConfig.includeFiles || []; const excludeFiles = dirConfig.excludeFiles || []; const scanDirectory = (currentDir: string, baseDir: string): void => { const items = this.fileSystem.readdirSync(currentDir); for (const item of items) { const itemPath = path.join(currentDir, item); const lstat = this.fileSystem.lstatSync(itemPath); // Skip symlinks — they may be broken (e.g. .bin/ entries in Docker overlay fs) if (lstat.isSymbolicLink()) { continue; } const stat = this.fileSystem.statSync(itemPath); if (stat.isDirectory()) { // Skip excluded subdirectories if (!excludeSubdirs.includes(item)) { scanDirectory(itemPath, baseDir); } } else { // Apply file inclusion/exclusion logic if (item && typeof item === 'string') { const relativePath = path.relative(baseDir, itemPath); let shouldInclude = false; // Priority 1: Check specific file inclusion (highest priority) if (includeFiles.length > 0) { shouldInclude = includeFiles.includes(item); } // Priority 2: Check extension-based inclusion (if no specific files defined) else if (allowedExtensions.length > 0) { const ext = path.extname(item).toLowerCase(); shouldInclude = allowedExtensions.includes(ext); } // Priority 3: Include all files if no inclusion rules defined else { shouldInclude = true; } // Apply exclusion rules (always applied if file was included) if (shouldInclude && excludeFiles.length > 0) { shouldInclude = !excludeFiles.includes(item); } if (shouldInclude) { files.push(relativePath); } } } } }; if (this.fileSystem.existsSync(dir)) { scanDirectory(dir, dir); } return files; } /** * Recursively deletes a directory and all its contents * @param {string} dirPath - The directory path to delete */ protected deleteDirectoryRecursively(dirPath: string): void { const files = this.fileSystem.readdirSync(dirPath); for (const file of files) { const filePath = path.join(dirPath, file); const stat = this.fileSystem.statSync(filePath); if (stat.isDirectory()) { this.deleteDirectoryRecursively(filePath); } else { this.fileSystem.unlinkSync(filePath); } } // Remove the empty directory this.fileSystem.rmdirSync(dirPath); } }