/** * CloneWeb Storage Manager * Gerencia versões, metadados e storage dos sites clonados */ const fs = require('fs'); const path = require('path'); const express = require('express'); const router = express.Router(); const BASE_DIR = '/app/cloned-sites'; const METADATA_FILE = 'clone-metadata.json'; /** * Get all clones with their metadata */ function getAllClones() { const clones = []; if (!fs.existsSync(BASE_DIR)) { return clones; } const dirs = fs.readdirSync(BASE_DIR).filter(f => { return fs.statSync(path.join(BASE_DIR, f)).isDirectory(); }); for (const dir of dirs) { const clonePath = path.join(BASE_DIR, dir); const metadata = loadMetadata(clonePath); const cloneInfo = loadCloneInfo(clonePath); const stats = getCloneStats(clonePath); // Extract base URL from directory name const baseUrl = extractBaseUrl(dir); clones.push({ id: dir, directory: dir, baseUrl, path: clonePath, metadata: metadata || {}, cloneInfo: cloneInfo || {}, stats, versions: listVersions(clonePath) }); } // Sort by most recent clones.sort((a, b) => { const dateA = new Date(a.cloneInfo.clonedAt || a.metadata.createdAt || 0); const dateB = new Date(b.cloneInfo.clonedAt || b.metadata.createdAt || 0); return dateB - dateA; }); return clones; } function loadMetadata(clonePath) { const metaFile = path.join(clonePath, METADATA_FILE); if (fs.existsSync(metaFile)) { try { return JSON.parse(fs.readFileSync(metaFile, 'utf8')); } catch (e) { return null; } } return null; } function loadCloneInfo(clonePath) { const infoFile = path.join(clonePath, 'clone-info.json'); if (fs.existsSync(infoFile)) { try { return JSON.parse(fs.readFileSync(infoFile, 'utf8')); } catch (e) { return null; } } return null; } function getCloneStats(clonePath) { let totalSize = 0; let fileCount = 0; function countFiles(dir) { if (!fs.existsSync(dir)) return; const items = fs.readdirSync(dir); for (const item of items) { const itemPath = path.join(dir, item); const stat = fs.statSync(itemPath); if (stat.isDirectory()) { // Skip cloned-sites subdirectory if (!item.includes('cloned-sites')) { countFiles(itemPath); } } else { totalSize += stat.size; fileCount++; } } } countFiles(clonePath); return { totalSize, fileCount, formattedSize: formatBytes(totalSize) }; } function listVersions(clonePath) { const versions = []; const versionsDir = path.join(clonePath, '.versions'); if (fs.existsSync(versionsDir)) { const versionDirs = fs.readdirSync(versionsDir); for (const vDir of versionDirs) { const vPath = path.join(versionsDir, vDir); const stat = fs.statSync(vPath); versions.push({ id: vDir, path: vPath, createdAt: stat.birthtime, size: getDirSize(vPath) }); } } return versions; } function getDirSize(dirPath) { let size = 0; function count(dir) { if (!fs.existsSync(dir)) return; for (const item of fs.readdirSync(dir)) { const stat = fs.statSync(path.join(dir, item)); if (stat.isDirectory()) { count(path.join(dir, item)); } else { size += stat.size; } } } count(dirPath); return size; } function extractBaseUrl(dirName) { // "example.com_2026-05-17T20-58-30-760Z" -> "example.com" const parts = dirName.split('_'); if (parts.length >= 2) { // Check if last part is a timestamp const lastPart = parts[parts.length - 1]; if (/^\d{4}-\d{2}-\d{2}T/.test(lastPart)) { return parts.slice(0, -1).join('_'); } } return parts[0]; } function formatBytes(bytes) { 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]; } // ============================================ // API ROUTES // ============================================ // GET /api/clones - List all clones router.get('/', (req, res) => { try { const clones = getAllClones(); const summary = { total: clones.length, totalSize: clones.reduce((sum, c) => sum + c.stats.totalSize, 0), formattedSize: formatBytes(clones.reduce((sum, c) => sum + c.stats.totalSize, 0)), byStatus: { active: clones.filter(c => c.metadata?.status === 'active').length, archived: clones.filter(c => c.metadata?.status === 'archived').length, redesign: clones.filter(c => c.metadata?.status === 'redesign').length } }; res.json({ success: true, data: { clones, summary } }); } catch (error) { console.error('Erro ao listar clones:', error); res.status(500).json({ success: false, error: error.message }); } }); // GET /api/clones/:id - Get single clone details router.get('/:id', (req, res) => { try { const clonePath = path.join(BASE_DIR, req.params.id); if (!fs.existsSync(clonePath)) { return res.status(404).json({ success: false, error: 'Clone não encontrado' }); } const metadata = loadMetadata(clonePath); const cloneInfo = loadCloneInfo(clonePath); const stats = getCloneStats(clonePath); const versions = listVersions(clonePath); // Get list of HTML files const htmlFiles = findFiles(clonePath, '.html'); res.json({ success: true, data: { id: req.params.id, baseUrl: extractBaseUrl(req.params.id), path: clonePath, metadata: metadata || { createdAt: cloneInfo?.clonedAt }, cloneInfo, stats, versions, htmlFiles } }); } catch (error) { console.error('Erro ao buscar clone:', error); res.status(500).json({ success: false, error: error.message }); } }); // PATCH /api/clones/:id - Update clone metadata router.patch('/:id', (req, res) => { try { const clonePath = path.join(BASE_DIR, req.params.id); if (!fs.existsSync(clonePath)) { return res.status(404).json({ success: false, error: 'Clone não encontrado' }); } const metaFile = path.join(clonePath, METADATA_FILE); let metadata = loadMetadata(clonePath) || {}; // Merge updates metadata = { ...metadata, ...req.body, updatedAt: new Date().toISOString() }; fs.writeFileSync(metaFile, JSON.stringify(metadata, null, 2)); res.json({ success: true, data: { metadata } }); } catch (error) { console.error('Erro ao atualizar clone:', error); res.status(500).json({ success: false, error: error.message }); } }); // POST /api/clones/:id/versions - Create a version snapshot router.post('/:id/versions', (req, res) => { try { const clonePath = path.join(BASE_DIR, req.params.id); if (!fs.existsSync(clonePath)) { return res.status(404).json({ success: false, error: 'Clone não encontrado' }); } const versionsDir = path.join(clonePath, '.versions'); const versionId = `v${Date.now()}`; const versionPath = path.join(versionsDir, versionId); fs.mkdirSync(versionPath, { recursive: true }); // Copy all files to version (except .versions itself) copyDirectory(clonePath, versionPath, ['.versions']); res.json({ success: true, data: { versionId, path: versionPath, createdAt: new Date().toISOString() } }); } catch (error) { console.error('Erro ao criar versão:', error); res.status(500).json({ success: false, error: error.message }); } }); // DELETE /api/clones/:id/versions/:versionId - Delete a version router.delete('/:id/versions/:versionId', (req, res) => { try { const versionPath = path.join(BASE_DIR, req.params.id, '.versions', req.params.versionId); if (!fs.existsSync(versionPath)) { return res.status(404).json({ success: false, error: 'Versão não encontrada' }); } fs.rmSync(versionPath, { recursive: true, force: true }); res.json({ success: true, message: 'Versão removida' }); } catch (error) { console.error('Erro ao remover versão:', error); res.status(500).json({ success: false, error: error.message }); } }); // POST /api/clones/:id/restore/:versionId - Restore from version router.post('/:id/restore/:versionId', (req, res) => { try { const clonePath = path.join(BASE_DIR, req.params.id); const versionPath = path.join(clonePath, '.versions', req.params.versionId); if (!fs.existsSync(versionPath)) { return res.status(404).json({ success: false, error: 'Versão não encontrada' }); } // Backup current state first const backupId = `pre-restore-${Date.now()}`; const backupPath = path.join(clonePath, '.versions', backupId); fs.mkdirSync(backupPath, { recursive: true }); copyDirectory(clonePath, backupPath, ['.versions']); // Create restore point const restorePointId = `pre-restore-${Date.now()}`; const restorePointPath = path.join(clonePath, '.versions', restorePointId); fs.mkdirSync(restorePointPath, { recursive: true }); copyDirectory(clonePath, restorePointPath, ['.versions']); // Copy version files back (overwrite) copyDirectory(versionPath, clonePath, ['.versions'], true); res.json({ success: true, data: { restoredFrom: req.params.versionId, backupCreated: restorePointId } }); } catch (error) { console.error('Erro ao restaurar versão:', error); res.status(500).json({ success: false, error: error.message }); } }); // DELETE /api/clones/:id - Delete a clone router.delete('/:id', (req, res) => { try { const clonePath = path.join(BASE_DIR, req.params.id); if (!fs.existsSync(clonePath)) { return res.status(404).json({ success: false, error: 'Clone não encontrado' }); } fs.rmSync(clonePath, { recursive: true, force: true }); res.json({ success: true, message: 'Clone removido' }); } catch (error) { console.error('Erro ao remover clone:', error); res.status(500).json({ success: false, error: error.message }); } }); // Helper functions function findFiles(dir, extension) { const files = []; function search(d) { if (!fs.existsSync(d)) return; for (const item of fs.readdirSync(d)) { const itemPath = path.join(d, item); const stat = fs.statSync(itemPath); if (stat.isDirectory() && item !== '.versions' && !item.includes('node_modules')) { search(itemPath); } else if (item.endsWith(extension)) { files.push(path.relative(dir, itemPath)); } } } search(dir); return files; } function copyDirectory(src, dest, exclude = [], overwrite = true) { if (!fs.existsSync(src)) return; fs.mkdirSync(dest, { recursive: true }); for (const item of fs.readdirSync(src)) { if (exclude.includes(item)) continue; const srcPath = path.join(src, item); const destPath = path.join(dest, item); const stat = fs.statSync(srcPath); if (stat.isDirectory()) { copyDirectory(srcPath, destPath, exclude, overwrite); } else { if (overwrite || !fs.existsSync(destPath)) { fs.copyFileSync(srcPath, destPath); } } } } module.exports = router;