Spaces:
Paused
Paused
| /** | |
| * ============================================================ | |
| * sftp.js — Extractor SFTP inteligente de TomateSMP v2.0 | |
| * ============================================================ | |
| * Sistema de prioridades: CRÍTICO (2MB), ALTA (512KB), NORMAL (256KB). | |
| * Elimina ruido: UUIDs, logs viejos, chunks. | |
| * Genera .txt.gz con árbol + JARs + contenido por prioridad. | |
| */ | |
| import { readConfig } from './utils.js'; | |
| const config = readConfig(); | |
| // ── Prioridades ─────────────────────────────────────────────────────────────── | |
| const PRIORITY = { CRITICAL: 1, HIGH: 2, NORMAL: 3 }; | |
| const SIZE_LIMIT = { | |
| [PRIORITY.CRITICAL]: 2 * 1024 * 1024, | |
| [PRIORITY.HIGH] : 512 * 1024, | |
| [PRIORITY.NORMAL] : 256 * 1024, | |
| }; | |
| const MAX_TOTAL_CHARS = 20_000_000; | |
| const PARALLEL = 16; | |
| const MAX_DEPTH = 12; | |
| const TEXT_EXT = new Set([ | |
| '.yml','.yaml','.json','.toml','.conf','.cfg','.config', | |
| '.properties','.txt','.log','.md','.xml','.ini','.sh', | |
| '.js','.java','.py','.sql','.csv','.ts','.hocon', | |
| ]); | |
| const BINARY_EXT = new Set([ | |
| '.jar','.class','.png','.jpg','.jpeg','.gif','.webp','.ico', | |
| '.ogg','.wav','.mp3','.flac', | |
| '.zip','.gz','.tar','.7z','.rar','.bz2', | |
| '.mca','.mcapm','.nbt','.dat_old', | |
| '.ldb','.sst','.bson','.db','.sqlite','.h2', | |
| '.lock','.lck','.trc','.dmp','.so','.dll', | |
| ]); | |
| const SKIP_DIRS = new Set([ | |
| 'region','entities','poi','DIM-1','DIM1','DIM2','datapacks', | |
| 'cache','libraries','versions','bundler','META-INF', | |
| '.git','node_modules','crash-reports','debug', | |
| 'web','maps','assets','temp','tmp','generated', | |
| ]); | |
| const CRITICAL_PATHS = new Set([ | |
| 'server.properties','bukkit.yml','spigot.yml','paper.yml', | |
| 'paper-global.yml','paper-world-defaults.yml','commands.yml', | |
| 'permissions.yml','ops.json','whitelist.json', | |
| 'plugins/NotRanks/ranks.yml', | |
| 'plugins/LuckPerms/config.yml', | |
| 'plugins/Essentials/config.yml', | |
| 'plugins/Essentials/worth.yml', | |
| 'plugins/Essentials/kits.yml', | |
| 'plugins/EconomyShopGUI/config.yml', | |
| 'plugins/AuraSkills/config.yml', | |
| 'plugins/AuraSkills/skills.yml', | |
| 'plugins/ExcellentEnchants/config.yml', | |
| 'plugins/ExcellentCrates/config.yml', | |
| 'plugins/BattlePass/config.yml', | |
| 'plugins/Quests/config.yml', | |
| 'plugins/UltimateMobCoins/config.yml', | |
| 'plugins/MythicMobs/config.yml', | |
| 'plugins/LevelledMobs/rules.yml', | |
| 'plugins/LevelledMobs/customdrops.yml', | |
| 'plugins/Multiverse-Core/worlds.yml', | |
| 'plugins/WorldGuard/config.yml', | |
| 'plugins/ProtectionStones/config.yml', | |
| 'plugins/BetterRTP/config.yml', | |
| 'plugins/Duel/config.yml', | |
| 'plugins/Duel/arenas.yml', | |
| 'plugins/SH-Koth/config.yml', | |
| 'plugins/Vulcan/config.yml', | |
| 'plugins/AxMinions/config.yml', | |
| 'plugins/AlonsoChat/config.yml', | |
| 'plugins/AlonsoTags/tags.yml', | |
| 'plugins/TAB/config.yml', | |
| 'plugins/TAB/groups.yml', | |
| 'plugins/PlasmoVoice/config.yml', | |
| 'plugins/FancyNpcs/npcs.yml', | |
| 'plugins/FancyHolograms/holograms.yml', | |
| 'plugins/CommandPanels/panels.yml', | |
| 'plugins/AxRewards/config.yml', | |
| 'plugins/AxEnvoy/config.yml', | |
| 'plugins/zVoteParty/config.yml', | |
| 'plugins/Jobs/config.yml', | |
| 'plugins/Jobs/jobConfig.yml', | |
| 'plugins/AdvancedBan/config.yml', | |
| 'plugins/NexAuth/config.yml', | |
| ]); | |
| const HIGH_DIRS = [ | |
| 'plugins/NotRanks', | |
| 'plugins/ExcellentEnchants/enchants', | |
| 'plugins/ExcellentCrates/crates', | |
| 'plugins/EconomyShopGUI/shops', | |
| 'plugins/MythicMobs/Mobs', | |
| 'plugins/MythicMobs/Skills', | |
| 'plugins/MythicMobs/Items', | |
| 'plugins/Quests/quests', | |
| 'plugins/BattlePass', | |
| 'plugins/AuraSkills', | |
| 'plugins/LevelledMobs', | |
| 'plugins/AlonsoTags', | |
| 'plugins/Jobs/jobs', | |
| 'plugins/TAB', | |
| 'plugins/ConditionalEvents', | |
| 'plugins/CommandPanels', | |
| 'plugins/AxMinions/minions', | |
| 'plugins/SH-Koth', | |
| ]; | |
| const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i; | |
| const SKIP_PATTERNS = [ | |
| UUID_RE, | |
| /^\d+\.dat$/, | |
| /^session\.lock$/, | |
| /^level\.dat_old$/, | |
| /^uid\.dat$/, | |
| /^usercache\.json$/, | |
| /^banned-ips\.json$/, | |
| ]; | |
| function shouldSkipFile(filename, dirPath) { | |
| if (SKIP_PATTERNS.some(p => p.test(filename))) return true; | |
| if (dirPath.includes('/logs') && filename !== 'latest.log') return true; | |
| const playerDirs = ['/playerdata', '/stats', '/advancements']; | |
| if (playerDirs.some(d => dirPath.includes(d)) && UUID_RE.test(filename)) return true; | |
| return false; | |
| } | |
| function getPriority(absPath, root) { | |
| const rel = absPath.startsWith(root + '/') ? absPath.slice(root.length + 1) : absPath.replace(/^\//, ''); | |
| if (CRITICAL_PATHS.has(rel)) return PRIORITY.CRITICAL; | |
| const base = rel.split('/').pop(); | |
| if (base === 'plugin.yml') return PRIORITY.HIGH; | |
| if (/^messages.*\.yml$/.test(base)) return PRIORITY.HIGH; | |
| for (const d of HIGH_DIRS) { | |
| if (rel.startsWith(d + '/')) return PRIORITY.HIGH; | |
| } | |
| return PRIORITY.NORMAL; | |
| } | |
| // ── SFTP helpers ────────────────────────────────────────────────────────────── | |
| async function makeSFTP(cfg) { | |
| const { Client } = await import('ssh2'); | |
| return new Promise((resolve, reject) => { | |
| const conn = new Client(); | |
| conn.on('ready', () => { | |
| conn.sftp((err, sftp) => { | |
| if (err) { conn.end(); return reject(err); } | |
| resolve({ sftp, conn }); | |
| }); | |
| }); | |
| conn.on('error', reject); | |
| conn.connect({ | |
| host: cfg.host, port: cfg.port ?? 22, | |
| username: cfg.username, password: cfg.password, | |
| readyTimeout: 20000, | |
| algorithms: { serverHostKey: ['ssh-rsa','ecdsa-sha2-nistp256','ssh-ed25519'] }, | |
| }); | |
| }); | |
| } | |
| function ls(sftp, dir) { | |
| return new Promise(resolve => { | |
| sftp.readdir(dir, (err, list) => resolve(err ? [] : (list || []))); | |
| }); | |
| } | |
| function readFile(sftp, path, maxBytes) { | |
| return new Promise(resolve => { | |
| const chunks = []; let total = 0, truncated = false; | |
| const stream = sftp.createReadStream(path, { encoding: 'utf8' }); | |
| stream.on('data', chunk => { | |
| if (total + chunk.length <= maxBytes) { chunks.push(chunk); total += chunk.length; } | |
| else { | |
| const remaining = maxBytes - total; | |
| if (remaining > 0) chunks.push(chunk.slice(0, remaining)); | |
| total = maxBytes; truncated = true; stream.destroy(); | |
| } | |
| }); | |
| stream.on('close', () => resolve({ text: chunks.join(''), truncated, bytes: total })); | |
| stream.on('end', () => resolve({ text: chunks.join(''), truncated, bytes: total })); | |
| stream.on('error', () => resolve(null)); | |
| }); | |
| } | |
| async function detectRoot(sftp, preferred) { | |
| const candidates = [preferred, '/', '/home/container', '/data', '/server']; | |
| for (const c of [...new Set(candidates)]) { | |
| const entries = await ls(sftp, c); | |
| const names = entries.map(e => e.filename); | |
| const isMC = names.some(n => ['plugins','world','server.properties','paper.yml','spigot.yml','bukkit.yml'].includes(n)); | |
| if (isMC) return { root: c, entries }; | |
| if (entries.length > 3) return { root: c, entries }; | |
| } | |
| return { root: preferred, entries: [] }; | |
| } | |
| async function buildFileTree(sftp, root, rootEntries, onProgress) { | |
| const fileQueue = [], jarList = [], treeLines = []; | |
| let fileCount = 0; | |
| async function crawl(dir, depth) { | |
| if (depth > MAX_DEPTH) return; | |
| const entries = dir === root ? rootEntries : await ls(sftp, dir); | |
| const relDir = dir.startsWith(root) ? dir.slice(root.length) || '/' : dir; | |
| const indent = ' '.repeat(depth); | |
| const subdirs = entries.filter(e => (e.attrs.mode & 0o040000) && !SKIP_DIRS.has(e.filename) && !e.filename.startsWith('.')); | |
| const files = entries.filter(e => !(e.attrs.mode & 0o040000)); | |
| treeLines.push(`${indent}📁 ${relDir}/`); | |
| for (const f of files) { | |
| const ext = f.filename.includes('.') ? '.' + f.filename.split('.').pop().toLowerCase() : ''; | |
| const absPath = `${dir}/${f.filename}`; | |
| const isBin = BINARY_EXT.has(ext); | |
| const isTxt = TEXT_EXT.has(ext); | |
| if (ext === '.jar' && dir.endsWith('/plugins')) jarList.push({ name: f.filename, sizeKB: Math.round(f.attrs.size / 1024) }); | |
| const skip = isBin || !isTxt || shouldSkipFile(f.filename, dir); | |
| treeLines.push(`${indent} ${skip ? '○' : '●'} ${f.filename} (${(f.attrs.size/1024).toFixed(1)}KB)`); | |
| if (!skip) { | |
| fileQueue.push({ path: absPath, size: f.attrs.size, priority: getPriority(absPath, root) }); | |
| fileCount++; | |
| if (fileCount % 200 === 0) onProgress?.(`🌲 Indexando árbol: ${fileCount} archivos...`); | |
| } | |
| } | |
| for (let i = 0; i < subdirs.length; i += 4) { | |
| await Promise.all(subdirs.slice(i, i+4).map(d => crawl(`${dir}/${d.filename}`, depth+1))); | |
| } | |
| } | |
| await crawl(root, 0); | |
| fileQueue.sort((a, b) => a.priority - b.priority); | |
| return { fileQueue, jarList, treeLines }; | |
| } | |
| async function readAllFiles(sftp, fileQueue, onProgress) { | |
| const results = new Map(); | |
| const queue = [...fileQueue]; | |
| let done = 0, totalChars = 0, lastUpdate = Date.now(); | |
| const total = queue.length; | |
| const skipped = { overflow: 0, error: 0 }; | |
| async function worker() { | |
| while (queue.length > 0) { | |
| if (totalChars >= MAX_TOTAL_CHARS) { skipped.overflow += queue.length; queue.length = 0; break; } | |
| const item = queue.shift(); if (!item) break; | |
| const maxBytes = SIZE_LIMIT[item.priority] ?? SIZE_LIMIT[PRIORITY.NORMAL]; | |
| const res = await readFile(sftp, item.path, maxBytes); | |
| if (res) { results.set(item.path, { ...res, priority: item.priority }); totalChars += res.text.length; } | |
| else skipped.error++; | |
| done++; | |
| if (Date.now() - lastUpdate > 1500) { | |
| onProgress?.(`📖 ${done}/${total} archivos | ${(totalChars/1024/1024).toFixed(1)}MB leídos...`); | |
| lastUpdate = Date.now(); | |
| } | |
| } | |
| } | |
| await Promise.all(Array.from({ length: PARALLEL }, worker)); | |
| return { results, skipped, totalChars }; | |
| } | |
| function buildDocument(root, treeLines, jarList, results, stats) { | |
| const sep = '═'.repeat(70); | |
| const lines = [ | |
| `TOMATESMP — EXTRACCIÓN COMPLETA v2.0`, | |
| `Fecha: ${new Date().toISOString()}`, | |
| `Raíz: ${root} | Host: ${config.sftp.host}:${config.sftp.port ?? 22}`, | |
| `Archivos leídos: ${results.size} | Tamaño texto: ${(stats.totalChars/1024/1024).toFixed(2)}MB`, | |
| sep, '', | |
| '🗂️ ÁRBOL DE ARCHIVOS', '─'.repeat(70), | |
| treeLines.join('\n'), '', | |
| sep, `📦 PLUGINS INSTALADOS (${jarList.length} JARs detectados)`, '─'.repeat(70), | |
| jarList.map(j => ` ${j.name} (${j.sizeKB}KB)`).join('\n'), '', | |
| sep, '🔴 ARCHIVOS CRÍTICOS', sep, | |
| ]; | |
| for (const [path, { text, truncated, priority }] of results.entries()) { | |
| if (priority !== PRIORITY.CRITICAL) continue; | |
| lines.push(`\n━━━ ${path}${truncated ? ' [TRUNCADO]' : ''}\n${text}`); | |
| } | |
| lines.push('', sep, '🟡 ARCHIVOS DE ALTA PRIORIDAD', sep); | |
| for (const [path, { text, truncated, priority }] of results.entries()) { | |
| if (priority !== PRIORITY.HIGH) continue; | |
| lines.push(`\n━━━ ${path}${truncated ? ' [TRUNCADO]' : ''}\n${text}`); | |
| } | |
| lines.push('', sep, '⚪ RESTO DE ARCHIVOS', sep); | |
| for (const [path, { text, truncated, priority }] of results.entries()) { | |
| if (priority !== PRIORITY.NORMAL) continue; | |
| lines.push(`\n━━━ ${path}${truncated ? ' [TRUNCADO]' : ''}\n${text}`); | |
| } | |
| lines.push('', sep, `FIN DE EXTRACCIÓN — ${results.size} archivos procesados`); | |
| return lines.join('\n'); | |
| } | |
| export async function extractServerInfo(onProgress, overridePassword = null) { | |
| const cfg = config.sftp; | |
| if (!cfg?.host || !cfg?.username || !cfg?.password) throw new Error('Credenciales SFTP no configuradas'); | |
| const effectiveCfg = overridePassword ? { ...cfg, password: overridePassword } : cfg; | |
| onProgress?.('🔌 Conectando...'); | |
| const { sftp, conn } = await makeSFTP(effectiveCfg); | |
| try { | |
| onProgress?.('🔍 Detectando raíz del servidor...'); | |
| const { root, entries: rootEntries } = await detectRoot(sftp, cfg.rootPath ?? '/home/container'); | |
| onProgress?.(`📂 Raíz: \`${root}\` — construyendo árbol de archivos...`); | |
| const { fileQueue, jarList, treeLines } = await buildFileTree(sftp, root, rootEntries, onProgress); | |
| const critical = fileQueue.filter(f => f.priority === PRIORITY.CRITICAL).length; | |
| const high = fileQueue.filter(f => f.priority === PRIORITY.HIGH).length; | |
| onProgress?.(`📋 ${fileQueue.length} archivos indexados — ${critical} críticos, ${high} alta prioridad, ${jarList.length} JARs`); | |
| const { results, skipped, totalChars } = await readAllFiles(sftp, fileQueue, onProgress); | |
| onProgress?.(`📦 Listo — ${results.size} archivos leídos (${(totalChars/1024/1024).toFixed(1)}MB) — comprimiendo...`); | |
| const document = buildDocument(root, treeLines, jarList, results, { totalChars, skipped }); | |
| const { gzipSync } = await import('zlib'); | |
| const compressed = gzipSync(Buffer.from(document, 'utf8'), { level: 9 }); | |
| return { | |
| buffer : compressed, | |
| filename: `tomatesmp_${new Date().toISOString().slice(0,10)}.txt.gz`, | |
| stats : { | |
| files : results.size, | |
| critical: [...results.values()].filter(r => r.priority === PRIORITY.CRITICAL).length, | |
| high : [...results.values()].filter(r => r.priority === PRIORITY.HIGH).length, | |
| normal : [...results.values()].filter(r => r.priority === PRIORITY.NORMAL).length, | |
| plugins : jarList.length, | |
| skipped : skipped.overflow + skipped.error, | |
| errors : skipped.error, | |
| sizeMB : (totalChars / 1024 / 1024).toFixed(2), | |
| }, | |
| sizeKB : Math.round(compressed.length / 1024), | |
| }; | |
| } finally { | |
| conn.end(); | |
| } | |
| } | |