Skip to content

Commit

Permalink
Add AutoBackupService; create backups for all projects on startup if …
Browse files Browse the repository at this point in the history
…necessary
  • Loading branch information
tkleinke committed Feb 14, 2025
1 parent 684315f commit 2469905
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 13 deletions.
22 changes: 17 additions & 5 deletions desktop/src/app/services/backup/auto-backup-service.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import { Injectable } from '@angular/core';
import { SettingsProvider } from '../settings/settings-provider';

const remote = window.require('@electron/remote');


@Injectable()
/**
* @author Thomas Kleinke
*/
export class AutoBackupService {

private worker: Worker;

constructor(private settingsProvider: SettingsProvider) {}


public start() {

const projectName: string = this.settingsProvider.getSettings().selectedProject;
const backupsInfoFilePath: string = remote.getGlobal('appDataPath') + '/backups.json';
const backupDirectoryPath: string = this.settingsProvider.getSettings().backupDirectoryPath;
const worker = new Worker(new URL('./create-backup.worker', import.meta.url));
worker.onmessage = ({ data }) => console.log(data);
worker.postMessage({ projectName, backupDirectoryPath });
}
this.worker = new Worker(new URL('./auto-backup.worker', import.meta.url));

this.worker.onmessage = ({ data }) => console.log(data);
this.worker.postMessage({
command: 'start',
settings: {
backupsInfoFilePath,
backupDirectoryPath,
projects: this.settingsProvider.getSettings().dbs
}
});
}
}
171 changes: 171 additions & 0 deletions desktop/src/app/services/backup/auto-backup.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/// <reference lib="webworker" />

const fs = require('fs');
const PouchDb = require('pouchdb-browser').default;


type BackupsInfo = {
backups: { [project: string]: Array<Backup> };
};


type Backup = {
fileName: string;
updateSequence: number;
creationDate: Date;
};


const projectQueue: string[] = [];

let backupsInfoFilePath: string;
let backupDirectoryPath: string;
let projects: string[];


addEventListener('message', async ({ data }) => {

const command: string = data.command;
if (data.settings?.backupsInfoFilePath) backupsInfoFilePath = data.settings.backupsInfoFilePath;
if (data.settings?.backupDirectoryPath) backupDirectoryPath = data.settings.backupDirectoryPath;
if (data.settings?.projects) projects = data.settings.projects;

switch (command) {
case 'start':
start();
break;
}
});


async function start() {

initialize();
await update();
await runNextInQueue();
}


function initialize() {

if (!fs.existsSync(backupDirectoryPath)) fs.mkdirSync(backupDirectoryPath);
}


async function update() {

const backupsInfo: BackupsInfo = loadBackupsInfo();

for (let project of projects) {
if (await needsBackup(project, backupsInfo)) {
projectQueue.push(project);
}
}
}


async function runNextInQueue() {

if (!projectQueue.length) return;

const project: string = projectQueue.shift();
await createBackup(project);

runNextInQueue();
}


function loadBackupsInfo(): BackupsInfo {

const backupsInfo: BackupsInfo = deserializeBackupsInfo();
cleanUpBackupsInfo(backupsInfo);

return backupsInfo;
}


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

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

const backups: Array<Backup> = backupsInfo.backups[project] ?? [];
if (!backups.length) return true;

return backups.find(backup => {
return updateSequence === backup.updateSequence;
}) === undefined;
}


function cleanUpBackupsInfo(backupsInfo: BackupsInfo) {

Object.entries(backupsInfo.backups).forEach(([project, backups]) => {
backupsInfo.backups[project] = backups.filter(backup => fs.existsSync(backupDirectoryPath + '/' + backup.fileName));
});

serializeBackupsInfo(backupsInfo);
}


async function createBackup(project: string) {

const updateSequence: number = await getUpdateSequence(project);
const creationDate: Date = new Date();
const backupFileName: string = project + '_' + creationDate.toISOString().replace(/:/g, '-') + '.jsonl';
const backupFilePath: string = backupDirectoryPath + '/' + backupFileName;

await createBackupInWorker(project, backupFilePath);
addToBackupsInfo(project, backupFileName, updateSequence, creationDate);
}


async function createBackupInWorker(project: string, targetFilePath: string) {

return new Promise<void>((resolve, reject) => {
const worker = new Worker((new URL('./create-backup.worker', import.meta.url)));
worker.onmessage = ({ data }) => {
if (data.success) {
resolve();
} else {
reject(data.error);
}
}
worker.postMessage({ project, targetFilePath });
});
}


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

const backupsInfo: BackupsInfo = deserializeBackupsInfo();

if (!backupsInfo.backups[project]) backupsInfo.backups[project] = [];
backupsInfo.backups[project].push({
fileName,
updateSequence,
creationDate
});

serializeBackupsInfo(backupsInfo);
}


function deserializeBackupsInfo(): BackupsInfo {

if (!fs.existsSync(backupsInfoFilePath)) return { backups: {} };

return JSON.parse(fs.readFileSync(backupsInfoFilePath, 'utf-8'));
}


function serializeBackupsInfo(backupsInfo: BackupsInfo) {

fs.writeFileSync(backupsInfoFilePath, JSON.stringify(backupsInfo, null, 2));
}


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

return (await new PouchDb(project).info()).update_seq;
}
16 changes: 8 additions & 8 deletions desktop/src/app/services/backup/create-backup.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ suppressDeprecationWarnings();

addEventListener('message', async ({ data }) => {

const projectName: string = data.projectName;
const backupDirectoryPath: string = data.backupDirectoryPath;
const targetFilePath: string = backupDirectoryPath + '/' + projectName + '.jsonl';
const project: string = data.project;
const targetFilePath: string = data.targetFilePath;

if (!fs.existsSync(backupDirectoryPath)) fs.mkdirSync(backupDirectoryPath);
try {
await dump(targetFilePath, project);
} catch (err) {
postMessage({ success: false, error: err });
}

await dump(targetFilePath, projectName);

const response = 'Backup of project ' + projectName + ' successful!';
postMessage(response);
postMessage({ success: true });
});


Expand Down

0 comments on commit 2469905

Please sign in to comment.