PRIMEIRO ENVIO

This commit is contained in:
2025-12-25 12:02:07 -03:00
parent ca49575224
commit c4d3bd9a86
92 changed files with 26976 additions and 1 deletions
+786
View File
@@ -0,0 +1,786 @@
const express = require('express');
const puppeteer = require('puppeteer');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const { URL } = require('url');
const app = express();
app.use(express.json());
class AdvancedWebCloner {
constructor() {
this.baseOutputDir = path.join(__dirname, '..', 'cloned-sites');
this.ensureDirectoryExists(this.baseOutputDir);
this.visitedPages = new Set();
this.downloadedAssets = new Map();
this.pageQueue = [];
}
ensureDirectoryExists(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
sanitizeFilename(filename) {
return filename.replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, '_').substring(0, 200);
}
getUniqueFilename(urlPath, existingFiles = new Set()) {
const parts = urlPath.split('/').filter(p => p);
const filename = parts.pop() || 'file';
const prefix = parts.slice(-3).join('_');
let baseName = this.sanitizeFilename(prefix ? `${prefix}_${filename}` : filename);
// Garantir unicidade
let finalName = baseName;
let counter = 1;
while (existingFiles.has(finalName)) {
const ext = path.extname(baseName);
const name = baseName.replace(ext, '');
finalName = `${name}_${counter}${ext}`;
counter++;
}
return finalName;
}
async downloadAsset(url, outputPath, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
// Para Google Fonts CSS, precisamos de user-agent específico para obter woff2
const isGoogleFonts = url.includes('fonts.googleapis.com');
const response = await axios.get(url, {
responseType: 'arraybuffer',
timeout: 30000,
maxContentLength: 50 * 1024 * 1024, // 50MB
headers: {
'User-Agent': isGoogleFonts
? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Referer': isGoogleFonts ? 'https://fonts.googleapis.com/' : url
}
});
this.ensureDirectoryExists(path.dirname(outputPath));
fs.writeFileSync(outputPath, response.data);
return true;
} catch (error) {
if (attempt === retries) {
// Não logar erro para fontes - muito verboso
if (!url.includes('.woff') && !url.includes('.ttf') && !url.includes('.eot')) {
console.log(` ⚠️ Falha após ${retries} tentativas: ${url.substring(0, 80)}`);
}
return false;
}
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
return false;
}
getAssetInfo(assetUrl, baseUrl, siteDir, existingFiles) {
try {
const fullUrl = new URL(assetUrl, baseUrl);
const pathname = fullUrl.pathname;
// Preservar a estrutura de pastas original
// Remove a barra inicial e mantém o caminho completo
let relativePath = pathname.startsWith('/') ? pathname.substring(1) : pathname;
// Se o caminho estiver vazio, gerar um nome
if (!relativePath || relativePath === '') {
relativePath = 'index_' + Date.now();
}
// Sanitizar apenas caracteres inválidos, mas manter a estrutura de pastas
relativePath = relativePath.replace(/[<>:"|?*]/g, '_');
const fullPath = path.join(siteDir, relativePath);
// Criar diretórios necessários
this.ensureDirectoryExists(path.dirname(fullPath));
return {
fullPath: fullPath,
relativePath: relativePath,
fullUrl: fullUrl.href
};
} catch (error) {
return null;
}
}
extractUrlsFromCSS(cssContent, cssUrl) {
const urls = [];
// Regex mais abrangente para capturar URLs em CSS
const patterns = [
/url\(['"]?([^'")\s]+)['"]?\)/g, // url(...)
/@import\s+['"]([^'"]+)['"]/g, // @import "..."
/@import\s+url\(['"]?([^'")\s]+)['"]?\)/g // @import url(...)
];
patterns.forEach(regex => {
let match;
while ((match = regex.exec(cssContent)) !== null) {
const url = match[1];
if (url && !url.startsWith('data:') && !url.startsWith('#')) {
try {
const fullUrl = new URL(url, cssUrl).href;
urls.push({ original: url, full: fullUrl });
} catch(e) {
// Tentar como caminho relativo
try {
const baseUrl = new URL(cssUrl);
const resolvedUrl = new URL(url, baseUrl.origin + baseUrl.pathname.substring(0, baseUrl.pathname.lastIndexOf('/') + 1)).href;
urls.push({ original: url, full: resolvedUrl });
} catch(e2) {}
}
}
}
});
return urls;
}
async extractAllAssets(page, pageUrl, $) {
const assets = new Map();
// 1. CSS - todos os tipos
$('link[rel="stylesheet"], link[rel="preload"][as="style"]').each((_, el) => {
const href = $(el).attr('href');
if (href && !href.startsWith('data:')) {
try { assets.set(href, new URL(href, pageUrl).href); } catch(e) {}
}
});
// 2. JavaScript - todos os tipos
$('script[src]').each((_, el) => {
const src = $(el).attr('src');
if (src && !src.startsWith('data:')) {
try { assets.set(src, new URL(src, pageUrl).href); } catch(e) {}
}
});
// 3. Imagens - múltiplos atributos
$('img, picture source, [data-src], [data-lazy-src], [data-original]').each((_, el) => {
const attrs = ['src', 'data-src', 'data-lazy-src', 'data-original', 'data-srcset', 'srcset'];
attrs.forEach(attr => {
const value = $(el).attr(attr);
if (value && !value.startsWith('data:')) {
// Processar srcset
if (attr.includes('srcset')) {
value.split(',').forEach(item => {
const url = item.trim().split(' ')[0];
if (url && !url.startsWith('data:')) {
try { assets.set(url, new URL(url, pageUrl).href); } catch(e) {}
}
});
} else {
try { assets.set(value, new URL(value, pageUrl).href); } catch(e) {}
}
}
});
});
// 4. Background images - style attributes e data attributes
$('[style*="url"], [data-background], [data-bg], [data-background-image]').each((_, el) => {
const attrs = ['style', 'data-background', 'data-bg', 'data-background-image'];
attrs.forEach(attr => {
const value = $(el).attr(attr) || '';
const matches = value.match(/url\(['"]?([^'")\s]+)['"]?\)/g);
if (matches) {
matches.forEach(match => {
const url = match.replace(/url\(['"]?/, '').replace(/['"]?\)/, '');
if (url && !url.startsWith('data:')) {
try { assets.set(url, new URL(url, pageUrl).href); } catch(e) {}
}
});
}
});
});
// 5. Vídeos e áudio
$('video, audio, source, video[poster]').each((_, el) => {
const attrs = ['src', 'poster'];
attrs.forEach(attr => {
const value = $(el).attr(attr);
if (value && !value.startsWith('data:')) {
try { assets.set(value, new URL(value, pageUrl).href); } catch(e) {}
}
});
});
// 6. Favicon e icons
$('link[rel*="icon"], link[rel="apple-touch-icon"]').each((_, el) => {
const href = $(el).attr('href');
if (href && !href.startsWith('data:')) {
try { assets.set(href, new URL(href, pageUrl).href); } catch(e) {}
}
});
// 7. Preload e prefetch
$('link[rel="preload"], link[rel="prefetch"]').each((_, el) => {
const href = $(el).attr('href');
if (href && !href.startsWith('data:')) {
try { assets.set(href, new URL(href, pageUrl).href); } catch(e) {}
}
});
// 8. Fonts do Google Fonts - baixar o CSS e as fontes
const googleFontsLinks = [];
$('link[href*="fonts.googleapis.com"]').each((_, el) => {
const href = $(el).attr('href');
if (href) {
googleFontsLinks.push(href);
try { assets.set(href, new URL(href, pageUrl).href); } catch(e) {}
}
});
// 9. Fonts locais e de outros CDNs
$('link[href*="fonts.gstatic.com"], link[href*="font"]').each((_, el) => {
const href = $(el).attr('href');
if (href) {
try { assets.set(href, new URL(href, pageUrl).href); } catch(e) {}
}
});
// 9. Extrair assets de inline styles
const inlineStyles = await page.evaluate(() => {
const styles = [];
document.querySelectorAll('style').forEach(style => {
styles.push(style.textContent);
});
return styles;
});
inlineStyles.forEach(styleContent => {
const matches = styleContent.match(/url\(['"]?([^'")\s]+)['"]?\)/g);
if (matches) {
matches.forEach(match => {
const url = match.replace(/url\(['"]?/, '').replace(/['"]?\)/, '');
if (url && !url.startsWith('data:') && !url.startsWith('#')) {
try { assets.set(url, new URL(url, pageUrl).href); } catch(e) {}
}
});
}
});
return assets;
}
async discoverInternalLinks(page, baseHostClean, startUrl) {
return await page.evaluate((baseHostClean, startUrl) => {
const links = new Set();
const baseUrl = new URL(startUrl);
document.querySelectorAll('a[href]').forEach(a => {
try {
const href = a.href;
const url = new URL(href);
// Verificar se é link interno
if (url.hostname.includes(baseHostClean) || url.hostname === baseUrl.hostname) {
// Filtrar extensões de arquivo
if (!url.pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|webp|ico|pdf|zip|doc|docx|xls|xlsx|mp4|mp3|avi)$/i)) {
// Remover query strings e fragments para normalizar
const cleanUrl = `${url.origin}${url.pathname}`.replace(/\/$/, '');
if (cleanUrl &&
!cleanUrl.includes('mailto:') &&
!cleanUrl.includes('tel:') &&
!cleanUrl.includes('javascript:')) {
links.add(cleanUrl);
}
}
}
} catch(e) {}
});
return Array.from(links);
}, baseHostClean, startUrl);
}
async cloneWebsite(startUrl) {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--disable-gpu'
]
});
try {
const page = await browser.newPage();
// Configurar viewport grande para capturar mais conteúdo
await page.setViewport({ width: 1920, height: 1080 });
// Configurar user agent realista
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
console.log(`\n🚀 INICIANDO CLONAGEM PROFUNDA DE: ${startUrl}\n`);
const parsedUrl = new URL(startUrl);
const baseHost = parsedUrl.hostname;
const baseHostClean = baseHost.replace('www.', '');
// Criar diretório
const siteName = this.sanitizeFilename(baseHost);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const siteDir = path.join(this.baseOutputDir, `${siteName}_${timestamp}`);
this.ensureDirectoryExists(siteDir);
console.log(`📁 Diretório: ${siteDir}\n`);
// Fase 1: Descobrir todas as páginas
console.log(`🔍 FASE 1: Descobrindo páginas do site...`);
const response = await page.goto(startUrl, {
waitUntil: 'networkidle0',
timeout: 60000
});
await page.waitForTimeout(3000);
// Descobrir links internos
const internalLinks = await this.discoverInternalLinks(page, baseHostClean, startUrl);
// Criar mapa de páginas
const pageMap = new Map();
const existingFiles = new Set();
// Adicionar página inicial
pageMap.set(startUrl, { file: 'index.html', title: 'Home' });
existingFiles.add('index.html');
// Adicionar páginas descobertas
for (const link of internalLinks) {
if (!pageMap.has(link)) {
const urlObj = new URL(link);
let pathname = urlObj.pathname.replace(/^\/|\/$/g, '');
let filename;
if (!pathname) {
filename = 'home.html';
} else {
filename = pathname.replace(/\//g, '_') + '.html';
}
// Garantir unicidade
let finalFilename = filename;
let counter = 1;
while (existingFiles.has(finalFilename)) {
finalFilename = filename.replace('.html', `_${counter}.html`);
counter++;
}
pageMap.set(link, { file: finalFilename, title: pathname || 'page' });
existingFiles.add(finalFilename);
}
}
console.log(`✅ Encontradas ${pageMap.size} páginas para clonar\n`);
// Fase 2: Clonar cada página
console.log(`📄 FASE 2: Clonando páginas e coletando assets...`);
const globalAssets = new Map();
const pageContents = new Map();
const assetFiles = new Set();
let pageIndex = 0;
for (const [pageUrl, pageInfo] of pageMap) {
pageIndex++;
console.log(`\n [${pageIndex}/${pageMap.size}] ${pageInfo.file}`);
try {
await page.goto(pageUrl, {
waitUntil: 'networkidle0',
timeout: 60000
});
// Esperar carregamento completo
await page.waitForTimeout(3000);
// Scroll completo para carregar lazy content
console.log(` 📜 Fazendo scroll para carregar conteúdo lazy...`);
await page.evaluate(async () => {
await new Promise((resolve) => {
let totalHeight = 0;
const distance = 300;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
window.scrollTo(0, 0);
setTimeout(resolve, 1000);
}
}, 100);
});
});
await page.waitForTimeout(2000);
// Capturar conteúdo
const content = await page.content();
pageContents.set(pageUrl, content);
const $ = cheerio.load(content);
// Extrair todos os assets
console.log(` 🔍 Extraindo assets...`);
const pageAssets = await this.extractAllAssets(page, pageUrl, $);
// Adicionar ao conjunto global
for (const [original, full] of pageAssets) {
globalAssets.set(original, full);
}
console.log(`${pageAssets.size} assets encontrados`);
} catch (err) {
console.log(` ❌ Erro: ${err.message}`);
}
}
console.log(`\n✅ Total de ${globalAssets.size} assets únicos encontrados\n`);
// Fase 3: Baixar todos os assets
console.log(`📥 FASE 3: Baixando assets...`);
const replacements = new Map();
const batchSize = 5; // Reduzido para evitar sobrecarga
const assetEntries = [...globalAssets.entries()];
let successCount = 0;
let failCount = 0;
for (let i = 0; i < assetEntries.length; i += batchSize) {
const batch = assetEntries.slice(i, i + batchSize);
const promises = batch.map(async ([originalUrl, fullUrl]) => {
const info = this.getAssetInfo(fullUrl, startUrl, siteDir, assetFiles);
if (info) {
const ok = await this.downloadAsset(info.fullUrl, info.fullPath);
if (ok) {
replacements.set(originalUrl, info.relativePath);
replacements.set(info.fullUrl, info.relativePath);
// Variações de protocolo
if (info.fullUrl.startsWith('https://')) {
replacements.set(info.fullUrl.replace('https://', 'http://'), info.relativePath);
} else if (info.fullUrl.startsWith('http://')) {
replacements.set(info.fullUrl.replace('http://', 'https://'), info.relativePath);
}
successCount++;
return { ok: true, info, fullUrl };
} else {
failCount++;
}
}
return { ok: false };
});
await Promise.all(promises);
const progress = Math.round((i + batch.length) / assetEntries.length * 100);
console.log(` 📊 Progresso: ${progress}% (${successCount} ok, ${failCount} falhas)`);
}
console.log(`\n${successCount} assets baixados com sucesso\n`);
// Fase 4: Extrair assets de CSS
console.log(`🎨 FASE 4: Extraindo assets de arquivos CSS...`);
const cssAssets = new Map();
let cssAssetCount = 0;
for (const [original, local] of replacements) {
if (local.endsWith('.css')) {
try {
const cssPath = path.join(siteDir, local);
if (fs.existsSync(cssPath)) {
const cssContent = fs.readFileSync(cssPath, 'utf8');
// Determinar URL base do CSS
let cssBaseUrl = original;
for (const [origUrl, localPath] of replacements) {
if (localPath === local) {
cssBaseUrl = origUrl;
break;
}
}
const cssUrls = this.extractUrlsFromCSS(cssContent, cssBaseUrl);
for (const cssUrl of cssUrls) {
if (!replacements.has(cssUrl.full) && !cssAssets.has(cssUrl.full)) {
cssAssets.set(cssUrl.original, cssUrl.full);
}
}
}
} catch(e) {}
}
}
if (cssAssets.size > 0) {
console.log(` 📦 Encontrados ${cssAssets.size} assets em CSS (fontes, imagens, etc.)`);
for (const [originalUrl, fullUrl] of cssAssets) {
const info = this.getAssetInfo(fullUrl, startUrl, siteDir, assetFiles);
if (info) {
const ok = await this.downloadAsset(info.fullUrl, info.fullPath);
if (ok) {
replacements.set(originalUrl, info.relativePath);
replacements.set(info.fullUrl, info.relativePath);
cssAssetCount++;
// Atualizar CSS files para substituir URLs
for (const [origCss, localCss] of replacements) {
if (localCss.endsWith('.css')) {
try {
const cssPath = path.join(siteDir, localCss);
if (fs.existsSync(cssPath)) {
let cssContent = fs.readFileSync(cssPath, 'utf8');
const escaped = originalUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
cssContent = cssContent.replace(new RegExp(escaped, 'g'), info.relativePath);
fs.writeFileSync(cssPath, cssContent, 'utf8');
}
} catch(e) {}
}
}
}
}
}
console.log(`${cssAssetCount} assets de CSS baixados`);
} else {
console.log(` ️ Nenhum asset adicional encontrado em CSS`);
}
// Fase 5: Processar e salvar páginas
console.log(`\n💾 FASE 5: Processando e salvando páginas HTML...\n`);
const sortedReplacements = [...replacements.entries()]
.sort((a, b) => b[0].length - a[0].length);
const elementsToRemove = [
'.cookie-banner', '.cookie-notice', '.cookie-consent', '.cookies-banner',
'.lgpd-banner', '.lgpd-notice', '.lgpd-popup', '.lgpd',
'#cookie-banner', '#cookie-notice', '#cookie-consent', '#cookies',
'#lgpd-banner', '#lgpd', '#lgpd-notice',
'[class*="cookie"]', '[class*="lgpd"]', '[class*="gdpr"]',
'[id*="cookie"]', '[id*="lgpd"]', '[id*="gdpr"]',
'.popup-overlay', '.modal-overlay', '.consent-banner',
'[class*="consent"]', '[class*="privacy-banner"]',
'.joinchat', '#joinchat', '.joinchat__button',
];
let pagesSaved = 0;
for (const [pageUrl, pageInfo] of pageMap) {
let html = pageContents.get(pageUrl);
if (!html) continue;
console.log(` 💾 ${pageInfo.file}`);
// Remover elementos indesejados
const $ = cheerio.load(html);
for (const selector of elementsToRemove) {
try { $(selector).remove(); } catch(e) {}
}
// Remover scripts problemáticos e externos que não funcionam offline
$('script').each((_, el) => {
const src = $(el).attr('src') || '';
const content = $(el).html() || '';
// Lista de scripts a remover
const removePatterns = [
'cookie', 'lgpd', 'consent', 'gdpr',
'recaptcha', 'google-analytics', 'googletagmanager', 'gtag', 'gtm',
'facebook.net', 'fbevents', 'fbq',
'doubleclick.net', 'googleadservices',
'analytics.js', 'ga.js'
];
const shouldRemove = removePatterns.some(pattern =>
src.includes(pattern) || content.includes(pattern)
);
if (shouldRemove && !src.includes('jquery') && !src.includes('elementor')) {
$(el).remove();
}
});
// Remover iframes externos (Google Tag Manager, etc)
$('iframe[src*="googletagmanager"], iframe[src*="facebook"], iframe[src*="doubleclick"]').remove();
// Remover noscript tags de tracking
$('noscript').each((_, el) => {
const content = $(el).html() || '';
if (content.includes('googletagmanager') || content.includes('facebook')) {
$(el).remove();
}
});
html = $.html();
// Substituir assets - usar substituição mais precisa
for (const [original, local] of sortedReplacements) {
// Escapar caracteres especiais
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Substituir em diferentes contextos
// 1. Em atributos HTML (src, href, data-src, etc)
html = html.replace(new RegExp(`(src|href|data-src|data-lazy-src|data-background|poster)=["']${escaped}["']`, 'gi'), `$1="${local}"`);
// 2. Em CSS inline (background-image, etc)
html = html.replace(new RegExp(`url\\(['"]?${escaped}['"]?\\)`, 'gi'), `url("${local}")`);
// 3. Substituição direta
html = html.replace(new RegExp(escaped, 'g'), local);
}
// Substituir links internos
for (const [url, info] of pageMap) {
const escaped = url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
html = html.replace(new RegExp(`href=["']${escaped}["']`, 'gi'), `href="${info.file}"`);
const urlObj = new URL(url);
if (urlObj.pathname && urlObj.pathname !== '/') {
const pathname = urlObj.pathname.replace(/\/$/, '');
html = html.replace(
new RegExp(`href=["']${pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']`, 'gi'),
`href="${info.file}"`
);
}
}
// Links restantes do domínio -> index.html
html = html.replace(
new RegExp(`href=["']https?://(www\\.)?${baseHostClean.replace(/\./g, '\\.')}[^"']*["']`, 'gi'),
'href="index.html"'
);
// Adicionar script para suprimir erros e melhorar compatibilidade offline
const offlineScript = `
<script>
// Suprimir erros de scripts externos
window.addEventListener('error', function(e) {
// Ignorar erros de recursos externos
if (e.filename && (
e.filename.includes('recaptcha') ||
e.filename.includes('google') ||
e.filename.includes('facebook') ||
e.filename.includes('doubleclick')
)) {
e.preventDefault();
return true;
}
}, true);
// Criar stubs para funções que podem estar faltando
window.fbq = window.fbq || function() { console.log('fbq stub called'); };
window.gtag = window.gtag || function() { console.log('gtag stub called'); };
window.dataLayer = window.dataLayer || [];
window.ga = window.ga || function() { console.log('ga stub called'); };
// Desabilitar reCAPTCHA se presente
if (typeof grecaptcha !== 'undefined') {
grecaptcha.ready = function(cb) { console.log('reCAPTCHA disabled offline'); };
}
console.log('🌐 Site clonado carregado em modo offline');
</script>
`;
// Inserir antes do </body>
html = html.replace('</body>', offlineScript + '</body>');
fs.writeFileSync(path.join(siteDir, pageInfo.file), html, 'utf8');
pagesSaved++;
}
// Salvar informações
const info = {
originalUrl: startUrl,
clonedAt: new Date().toISOString(),
pages: [...pageMap.entries()].map(([url, info]) => ({ url, file: info.file })),
assetsDownloaded: successCount + cssAssetCount,
assetsFailed: failCount,
totalAssets: globalAssets.size + cssAssets.size,
directory: siteDir
};
fs.writeFileSync(path.join(siteDir, 'clone-info.json'), JSON.stringify(info, null, 2));
fs.writeFileSync(path.join(siteDir, 'abrir-site.bat'), `@echo off\nstart index.html`);
console.log(`\n${'='.repeat(60)}`);
console.log(`🎉 CLONAGEM CONCLUÍDA COM SUCESSO!`);
console.log(`${'='.repeat(60)}`);
console.log(`📄 Páginas clonadas: ${pagesSaved}`);
console.log(`📦 Assets baixados: ${successCount + cssAssetCount}`);
console.log(`⚠️ Assets com falha: ${failCount}`);
console.log(`📁 Localização: ${siteDir}`);
console.log(`${'='.repeat(60)}\n`);
return {
url: startUrl,
title: 'Site Clonado',
statusCode: response.status(),
links: [],
images: [],
contentLength: 0,
timestamp: new Date().toISOString(),
cloneInfo: {
directory: siteDir,
pagesCloned: pagesSaved,
assetsDownloaded: successCount + cssAssetCount,
assetsFailed: failCount,
totalAssets: globalAssets.size + cssAssets.size,
success: true
}
};
} finally {
await browser.close();
}
}
}
// API
app.post('/crawl', async (req, res) => {
try {
const { url } = req.body;
if (!url) return res.status(400).json({ error: 'URL obrigatória' });
const cloner = new AdvancedWebCloner();
const result = await cloner.cloneWebsite(url);
res.json({ success: true, data: result });
} catch (error) {
console.error('❌ Erro:', error.message);
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'advanced-web-cloner' });
});
app.listen(3001, () => {
console.log('🌐 Advanced Web Cloner rodando em http://localhost:3001');
console.log('🚀 Pronto para clonagem profunda de sites!\n');
});