fix(server): use provided database username for restore & ensure name is not mangled (#25679)

* fix(server): use provided database name/username for restore & ensure name is not mangled

fixes #25633

Signed-off-by: izzy <me@insrt.uk>

* chore: add db switch back but with comments

Signed-off-by: izzy <me@insrt.uk>

* refactor: no need to restore database since it's not technically possible
chore: late fallback for username in parameter builder

Signed-off-by: izzy <me@insrt.uk>

* chore: type fix

Signed-off-by: izzy <me@insrt.uk>

* chore: re-use the username we just pulled out

---------

Signed-off-by: izzy <me@insrt.uk>
This commit is contained in:
Paul Makles 2026-02-05 18:59:05 +00:00 committed by GitHub
parent ac9f6921cc
commit ed4d9abdae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -59,6 +59,7 @@ export async function buildPostgresLaunchArguments(
): Promise<{ ): Promise<{
bin: string; bin: string;
args: string[]; args: string[];
databaseUsername: string;
databasePassword: string; databasePassword: string;
databaseVersion: string; databaseVersion: string;
databaseMajorVersion?: number; databaseMajorVersion?: number;
@ -73,6 +74,7 @@ export async function buildPostgresLaunchArguments(
const databaseMajorVersion = databaseSemver?.major; const databaseMajorVersion = databaseSemver?.major;
const args: string[] = []; const args: string[] = [];
let databaseUsername;
if (isUrlConnection) { if (isUrlConnection) {
if (bin !== 'pg_dump') { if (bin !== 'pg_dump') {
@ -85,23 +87,18 @@ export async function buildPostgresLaunchArguments(
// remove known bad parameters // remove known bad parameters
parsedUrl.searchParams.delete('uselibpqcompat'); parsedUrl.searchParams.delete('uselibpqcompat');
if (options.username) { databaseUsername = parsedUrl.username;
parsedUrl.username = options.username;
}
url = parsedUrl.toString(); url = parsedUrl.toString();
} }
// assume typical values if we can't parse URL or not present
databaseUsername ??= 'postgres';
args.push(url); args.push(url);
} else { } else {
args.push( databaseUsername = databaseConfig.username;
'--username',
options.username ?? databaseConfig.username, args.push('--username', databaseUsername, '--host', databaseConfig.host, '--port', databaseConfig.port.toString());
'--host',
databaseConfig.host,
'--port',
databaseConfig.port.toString(),
);
switch (bin) { switch (bin) {
case 'pg_dumpall': { case 'pg_dumpall': {
@ -151,6 +148,7 @@ export async function buildPostgresLaunchArguments(
return { return {
bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`, bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`,
args, args,
databaseUsername,
databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password, databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password,
databaseVersion, databaseVersion,
databaseMajorVersion, databaseMajorVersion,
@ -207,44 +205,35 @@ const SQL_DROP_CONNECTIONS = `
AND pid <> pg_backend_pid(); AND pid <> pg_backend_pid();
`; `;
const SQL_RESET_SCHEMA = ` const SQL_RESET_SCHEMA = (username: string) => `
-- re-create the default schema -- re-create the default schema
DROP SCHEMA public CASCADE; DROP SCHEMA public CASCADE;
CREATE SCHEMA public; CREATE SCHEMA public;
-- restore access to schema -- restore access to schema
GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO "${username}";
GRANT ALL ON SCHEMA public TO public; GRANT ALL ON SCHEMA public TO public;
`; `;
async function* sql(inputStream: Readable, isPgClusterDump: boolean) { async function* sql(inputStream: Readable, databaseUsername: string, isPgClusterDump: boolean) {
yield SQL_DROP_CONNECTIONS; yield SQL_DROP_CONNECTIONS;
yield isPgClusterDump yield isPgClusterDump
? String.raw` ? // it is likely the dump contains SQL to try to drop the currently active
// database to ensure we have a fresh slate; if the `postgres` database exists
// then prefer to switch before continuing otherwise this will just silently fail
String.raw`
\c postgres \c postgres
` `
: SQL_RESET_SCHEMA; : SQL_RESET_SCHEMA(databaseUsername);
for await (const chunk of inputStream) { for await (const chunk of inputStream) {
yield chunk; yield chunk;
} }
} }
async function* sqlRollback(inputStream: Readable, isPgClusterDump: boolean) { async function* sqlRollback(inputStream: Readable, databaseUsername: string) {
yield SQL_DROP_CONNECTIONS; yield SQL_DROP_CONNECTIONS;
yield SQL_RESET_SCHEMA(databaseUsername);
if (isPgClusterDump) {
yield String.raw`
-- try to create database
-- may fail but script will continue running
CREATE DATABASE immich;
-- switch to database / newly created database
\c immich
`;
}
yield SQL_RESET_SCHEMA;
for await (const chunk of inputStream) { for await (const chunk of inputStream) {
yield chunk; yield chunk;
@ -273,12 +262,11 @@ export async function restoreDatabaseBackup(
isPgClusterDump = true; isPgClusterDump = true;
} }
const { bin, args, databasePassword, databaseMajorVersion } = await buildPostgresLaunchArguments( const { bin, args, databaseUsername, databasePassword, databaseMajorVersion } = await buildPostgresLaunchArguments(
{ logger, database: databaseRepository, ...pgRepos }, { logger, database: databaseRepository, ...pgRepos },
'psql', 'psql',
{ {
singleTransaction: !isPgClusterDump, singleTransaction: !isPgClusterDump,
username: isPgClusterDump ? 'postgres' : undefined,
}, },
); );
@ -301,7 +289,7 @@ export async function restoreDatabaseBackup(
inputStream = storage.createPlainReadStream(backupFilePath); inputStream = storage.createPlainReadStream(backupFilePath);
} }
const sqlStream = Readable.from(sql(inputStream, isPgClusterDump)); const sqlStream = Readable.from(sql(inputStream, databaseUsername, isPgClusterDump));
const psql = processRepository.spawnDuplexStream(bin, args, { const psql = processRepository.spawnDuplexStream(bin, args, {
env: { env: {
PATH: process.env.PATH, PATH: process.env.PATH,
@ -332,7 +320,7 @@ export async function restoreDatabaseBackup(
fileStream.pipe(gunzip); fileStream.pipe(gunzip);
inputStream = gunzip; inputStream = gunzip;
const sqlStream = Readable.from(sqlRollback(inputStream, isPgClusterDump)); const sqlStream = Readable.from(sqlRollback(inputStream, databaseUsername));
const psql = processRepository.spawnDuplexStream(bin, args, { const psql = processRepository.spawnDuplexStream(bin, args, {
env: { env: {
PATH: process.env.PATH, PATH: process.env.PATH,