chore: prettier everywhere
This commit is contained in:
@@ -57,15 +57,24 @@ export class BackupService {
|
||||
try {
|
||||
// Pour le service de backup, on utilise un userId par défaut
|
||||
// car il n'a pas accès à la session
|
||||
const preferences = await userPreferencesService.getAllPreferences('default');
|
||||
if (preferences.viewPreferences && typeof preferences.viewPreferences === 'object') {
|
||||
const backupConfig = (preferences.viewPreferences as Record<string, unknown>).backupConfig;
|
||||
const preferences =
|
||||
await userPreferencesService.getAllPreferences('default');
|
||||
if (
|
||||
preferences.viewPreferences &&
|
||||
typeof preferences.viewPreferences === 'object'
|
||||
) {
|
||||
const backupConfig = (
|
||||
preferences.viewPreferences as Record<string, unknown>
|
||||
).backupConfig;
|
||||
if (backupConfig) {
|
||||
this.config = { ...this.defaultConfig, ...backupConfig };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not load backup config from DB, using defaults:', error);
|
||||
console.warn(
|
||||
'Could not load backup config from DB, using defaults:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,18 +88,22 @@ export class BackupService {
|
||||
await prisma.userPreferences.upsert({
|
||||
where: { userId: 'default' },
|
||||
update: {
|
||||
viewPreferences: JSON.parse(JSON.stringify({
|
||||
...(await userPreferencesService.getViewPreferences('default')),
|
||||
backupConfig: this.config
|
||||
}))
|
||||
viewPreferences: JSON.parse(
|
||||
JSON.stringify({
|
||||
...(await userPreferencesService.getViewPreferences('default')),
|
||||
backupConfig: this.config,
|
||||
})
|
||||
),
|
||||
},
|
||||
create: {
|
||||
userId: 'default',
|
||||
kanbanFilters: {},
|
||||
viewPreferences: JSON.parse(JSON.stringify({ backupConfig: this.config })),
|
||||
viewPreferences: JSON.parse(
|
||||
JSON.stringify({ backupConfig: this.config })
|
||||
),
|
||||
columnVisibility: {},
|
||||
jiraConfig: {}
|
||||
}
|
||||
jiraConfig: {},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save backup config to DB:', error);
|
||||
@@ -117,7 +130,7 @@ export class BackupService {
|
||||
try {
|
||||
const currentHash = await this.calculateDatabaseHash();
|
||||
const backups = await this.listBackups();
|
||||
|
||||
|
||||
if (backups.length === 0) {
|
||||
// Pas de backup précédent, donc il y a forcément des changements
|
||||
return true;
|
||||
@@ -126,7 +139,7 @@ export class BackupService {
|
||||
// Récupérer le hash du dernier backup
|
||||
const lastBackup = backups[0]; // Les backups sont triés par date décroissante
|
||||
const lastBackupHash = await this.getBackupHash(lastBackup.filename);
|
||||
|
||||
|
||||
if (!lastBackupHash) {
|
||||
// Pas de hash disponible pour le dernier backup, considérer qu'il y a des changements
|
||||
console.log('No hash available for last backup, assuming changes');
|
||||
@@ -134,8 +147,10 @@ export class BackupService {
|
||||
}
|
||||
|
||||
const hasChanged = currentHash !== lastBackupHash;
|
||||
console.log(`Database hash comparison: current=${currentHash.substring(0, 8)}..., last=${lastBackupHash.substring(0, 8)}..., changed=${hasChanged}`);
|
||||
|
||||
console.log(
|
||||
`Database hash comparison: current=${currentHash.substring(0, 8)}..., last=${lastBackupHash.substring(0, 8)}..., changed=${hasChanged}`
|
||||
);
|
||||
|
||||
return hasChanged;
|
||||
} catch (error) {
|
||||
console.error('Error checking database changes:', error);
|
||||
@@ -149,8 +164,11 @@ export class BackupService {
|
||||
*/
|
||||
private async getBackupHash(filename: string): Promise<string | null> {
|
||||
try {
|
||||
const metadataPath = path.join(this.getCurrentBackupPath(), `${filename}.meta.json`);
|
||||
|
||||
const metadataPath = path.join(
|
||||
this.getCurrentBackupPath(),
|
||||
`${filename}.meta.json`
|
||||
);
|
||||
|
||||
try {
|
||||
const metadataContent = await fs.readFile(metadataPath, 'utf-8');
|
||||
const metadata = JSON.parse(metadataContent);
|
||||
@@ -168,21 +186,26 @@ export class BackupService {
|
||||
/**
|
||||
* Calcule le hash d'un fichier de backup existant
|
||||
*/
|
||||
private async calculateBackupFileHash(filename: string): Promise<string | null> {
|
||||
private async calculateBackupFileHash(
|
||||
filename: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const backupPath = path.join(this.getCurrentBackupPath(), filename);
|
||||
|
||||
|
||||
// Si le fichier est compressé, il faut le décompresser temporairement
|
||||
if (filename.endsWith('.gz')) {
|
||||
const tempFile = path.join(this.getCurrentBackupPath(), `temp_${Date.now()}.db`);
|
||||
|
||||
const tempFile = path.join(
|
||||
this.getCurrentBackupPath(),
|
||||
`temp_${Date.now()}.db`
|
||||
);
|
||||
|
||||
try {
|
||||
await BackupUtils.decompressFileTemp(backupPath, tempFile);
|
||||
const hash = await BackupUtils.calculateFileHash(tempFile);
|
||||
|
||||
|
||||
// Nettoyer le fichier temporaire
|
||||
await fs.unlink(tempFile);
|
||||
|
||||
|
||||
return hash;
|
||||
} catch (error) {
|
||||
// Nettoyer le fichier temporaire en cas d'erreur
|
||||
@@ -196,7 +219,10 @@ export class BackupService {
|
||||
return await BackupUtils.calculateFileHash(backupPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error calculating hash for backup file ${filename}:`, error);
|
||||
console.error(
|
||||
`Error calculating hash for backup file ${filename}:`,
|
||||
error
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -204,9 +230,15 @@ export class BackupService {
|
||||
/**
|
||||
* Sauvegarde les métadonnées d'un backup
|
||||
*/
|
||||
private async saveBackupMetadata(filename: string, metadata: { databaseHash: string; createdAt: Date; type: string }): Promise<void> {
|
||||
private async saveBackupMetadata(
|
||||
filename: string,
|
||||
metadata: { databaseHash: string; createdAt: Date; type: string }
|
||||
): Promise<void> {
|
||||
try {
|
||||
const metadataPath = path.join(this.getCurrentBackupPath(), `${filename}.meta.json`);
|
||||
const metadataPath = path.join(
|
||||
this.getCurrentBackupPath(),
|
||||
`${filename}.meta.json`
|
||||
);
|
||||
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
||||
} catch (error) {
|
||||
console.error(`Error saving backup metadata for ${filename}:`, error);
|
||||
@@ -214,11 +246,15 @@ export class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Écrit une entrée dans le fichier de log des backups
|
||||
*/
|
||||
private async logBackupAction(type: 'manual' | 'automatic', action: 'created' | 'skipped' | 'failed', details: string, extra?: { hash?: string; size?: number; previousHash?: string }): Promise<void> {
|
||||
private async logBackupAction(
|
||||
type: 'manual' | 'automatic',
|
||||
action: 'created' | 'skipped' | 'failed',
|
||||
details: string,
|
||||
extra?: { hash?: string; size?: number; previousHash?: string }
|
||||
): Promise<void> {
|
||||
const logPath = path.join(this.getCurrentBackupPath(), 'backup.log');
|
||||
await BackupUtils.writeLogEntry(logPath, type, action, details, extra);
|
||||
}
|
||||
@@ -227,7 +263,10 @@ export class BackupService {
|
||||
* Crée une sauvegarde complète de la base de données
|
||||
* Vérifie d'abord s'il y a eu des changements (sauf si forcé)
|
||||
*/
|
||||
async createBackup(type: 'manual' | 'automatic' = 'manual', forceCreate: boolean = false): Promise<BackupInfo | null> {
|
||||
async createBackup(
|
||||
type: 'manual' | 'automatic' = 'manual',
|
||||
forceCreate: boolean = false
|
||||
): Promise<BackupInfo | null> {
|
||||
const backupId = `backup_${Date.now()}`;
|
||||
const filename = BackupUtils.generateBackupFilename(type);
|
||||
const backupPath = path.join(this.getCurrentBackupPath(), filename);
|
||||
@@ -238,21 +277,24 @@ export class BackupService {
|
||||
// Vérifier les changements (sauf si forcé)
|
||||
if (!forceCreate) {
|
||||
const hasChanged = await this.hasChangedSinceLastBackup();
|
||||
|
||||
|
||||
if (!hasChanged) {
|
||||
const currentHash = await this.calculateDatabaseHash();
|
||||
const backups = await this.listBackups();
|
||||
const lastBackupHash = backups.length > 0 ? await this.getBackupHash(backups[0].filename) : null;
|
||||
|
||||
const lastBackupHash =
|
||||
backups.length > 0
|
||||
? await this.getBackupHash(backups[0].filename)
|
||||
: null;
|
||||
|
||||
const message = `No changes detected since last backup`;
|
||||
console.log(`⏭️ Skipping ${type} backup: ${message}`);
|
||||
await this.logBackupAction(type, 'skipped', message, {
|
||||
hash: currentHash,
|
||||
previousHash: lastBackupHash || undefined
|
||||
await this.logBackupAction(type, 'skipped', message, {
|
||||
hash: currentHash,
|
||||
previousHash: lastBackupHash || undefined,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
console.log(`📝 Changes detected, proceeding with ${type} backup`);
|
||||
} else {
|
||||
console.log(`🔧 Forced ${type} backup, skipping change detection`);
|
||||
@@ -300,17 +342,22 @@ export class BackupService {
|
||||
|
||||
const successMessage = `${backupInfo.filename} created successfully`;
|
||||
console.log(`✅ Backup completed: ${successMessage}`);
|
||||
await this.logBackupAction(type, 'created', successMessage, {
|
||||
hash: databaseHash,
|
||||
size: backupInfo.size
|
||||
await this.logBackupAction(type, 'created', successMessage, {
|
||||
hash: databaseHash,
|
||||
size: backupInfo.size,
|
||||
});
|
||||
|
||||
|
||||
return backupInfo;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`❌ Backup failed:`, error);
|
||||
await this.logBackupAction(type, 'failed', `${filename} failed: ${errorMessage}`);
|
||||
|
||||
await this.logBackupAction(
|
||||
type,
|
||||
'failed',
|
||||
`${filename} failed: ${errorMessage}`
|
||||
);
|
||||
|
||||
return {
|
||||
id: backupId,
|
||||
filename,
|
||||
@@ -323,13 +370,12 @@ export class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Restaure une sauvegarde
|
||||
*/
|
||||
async restoreBackup(filename: string): Promise<void> {
|
||||
const backupPath = path.join(this.getCurrentBackupPath(), filename);
|
||||
|
||||
|
||||
// Résoudre le chemin de la base de données
|
||||
let dbPath: string;
|
||||
if (process.env.BACKUP_DATABASE_PATH) {
|
||||
@@ -342,7 +388,7 @@ export class BackupService {
|
||||
// Chemin par défaut vers prisma/dev.db
|
||||
dbPath = path.resolve(process.cwd(), 'prisma', 'dev.db');
|
||||
}
|
||||
|
||||
|
||||
console.log(`🔄 Restore paths - backup: ${backupPath}, target: ${dbPath}`);
|
||||
|
||||
console.log(`🔄 Starting restore from: ${filename}`);
|
||||
@@ -356,15 +402,15 @@ export class BackupService {
|
||||
if (filename.endsWith('.gz')) {
|
||||
const tempFile = backupPath.replace('.gz', '');
|
||||
console.log(`🔄 Decompressing ${backupPath} to ${tempFile}`);
|
||||
|
||||
|
||||
try {
|
||||
await BackupUtils.decompressFileTemp(backupPath, tempFile);
|
||||
console.log(`✅ Decompression successful`);
|
||||
|
||||
|
||||
// Vérifier que le fichier décompressé existe
|
||||
await fs.access(tempFile);
|
||||
console.log(`✅ Decompressed file exists: ${tempFile}`);
|
||||
|
||||
|
||||
sourceFile = tempFile;
|
||||
} catch (decompError) {
|
||||
console.error(`❌ Decompression failed:`, decompError);
|
||||
@@ -375,7 +421,9 @@ export class BackupService {
|
||||
// Créer une sauvegarde de la base actuelle avant restauration
|
||||
const currentBackup = await this.createBackup('manual', true); // Forcer la création
|
||||
if (currentBackup) {
|
||||
console.log(`✅ Current database backed up as: ${currentBackup.filename}`);
|
||||
console.log(
|
||||
`✅ Current database backed up as: ${currentBackup.filename}`
|
||||
);
|
||||
}
|
||||
|
||||
// Fermer toutes les connexions
|
||||
@@ -384,7 +432,7 @@ export class BackupService {
|
||||
// Vérifier que le fichier source existe
|
||||
await fs.access(sourceFile);
|
||||
console.log(`✅ Source file verified: ${sourceFile}`);
|
||||
|
||||
|
||||
// Remplacer la base de données
|
||||
console.log(`🔄 Copying ${sourceFile} to ${dbPath}`);
|
||||
await fs.copyFile(sourceFile, dbPath);
|
||||
@@ -426,18 +474,21 @@ export class BackupService {
|
||||
const currentBackupPath = this.getCurrentBackupPath();
|
||||
await BackupUtils.ensureDirectory(currentBackupPath);
|
||||
const files = await fs.readdir(currentBackupPath);
|
||||
|
||||
|
||||
const backups: BackupInfo[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('towercontrol_') && (file.endsWith('.db') || file.endsWith('.db.gz'))) {
|
||||
|
||||
for (const file of files) {
|
||||
if (
|
||||
file.startsWith('towercontrol_') &&
|
||||
(file.endsWith('.db') || file.endsWith('.db.gz'))
|
||||
) {
|
||||
const filePath = path.join(currentBackupPath, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
|
||||
|
||||
// Utiliser l'utilitaire pour parser le nom de fichier
|
||||
const { type, date } = BackupUtils.parseBackupFilename(file);
|
||||
const createdAt = date || stats.birthtime;
|
||||
|
||||
|
||||
backups.push({
|
||||
id: file,
|
||||
filename: file,
|
||||
@@ -448,8 +499,10 @@ export class BackupService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return backups.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
return backups.sort(
|
||||
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error listing backups:', error);
|
||||
return [];
|
||||
@@ -461,19 +514,22 @@ export class BackupService {
|
||||
*/
|
||||
async deleteBackup(filename: string): Promise<void> {
|
||||
const backupPath = path.join(this.getCurrentBackupPath(), filename);
|
||||
const metadataPath = path.join(this.getCurrentBackupPath(), `${filename}.meta.json`);
|
||||
|
||||
const metadataPath = path.join(
|
||||
this.getCurrentBackupPath(),
|
||||
`${filename}.meta.json`
|
||||
);
|
||||
|
||||
try {
|
||||
// Supprimer le fichier de backup
|
||||
await fs.unlink(backupPath);
|
||||
|
||||
|
||||
// Supprimer le fichier de métadonnées s'il existe
|
||||
try {
|
||||
await fs.unlink(metadataPath);
|
||||
} catch {
|
||||
// Ignorer si le fichier de métadonnées n'existe pas
|
||||
}
|
||||
|
||||
|
||||
console.log(`✅ Backup deleted: ${filename}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to delete backup ${filename}:`, error);
|
||||
@@ -488,14 +544,18 @@ export class BackupService {
|
||||
try {
|
||||
// Test de connexion simple
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
|
||||
|
||||
// Vérification de l'intégrité SQLite
|
||||
const result = await prisma.$queryRaw<{integrity_check: string}[]>`PRAGMA integrity_check`;
|
||||
|
||||
const result = await prisma.$queryRaw<
|
||||
{ integrity_check: string }[]
|
||||
>`PRAGMA integrity_check`;
|
||||
|
||||
if (result.length > 0 && result[0].integrity_check !== 'ok') {
|
||||
throw new Error(`Database integrity check failed: ${result[0].integrity_check}`);
|
||||
throw new Error(
|
||||
`Database integrity check failed: ${result[0].integrity_check}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
console.log('✅ Database health check passed');
|
||||
} catch (error) {
|
||||
console.error('❌ Database health check failed:', error);
|
||||
@@ -509,14 +569,14 @@ export class BackupService {
|
||||
private async cleanOldBackups(): Promise<void> {
|
||||
try {
|
||||
const backups = await this.listBackups();
|
||||
|
||||
|
||||
if (backups.length > this.config.maxBackups) {
|
||||
const toDelete = backups.slice(this.config.maxBackups);
|
||||
|
||||
|
||||
for (const backup of toDelete) {
|
||||
await this.deleteBackup(backup.filename);
|
||||
}
|
||||
|
||||
|
||||
console.log(`🧹 Cleaned ${toDelete.length} old backups`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -524,7 +584,6 @@ export class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Met à jour la configuration
|
||||
*/
|
||||
@@ -540,7 +599,7 @@ export class BackupService {
|
||||
// Retourner une config avec le chemin à jour
|
||||
return {
|
||||
...this.config,
|
||||
backupPath: this.getCurrentBackupPath()
|
||||
backupPath: this.getCurrentBackupPath(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -550,11 +609,14 @@ export class BackupService {
|
||||
async getBackupLogs(maxLines: number = 100): Promise<string[]> {
|
||||
try {
|
||||
const logPath = path.join(this.getCurrentBackupPath(), 'backup.log');
|
||||
|
||||
|
||||
try {
|
||||
const logContent = await fs.readFile(logPath, 'utf-8');
|
||||
const lines = logContent.trim().split('\n').filter(line => line.length > 0);
|
||||
|
||||
const lines = logContent
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
// Retourner les dernières lignes (les plus récentes)
|
||||
return lines.slice(-maxLines).reverse();
|
||||
} catch {
|
||||
@@ -570,33 +632,43 @@ export class BackupService {
|
||||
/**
|
||||
* Récupère les statistiques de sauvegarde par jour pour les N derniers jours
|
||||
*/
|
||||
async getBackupStats(days: number = 30): Promise<Array<{
|
||||
date: string;
|
||||
manual: number;
|
||||
automatic: number;
|
||||
total: number;
|
||||
}>> {
|
||||
async getBackupStats(days: number = 30): Promise<
|
||||
Array<{
|
||||
date: string;
|
||||
manual: number;
|
||||
automatic: number;
|
||||
total: number;
|
||||
}>
|
||||
> {
|
||||
try {
|
||||
const backups = await this.listBackups();
|
||||
const now = new Date();
|
||||
const stats: { [date: string]: { manual: number; automatic: number; total: number } } = {};
|
||||
const stats: {
|
||||
[date: string]: { manual: number; automatic: number; total: number };
|
||||
} = {};
|
||||
|
||||
// Initialiser les stats pour chaque jour
|
||||
for (let i = 0; i < days; i++) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
// Utiliser la date locale pour éviter les décalages UTC
|
||||
const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
|
||||
const localDate = new Date(
|
||||
date.getTime() - date.getTimezoneOffset() * 60000
|
||||
);
|
||||
const dateStr = localDate.toISOString().split('T')[0]; // Format YYYY-MM-DD
|
||||
stats[dateStr] = { manual: 0, automatic: 0, total: 0 };
|
||||
}
|
||||
|
||||
// Compter les sauvegardes par jour et par type
|
||||
backups.forEach(backup => {
|
||||
backups.forEach((backup) => {
|
||||
// Utiliser la date locale pour éviter les décalages UTC
|
||||
const backupDate = new Date(backup.createdAt.getTime() - backup.createdAt.getTimezoneOffset() * 60000)
|
||||
.toISOString().split('T')[0];
|
||||
|
||||
const backupDate = new Date(
|
||||
backup.createdAt.getTime() -
|
||||
backup.createdAt.getTimezoneOffset() * 60000
|
||||
)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
|
||||
if (stats[backupDate]) {
|
||||
if (backup.type === 'manual') {
|
||||
stats[backupDate].manual++;
|
||||
@@ -611,7 +683,7 @@ export class BackupService {
|
||||
return Object.entries(stats)
|
||||
.map(([date, counts]) => ({
|
||||
date,
|
||||
...counts
|
||||
...counts,
|
||||
}))
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user