diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 55c226d507..8863a13ee7 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -93,7 +93,7 @@ Information on the current workers can be found [here](/administration/jobs-work All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`. `DB_URL` must be in the format `postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename`. -You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&sslmode=no-verify`. +You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&uselibpqcompat=true`. This allows both immich and `pg_dumpall` (the utility used for database backups) to [properly connect](https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string#tcp-connections) to your database. When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored. diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 8aa20aa868..9e25fbaf2e 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -153,6 +153,37 @@ describe(BackupService.name, () => { mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); }); + it('should sanitize DB_URL (remove uselibpqcompat) before calling pg_dumpall', async () => { + // create a service instance with a URL connection that includes libpqcompat + const dbUrl = 'postgresql://postgres:pwd@host:5432/immich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + ({ sut, mocks } = newTestService(BackupService, { config: configMock })); + + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + mocks.database.getPostgresVersion.mockResolvedValue('14.10'); + + await sut.handleBackupDatabase(); + + expect(mocks.process.spawn).toHaveBeenCalled(); + const call = mocks.process.spawn.mock.calls[0]; + const args = call[1] as string[]; + // ['--dbname', '', '--clean', '--if-exists'] + expect(args[0]).toBe('--dbname'); + const passedUrl = args[1]; + expect(passedUrl).not.toContain('uselibpqcompat'); + expect(passedUrl).toContain('sslmode=require'); + }); + it('should run a database backup successfully', async () => { const result = await sut.handleBackupDatabase(); expect(result).toBe(JobStatus.Success); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index 6f8cc0e34a..3731b1810f 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -81,8 +81,16 @@ export class BackupService extends BaseService { const isUrlConnection = config.connectionType === 'url'; + let connectionUrl: string = isUrlConnection ? config.url : ''; + if (URL.canParse(connectionUrl)) { + // remove known bad url parameters for pg_dumpall + const url = new URL(connectionUrl); + url.searchParams.delete('uselibpqcompat'); + connectionUrl = url.toString(); + } + const databaseParams = isUrlConnection - ? ['--dbname', config.url] + ? ['--dbname', connectionUrl] : [ '--username', config.username, @@ -118,7 +126,7 @@ export class BackupService extends BaseService { { env: { PATH: process.env.PATH, - PGPASSWORD: isUrlConnection ? new URL(config.url).password : config.password, + PGPASSWORD: isUrlConnection ? new URL(connectionUrl).password : config.password, }, }, );