pl / src /backup.ts
ghuser1's picture
Update src/backup.ts
b8a1d79 verified
Raw
History Blame Contribute Delete
2.84 kB
import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { sqlite } from './db';
type BackupDisabled = {
enabled: false;
reason: string;
};
type BackupSuccess = {
enabled: true;
ok: true;
file: string;
remote: string;
};
type BackupFailure = {
enabled: true;
ok: false;
error: string;
};
export type BackupResult = BackupDisabled | BackupSuccess | BackupFailure;
const REMOTE = process.env.RCLONE_BACKUP_REMOTE || '';
const TIMEOUT_MS = Number(process.env.RCLONE_BACKUP_TIMEOUT_MS || 120_000);
const BACKUP_FILE = 'promptlib-latest.sqlite';
function joinRcloneTarget(remote: string, fileName: string): string {
if (remote.endsWith(':') || remote.endsWith('/')) return `${remote}${fileName}`;
return `${remote}/${fileName}`;
}
function truncateOutput(value: string): string {
return value.length > 4000 ? `${value.slice(0, 4000)}...` : value;
}
function runRclone(args: string[], timeoutMs: number): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn('rclone', args, {
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
const timer = setTimeout(() => {
child.kill('SIGTERM');
reject(new Error(`rclone timed out after ${timeoutMs}ms`));
}, timeoutMs);
child.stdout.on('data', (chunk) => {
stdout += String(chunk);
});
child.stderr.on('data', (chunk) => {
stderr += String(chunk);
});
child.on('error', (err) => {
clearTimeout(timer);
reject(err);
});
child.on('close', (code) => {
clearTimeout(timer);
if (code === 0) {
resolve();
return;
}
const output = truncateOutput([stdout.trim(), stderr.trim()].filter(Boolean).join('\n'));
reject(new Error(`rclone exited with code ${code}${output ? `: ${output}` : ''}`));
});
});
}
export async function backupDatabaseToRclone(): Promise<BackupResult> {
if (!REMOTE) {
return { enabled: false, reason: 'RCLONE_BACKUP_REMOTE is not set' };
}
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'promptlib-backup-'));
const fileName = BACKUP_FILE;
const snapshot = path.join(tmpDir, fileName);
const target = joinRcloneTarget(REMOTE, fileName);
try {
await sqlite.backup(snapshot);
await runRclone(['copyto', snapshot, target], TIMEOUT_MS);
console.log('[backup] uploaded', target);
return { enabled: true, ok: true, file: fileName, remote: target };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error('[backup] failed', message);
return { enabled: true, ok: false, error: message };
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
}