UNPKG

i18ntk

Version:

i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.

378 lines (328 loc) • 14.7 kB
/** * File Management Service * Handles file operations, globbing, and cleanup operations * @module services/FileManagementService */ const path = require('path'); const fs = require('fs'); const SecurityUtils = require('../../../utils/security'); const cliHelper = require('../../../utils/cli-helper'); module.exports = class FileManagementService { constructor(config = {}) { this.config = config; this.settings = null; this.configManager = null; } /** * Initialize the service with required dependencies * @param {Object} configManager - Configuration manager instance */ initialize(configManager) { this.configManager = configManager; this.settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {}); } /** * Custom glob implementation using Node.js built-in modules (zero dependencies) * @param {string[]} patterns - Array of glob patterns * @param {Object} options - Options object with cwd and ignore properties * @returns {Promise<string[]>} Array of matching file paths */ async customGlob(patterns, options = {}) { const cwd = options.cwd || process.cwd(); const ignorePatterns = options.ignore || []; function matchesPattern(filename, pattern) { // Simple pattern matching for **/*.{js,jsx,ts,tsx} style patterns if (pattern.includes('**/*')) { const extensionPart = pattern.split('*.')[1]; if (extensionPart) { const extensions = extensionPart.replace('{', '').replace('}', '').split(','); return extensions.some(ext => filename.endsWith('.' + ext.trim())); } } return filename.includes(pattern.replace('**/', '')); } function shouldIgnore(filePath) { return ignorePatterns.some(pattern => { if (pattern.includes('**/')) { const patternEnd = pattern.replace('**/', ''); return filePath.includes('/' + patternEnd) || filePath.includes('\\' + patternEnd); } return filePath.includes(pattern); }); } function findFiles(dir, results = []) { try { const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const relativePath = path.relative(cwd, fullPath); if (shouldIgnore(relativePath)) { continue; } try { const stat = fs.statSync(fullPath); if (stat.isDirectory()) { findFiles(fullPath, results); } else if (stat.isFile()) { // Check if file matches any of our patterns for (const pattern of patterns) { if (matchesPattern(item, pattern)) { results.push(relativePath); break; } } } } catch (error) { // Skip files we can't access continue; } } } catch (error) { // Skip directories we can't access } return results; } return findFiles(cwd); } /** * Enhanced delete reports and logs functionality * @param {Object} prompt - Prompt interface for user interaction * @param {Object} ui - UI instance for translations (optional) * @returns {Promise<void>} */ async deleteReports(prompt, ui = null) { // Check for PIN protection const authRequired = await this.isAuthRequiredForScript('deleteReports'); if (authRequired) { console.log(`\n${ui ? ui.t('adminPin.protectedAccess') : 'Protected Access'}`); const pin = await cliHelper.promptPin((ui ? ui.t('adminPin.enterPin') : 'Enter PIN: ') + ': '); const isValid = await this.verifyPin(pin); if (!isValid) { console.log(ui ? ui.t('adminPin.invalidPin') : 'Invalid PIN'); await prompt(ui ? ui.t('menu.pressEnterToContinue') : 'Press Enter to continue...'); return; } console.log(ui ? ui.t('adminPin.accessGranted') : 'Access granted'); } console.log(`\n${ui ? ui.t('operations.deleteReportsTitle') : 'Delete Reports and Logs'}`); console.log('============================================================'); const targetDirs = [ { path: path.join(process.cwd(), 'i18ntk-reports'), name: 'Reports', type: 'reports' }, { path: path.join(process.cwd(), 'reports'), name: 'Legacy Reports', type: 'reports' }, { path: path.join(process.cwd(), 'reports', 'backups'), name: 'Reports Backups', type: 'backups' }, { path: path.join(process.cwd(), 'scripts', 'debug', 'logs'), name: 'Debug Logs', type: 'logs' }, { path: path.join(process.cwd(), 'scripts', 'debug', 'reports'), name: 'Debug Reports', type: 'reports' }, { path: path.join(process.cwd(), 'settings', 'backups'), name: 'Settings Backups', type: 'backups' }, { path: path.join(process.cwd(), 'utils', 'i18ntk-reports'), name: 'Utils Reports', type: 'reports' }, { path: path.join(process.cwd(), '.cache'), name: 'Cache', type: 'cache', includeAllFiles: true }, { path: path.join(process.cwd(), 'settings', '.cache'), name: 'Settings Cache', type: 'cache', includeAllFiles: true }, { path: path.join(process.cwd(), '.cache-ultra'), name: 'Performance Cache', type: 'cache', includeAllFiles: true } ].filter(dir => dir.path && typeof dir.path === 'string'); try { console.log(ui ? ui.t('operations.scanningForFiles') : 'Scanning for files...'); let availableDirs = []; // Check which directories exist and have files for (const dir of targetDirs) { if (SecurityUtils.safeExistsSync(dir.path)) { const files = this.getAllReportFiles(dir.path, { includeAllFiles: dir.includeAllFiles === true }); if (files.length > 0) { availableDirs.push({ ...dir, files: files.map(file => ({ path: file, dir: dir.path })), count: files.length }); } } } if (availableDirs.length === 0) { console.log(ui ? ui.t('operations.noFilesFoundToDelete') : 'No files found to delete'); await prompt(ui ? ui.t('menu.pressEnterToContinue') : 'Press Enter to continue...'); return; } // Show available directories console.log(ui ? ui.t('operations.availableDirectories') : 'Available directories:'); availableDirs.forEach((dir, index) => { console.log(` ${index + 1}. ${dir.name} (${dir.count} files)`); }); console.log(` ${availableDirs.length + 1}. ${ui ? ui.t('operations.allDirectories') : 'All directories'}`); console.log(` 0. ${ui ? ui.t('operations.cancelOption') : 'Cancel'}`); const dirChoice = await prompt(`\nSelect directory to clean (0-${availableDirs.length + 1}): `); const dirIndex = parseInt(dirChoice) - 1; let selectedDirs = []; if (dirChoice.trim() === '0') { console.log(ui ? ui.t('operations.cancelled') : 'Cancelled'); await prompt(ui ? ui.t('menu.pressEnterToContinue') : 'Press Enter to continue...'); return; } else if (dirIndex === availableDirs.length) { selectedDirs = availableDirs; } else if (dirIndex >= 0 && dirIndex < availableDirs.length) { selectedDirs = [availableDirs[dirIndex]]; } else { console.log(ui ? ui.t('operations.invalidSelection') : 'Invalid selection'); await prompt(ui ? ui.t('menu.pressEnterToContinue') : 'Press Enter to continue...'); return; } // Collect all files from selected directories let allFiles = []; selectedDirs.forEach(dir => { allFiles.push(...dir.files); }); console.log(ui ? ui.t('operations.foundFilesInSelectedDirectories', { count: allFiles.length }) : `Found ${allFiles.length} files in selected directories`); selectedDirs.forEach(dir => { console.log(` šŸ“ ${dir.name}: ${dir.count} files`); }); // Show deletion options console.log(ui ? ui.t('operations.deletionOptions') : 'Deletion options:'); console.log(` 1. ${ui ? ui.t('operations.deleteAllFiles') : 'Delete all files'}`); console.log(` 2. ${ui ? ui.t('operations.keepLast3Files') : 'Keep last 3 files'}`); console.log(` 3. ${ui ? ui.t('operations.keepLast5Files') : 'Keep last 5 files'}`); console.log(` 0. ${ui ? ui.t('operations.cancelReportOption') : 'Cancel'}`); const option = await prompt('\nSelect option (0-3): '); let filesToDelete = []; switch (option.trim()) { case '1': filesToDelete = allFiles; break; case '2': filesToDelete = this.getFilesToDeleteKeepLast(allFiles, 3); break; case '3': filesToDelete = this.getFilesToDeleteKeepLast(allFiles, 5); break; case '0': console.log(ui ? ui.t('operations.cancelled') : 'Cancelled'); await prompt(ui ? ui.t('menu.pressEnterToContinue') : 'Press Enter to continue...'); return; default: console.log(ui ? ui.t('menu.invalidOption') : 'Invalid option'); await prompt(ui ? ui.t('menu.pressEnterToContinue') : 'Press Enter to continue...'); return; } if (filesToDelete.length === 0) { console.log(ui ? ui.t('operations.noFilesToDelete') : 'No files to delete'); await prompt(ui ? ui.t('menu.pressEnterToContinue') : 'Press Enter to continue...'); return; } console.log(ui ? ui.t('operations.filesToDeleteCount', { count: filesToDelete.length }) : `Files to delete: ${filesToDelete.length}`); console.log(ui ? ui.t('operations.filesToKeepCount', { count: allFiles.length - filesToDelete.length }) : `Files to keep: ${allFiles.length - filesToDelete.length}`); const confirm = await prompt(ui ? ui.t('operations.confirmDeletion') : 'Are you sure you want to delete these files? (y/N): '); if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 'yes') { let deletedCount = 0; for (const fileInfo of filesToDelete) { try { fs.unlinkSync(fileInfo.path); console.log(ui ? ui.t('operations.deletedFile', { filename: path.basename(fileInfo.path) }) : `Deleted: ${path.basename(fileInfo.path)}`); deletedCount++; } catch (error) { console.log(ui ? ui.t('operations.failedToDeleteFile', { filename: path.basename(fileInfo.path), error: error.message }) : `Failed to delete ${path.basename(fileInfo.path)}: ${error.message}`); } } console.log(`\nšŸŽ‰ Successfully deleted ${deletedCount} files!`); } else { console.log(ui ? ui.t('operations.cancelled') : 'Cancelled'); } } catch (error) { console.error(`āŒ Error during deletion process: ${error.message}`); } await prompt(ui ? ui.t('menu.pressEnterToContinue') : 'Press Enter to continue...'); } /** * Helper method to get all report and log files recursively * @param {string} dir - Directory to scan * @returns {string[]} Array of file paths */ getAllReportFiles(dir, options = {}) { if (!dir || typeof dir !== 'string') { return []; } let files = []; const includeAllFiles = options.includeAllFiles === true; try { if (!SecurityUtils.safeExistsSync(dir)) { return []; } const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); try { const stat = fs.statSync(fullPath); if (stat.isDirectory()) { files.push(...this.getAllReportFiles(fullPath, options)); } else if (includeAllFiles || ( // Common report file extensions item.endsWith('.json') || item.endsWith('.html') || item.endsWith('.txt') || item.endsWith('.log') || item.endsWith('.csv') || item.endsWith('.md') || // Specific report filename patterns item.includes('-report.') || item.includes('_report.') || item.includes('report-') || item.includes('report_') || item.includes('analysis-') || item.includes('validation-') )) { files.push(fullPath); } } catch (error) { // Skip individual files that can't be accessed continue; } } } catch (error) { // Silent fail for inaccessible directories console.log(`āš ļø Could not access directory: ${dir}`); } return files; } /** * Helper method to determine which files to delete when keeping last N files * @param {Array} allFiles - Array of file objects with path property * @param {number} keepCount - Number of files to keep * @returns {Array} Array of files to delete */ getFilesToDeleteKeepLast(allFiles, keepCount = 3) { // Sort files by modification time (newest first) const sortedFiles = allFiles.sort((a, b) => { try { const statA = fs.statSync(a.path || a); const statB = fs.statSync(b.path || b); return statB.mtime.getTime() - statA.mtime.getTime(); } catch (error) { // If stat fails, sort by filename as fallback const pathA = a.path || a; const pathB = b.path || b; return pathB.localeCompare(pathA); } }); // Keep the N newest files, delete the rest return sortedFiles.slice(keepCount); } /** * Check if authentication is required for a script * @param {string} scriptName - Name of the script * @returns {Promise<boolean>} True if authentication is required */ async isAuthRequiredForScript(scriptName) { try { const SecurityUtils = require('../../../utils/security'); const configPath = path.join(process.cwd(), 'settings', 'admin-pin.json'); return SecurityUtils.safeExistsSync(configPath); } catch { return false; } } async verifyPin(pin) { try { const AdminAuth = require('../../../utils/admin-auth'); const auth = new AdminAuth(); return await auth.verifyPin(pin); } catch { return false; } } };