From dfa8a8a6e18aa5adeff8c47b1734ee3ce2cd4463 Mon Sep 17 00:00:00 2001
From: Zack Pollard <zackpollard@ymail.com>
Date: Tue, 12 Nov 2024 14:58:29 +0000
Subject: [PATCH] feat(server): use pg_dumpall version that matches the
 database version (#14083)

---
 server/src/services/backup.service.spec.ts | 29 ++++++++++++++++++++++
 server/src/services/backup.service.ts      | 29 +++++++++++++++++-----
 2 files changed, 52 insertions(+), 6 deletions(-)

diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts
index 7ec2bb87c1..4152d88cb1 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 40753a2c76..76b8fcd85b 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<void>((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;
   }