import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import archiver from 'archiver'; import crypto from 'crypto'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PUBLIC_DIR = path.join(__dirname, 'public'); const BACKUP_DIR = path.join(PUBLIC_DIR, 'backups'); const SYSTEM_DIR = path.join(__dirname, 'source_mirror'); // Ensure directories exist if (!fs.existsSync(BACKUP_DIR)) { fs.mkdirSync(BACKUP_DIR, { recursive: true }); } export const Archivist = { /** * Creates a zip snapshot of all provided papers + system source code. * @param {Array} papers - Array of paper objects { id, title, content, ... } * @returns {Promise} - Metadata { zipUrl, ed2kLink, size, date } */ async createSnapshot(papers) { const dateStr = new Date().toISOString().split('T')[0]; const zipName = `p2pclaw_full_system_${dateStr}.zip`; const zipPath = path.join(BACKUP_DIR, zipName); const relativeZipUrl = `/backups/${zipName}`; console.log(`[Archivist] Starting snapshot generation: ${zipName}`); // 1. Create ZIP (Papers + System Source) await new Promise((resolve, reject) => { const output = fs.createWriteStream(zipPath); const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', resolve); archive.on('error', reject); archive.pipe(output); // Add Manifesto archive.append( `P2PCLAW Hive Mind - Full System Snapshot ${dateStr}\n\n` + `This archive contains:\n` + `1. The complete Research Library (Markdown)\n` + `2. The Source Code for the P2P Node (system/index.html)\n\n` + `INSTRUCTIONS:\n` + `- To run the node: Open 'system/index.html' in any browser.\n` + `- To help the network: Keep this file shared to ensure redundancy.\n`, { name: 'README.txt' } ); // Add Metadata Index archive.append(JSON.stringify(papers, null, 2), { name: 'library_index.json' }); // Add Papers papers.forEach(p => { const safeTitle = (p.title || 'untitled').replace(/[^a-z0-9]/gi, '_').substring(0, 50); const content = p.content || ''; const meta = `--- title: ${p.title} author: ${p.author || 'Collective'} date: ${new Date(p.timestamp || Date.now()).toISOString()} id: ${p.id} tags: ${(p.tags || []).join(', ')} --- ${content}`; archive.append(meta, { name: `papers/${safeTitle}.md` }); }); // Add System Source Code if (fs.existsSync(path.join(SYSTEM_DIR, 'index.html'))) { archive.file(path.join(SYSTEM_DIR, 'index.html'), { name: 'system/index.html' }); } if (fs.existsSync(path.join(SYSTEM_DIR, 'PROTOCOL.md'))) { archive.file(path.join(SYSTEM_DIR, 'PROTOCOL.md'), { name: 'system/PROTOCOL.md' }); } archive.finalize(); }); const zipBuffer = fs.readFileSync(zipPath); const zipSize = zipBuffer.length; console.log(`[Archivist] ZIP created: ${zipSize} bytes`); // 2. Seeding Disabled (Phase 55: Torrenting is Banned on Railway) // We rely on static ZIP downloads and IPFS redundancy instead. const torrentData = { magnetURI: null }; // 3. Create 'latest' references try { fs.copyFileSync(zipPath, path.join(BACKUP_DIR, 'latest.zip')); console.log(`[Archivist] 'latest' snapshot updated.`); } catch (symErr) { console.error(`[Archivist] Failed to update 'latest' files:`, symErr); } // 4. Generate eD2K Link (eMule) - Handled with MD4 fallback let ed2kLink = ''; try { const md4 = crypto.createHash('md4'); md4.update(zipBuffer); const ed2kHash = md4.digest('hex').toLowerCase(); ed2kLink = `ed2k://|file|${zipName}|${zipSize}|${ed2kHash}|/`; } catch { const fallbackHash = crypto.createHash('sha256').update(zipBuffer).digest('hex').slice(0, 32); ed2kLink = `ed2k://|file|${zipName}|${zipSize}|${fallbackHash}|/`; } return { filename: zipName, size: (zipSize / 1024 / 1024).toFixed(2) + ' MB', date: dateStr, downloadUrl: relativeZipUrl, latestZipUrl: `/backups/latest.zip`, magnetLink: null, // Disabled on Railway ed2kLink: ed2kLink }; } };