Skip to content

Commit

Permalink
added abstract mysql backup service
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-fabian committed Jan 4, 2024
1 parent 8ca472e commit 7264dd6
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 8 deletions.
3 changes: 2 additions & 1 deletion cspell.words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ datasource
datasources
Booter
Mustermann
Musterfrau
Musterfrau
mysqldump
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lbx-change-sets",
"version": "1.1.1",
"version": "1.1.2",
"description": "lbx-change-sets",
"keywords": [
"loopback-extension",
Expand Down Expand Up @@ -61,4 +61,4 @@
"source-map-support": "^0.5.21",
"typescript": "~5.1.6"
}
}
}
19 changes: 18 additions & 1 deletion showcase/src/controllers/test.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { service } from '@loopback/core';
import { Model, model, property, repository } from '@loopback/repository';
import { del, get, getModelSchemaRef, param, patch, post, put, requestBody } from '@loopback/rest';
import { TestChangeSetEntity } from '../models';
import { TestChangeSetEntityRepository } from '../repositories';
import { BackupService } from '../services';

@model()
class RollbackModel extends Model {
Expand All @@ -19,9 +21,24 @@ class RollbackModel extends Model {
export class TestController {
constructor(
@repository(TestChangeSetEntityRepository)
public testRepository: TestChangeSetEntityRepository
public testRepository: TestChangeSetEntityRepository,
@service(BackupService)
private readonly backupService: BackupService
) {}

@post('/create-backup')
async createBackup(): Promise<void> {
await this.backupService.createBackup();
}

@post('/restore-backup')
async restoreBackup(
@requestBody()
date: Date
): Promise<void> {
await this.backupService.restoreBackupFromDate(date);
}

@post('/test')
async create(
@requestBody({
Expand Down
31 changes: 31 additions & 0 deletions showcase/src/services/backup.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { BindingScope, bind } from '@loopback/core';
import { HttpErrors } from '@loopback/rest';
import { MySqlBackupService } from 'lbx-change-sets';

@bind({ scope: BindingScope.TRANSIENT })
export class BackupService extends MySqlBackupService {
protected override readonly rootPw: string = 'C2bcAJ2woT8UMewungW7qeFpjxwG3qTfB2GudQmv';

protected override async saveBackup(): Promise<void> {
await this.execAsync(`cp ${this.backupTempName} backup-${this.formatDate(new Date())}.sql`);
}

async restoreBackupFromDate(date: Date): Promise<void> {
this.loadBackupForDate(date)
await this.restoreMySqlData();
await this.removeTempRestoreBackup();
}

protected async loadBackupForDate(date: Date): Promise<void> {
try {
await this.execAsync(`cp backup-${this.formatDate(date)}.sql ${this.restoreBackupTempName}`);
}
catch (error) {
throw new HttpErrors.BadRequest(`Could not find a backup for the date ${this.formatDate(date)}`)
}
}

private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('de', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
}
1 change: 1 addition & 0 deletions showcase/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './backup.service'
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './component';
export * from './keys';
export * from './types';
export * from './models';
export * from './repositories';
export * from './repositories';
export * from './types';
export * from './services';
1 change: 1 addition & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './mysql-backup.service';
125 changes: 125 additions & 0 deletions src/services/mysql-backup.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { inject } from '@loopback/core';
import { IsolationLevel, juggler } from '@loopback/repository';
import { File } from 'buffer';
import { exec } from 'child_process';
import { rm, writeFile } from 'fs/promises';
import { LbxChangeSetsBindings } from '../keys';

/**
* A backup service that uses the mysqldump utility to create and restore backups.
*/
export abstract class MySqlBackupService {

/**
* The root database password. Is needed to backup everything, including system and user tables.
*/
protected abstract readonly rootPw: string;

/**
* The host of the database.
* @default '127.0.0.1'
*/
protected readonly host: string = '127.0.0.1';

/**
* The name of the temporary backup file. Is saved in the root directory of the project.
* @default 'backup.temp.sql'
*/
protected readonly backupTempName: string = 'backup.temp.sql';

/**
* The name of the temporary restore backup file. Is saved in the root directory of the project.
* @default 'restore-backup.temp.sql'
*/
protected readonly restoreBackupTempName: string = 'restore-backup.temp.sql';

constructor(
@inject(LbxChangeSetsBindings.DATASOURCE_KEY)
private readonly dataSource: juggler.DataSource
) {}

/**
* Creates a backup.
*/
async createBackup(): Promise<void> {
const transaction: juggler.Transaction = await this.dataSource.beginTransaction(IsolationLevel.READ_COMMITTED);
try {
await this.createDump();
await this.saveBackup();
await this.removeTempBackup();
await transaction.commit();
}
catch (error) {
await transaction.rollback();
throw error;
}
}

/**
* Restores the backup from the given date.
* @param dump - THe mysql dump file to restore from.
*/
async restoreBackup(dump: File): Promise<void> {
await this.loadBackup(dump);
await this.restoreMySqlData();
await this.removeTempRestoreBackup();
}

/**
* Creates a mysql dump and saves it under this.backupTempName.
*/
protected async createDump(): Promise<void> {
await this.execAsync(`mysqldump --all-databases -h ${this.dataSource.settings['host']} -u root -p${this.rootPw} > ${this.backupTempName}`, 'Could not create a mysql dump');
}

/**
* Method that handles saving the temporary dump file under this.backupTempName before it gets deleted.
*/
protected abstract saveBackup(): Promise<void>;

/**
* Deletes the temporary backup file.
*/
protected async removeTempBackup(): Promise<void> {
await rm(this.backupTempName);
}

/**
* Deletes the temporary restore backup file.
*/
protected async removeTempRestoreBackup(): Promise<void> {
await rm(this.restoreBackupTempName);
}

/**
* Restores the mysql data from the file under this.restoreBackupTempName.
*/
async restoreMySqlData(): Promise<void> {
await this.execAsync(`mysql -h ${this.dataSource.settings['host']} -u root -p${this.rootPw} < ${this.restoreBackupTempName}`, 'Could not restore the sql dump');
}

/**
* Loads the backup file for the given date into this.restoreBackupTempName.
* @param dump - The mysql dump file.
*/
protected async loadBackup(dump: File): Promise<void> {
await writeFile(this.restoreBackupTempName, dump.stream());
}

/**
* Runs the exec method from the child_process package as async.
* @param command - The command to execute.
* @param errorMessage - The error message to display if the command fails.
*/
protected async execAsync(command: string, errorMessage: string = 'Error executing the command'): Promise<void> {
return new Promise((resolve, reject) => {
exec(command, (error) => {
if (error) {
reject(errorMessage);
return;
}
resolve();
});
});
}
}

0 comments on commit 7264dd6

Please sign in to comment.