feat: skript pro import dat z Redis databáze

This commit is contained in:
2026-06-12 19:19:36 +02:00
parent c404a3a03b
commit bcec015c37
2 changed files with 154 additions and 1 deletions
+2 -1
View File
@@ -8,7 +8,8 @@
"start": "ts-node src/index.ts",
"startReload": "nodemon --watch src src/index.ts",
"build": "tsc -p .",
"test": "jest"
"test": "jest",
"export:redis": "ts-node scripts/exportRedisToJson.ts"
},
"devDependencies": {
"@babel/core": "^7.28.5",
+152
View File
@@ -0,0 +1,152 @@
/**
* 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 <host> (REDIS_HOST) výchozí: localhost
* --port <port> (REDIS_PORT) výchozí: 6379
* --out <cesta> výchozí: server/data/db.json
* --filter <text> 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<string, string | boolean> {
const out: Record<string, string | boolean> = {};
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<boolean> {
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<unknown> {
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<string, unknown> = {};
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);
});