import { Socket } from 'socket.io'; import { spawn, ChildProcess } from 'child_process'; import fs from 'fs'; import path from 'path'; const ZIMS_DIR = '/library/zims/content/'; async function getKiwixCatalog() { try { console.log('[Kiwix] Querying server and cross-referencing with local disk...'); const response = await fetch('https://download.kiwix.org/zim/wikipedia/'); const html = await response.text(); let localFiles = new Set(); if (fs.existsSync(ZIMS_DIR)) { localFiles = new Set(fs.readdirSync(ZIMS_DIR)); } const regex = /.*?<\/a>\s+([\d-]+ \d{2}:\d{2})\s+([0-9.]+[KMG]?)/g; const results = []; let match; while ((match = regex.exec(html)) !== null) { const file = match[1]; const fullDate = match[2]; const size = match[3]; const cleanTitle = file.replace('.zim', '').split(/[-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' '); const isDownloaded = localFiles.has(file); results.push({ id: file, title: cleanTitle, date: fullDate.split(' ')[0], size: size, isDownloaded: isDownloaded }); } return results; } catch (error) { console.error('[Kiwix] Error querying the catalog:', error); return []; } } export const handleKiwixEvents = (socket: Socket) => { let downloadProcess: ChildProcess | null = null; let indexProcess: ChildProcess | null = null; let currentDownloads: string[] = []; // <-- NEW: Memory of what we are currently downloading socket.on('request_kiwix_catalog', async () => { socket.emit('kiwix_status', { message: 'Synchronizing catalog with local disk...' }); const catalog = await getKiwixCatalog(); socket.emit('kiwix_catalog_ready', catalog); }); socket.on('check_kiwix_tools', () => { const hasAria2 = fs.existsSync('/usr/bin/aria2c'); const hasIndexer = fs.existsSync('/usr/bin/iiab-make-kiwix-lib'); socket.emit('kiwix_tools_status', { hasAria2, hasIndexer }); }); socket.on('start_kiwix_download', (zims: string[]) => { if (downloadProcess || indexProcess) { socket.emit('kiwix_terminal_output', '\n[System] A process is already running.\n'); return; } if (zims.length === 0) return; currentDownloads = zims; // Save what we are downloading const baseUrl = 'https://download.kiwix.org/zim/wikipedia/'; const urls = zims.map(zim => baseUrl + zim); socket.emit('kiwix_terminal_output', `\n[System] Starting download...\n`); socket.emit('kiwix_process_status', { isRunning: true }); const args = ['-d', ZIMS_DIR, '-c', '-Z', '-x', '4', '-s', '4', '-j', '5', '--async-dns=false', ...urls]; downloadProcess = spawn('/usr/bin/aria2c', args); downloadProcess.stdout?.on('data', (data) => socket.emit('kiwix_terminal_output', data.toString())); downloadProcess.stderr?.on('data', (data) => socket.emit('kiwix_terminal_output', data.toString())); // Handle natural process exit downloadProcess.on('exit', (code, signal) => { downloadProcess = null; // If killed manually (Cancellation), ignore this block if (signal === 'SIGKILL') return; if (code === 0) { currentDownloads = []; // Clear memory socket.emit('kiwix_terminal_output', `\n[System] ๐ŸŸข Downloads finished. Starting indexing...\n`); if (fs.existsSync('/usr/bin/iiab-make-kiwix-lib')) { indexProcess = spawn('/usr/bin/iiab-make-kiwix-lib'); indexProcess.stdout?.on('data', (data) => socket.emit('kiwix_terminal_output', data.toString())); indexProcess.on('exit', (idxCode) => { socket.emit('kiwix_terminal_output', `\n[System] ๐Ÿ Indexing complete.\n`); indexProcess = null; socket.emit('kiwix_process_status', { isRunning: false }); socket.emit('refresh_kiwix_catalog'); }); } else { socket.emit('kiwix_process_status', { isRunning: false }); socket.emit('refresh_kiwix_catalog'); } } else { currentDownloads = []; socket.emit('kiwix_process_status', { isRunning: false }); } }); }); // NEW: PANIC BUTTON (Cancel Download) socket.on('cancel_kiwix_download', () => { if (downloadProcess) { socket.emit('kiwix_terminal_output', '\n[System] ๐Ÿ›‘ ABORTING: Killing Aria2c process...\n'); // 1. Kill the process (Ctrl+C equivalent) downloadProcess.kill('SIGKILL'); downloadProcess = null; // 2. Sweep the trash (Incomplete files) socket.emit('kiwix_terminal_output', '[System] ๐Ÿงน Cleaning temporary and incomplete files...\n'); currentDownloads.forEach(zim => { const filePath = path.join(ZIMS_DIR, zim); const ariaPath = filePath + '.aria2'; if (fs.existsSync(filePath)) fs.unlinkSync(filePath); // Delete the .zim if (fs.existsSync(ariaPath)) fs.unlinkSync(ariaPath); // Delete the .aria2 map }); currentDownloads = []; // Clear memory socket.emit('kiwix_terminal_output', '[System] โœ”๏ธ Cancellation complete. System ready.\n'); socket.emit('kiwix_process_status', { isRunning: false }); socket.emit('refresh_kiwix_catalog'); } }); // Delete ZIMs from UI socket.on('delete_zim', (zimId: string) => { const filePath = path.join(ZIMS_DIR, zimId); if (fs.existsSync(filePath)) { socket.emit('kiwix_terminal_output', `\n[System] ๐Ÿ—‘๏ธ Deleting file ${zimId}...\n`); fs.unlinkSync(filePath); if (fs.existsSync('/usr/bin/iiab-make-kiwix-lib')) { const idx = spawn('/usr/bin/iiab-make-kiwix-lib'); idx.stdout?.on('data', (data) => socket.emit('kiwix_terminal_output', data.toString())); idx.stderr?.on('data', (data) => socket.emit('kiwix_terminal_output', data.toString())); idx.on('exit', () => { socket.emit('kiwix_terminal_output', `\n[System] ๐Ÿ Index updated after deletion. Reloading interface...\n`); socket.emit('refresh_kiwix_catalog'); }); } else { socket.emit('kiwix_terminal_output', `\n[System] โœ… File deleted (no indexer). Reloading interface...\n`); socket.emit('refresh_kiwix_catalog'); } } }); socket.on('disconnect', () => { if (downloadProcess) downloadProcess.kill('SIGKILL'); if (indexProcess) indexProcess.kill('SIGKILL'); }); };