/** * Vývojový skript pro export dat z Redisu do JSON souboru (data/db.json). * * Použití: typicky proti produkčnímu Redisu zpřístupněnému přes dočasný SSH tunel: * ssh -L 6379:localhost:6379 user@prod-host * cd server && yarn export:redis * * Skript jde přes stejné StorageInterface jako aplikace (RedisStorage), takže * korektně přečte i hodnoty uložené přes RedisJSON modul a zapíše je ve tvaru, * který očekává JsonStorage (simple-json-db) — tedy plochý objekt { klíč: hodnota }. * * Volitelné parametry (CLI nebo env): * --host (REDIS_HOST) výchozí: localhost * --port (REDIS_PORT) výchozí: 6379 * --out výchozí: server/data/db.json * --filter exportovat jen klíče obsahující daný podřetězec * --yes přeskočit interaktivní potvrzení přepisu */ import * as fs from 'fs'; import * as path from 'path'; import * as readline from 'readline'; import RedisStorage, { shutdownRedisStorage } from '../src/storage/redis'; // Bezpečnostní pojistka — skript nikdy nepouštět v produkčním režimu. if ((process.env.NODE_ENV ?? 'development') === 'production') { console.error('Tento skript nelze spustit s NODE_ENV=production.'); process.exit(1); } /** Jednoduché parsování CLI argumentů typu --klíč hodnota a --flag. */ function parseArgs(argv: string[]): Record { const out: Record = {}; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (!arg.startsWith('--')) continue; const key = arg.slice(2); const next = argv[i + 1]; if (next === undefined || next.startsWith('--')) { out[key] = true; } else { out[key] = next; i++; } } return out; } /** Dotaz na potvrzení (y/n) ve stdin. */ function confirm(question: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(resolve => { rl.question(question, answer => { rl.close(); resolve(/^(y|a|ano|yes)$/i.test(answer.trim())); }); }); } async function main() { const args = parseArgs(process.argv.slice(2)); // Host/port nastavíme do env PŘED načtením RedisStorage — jeho konstruktor je čte z env. if (typeof args.host === 'string') process.env.REDIS_HOST = args.host; if (typeof args.port === 'string') process.env.REDIS_PORT = args.port; const host = process.env.REDIS_HOST ?? 'localhost'; const port = process.env.REDIS_PORT ?? '6379'; const outPath = typeof args.out === 'string' ? path.resolve(process.cwd(), args.out) : path.resolve(__dirname, '../data/db.json'); const filter = typeof args.filter === 'string' ? args.filter : undefined; // RedisStorage čte REDIS_HOST/PORT z env až ve svém konstruktoru (ne při importu), // proto stačí je nastavit výše a teprve teď vytvořit instanci. console.log(`Připojuji se k Redisu na ${host}:${port} ...`); const storage = new RedisStorage(); await storage.initialize!(); const keys = await storage.listKeys(filter); console.log(`Nalezeno ${keys.length} klíčů${filter ? ` (filtr: "${filter}")` : ''}.`); if (keys.length === 0) { console.warn('Žádná data k exportu — končím bez zápisu.'); await shutdownRedisStorage(); return; } // Bezpečné načtení jednoho klíče — getData jde přes json.get, takže ne-JSON klíče // (např. lease připomínkovače uložené jako plain string přes SET NX EX) vyhodí chybu. // Takové klíče nejsou aplikační data a do db.json nepatří, proto je přeskočíme. const skipped: string[] = []; async function readSafe(key: string): Promise { try { return await storage.getData(key); } catch { skipped.push(key); return undefined; } } // Načtení hodnot po dávkách, ať zbytečně nezahltíme spojení. const BATCH = 20; const result: Record = {}; for (let i = 0; i < keys.length; i += BATCH) { const batch = keys.slice(i, i + BATCH); const values = await Promise.all(batch.map(k => readSafe(k))); batch.forEach((k, idx) => { const value = values[idx]; if (value !== undefined && value !== null) { result[k] = value; } }); console.log(`Načteno ${Math.min(i + BATCH, keys.length)}/${keys.length} klíčů ...`); } if (skipped.length > 0) { console.warn(`Přeskočeno ${skipped.length} ne-JSON klíčů: ${skipped.join(', ')}`); } await shutdownRedisStorage(); // Potvrzení přepisu existujícího souboru (pokud není --yes) + záloha. if (fs.existsSync(outPath) && args.yes !== true) { const ok = await confirm(`Soubor ${outPath} už existuje a bude přepsán. Pokračovat? [a/N] `); if (!ok) { console.log('Zrušeno uživatelem.'); return; } } if (fs.existsSync(outPath)) { const backupPath = `${outPath}.bak`; fs.copyFileSync(outPath, backupPath); console.log(`Záloha původního souboru: ${backupPath}`); } const dir = path.dirname(outPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // simple-json-db ukládá data jako plochý JSON objekt { klíč: hodnota }. fs.writeFileSync(outPath, JSON.stringify(result), 'utf-8'); console.log(`Hotovo — ${Object.keys(result).length} klíčů zapsáno do ${outPath}.`); } main().catch(err => { console.error('Export selhal:', err); process.exit(1); });