UNPKG

empty-folder-manager

Version:

Interactive CLI tool for analyzing and removing empty folders from filesystem with .gitignore support and custom glob patterns

345 lines (278 loc) • 12 kB
const express = require('express'); const path = require('path'); const fs = require('fs').promises; const EmptyFolderCLI = require('../../remove-empty-folders.js'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(express.json()); app.use(express.static(path.join(__dirname, '../public'))); // Custom EmptyFolderManager class for web API class EmptyFolderManager extends EmptyFolderCLI { constructor() { super(); this.progressCallback = null; } setProgressCallback(callback) { this.progressCallback = callback; } // Override the findEmptyFolders method to provide progress updates async findEmptyFolders(basePath, emptyFolders = [], processedCount = { value: 0 }, ignoredCount = { value: 0 }) { try { const entries = await fs.readdir(basePath, { withFileTypes: true }); const directories = entries.filter(entry => entry.isDirectory()); for (let i = 0; i < directories.length; i += this.speedSettings.batchSize) { const batch = directories.slice(i, i + this.speedSettings.batchSize); for (const dir of batch) { const fullPath = path.join(basePath, dir.name); processedCount.value++; // Send progress updates if (this.progressCallback && processedCount.value % this.speedSettings.progressInterval === 0) { const percentage = Math.min(90, (processedCount.value / 1000) * 100); // Rough estimate this.progressCallback({ type: 'progress', percentage, message: `šŸ” Processed ${processedCount.value} folders (ignored ${ignoredCount.value})...` }); } try { const shouldIgnore = await this.shouldIgnorePath(fullPath, this.config.currentPath); if (shouldIgnore) { ignoredCount.value++; continue; } await this.findEmptyFolders(fullPath, emptyFolders, processedCount, ignoredCount); if (await this.isDirectoryEmpty(fullPath)) { emptyFolders.push(fullPath); } if (this.speedSettings.sleepBetweenOps > 0) { await this.sleep(this.speedSettings.sleepBetweenOps); } } catch (error) { continue; } } } } catch (error) { // Skip directories we can't read } return emptyFolders; } async analyzeDirectory(config) { // Set up configuration this.config.currentPath = config.path; this.useGitIgnore = config.gitignoreEnabled; this.customPatterns = config.customPatterns || []; // Set speed settings const speedPresets = { 'turbo': { sleepBetweenOps: 0, batchSize: 500, progressInterval: 100 }, 'fast': { sleepBetweenOps: 10, batchSize: 200, progressInterval: 75 }, 'normal': { sleepBetweenOps: 50, batchSize: 100, progressInterval: 50 }, 'gentle': { sleepBetweenOps: 100, batchSize: 50, progressInterval: 25 }, 'ultra-gentle': { sleepBetweenOps: 250, batchSize: 25, progressInterval: 10 } }; this.speedSettings = speedPresets[config.speed] || speedPresets.normal; // Load gitignore files await this.loadGitIgnoreFiles(this.config.currentPath); const startTime = Date.now(); const emptyFolders = []; const processedCount = { value: 0 }; const ignoredCount = { value: 0 }; await this.findEmptyFolders(this.config.currentPath, emptyFolders, processedCount, ignoredCount); const endTime = Date.now(); const duration = ((endTime - startTime) / 1000).toFixed(2); const analysisData = { basePath: this.config.currentPath, timestamp: Date.now(), emptyFolders: emptyFolders.sort(), totalProcessed: processedCount.value, ignoredCount: ignoredCount.value, duration: duration, speedSettings: config.speed, filteringEnabled: this.useGitIgnore || this.customPatterns.length > 0, gitIgnoreEnabled: this.useGitIgnore, customPatterns: [...this.customPatterns] }; // Save analysis results to file (like CLI version) await this.ensureAnalysisDir(); const pathHash = this.generatePathHash(this.config.currentPath); const analysisFile = path.join('.analysis', `analysis-${pathHash}.json`); await fs.writeFile(analysisFile, JSON.stringify(analysisData, null, 2)); return analysisData; } async removeFolders(folders, isDryRun = false) { let removedCount = 0; let errorCount = 0; for (let i = 0; i < folders.length; i++) { const folderPath = folders[i]; try { const exists = await fs.access(folderPath).then(() => true).catch(() => false); if (!exists) continue; const isEmpty = await this.isDirectoryEmpty(folderPath); if (!isEmpty) continue; if (!isDryRun) { await fs.rmdir(folderPath); } removedCount++; if (this.speedSettings.sleepBetweenOps > 0) { await this.sleep(this.speedSettings.sleepBetweenOps); } } catch (error) { errorCount++; } } return { removedCount, errorCount }; } } // API Routes // Get existing analysis for a path app.get('/api/analysis/:pathHash', async (req, res) => { try { const { pathHash } = req.params; const analysisFile = path.join('.analysis', `analysis-${pathHash}.json`); try { const analysisData = JSON.parse(await fs.readFile(analysisFile, 'utf8')); res.json(analysisData); } catch (error) { res.status(404).json({ error: 'No analysis found for this path' }); } } catch (error) { console.error('Get analysis error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Generate path hash endpoint app.post('/api/path-hash', async (req, res) => { try { const { path: dirPath } = req.body; if (!dirPath) { return res.status(400).json({ error: 'Path is required' }); } const manager = new EmptyFolderManager(); const pathHash = manager.generatePathHash(dirPath); res.json({ pathHash }); } catch (error) { console.error('Path hash error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Analyze directory endpoint app.post('/api/analyze', async (req, res) => { try { const config = req.body; // Validate input if (!config.path) { return res.status(400).json({ error: 'Path is required' }); } // Check if path exists and is a directory try { const stats = await fs.stat(config.path); if (!stats.isDirectory()) { return res.status(400).json({ error: 'Path must be a directory' }); } } catch (error) { return res.status(400).json({ error: 'Path does not exist or is not accessible' }); } // Set up streaming response res.writeHead(200, { 'Content-Type': 'application/json', 'Transfer-Encoding': 'chunked' }); const manager = new EmptyFolderManager(); // Set up progress callback manager.setProgressCallback((data) => { res.write(JSON.stringify(data) + '\n'); }); try { // Send initial progress res.write(JSON.stringify({ type: 'progress', percentage: 5, message: 'Initializing analysis...' }) + '\n'); const result = await manager.analyzeDirectory(config); // Send completion res.write(JSON.stringify({ type: 'complete', result: result }) + '\n'); } catch (error) { res.write(JSON.stringify({ type: 'error', message: error.message }) + '\n'); } res.end(); } catch (error) { console.error('Analysis error:', error); if (!res.headersSent) { res.status(500).json({ error: 'Internal server error' }); } } }); // Remove folders endpoint app.post('/api/remove', async (req, res) => { try { const { folders, dryRun = false, speed = 'normal' } = req.body; if (!folders || !Array.isArray(folders)) { return res.status(400).json({ error: 'Folders array is required' }); } const manager = new EmptyFolderManager(); // Set speed settings const speedPresets = { 'turbo': { sleepBetweenOps: 0, batchSize: 500, progressInterval: 100 }, 'fast': { sleepBetweenOps: 10, batchSize: 200, progressInterval: 75 }, 'normal': { sleepBetweenOps: 50, batchSize: 100, progressInterval: 50 }, 'gentle': { sleepBetweenOps: 100, batchSize: 50, progressInterval: 25 }, 'ultra-gentle': { sleepBetweenOps: 250, batchSize: 25, progressInterval: 10 } }; manager.speedSettings = speedPresets[speed] || speedPresets.normal; const result = await manager.removeFolders(folders, dryRun); res.json(result); } catch (error) { console.error('Removal error:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Health check endpoint app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Serve the main page app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../public/index.html')); }); // Start server const server = app.listen(PORT, () => { console.log(`šŸ—‚ļø Empty Folder Manager Web UI running on http://localhost:${PORT}`); console.log(`šŸ“ Open your browser and navigate to the URL above to get started!`); }); // Handle port conflicts gracefully server.on('error', (error) => { if (error.code === 'EADDRINUSE') { console.error(`āŒ Port ${PORT} is already in use.`); console.error('šŸ’” Another instance of the web UI might be running.'); console.error('šŸ’” Try stopping other instances or use a different port.'); console.error('šŸ’” You can kill existing processes with: pkill -f "node.*server.js"'); process.exit(1); } else { console.error('āŒ Server error:', error.message); process.exit(1); } }); // Graceful shutdown handling process.on('SIGINT', () => { console.log('\nšŸ‘‹ Shutting down web server gracefully...'); server.close(() => { console.log('āœ… Web server stopped.'); process.exit(0); }); }); process.on('SIGTERM', () => { console.log('\nšŸ‘‹ Received SIGTERM, shutting down gracefully...'); server.close(() => { console.log('āœ… Web server stopped.'); process.exit(0); }); }); module.exports = app;