Skip to content

Commit

Permalink
Store only latest update sequence per project in backups info file; a…
Browse files Browse the repository at this point in the history
…djust backup file names
  • Loading branch information
tkleinke committed Mar 3, 2025
1 parent 00d7c5b commit 7983380
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 345 deletions.
9 changes: 3 additions & 6 deletions desktop/src/app/components/app-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ import { ConfigurationIndex } from '../services/configuration/index/configuratio
import { copyThumbnailsFromDatabase } from '../migration/thumbnail-copy';
import { Languages } from '../services/languages';
import { createDisplayVariant } from '../services/imagestore/create-display-variant';
import { BackupsInfo } from '../services/backup/model/backups-info';
import { BackupsInfoSerializer } from '../services/backup/auto-backup/backups-info-serializer';
import { Backup } from '../services/backup/model/backup';
import { BackupService, RestoreBackupResult } from '../services/backup/backup-service';
import { getExistingBackups } from '../services/backup/auto-backup/get-existing-backups';

const ipcRenderer = window.require('electron')?.ipcRenderer;
const remote = window.require('@electron/remote');
Expand Down Expand Up @@ -341,11 +340,9 @@ const restoreLatestBackup = async (settingsService: SettingsService, settings: S

const getPathToLatestBackupFile = (settings: Settings): string|undefined => {

const backupsInfoFilePath: string = remote.getGlobal('appDataPath') + '/backups.json';
const backupsInfo: BackupsInfo = new BackupsInfoSerializer(backupsInfoFilePath, fs).load();
const backups: Array<Backup> = backupsInfo.backups[settings.selectedProject] ?? [];
const backups: Array<Backup> = getExistingBackups(settings.backupDirectoryPath)[settings.selectedProject] ?? [];

return backups.map(backup => {
return backups.reverse().map(backup => {
return Backup.getFilePath(backup, settings.backupDirectoryPath);
}).find(filePath => fs.existsSync(filePath));
}
48 changes: 15 additions & 33 deletions desktop/src/app/services/backup/auto-backup/auto-backup.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import { AutoBackupSettings } from '../model/auto-backup-settings';
import { Backup } from '../model/backup';
import { BackupsInfo } from '../model/backups-info';
import { BackupsMap } from '../model/backups-map';
import { INVALID_BACKUP_DIRECTORY_PATH } from './auto-backup-errors';
import { buildBackupFileName } from './backup-file-name-utils';
import { BackupsInfoSerializer } from './backups-info-serializer';
import { getBackupsToDelete } from './get-backups-to-delete';
import { getExistingBackups } from './get-existing-backups';

const fs = require('fs');
const PouchDb = require('pouchdb-browser').default;
Expand Down Expand Up @@ -63,7 +65,7 @@ function createWorker() {
const worker = new Worker((new URL('../create-backup.worker', import.meta.url)));
worker.onmessage = ({ data }) => {
if (data.success) {
addToBackupsInfo(data.project, data.targetFilePath, data.updateSequence, data.creationDate);
updateBackupsInfo(data.project, data.updateSequence);
onWorkerFinished(worker);
} else {
console.error('Error while creating backup file:', data.error);
Expand Down Expand Up @@ -106,10 +108,10 @@ async function updateBackups() {
postMessage({ running: true });

const backupsInfo: BackupsInfo = backupsInfoSerializer.load();
deleteOldBackups(backupsInfo);
cleanUpBackupsInfo(backupsInfo);
const existingBackups: BackupsMap = getExistingBackups(settings.backupDirectoryPath);
deleteOldBackups(existingBackups);
backupsInfoSerializer.store(backupsInfo);
await fillQueue(backupsInfo);
await fillQueue(backupsInfo, existingBackups);
startWorkers();

if (!activeWorkers.length && !projectQueue.length) {
Expand All @@ -133,10 +135,10 @@ function initializeBackupDirectory(): boolean {
}


async function fillQueue(backupsInfo: BackupsInfo) {
async function fillQueue(backupsInfo: BackupsInfo, existingBackups: BackupsMap) {

for (let project of settings.projects) {
if (await needsBackup(project, backupsInfo)) {
if (await needsBackup(project, backupsInfo, existingBackups)) {
projectQueue.push(project);
}
}
Expand Down Expand Up @@ -172,17 +174,14 @@ function startNextWorker(): boolean {
}


async function needsBackup(project: string, backupsInfo: BackupsInfo): Promise<boolean> {
async function needsBackup(project: string, backupsInfo: BackupsInfo, existingBackups: BackupsMap): Promise<boolean> {

if (project === 'test') return false;

const updateSequence = await getUpdateSequence(project);
if (!updateSequence) return false;

const backups: Array<Backup> = backupsInfo.backups[project] ?? [];
return backups.find(backup => {
return updateSequence === backup.updateSequence;
}) === undefined;

return !existingBackups[project]?.length || backupsInfo.lastUpdateSequence[project] !== updateSequence;
}


Expand All @@ -192,38 +191,21 @@ function buildBackupFilePath(project: string, creationDate: Date): string {
}


function addToBackupsInfo(project: string, targetFilePath: string, updateSequence: number, creationDate: Date) {
function updateBackupsInfo(project: string, updateSequence: number) {

const backupsInfo: BackupsInfo = backupsInfoSerializer.load();

if (!backupsInfo.backups[project]) backupsInfo.backups[project] = [];
backupsInfo.backups[project].push({
fileName: targetFilePath.split('/').pop(),
updateSequence,
creationDate
});

backupsInfo.lastUpdateSequence[project] = updateSequence;
backupsInfoSerializer.store(backupsInfo);
}


function deleteOldBackups(backupsInfo: BackupsInfo) {
function deleteOldBackups(existingBackups: BackupsMap) {

const backupsToDelete: Array<Backup> = getBackupsToDelete(backupsInfo, settings.keepBackups, new Date());
const backupsToDelete: Array<Backup> = getBackupsToDelete(existingBackups, settings.keepBackups, new Date());
backupsToDelete.forEach(backup => fs.rmSync(Backup.getFilePath(backup, settings.backupDirectoryPath)));
}


function cleanUpBackupsInfo(backupsInfo: BackupsInfo) {

Object.entries(backupsInfo.backups).forEach(([project, backups]) => {
backupsInfo.backups[project] = backups.filter(backup => {
return fs.existsSync(Backup.getFilePath(backup, settings.backupDirectoryPath));
});
});
}


async function getUpdateSequence(project: string): Promise<number|undefined> {

return (await new PouchDb(project).info()).update_seq;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
export type ParseBackupFileNameResult = {
project: string;
creationDate: Date;
}


export function buildBackupFileName(project: string, creationDate: Date) {

const year: string = creationDate.getFullYear().toString();
const month: string = getNumberString(creationDate.getMonth() + 1);
const day: string = getNumberString(creationDate.getDate());
const hour: string = getNumberString(creationDate.getHours());
const hours: string = getNumberString(creationDate.getHours());
const minutes: string = getNumberString(creationDate.getMinutes());
const seconds: string = getNumberString(creationDate.getSeconds());

return project + '_'
+ year + '-' + month + '-' + day + '_'
+ hour + '-' + minutes + '-' + seconds
return project + '.'
+ year + '-' + month + '-' + day + '.'
+ hours + '-' + minutes + '-' + seconds
+ '.jsonl';
}


export function parseBackupFileName(fileName: string): ParseBackupFileNameResult|undefined {

try {
const segments: string[] = fileName.split('.');
if (segments.length !== 4) return undefined;

const project: string = segments[0];
const date: string = segments[1];
const time: string = segments[2];

const dateSegments: string[] = date.split('-');
if (dateSegments.length !== 3) return undefined;

const year: number = parseInt(dateSegments[0]);
const month: number = parseInt(dateSegments[1]) - 1;
const day: number = parseInt(dateSegments[2]);

const timeSegments: string[] = time.split('-');
if (timeSegments.length !== 3) return undefined;

const hours: number = parseInt(timeSegments[0]);
const minutes: number = parseInt(timeSegments[1]);
const seconds: number = parseInt(timeSegments[2]);

return {
project,
creationDate: new Date(year, month, day, hours, minutes, seconds)
}
} catch (err) {
return undefined;
}
}


function getNumberString(value: number): string {

const result: string = value.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class BackupsInfoSerializer {

public load(): BackupsInfo {

if (!this.fs.existsSync(this.filePath)) return { backups: {} };
if (!this.fs.existsSync(this.filePath)) return { lastUpdateSequence: {} };

return JSON.parse(this.fs.readFileSync(this.filePath, 'utf-8'));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { isSameDay, isSameWeek, isSameMonth, differenceInDays, differenceInWeeks
differenceInCalendarMonths } from 'date-fns';
import { KeepBackupsSettings } from '../../settings/keep-backups-settings';
import { Backup } from '../model/backup';
import { BackupsInfo } from '../model/backups-info';
import { BackupsMap } from '../model/backups-map';


export function getBackupsToDelete(backupsInfo: BackupsInfo, settings: KeepBackupsSettings,
export function getBackupsToDelete(backups: BackupsMap, settings: KeepBackupsSettings,
currentDate: Date): Array<Backup> {

return Object.values(backupsInfo.backups).reduce((result, backups) => {
return Object.values(backups).reduce((result, backups) => {
return result.concat(getBackupsToDeleteForProject(backups, settings, currentDate));
}, []);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Backup } from '../model/backup';
import { BackupsMap } from '../model/backups-map';
import { parseBackupFileName } from './backup-file-name-utils';

const fs = require('fs');


export function getExistingBackups(backupDirectoryPath: string): BackupsMap {

const fileNames: string[] = fs.readdirSync(backupDirectoryPath);

const existingBackups: BackupsMap = fileNames.reduce((result, fileName) => {
const { project, creationDate } = parseBackupFileName(fileName) ?? {};
if (project && creationDate) {
if (!result[project]) result[project] = [];
result[project].push({ fileName, creationDate });
}
return result;
}, {});

sortBackups(existingBackups);

return existingBackups;
}


function sortBackups(backups: BackupsMap) {

Object.values(backups).forEach(projectBackups => {
projectBackups.sort((backup1, backup2) => {
return backup1.creationDate.getTime() - backup2.creationDate.getTime();
});
});
}
1 change: 0 additions & 1 deletion desktop/src/app/services/backup/model/backup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export interface Backup {

fileName: string;
updateSequence: number;
creationDate: Date;
}

Expand Down
5 changes: 1 addition & 4 deletions desktop/src/app/services/backup/model/backups-info.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { Backup } from './backup';


export interface BackupsInfo {

backups: { [project: string]: Array<Backup> };
lastUpdateSequence: { [project: string]: number };
}
3 changes: 3 additions & 0 deletions desktop/src/app/services/backup/model/backups-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Backup } from './backup';

export type BackupsMap = { [project: string]: Array<Backup> };
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildBackupFileName } from '../../../../../src/app/services/backup/auto-backup/backup-file-name-utils';
import { buildBackupFileName, parseBackupFileName } from '../../../../../src/app/services/backup/auto-backup/backup-file-name-utils';


/**
Expand All @@ -9,9 +9,28 @@ describe('backup file name utils', () => {
test('build backup file name', () => {

expect(buildBackupFileName('project', new Date('2025-01-02T10:30:20+01:00')))
.toBe('project_2025-01-02_10-30-20.jsonl');
.toBe('project.2025-01-02.10-30-20.jsonl');

expect(buildBackupFileName('project', new Date('2025-02-03T01:02:03+01:00')))
.toBe('project_2025-02-03_01-02-03.jsonl');
.toBe('project.2025-02-03.01-02-03.jsonl');
});


test('parse backup file name', () => {

expect(parseBackupFileName('project.2025-01-02.10-30-20.jsonl')).toEqual({
project: 'project',
creationDate: new Date('2025-01-02T10:30:20+01:00')
});

expect(parseBackupFileName('project.2025-02-03.01-02-03.jsonl')).toEqual({
project: 'project',
creationDate: new Date('2025-02-03T01:02:03+01:00')
});

expect(parseBackupFileName('project-with-special_characters.2025-01-02.10-30-20.jsonl')).toEqual({
project: 'project-with-special_characters',
creationDate: new Date('2025-01-02T10:30:20+01:00')
});
});
});
Loading

0 comments on commit 7983380

Please sign in to comment.