UNPKG

docker-pilot

Version:

A powerful, scalable Docker CLI library for managing containerized applications of any size

647 lines 22.5 kB
"use strict"; /** * File system utility functions * Provides file and directory operations with error handling */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.FileUtils = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const glob_1 = require("glob"); const yaml = __importStar(require("yaml")); const Logger_1 = require("./Logger"); class FileUtils { constructor(logger) { this.logger = logger || new Logger_1.Logger(); } /** * Check if path exists */ async exists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } /** * Read file content */ async readFile(filePath, encoding = 'utf8') { try { return await fs.readFile(filePath, encoding); } catch (error) { this.logger.error(`Failed to read file: ${filePath}`, error); throw error; } } /** * Write file content */ async writeFile(filePath, content, encoding = 'utf8') { try { // Ensure directory exists await this.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, content, encoding); this.logger.debug(`File written: ${filePath}`); } catch (error) { this.logger.error(`Failed to write file: ${filePath}`, error); throw error; } } /** * Append to file */ async appendFile(filePath, content, encoding = 'utf8') { try { await fs.appendFile(filePath, content, encoding); this.logger.debug(`Content appended to file: ${filePath}`); } catch (error) { this.logger.error(`Failed to append to file: ${filePath}`, error); throw error; } } /** * Read JSON file */ async readJson(filePath) { try { return await fs.readJson(filePath); } catch (error) { this.logger.error(`Failed to read JSON file: ${filePath}`, error); throw error; } } /** * Write JSON file */ async writeJson(filePath, data, options = {}) { try { await this.ensureDir(path.dirname(filePath)); await fs.writeJson(filePath, data, { spaces: options.spaces || 2 }); this.logger.debug(`JSON file written: ${filePath}`); } catch (error) { this.logger.error(`Failed to write JSON file: ${filePath}`, error); throw error; } } /** * Read YAML file */ async readYaml(filePath) { try { const content = await this.readFile(filePath); return yaml.parse(content); } catch (error) { this.logger.error(`Failed to read YAML file: ${filePath}`, error); throw error; } } /** * Write YAML file */ async writeYaml(filePath, data, options = {}) { try { await this.ensureDir(path.dirname(filePath)); const yamlContent = yaml.stringify(data, options); await this.writeFile(filePath, yamlContent); this.logger.debug(`YAML file written: ${filePath}`); } catch (error) { this.logger.error(`Failed to write YAML file: ${filePath}`, error); throw error; } } /** * Ensure directory exists */ async ensureDir(dirPath) { try { await fs.ensureDir(dirPath); } catch (error) { this.logger.error(`Failed to ensure directory: ${dirPath}`, error); throw error; } } /** * Remove file or directory */ async remove(targetPath) { try { await fs.remove(targetPath); this.logger.debug(`Removed: ${targetPath}`); } catch (error) { this.logger.error(`Failed to remove: ${targetPath}`, error); throw error; } } /** * Copy file or directory */ async copy(src, dest, options = {}) { try { await fs.copy(src, dest, options); this.logger.debug(`Copied: ${src} -> ${dest}`); } catch (error) { this.logger.error(`Failed to copy: ${src} -> ${dest}`, error); throw error; } } /** * Move file or directory */ async move(src, dest) { try { await fs.move(src, dest); this.logger.debug(`Moved: ${src} -> ${dest}`); } catch (error) { this.logger.error(`Failed to move: ${src} -> ${dest}`, error); throw error; } } /** * Get file information */ async getFileInfo(filePath) { try { const stats = await fs.stat(filePath); const parsedPath = path.parse(filePath); return { path: filePath, name: parsedPath.name, extension: parsedPath.ext, size: stats.size, isDirectory: stats.isDirectory(), isFile: stats.isFile(), created: stats.birthtime, modified: stats.mtime, accessed: stats.atime }; } catch (error) { this.logger.error(`Failed to get file info: ${filePath}`, error); throw error; } } /** * List directory contents */ async listDirectory(dirPath, recursive = false) { try { if (recursive) { const pattern = path.join(dirPath, '**', '*'); return await (0, glob_1.glob)(pattern, { dot: true }); } else { const items = await fs.readdir(dirPath); return items.map(item => path.join(dirPath, item)); } } catch (error) { this.logger.error(`Failed to list directory: ${dirPath}`, error); throw error; } } /** * Get directory information with detailed file info */ async getDirectoryInfo(dirPath) { try { const items = await fs.readdir(dirPath); const files = []; const directories = []; let totalFiles = 0; let totalSize = 0; for (const item of items) { const itemPath = path.join(dirPath, item); const stats = await fs.stat(itemPath); if (stats.isFile()) { const fileInfo = await this.getFileInfo(itemPath); files.push(fileInfo); totalFiles++; totalSize += fileInfo.size; } else if (stats.isDirectory()) { const subDirInfo = await this.getDirectoryInfo(itemPath); directories.push(subDirInfo); totalFiles += subDirInfo.totalFiles; totalSize += subDirInfo.totalSize; } } return { path: dirPath, name: path.basename(dirPath), files, directories, totalFiles, totalSize }; } catch (error) { this.logger.error(`Failed to get directory info: ${dirPath}`, error); throw error; } } /** * Find files by pattern */ async findFiles(pattern, options = {}) { try { const globOptions = { cwd: options.cwd || process.cwd(), dot: true, absolute: true }; if (options.ignore) { globOptions.ignore = options.ignore; } return await (0, glob_1.glob)(pattern, globOptions); } catch (error) { this.logger.error(`Failed to find files with pattern: ${pattern}`, error); throw error; } } /** * Find Docker-related files */ async findDockerFiles(baseDir = process.cwd()) { const dockerfiles = await this.findFiles('**/Dockerfile*', { cwd: baseDir, ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] }); const composeFiles = await this.findFiles('**/docker-compose*.{yml,yaml}', { cwd: baseDir, ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] }); const dockerignore = await this.findFiles('**/.dockerignore', { cwd: baseDir, ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'] }); return { dockerfiles, composeFiles, dockerignore }; } /** * Find Docker Compose files recursively with enhanced search */ async findDockerComposeFiles(startDir, options) { const searchDir = startDir || process.cwd(); const composeFiles = []; const maxDepth = options?.maxDepth || 6; const includeVariants = options?.includeVariants !== false; const additionalSkipDirs = options?.skipDirectories || []; const composeFilenames = [ // Main compose files 'docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml', ]; if (includeVariants) { composeFilenames.push( // Environment variants 'docker-compose.dev.yml', 'docker-compose.dev.yaml', 'docker-compose.development.yml', 'docker-compose.development.yaml', 'docker-compose.prod.yml', 'docker-compose.prod.yaml', 'docker-compose.production.yml', 'docker-compose.production.yaml', 'docker-compose.local.yml', 'docker-compose.local.yaml', 'docker-compose.test.yml', 'docker-compose.test.yaml', 'docker-compose.testing.yml', 'docker-compose.testing.yaml', 'docker-compose.staging.yml', 'docker-compose.staging.yaml', 'docker-compose.override.yml', 'docker-compose.override.yaml', // Alternative formats 'compose.dev.yml', 'compose.dev.yaml', 'compose.prod.yml', 'compose.prod.yaml', 'compose.local.yml', 'compose.local.yaml', 'compose.test.yml', 'compose.test.yaml'); } const searchRecursively = async (dir, currentDepth = 0) => { if (currentDepth > maxDepth) return; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isFile() && composeFilenames.includes(entry.name)) { composeFiles.push(fullPath); } else if (entry.isDirectory() && !this.shouldSkipDirectory(entry.name, additionalSkipDirs)) { await searchRecursively(fullPath, currentDepth + 1); } } } catch (error) { this.logger.debug(`Failed to read directory ${dir}:`, error); } }; await searchRecursively(searchDir); return composeFiles; } /** * Check if directory should be skipped during Docker Compose search */ shouldSkipDirectory(dirName, additionalSkipDirs = []) { const skipDirectories = [ 'node_modules', '.git', '.vscode', '.idea', 'dist', 'build', 'target', 'out', 'tmp', 'temp', '.next', '.nuxt', 'coverage', '__pycache__', '.pytest_cache', 'venv', 'env', '.env', 'vendor', 'logs', '.docker', 'bin', 'obj', '.vs', 'packages', 'bower_components', '.sass-cache', '.gradle', '.mvn', 'target' ]; const allSkipDirectories = [...skipDirectories, ...additionalSkipDirs]; return allSkipDirectories.includes(dirName) || dirName.startsWith('.'); } /** * Find all Docker Compose files with detailed information and enhanced search */ async findDockerComposeFilesWithInfo(startDir, options) { const searchDir = startDir || process.cwd(); const composeFiles = await this.findDockerComposeFiles(searchDir, options); const filesWithInfo = []; this.logger.debug(`Found ${composeFiles.length} docker-compose files to analyze`); for (const filePath of composeFiles) { try { const stats = await fs.stat(filePath); const relativePath = path.relative(searchDir, filePath); const filename = path.basename(filePath); const directory = path.dirname(filePath); // Skip empty files unless explicitly requested if (!options?.includeEmptyFiles && stats.size === 0) { this.logger.debug(`Skipping empty file: ${relativePath}`); continue; } // Calculate depth level const depth = relativePath.split(path.sep).length - 1; // Determine environment and priority const environment = this.extractEnvironmentFromFilename(filename); const isMainFile = this.isMainComposeFile(filename); // Try to read and parse the compose file let hasServices = false; let serviceCount = 0; let services = []; try { const composeData = await this.readYaml(filePath); if (composeData.services && typeof composeData.services === 'object') { hasServices = true; services = Object.keys(composeData.services); serviceCount = services.length; } } catch (error) { this.logger.debug(`Failed to parse compose file ${filePath}:`, error); } // Calculate priority score (lower is better) let priority = 0; priority += depth * 10; // Prefer files closer to root priority += isMainFile ? 0 : 5; // Prefer main files over variants priority += serviceCount > 0 ? 0 : 20; // Prefer files with services priority -= serviceCount; // More services = higher priority (lower score) filesWithInfo.push({ path: filePath, relativePath, filename, directory, size: stats.size, modified: stats.mtime, hasServices, serviceCount, services, depth, environment, isMainFile, priority }); } catch (error) { this.logger.debug(`Failed to get info for compose file ${filePath}:`, error); } } // Sort by priority (lower priority number = higher importance) return filesWithInfo.sort((a, b) => { if (a.priority !== b.priority) { return a.priority - b.priority; } // If same priority, sort alphabetically return a.relativePath.localeCompare(b.relativePath); }); } /** * Extract environment type from compose filename */ extractEnvironmentFromFilename(filename) { const envPatterns = { 'dev': /\.(dev|development)\./, 'prod': /\.(prod|production)\./, 'test': /\.(test|testing)\./, 'staging': /\.staging\./, 'local': /\.local\./, 'override': /\.override\./ }; for (const [env, pattern] of Object.entries(envPatterns)) { if (pattern.test(filename)) { return env; } } return undefined; } /** * Check if filename is a main compose file (not an environment variant) */ isMainComposeFile(filename) { const mainFiles = [ 'docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml' ]; return mainFiles.includes(filename); } /** * Check if file is empty */ async isEmpty(filePath) { try { const stats = await fs.stat(filePath); return stats.size === 0; } catch (error) { this.logger.error(`Failed to check if file is empty: ${filePath}`, error); throw error; } } /** * Get file size in human readable format */ formatFileSize(bytes) { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); const size = bytes / Math.pow(1024, i); return `${size.toFixed(2)} ${sizes[i]}`; } /** * Watch file or directory for changes */ watchPath(targetPath, callback) { this.logger.debug(`Watching path: ${targetPath}`); return fs.watch(targetPath, { recursive: true }, callback); } /** * Create backup of file (maintains only one backup) */ async backupFile(filePath, backupDir) { try { const fileInfo = await this.getFileInfo(filePath); const backupName = `${fileInfo.name}.backup${fileInfo.extension}`; const backupPath = backupDir ? path.join(backupDir, backupName) : path.join(path.dirname(filePath), backupName); // Remove existing backup if it exists to avoid accumulation if (await this.exists(backupPath)) { await fs.remove(backupPath); this.logger.debug(`Removed existing backup: ${backupPath}`); } await this.copy(filePath, backupPath); this.logger.info(`Backup created: ${backupPath}`); return backupPath; } catch (error) { this.logger.error(`Failed to backup file: ${filePath}`, error); throw error; } } /** * Clean old backup files with timestamp pattern */ async cleanOldBackups(directory, pattern = '*-backup-*') { try { const glob = require('glob'); const backupFiles = glob.sync(path.join(directory, pattern)); for (const backupFile of backupFiles) { await fs.remove(backupFile); this.logger.debug(`Removed old backup: ${backupFile}`); } if (backupFiles.length > 0) { this.logger.info(`Cleaned ${backupFiles.length} old backup files`); } } catch (error) { this.logger.error(`Failed to clean old backups in: ${directory}`, error); } } /** * Clean directory (remove all contents) */ async cleanDirectory(dirPath) { try { await fs.emptyDir(dirPath); this.logger.debug(`Directory cleaned: ${dirPath}`); } catch (error) { this.logger.error(`Failed to clean directory: ${dirPath}`, error); throw error; } } /** * Get temporary file path */ getTempPath(prefix = 'docker-pilot', extension = '.tmp') { const timestamp = Date.now(); const random = Math.random().toString(36).substring(7); const filename = `${prefix}-${timestamp}-${random}${extension}`; return path.join(require('os').tmpdir(), filename); } /** * Resolve path relative to current working directory */ resolvePath(inputPath, basePath = process.cwd()) { if (path.isAbsolute(inputPath)) { return inputPath; } return path.resolve(basePath, inputPath); } /** * Check if path is within base directory (security check) */ isPathSafe(targetPath, basePath) { const resolvedTarget = path.resolve(targetPath); const resolvedBase = path.resolve(basePath); return resolvedTarget.startsWith(resolvedBase); } /** * Get relative path from base */ getRelativePath(targetPath, basePath = process.cwd()) { return path.relative(basePath, targetPath); } /** * Normalize path separators */ normalizePath(inputPath) { return path.normalize(inputPath).replace(/\\/g, '/'); } } exports.FileUtils = FileUtils; //# sourceMappingURL=FileUtils.js.map