diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 7ec2bb87c119f..4152d88cb17c8 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -149,6 +149,7 @@ describe(BackupService.name, () => { storageMock.unlink.mockResolvedValue(); systemMock.get.mockResolvedValue(systemConfigStub.backupEnabled); storageMock.createWriteStream.mockReturnValue(new PassThrough()); + databaseMock.getPostgresVersion.mockResolvedValue('14.3.2'); }); it('should run a database backup successfully', async () => { const result = await sut.handleBackupDatabase(); @@ -196,5 +197,33 @@ describe(BackupService.name, () => { expect(storageMock.unlink).toHaveBeenCalled(); expect(result).toBe(JobStatus.FAILED); }); + it.each` + postgresVersion | expectedVersion + ${'14.6.4'} | ${14} + ${'15.3.3'} | ${15} + ${'16.4.2'} | ${16} + ${'17.15.1'} | ${17} + `( + `should use pg_dumpall $expectedVersion with postgres version $postgresVersion`, + async ({ postgresVersion, expectedVersion }) => { + databaseMock.getPostgresVersion.mockResolvedValue(postgresVersion); + await sut.handleBackupDatabase(); + expect(processMock.spawn).toHaveBeenCalledWith( + `/usr/lib/postgresql/${expectedVersion}/bin/pg_dumpall`, + expect.any(Array), + expect.any(Object), + ); + }, + ); + it.each` + postgresVersion + ${'13.99.99'} + ${'18.0.0'} + `(`should fail if postgres version $postgresVersion is not supported`, async ({ postgresVersion }) => { + databaseMock.getPostgresVersion.mockResolvedValue(postgresVersion); + const result = await sut.handleBackupDatabase(); + expect(processMock.spawn).not.toHaveBeenCalled(); + expect(result).toBe(JobStatus.FAILED); + }); }); }); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index 40753a2c76622..76b8fcd85bd9a 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { default as path } from 'node:path'; +import semver from 'semver'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; import { ImmichWorker, StorageFolder } from 'src/enum'; @@ -101,14 +102,30 @@ export class BackupService extends BaseService { `immich-db-backup-${Date.now()}.sql.gz.tmp`, ); + const databaseVersion = await this.databaseRepository.getPostgresVersion(); + const databaseSemver = semver.coerce(databaseVersion); + const databaseMajorVersion = databaseSemver?.major; + const databaseSupported = semver.satisfies(databaseVersion, '>=14.0.0 <18.0.0'); + + if (!databaseMajorVersion || !databaseSupported) { + this.logger.error(`Database Backup Failure: Unsupported PostgreSQL version: ${databaseVersion}`); + return JobStatus.FAILED; + } + + this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); + try { await new Promise((resolve, reject) => { - const pgdump = this.processRepository.spawn(`pg_dumpall`, databaseParams, { - env: { - PATH: process.env.PATH, - PGPASSWORD: isUrlConnection ? undefined : config.password, + const pgdump = this.processRepository.spawn( + `/usr/lib/postgresql/${databaseMajorVersion}/bin/pg_dumpall`, + databaseParams, + { + env: { + PATH: process.env.PATH, + PGPASSWORD: isUrlConnection ? undefined : config.password, + }, }, - }); + ); // NOTE: `--rsyncable` is only supported in GNU gzip const gzip = this.processRepository.spawn(`gzip`, ['--rsyncable']); @@ -169,7 +186,7 @@ export class BackupService extends BaseService { return JobStatus.FAILED; } - this.logger.debug(`Database Backup Success`); + this.logger.log(`Database Backup Success`); await this.cleanupDatabaseBackups(); return JobStatus.SUCCESS; }