422 lines
13 KiB
JavaScript
422 lines
13 KiB
JavaScript
/**
|
|
* 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;
|