docker-pilot
Version:
A powerful, scalable Docker CLI library for managing containerized applications of any size
647 lines • 22.5 kB
JavaScript
;
/**
* 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