PRIMEIRO ENVIO
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# HSTS 1.0 Known Hosts database for GNU Wget.
|
||||
# Edit at your own risk.
|
||||
# <hostname> <port> <incl. subdomains> <created> <max-age>
|
||||
www.linkedin.com 0 0 1766599772 31536000
|
||||
Generated
+2558
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "simple-crawler-test",
|
||||
"version": "1.0.0",
|
||||
"description": "Simple crawler for testing",
|
||||
"main": "test-crawler.js",
|
||||
"scripts": {
|
||||
"start": "node test-crawler.js",
|
||||
"dev": "node test-crawler.js",
|
||||
"test": "echo \"Test with curl command\""
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"express": "^4.18.2",
|
||||
"puppeteer": "^21.5.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Servidor simples para servir sites clonados
|
||||
const app = express();
|
||||
const PORT = 8080;
|
||||
|
||||
// Middleware para servir arquivos estáticos com CORS habilitado
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
next();
|
||||
});
|
||||
|
||||
// Servir arquivos estáticos do diretório cloned-sites
|
||||
const clonedSitesDir = path.join(__dirname, '..', 'cloned-sites');
|
||||
app.use(express.static(clonedSitesDir));
|
||||
|
||||
// Listar sites clonados
|
||||
app.get('/api/sites', (req, res) => {
|
||||
try {
|
||||
const dirs = fs.readdirSync(clonedSitesDir)
|
||||
.filter(file => fs.statSync(path.join(clonedSitesDir, file)).isDirectory())
|
||||
.map(dir => ({
|
||||
name: dir,
|
||||
path: `/${dir}/index.html`,
|
||||
fullPath: path.join(clonedSitesDir, dir)
|
||||
}));
|
||||
res.json({ sites: dirs });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Página inicial com lista de sites
|
||||
app.get('/', (req, res) => {
|
||||
try {
|
||||
const dirs = fs.readdirSync(clonedSitesDir)
|
||||
.filter(file => fs.statSync(path.join(clonedSitesDir, file)).isDirectory());
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sites Clonados - Servidor Local</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.sites-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.site-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.site-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.3);
|
||||
}
|
||||
.site-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
.site-link {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.site-link:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
padding: 3rem;
|
||||
}
|
||||
.info {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🌐 Sites Clonados</h1>
|
||||
<div class="info">
|
||||
<p>Servidor local rodando em <strong>http://localhost:${PORT}</strong></p>
|
||||
<p>Sem problemas de CORS ou permissões!</p>
|
||||
</div>
|
||||
${dirs.length > 0 ? `
|
||||
<div class="sites-grid">
|
||||
${dirs.map(dir => `
|
||||
<div class="site-card">
|
||||
<div class="site-name">${dir}</div>
|
||||
<a href="/${dir}/index.html" class="site-link" target="_blank">
|
||||
Abrir Site →
|
||||
</a>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : `
|
||||
<div class="empty-state">
|
||||
<p>Nenhum site clonado ainda.</p>
|
||||
<p>Use o CloneWeb para clonar um site!</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
res.send(html);
|
||||
} catch (error) {
|
||||
res.status(500).send(`<h1>Erro: ${error.message}</h1>`);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`🌐 Servidor de Sites Clonados`);
|
||||
console.log(`${'='.repeat(60)}`);
|
||||
console.log(`📡 Rodando em: http://localhost:${PORT}`);
|
||||
console.log(`📁 Servindo de: ${clonedSitesDir}`);
|
||||
console.log(`✅ Sem problemas de CORS!`);
|
||||
console.log(`${'='.repeat(60)}\n`);
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const puppeteer = require('puppeteer');
|
||||
const { URL } = require('url');
|
||||
|
||||
class SmartCloner {
|
||||
constructor() {
|
||||
this.baseOutputDir = path.join(__dirname, '..', 'cloned-sites');
|
||||
this.ensureDirectoryExists(this.baseOutputDir);
|
||||
this.resourceMap = new Map(); // Map<Url, LocalPath>
|
||||
}
|
||||
|
||||
ensureDirectoryExists(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeFilename(filename) {
|
||||
if (!filename) return 'unnamed';
|
||||
return filename.replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, '_').substring(0, 200);
|
||||
}
|
||||
|
||||
generateLocalPath(url, contentType, baseDir) {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
let pathname = urlObj.pathname;
|
||||
|
||||
if (pathname.endsWith('/') || pathname === '') {
|
||||
const isAsset = /\.(js|css|png|jpg|jpeg|gif|svg|woff2?|ttf|otf)$/i.test(pathname.slice(0, -1));
|
||||
if (!isAsset) {
|
||||
pathname += 'index.html';
|
||||
} else {
|
||||
pathname = pathname.slice(0, -1); // Remove trailing slash if it's an asset
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a contentType, try to get the correct extension
|
||||
let ext = path.extname(pathname);
|
||||
if (!ext) {
|
||||
if (contentType) {
|
||||
if (contentType.includes('text/html')) ext = '.html';
|
||||
else if (contentType.includes('text/css')) ext = '.css';
|
||||
else if (contentType.includes('javascript')) ext = '.js';
|
||||
else if (contentType.includes('image/png')) ext = '.png';
|
||||
else if (contentType.includes('image/jpeg')) ext = '.jpg';
|
||||
else if (contentType.includes('image/gif')) ext = '.gif';
|
||||
else if (contentType.includes('image/svg')) ext = '.svg';
|
||||
else if (contentType.includes('application/json')) ext = '.json';
|
||||
else if (contentType.includes('font/')) {
|
||||
if (contentType.includes('woff2')) ext = '.woff2';
|
||||
else if (contentType.includes('woff')) ext = '.woff';
|
||||
else if (contentType.includes('ttf')) ext = '.ttf';
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback if still no extension and looks like a route
|
||||
if (!ext && !pathname.includes('.')) {
|
||||
ext = '.html';
|
||||
}
|
||||
|
||||
if (ext) pathname += ext;
|
||||
}
|
||||
|
||||
// Sanitize the path for Windows
|
||||
const segments = pathname.split('/').filter(s => s).map(p => this.sanitizeFilename(p));
|
||||
return path.join(baseDir, ...segments);
|
||||
} catch (e) {
|
||||
return path.join(baseDir, 'assets', `resource_${Date.now()}`);
|
||||
}
|
||||
}
|
||||
|
||||
async cloneSite(url, maxPages = 20) {
|
||||
console.log(`\n🚀 INICIANDO DEEP SMART CLONE: ${url}`);
|
||||
console.log(`📂 Limite de páginas: ${maxPages}\n`);
|
||||
|
||||
this.resourceMap.clear();
|
||||
let browser = null;
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const siteName = this.sanitizeFilename(urlObj.hostname);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const siteDir = path.join(this.baseOutputDir, `${siteName}_${timestamp}`);
|
||||
this.ensureDirectoryExists(siteDir);
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--window-size=1920,1080']
|
||||
});
|
||||
|
||||
const pagesToVisit = [url];
|
||||
const visitedPages = new Set();
|
||||
const pageMap = new Map(); // URL -> LocalFilename
|
||||
|
||||
// Pre-seed the map so links can be rewritten to files we haven't visited yet
|
||||
pageMap.set(url.replace(/\/$/, ''), 'index.html');
|
||||
|
||||
let pageCount = 0;
|
||||
while (pagesToVisit.length > 0 && pageCount < maxPages) {
|
||||
const currentUrl = pagesToVisit.shift();
|
||||
const cleanCurrentUrl = currentUrl.replace(/\/$/, '');
|
||||
|
||||
if (visitedPages.has(cleanCurrentUrl)) continue;
|
||||
|
||||
pageCount++;
|
||||
visitedPages.add(cleanCurrentUrl);
|
||||
|
||||
console.log(`📄 [${pageCount}/${maxPages}] Clonando: ${currentUrl}`);
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
// Asset interception
|
||||
page.on('response', async (response) => {
|
||||
const rUrl = response.url();
|
||||
if (rUrl.startsWith('data:') || !response.ok() || response.request().method() !== 'GET') return;
|
||||
try {
|
||||
const contentType = response.headers()['content-type'];
|
||||
const buffer = await response.buffer();
|
||||
const localPath = this.generateLocalPath(rUrl, contentType, siteDir);
|
||||
this.ensureDirectoryExists(path.dirname(localPath));
|
||||
fs.writeFileSync(localPath, buffer);
|
||||
this.resourceMap.set(rUrl, path.relative(siteDir, localPath).replace(/\\/g, '/'));
|
||||
} catch (e) { }
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(currentUrl, { waitUntil: 'networkidle2', timeout: 60000 });
|
||||
} catch (e) {
|
||||
console.log(`⚠️ Falha ao carregar ${currentUrl}: ${e.message}`);
|
||||
await page.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.autoScroll(page);
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
// Discover and map internal links BEFORE rewriting
|
||||
const discovered = await page.evaluate((domain) => {
|
||||
return Array.from(document.querySelectorAll('a[href]'))
|
||||
.map(a => a.href.split('#')[0].replace(/\/$/, ''))
|
||||
.filter(href => href.startsWith(window.location.origin) && !href.includes('wp-admin') && !href.includes('login'));
|
||||
}, urlObj.origin);
|
||||
|
||||
discovered.forEach(link => {
|
||||
if (!pageMap.has(link)) {
|
||||
let name = link.split('/').pop().split('?')[0] || `page_${pageMap.size + 1}`;
|
||||
if (!name.endsWith('.html')) name += '.html';
|
||||
pageMap.set(link, name);
|
||||
}
|
||||
if (!visitedPages.has(link) && !pagesToVisit.includes(link)) {
|
||||
pagesToVisit.push(link);
|
||||
}
|
||||
});
|
||||
|
||||
// Rewrite and Clean
|
||||
const resMapArr = Array.from(this.resourceMap.entries());
|
||||
const pagMapArr = Array.from(pageMap.entries());
|
||||
|
||||
await page.evaluate((resArr, pagArr) => {
|
||||
const rMap = new Map(resArr);
|
||||
const pMap = new Map(pagArr);
|
||||
|
||||
function getLocal(url) {
|
||||
if (!url) return null;
|
||||
try {
|
||||
const full = new URL(url, document.baseURI).href.split('#')[0].replace(/\/$/, '');
|
||||
if (rMap.has(full)) return rMap.get(full);
|
||||
if (pMap.has(full)) return pMap.get(full);
|
||||
return null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
// 1. Rewrite Links, Images, Scripts
|
||||
document.querySelectorAll('[src], [href], [data-src], [srcset]').forEach(el => {
|
||||
['src', 'href', 'data-src'].forEach(attr => {
|
||||
if (el.hasAttribute(attr)) {
|
||||
const local = getLocal(el.getAttribute(attr));
|
||||
if (local) el.setAttribute(attr, local);
|
||||
}
|
||||
});
|
||||
|
||||
if (el.hasAttribute('srcset')) {
|
||||
const parts = el.getAttribute('srcset').split(',');
|
||||
const mapped = parts.map(p => {
|
||||
const [u, ...rest] = p.trim().split(/\s+/);
|
||||
const l = getLocal(u);
|
||||
return l ? [l, ...rest].join(' ') : p;
|
||||
});
|
||||
el.setAttribute('srcset', mapped.join(', '));
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Clear common barriers
|
||||
const problematic = ['script[src*="analytics"]', 'script[src*="facebook"]', 'iframe[src*="google.com/maps"]', '.cookie-banner', '.modal-backdrop'];
|
||||
problematic.forEach(s => document.querySelectorAll(s).forEach(e => e.remove()));
|
||||
|
||||
// 3. Smart Fixer Script
|
||||
const script = document.createElement('script');
|
||||
script.textContent = `
|
||||
(function() {
|
||||
function fix() {
|
||||
document.documentElement.style.setProperty('overflow', 'auto', 'important');
|
||||
document.body.style.setProperty('overflow', 'auto', 'important');
|
||||
document.body.style.setProperty('pointer-events', 'auto', 'important');
|
||||
const loaders = ['#preloader', '.loader', '.loading-overlay', '.elementor-loader'];
|
||||
loaders.forEach(s => document.querySelectorAll(s).forEach(e => e.remove()));
|
||||
}
|
||||
fix(); setInterval(fix, 2000);
|
||||
window.fbq = window.gtag = function(){};
|
||||
})();
|
||||
`;
|
||||
document.body.appendChild(script);
|
||||
}, resMapArr, pagMapArr);
|
||||
|
||||
const html = await page.content();
|
||||
const localFile = pageMap.get(cleanCurrentUrl) || 'index.html';
|
||||
fs.writeFileSync(path.join(siteDir, localFile), html);
|
||||
await page.close();
|
||||
}
|
||||
|
||||
const info = { url, timestamp: new Date().toISOString(), pages: pageCount, directory: siteDir };
|
||||
fs.writeFileSync(path.join(siteDir, 'clone-info.json'), JSON.stringify(info, null, 2));
|
||||
|
||||
return { success: true, path: siteDir, pagesCloned: pageCount };
|
||||
} catch (e) {
|
||||
console.error('❌ Erro Deep Clone:', e);
|
||||
return { success: false, error: e.message };
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
async autoScroll(page) {
|
||||
await page.evaluate(async () => {
|
||||
await new Promise((resolve) => {
|
||||
let totalHeight = 0;
|
||||
const distance = 100;
|
||||
const timer = setInterval(() => {
|
||||
const scrollHeight = document.body.scrollHeight;
|
||||
window.scrollBy(0, distance);
|
||||
totalHeight += distance;
|
||||
if (totalHeight >= scrollHeight - window.innerHeight) {
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const urlArg = args.find(arg => arg.startsWith('--url='));
|
||||
const url = urlArg ? urlArg.split('=')[1] : args[0];
|
||||
|
||||
if (url) {
|
||||
const cloner = new SmartCloner();
|
||||
cloner.cloneSite(url).then(res => {
|
||||
console.log(JSON.stringify(res, null, 2));
|
||||
process.exit(res.success ? 0 : 1);
|
||||
});
|
||||
} else {
|
||||
console.log('Usage: node smart-cloner.js --url=<url>');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SmartCloner;
|
||||
@@ -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');
|
||||
});
|
||||
@@ -0,0 +1,338 @@
|
||||
const express = require('express');
|
||||
const { exec, spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const axios = require('axios');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
class WgetCloner {
|
||||
constructor() {
|
||||
this.baseOutputDir = path.join(__dirname, '..', 'cloned-sites');
|
||||
this.wgetPath = path.join(__dirname, 'wget.exe');
|
||||
this.ensureDirectoryExists(this.baseOutputDir);
|
||||
}
|
||||
|
||||
ensureDirectoryExists(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeFilename(filename) {
|
||||
return filename.replace(/[<>:"/\\|?*]/g, '_').replace(/\s+/g, '_');
|
||||
}
|
||||
|
||||
async checkWget() {
|
||||
return new Promise((resolve) => {
|
||||
exec('where wget', (error, stdout) => {
|
||||
if (error || !stdout) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async downloadWget() {
|
||||
console.log('📥 Baixando wget para Windows...');
|
||||
|
||||
try {
|
||||
// URL do wget para Windows (versão portável)
|
||||
const wgetUrl = 'https://eternallybored.org/misc/wget/1.21.4/64/wget.exe';
|
||||
|
||||
console.log('🔗 Conectando ao servidor...');
|
||||
const response = await axios.get(wgetUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 120000,
|
||||
maxContentLength: 50 * 1024 * 1024, // 50MB
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
process.stdout.write(`\r📦 Baixando: ${percentCompleted}%`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n💾 Salvando arquivo...');
|
||||
fs.writeFileSync(this.wgetPath, response.data);
|
||||
console.log('✅ wget baixado com sucesso!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('\n❌ Erro ao baixar wget:', error.message);
|
||||
console.log('💡 Você pode baixar manualmente de: https://eternallybored.org/misc/wget/');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async cloneWebsite(url) {
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`🚀 CLONAGEM PROFISSIONAL COM WGET`);
|
||||
console.log(`${'='.repeat(60)}`);
|
||||
console.log(`🌐 URL: ${url}\n`);
|
||||
|
||||
// Sempre usar o wget.exe local primeiro
|
||||
let wgetCommand = this.wgetPath;
|
||||
|
||||
// Verificar se o wget.exe local existe
|
||||
if (!fs.existsSync(this.wgetPath)) {
|
||||
console.log('⚠️ wget.exe local não encontrado');
|
||||
|
||||
// Tentar baixar
|
||||
const downloaded = await this.downloadWget();
|
||||
if (!downloaded) {
|
||||
// Tentar usar wget do sistema como fallback
|
||||
const hasWget = await this.checkWget();
|
||||
if (hasWget) {
|
||||
console.log('✅ Usando wget do sistema');
|
||||
wgetCommand = 'wget';
|
||||
} else {
|
||||
throw new Error('Não foi possível baixar o wget. Instale manualmente: https://eternallybored.org/misc/wget/');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('✅ Usando wget.exe local');
|
||||
}
|
||||
|
||||
// Criar diretório para o site
|
||||
const urlObj = new URL(url);
|
||||
const siteName = this.sanitizeFilename(urlObj.hostname);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const siteDir = path.join(this.baseOutputDir, `${siteName}_${timestamp}`);
|
||||
this.ensureDirectoryExists(siteDir);
|
||||
|
||||
console.log(`📁 Salvando em: ${siteDir}\n`);
|
||||
|
||||
// Argumentos do wget - ABORDAGEM SIMPLES E EFICAZ
|
||||
const wgetArgs = [
|
||||
'--recursive', // Baixar recursivamente
|
||||
'--no-clobber', // Não sobrescrever arquivos
|
||||
'--page-requisites', // Baixar CSS, JS, imagens
|
||||
'--html-extension', // Adicionar .html
|
||||
'--convert-links', // Converter links para locais
|
||||
'--restrict-file-names=windows', // Nomes compatíveis com Windows
|
||||
'--domains=' + urlObj.hostname, // Apenas este domínio
|
||||
'--no-parent', // Não subir diretórios
|
||||
'--no-check-certificate', // Ignorar SSL
|
||||
'-e', 'robots=off', // Ignorar robots.txt
|
||||
`-P`, siteDir, // Diretório de saída
|
||||
url
|
||||
];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('🔄 Iniciando clonagem...\n');
|
||||
|
||||
const wgetProcess = spawn(wgetCommand, wgetArgs, {
|
||||
cwd: this.baseOutputDir,
|
||||
shell: true
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
wgetProcess.stdout.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
|
||||
// Mostrar progresso
|
||||
const lines = text.split('\n');
|
||||
lines.forEach(line => {
|
||||
if (line.includes('%') || line.includes('saved') || line.includes('=>')) {
|
||||
console.log(` ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
wgetProcess.stderr.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
errorOutput += text;
|
||||
|
||||
// Mostrar erros importantes
|
||||
if (text.includes('ERROR') || text.includes('failed')) {
|
||||
console.log(` ⚠️ ${text.trim()}`);
|
||||
}
|
||||
});
|
||||
|
||||
wgetProcess.on('close', (code) => {
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
|
||||
if (code === 0 || code === 8) { // 8 = alguns arquivos falharam, mas ok
|
||||
console.log(`✅ CLONAGEM CONCLUÍDA!`);
|
||||
console.log(`${'='.repeat(60)}`);
|
||||
console.log(`📁 Localização: ${siteDir}`);
|
||||
|
||||
// Baixar imagens faltantes manualmente
|
||||
console.log(`\n🔍 Verificando imagens faltantes...`);
|
||||
this.downloadMissingImages(siteDir, url).then(() => {
|
||||
console.log(`🌐 Abra o arquivo index.html no navegador`);
|
||||
console.log(`${'='.repeat(60)}\n`);
|
||||
});
|
||||
|
||||
// Criar arquivo de informações
|
||||
const info = {
|
||||
originalUrl: url,
|
||||
clonedAt: new Date().toISOString(),
|
||||
directory: siteDir,
|
||||
method: 'wget',
|
||||
success: true
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(siteDir, 'clone-info.json'),
|
||||
JSON.stringify(info, null, 2)
|
||||
);
|
||||
|
||||
// Criar arquivo .bat para abrir
|
||||
const indexPath = this.findIndexFile(siteDir);
|
||||
if (indexPath) {
|
||||
fs.writeFileSync(
|
||||
path.join(siteDir, 'abrir-site.bat'),
|
||||
`@echo off\nstart "" "${indexPath}"`
|
||||
);
|
||||
}
|
||||
|
||||
resolve({
|
||||
url: url,
|
||||
success: true,
|
||||
directory: siteDir,
|
||||
method: 'wget',
|
||||
exitCode: code,
|
||||
cloneInfo: info
|
||||
});
|
||||
} else {
|
||||
console.log(`❌ ERRO NA CLONAGEM (código: ${code})`);
|
||||
console.log(`${'='.repeat(60)}\n`);
|
||||
|
||||
reject(new Error(`wget falhou com código ${code}\n${errorOutput}`));
|
||||
}
|
||||
});
|
||||
|
||||
wgetProcess.on('error', (error) => {
|
||||
console.error(`❌ Erro ao executar wget: ${error.message}`);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async downloadMissingImages(siteDir, baseUrl) {
|
||||
try {
|
||||
const indexPath = path.join(siteDir, 'index.html');
|
||||
if (!fs.existsSync(indexPath)) return;
|
||||
|
||||
const html = fs.readFileSync(indexPath, 'utf8');
|
||||
|
||||
// Procurar por todas as referências de imagens
|
||||
const imagePatterns = [
|
||||
/background-image="([^"]+)"/g,
|
||||
/src="([^"]+\.(jpg|jpeg|png|gif|webp|svg))"/gi,
|
||||
/href="([^"]+\.(jpg|jpeg|png|gif|webp|svg))"/gi,
|
||||
];
|
||||
|
||||
const imagesToDownload = new Set();
|
||||
|
||||
for (const pattern of imagePatterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(html)) !== null) {
|
||||
const imagePath = match[1];
|
||||
if (imagePath.startsWith('/ws/') || imagePath.startsWith('ws/')) {
|
||||
imagesToDownload.add(imagePath.replace(/^\//, ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📦 Encontradas ${imagesToDownload.size} referências de imagens`);
|
||||
|
||||
let downloaded = 0;
|
||||
for (const imagePath of imagesToDownload) {
|
||||
const localPath = path.join(siteDir, imagePath);
|
||||
|
||||
// Se já existe, pular
|
||||
if (fs.existsSync(localPath)) continue;
|
||||
|
||||
// Criar diretório se não existir
|
||||
const dir = path.dirname(localPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Baixar imagem
|
||||
try {
|
||||
const urlObj = new URL(baseUrl);
|
||||
const imageUrl = `${urlObj.protocol}//${urlObj.hostname}/${imagePath}`;
|
||||
|
||||
console.log(` 📥 Baixando: ${imagePath}`);
|
||||
const response = await axios.get(imageUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
});
|
||||
|
||||
fs.writeFileSync(localPath, response.data);
|
||||
downloaded++;
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Falha ao baixar: ${imagePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ ${downloaded} imagens adicionais baixadas`);
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Erro ao baixar imagens faltantes: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
findIndexFile(directory) {
|
||||
// Procurar por index.html recursivamente
|
||||
const findIndex = (dir) => {
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const found = findIndex(fullPath);
|
||||
if (found) return found;
|
||||
} else if (file === 'index.html') {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return findIndex(directory);
|
||||
}
|
||||
}
|
||||
|
||||
// API
|
||||
app.post('/wget-clone', async (req, res) => {
|
||||
try {
|
||||
const { url } = req.body;
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'URL obrigatória' });
|
||||
}
|
||||
|
||||
const cloner = new WgetCloner();
|
||||
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: 'wget-cloner' });
|
||||
});
|
||||
|
||||
app.listen(3002, () => {
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('🌐 WGET Cloner - Clonagem Profissional de Sites');
|
||||
console.log('='.repeat(60));
|
||||
console.log('📡 Rodando em: http://localhost:3002');
|
||||
console.log('✅ Clonagem de alta fidelidade com wget');
|
||||
console.log('='.repeat(60) + '\n');
|
||||
});
|
||||
Binary file not shown.
Reference in New Issue
Block a user