super-productivity/electron/backup.ts
felix bear 00cd665e1a
feat(backup): configure automatic backup file limit #6690 (#8094)
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
2026-06-08 12:09:49 +02:00

131 lines
3.9 KiB
TypeScript

import { app, ipcMain, IpcMainEvent } from 'electron';
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
unlinkSync,
writeFileSync,
} from 'fs';
import { IPC } from './shared-with-frontend/ipc-events.const';
import { LocalBackupMeta } from '../src/app/imex/local-backup/local-backup.model';
import * as path from 'path';
import { error, log } from 'electron-log/main';
import type { AppDataCompleteLegacy } from '../src/app/imex/sync/sync.model';
import type { AppDataComplete } from '../src/app/op-log/model/model-config';
import { getBackupTimestamp } from './shared-with-frontend/get-backup-timestamp';
import {
DEFAULT_MAX_BACKUP_FILES,
selectBackupFilesToDelete,
} from './shared-with-frontend/backup-file-cleanup.util';
export const BACKUP_DIR = path.join(app.getPath('userData'), `backups`);
export const BACKUP_DIR_WINSTORE = BACKUP_DIR.replace(
'Roaming',
`Local\\Packages\\53707johannesjo.SuperProductivity_ch45amy23cdv6\\LocalCache\\Roaming`,
);
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export function initBackupAdapter(): void {
console.log('Saving backups to', BACKUP_DIR);
log('Saving backups to', BACKUP_DIR);
// BACKUP
ipcMain.on(IPC.BACKUP, backupData);
// IS_BACKUP_AVAILABLE
ipcMain.handle(IPC.BACKUP_IS_AVAILABLE, (): LocalBackupMeta | false => {
if (!existsSync(BACKUP_DIR)) {
return false;
}
const files = readdirSync(BACKUP_DIR);
if (!files.length) {
return false;
}
const filesWithMeta: LocalBackupMeta[] = files.map(
(fileName: string): LocalBackupMeta => ({
name: fileName,
path: path.join(BACKUP_DIR, fileName),
folder: BACKUP_DIR,
created: statSync(path.join(BACKUP_DIR, fileName)).mtime.getTime(),
}),
);
filesWithMeta.sort((a: LocalBackupMeta, b: LocalBackupMeta) => a.created - b.created);
log(
'Avilable Backup Files: ',
filesWithMeta?.map && filesWithMeta.map((f) => f.path),
);
return filesWithMeta.reverse()[0];
});
// RESTORE_BACKUP
ipcMain.handle(IPC.BACKUP_LOAD_DATA, (ev, backupPath: string): string => {
log('Reading backup file: ', backupPath);
return readFileSync(backupPath, { encoding: 'utf8' });
});
}
interface BackupDataArgs {
data: AppDataCompleteLegacy | AppDataComplete;
maxBackupFiles?: number | null;
}
const isBackupDataArgs = (arg: unknown): arg is BackupDataArgs =>
!!arg &&
typeof arg === 'object' &&
'data' in arg &&
typeof (arg as { data?: unknown }).data === 'object';
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function backupData(
ev: IpcMainEvent,
dataOrArgs: AppDataCompleteLegacy | BackupDataArgs,
): void {
if (!existsSync(BACKUP_DIR)) {
mkdirSync(BACKUP_DIR);
}
const filePath = `${BACKUP_DIR}/${getBackupTimestamp()}.json`;
const data = isBackupDataArgs(dataOrArgs) ? dataOrArgs.data : dataOrArgs;
const maxBackupFiles = isBackupDataArgs(dataOrArgs)
? dataOrArgs.maxBackupFiles
: DEFAULT_MAX_BACKUP_FILES;
try {
const backup = JSON.stringify(data);
writeFileSync(filePath, backup);
cleanupOldBackups(maxBackupFiles);
} catch (e) {
log('Error while backing up');
error(e);
}
}
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function cleanupOldBackups(maxBackupFiles?: number | null): void {
if (!existsSync(BACKUP_DIR)) {
return;
}
try {
const files = readdirSync(BACKUP_DIR).filter((f) => f.endsWith('.json'));
const filesWithMtime = files.map((fileName) => {
const filePath = path.join(BACKUP_DIR, fileName);
return { fileName, filePath, mtime: statSync(filePath).mtime.getTime() };
});
for (const file of selectBackupFilesToDelete(filesWithMtime, maxBackupFiles)) {
try {
unlinkSync(file.filePath);
} catch (e) {
log(`Error deleting backup file ${file.fileName}`);
error(e);
}
}
} catch (e) {
log('Error during backup cleanup');
error(e);
}
}