Files
TREK/server/src/nest/backup/backup.service.ts
T
Maurice 0aa5b36661 fix(backup): back up and restore through an injected database port
Backup, restore and the demo reset each handled the SQLite file
themselves: backup.impl closed and reinitialized the connection, built
MaintenanceRepository with new, and assumed the database was
data/travel.db. That tied the whole chain to one local file and hid two
bugs.

They now go through DATABASE_BACKUP, a port declared in the shared
database kernel (snapshot, verify, keep a copy before a restore, replace).
SqliteDatabaseBackup implements it with what the code did before:
VACUUM INTO for a consistent copy, and a close, copy, sidecar drop,
rename and reopen through DatabaseLifecycle for a restore. BackupService
injects it and hands it to backup.impl. The demo reset and the baseline
save use the same port; the first baseline is saved by DemoResetJob once
the app is up, because the boot-time seed runs before anything can be
injected. Another engine can plug in a logical dump behind the port.

Fixes on the way:

- The backup and the restore ignored TREK_DB_FILE. The backup looked for
  data/travel.db and, with the database elsewhere, silently left it out
  of the archive; the restore wrote data/travel.db and never replaced
  the live database. Both now use DatabaseLifecycle.file, the path the
  connection runs on, and the pre-restore copy sits next to it.
- A failed VACUUM INTO fell back to archiving the live WAL-mode file
  without a word, the torn copy the snapshot exists to prevent. The
  backup now fails: nothing is archived or committed, the existing
  backups are untouched, a partial snapshot is removed, and the log says
  why. A missing database file and a failed checkpoint are logged too.
2026-10-08 18:38:28 +02:00

42 lines
2.0 KiB
TypeScript

import { Inject, Injectable } from '@nestjs/common';
import type { Response } from 'express';
import { StorageService } from '../storage/storage.service';
import { DATABASE_BACKUP, type DatabaseBackupStrategy } from '../database/database-backup.interface';
import * as svc from './backup.impl';
/**
* The backup domain's injectable face.
*
* The implementation moved here from src/services/backupService.ts with the code
* unchanged, and stays a module rather than becoming methods on this class. What
* made that the cautious choice was that the zip packing and the restore path
* closed and reinitialized the core DB handle themselves. They no longer do:
* every database step goes through the injected backup port
* (`DATABASE_BACKUP`, `SqliteDatabaseBackup` today), which owns the snapshot and
* the swap, and through it `DatabaseLifecycle`, which owns the connection.
*/
@Injectable()
export class BackupService {
private readonly deps: svc.BackupDeps;
constructor(
private readonly storage: StorageService,
@Inject(DATABASE_BACKUP) database: DatabaseBackupStrategy,
) {
this.deps = { storage, database };
}
listBackups() { return svc.listBackups(this.storage); }
createBackup(prefix?: 'backup' | 'auto-backup') { return svc.createBackup(this.deps, prefix); }
restoreFromZip(zipPath: string) { return svc.restoreFromZip(this.deps, zipPath); }
restoreBackup(filename: string) { return svc.restoreBackup(this.deps, filename); }
deleteBackup(filename: string) { return svc.deleteBackup(this.storage, filename); }
isValidBackupFilename(filename: string) { return svc.isValidBackupFilename(filename); }
backupFileExists(filename: string) { return svc.backupFileExists(this.storage, filename); }
sendBackupToResponse(filename: string, res: Response) { return svc.sendBackupToResponse(this.storage, filename, res); }
checkRateLimit(key: string, maxAttempts: number, windowMs: number) { return svc.checkRateLimit(key, maxAttempts, windowMs); }
get rateWindow() { return svc.BACKUP_RATE_WINDOW; }
}