refactor: permit any .sql(.gz) to be listed/restored

This commit is contained in:
izzy
2025-11-21 14:24:35 +00:00
parent 19ba23056c
commit a3c6d71a58
6 changed files with 54 additions and 44 deletions

View File

@@ -30,7 +30,6 @@ export class MaintenanceStatusResponseDto {
export class MaintenanceListBackupsResponseDto {
backups!: string[];
failedBackups!: string[];
}
export class MaintenanceUploadBackupDto {

View File

@@ -275,8 +275,8 @@ export class MaintenanceWorkerService {
});
}
async listBackups(): Promise<Record<'backups' | 'failedBackups', string[]>> {
return listBackups(this.backupRepos);
async listBackups(): Promise<{ backups: string[] }> {
return { backups: await listBackups(this.backupRepos) };
}
async deleteBackup(filename: string): Promise<void> {

View File

@@ -5,7 +5,12 @@ import { OnEvent, OnJob } from 'src/decorators';
import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum';
import { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { createBackup, listBackups, UnsupportedPostgresError } from 'src/utils/backups';
import {
createBackup,
isFailedBackupName,
isValidRoutineBackupName,
UnsupportedPostgresError,
} from 'src/utils/backups';
import { handlePromiseError } from 'src/utils/misc';
@Injectable()
@@ -50,7 +55,9 @@ export class BackupService extends BaseService {
} = await this.getConfig({ withCache: false });
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
const { backups, failedBackups } = await listBackups(this.backupRepos);
const files = await this.storageRepository.readdir(backupsFolder);
const backups = files.filter(isValidRoutineBackupName);
const failedBackups = files.filter(isFailedBackupName);
const toDelete = backups.slice(config.keepLastAmount);
toDelete.push(...failedBackups);

View File

@@ -75,8 +75,8 @@ export class MaintenanceService extends BaseService {
* Backups
*/
async listBackups(): Promise<Record<'backups' | 'failedBackups', string[]>> {
return listBackups(this.backupRepos);
async listBackups(): Promise<{ backups: string[] }> {
return { backups: await listBackups(this.backupRepos) };
}
async deleteBackup(filename: string): Promise<void> {

View File

@@ -16,14 +16,18 @@ import { ProcessRepository } from 'src/repositories/process.repository';
import { StorageRepository } from 'src/repositories/storage.repository';
export function isValidBackupName(filename: string) {
const oldBackupStyle = filename.match(/immich-db-backup-\d+\.sql\.gz$/);
return filename.match(/^[\d\w-.]+\.sql(?:\.gz)?$/);
}
export function isValidRoutineBackupName(filename: string) {
const oldBackupStyle = filename.match(/^immich-db-backup-\d+\.sql\.gz$/);
//immich-db-backup-20250729T114018-v1.136.0-pg14.17.sql.gz
const newBackupStyle = filename.match(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/);
const newBackupStyle = filename.match(/^immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/);
return oldBackupStyle || newBackupStyle;
}
export function isFailedBackupName(filename: string) {
return filename.match(/immich-db-backup-.*\.sql\.gz\.tmp$/);
return filename.match(/^immich-db-backup-.*\.sql\.gz\.tmp$/);
}
type BackupRepos = {
@@ -127,7 +131,7 @@ export async function buildPostgresLaunchArguments(
export async function createBackup(
{ logger, storage, process: processRepository, ...pgRepos }: BackupRepos,
filenameSuffix: string = '',
filenamePrefix: string = '',
): Promise<void> {
logger.debug(`Database Backup Started`);
@@ -140,7 +144,7 @@ export async function createBackup(
const backupFilePath = join(
StorageCore.getBaseFolder(StorageFolder.Backups),
`immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}${filenameSuffix}.sql.gz.tmp`,
`${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`,
);
try {
@@ -176,10 +180,7 @@ export async function restoreBackup(
let complete = false;
try {
if (!isValidBackupName(filename) && !filename.startsWith('development-')) {
// if we want to allow custom file names
// replace this with a check that we aren't
// traversing out of the backup directory
if (!isValidBackupName(filename)) {
throw new Error('Invalid backup file format!');
}
@@ -193,7 +194,7 @@ export async function restoreBackup(
progressCb?.('backup', 0.05);
await createBackup({ logger, storage, process: processRepository, ...pgRepos }, '-maintenance');
await createBackup({ logger, storage, process: processRepository, ...pgRepos }, 'restore-point-');
logger.log(`Database Restore Starting. Database Version: ${databaseMajorVersion}`);
@@ -256,24 +257,23 @@ export async function deleteBackup({ storage }: Pick<BackupRepos, 'storage'>, fi
await storage.unlink(path.join(backupsFolder, filename));
}
export async function listBackups({
storage,
}: Pick<BackupRepos, 'storage'>): Promise<Record<'backups' | 'failedBackups', string[]>> {
export async function listBackups({ storage }: Pick<BackupRepos, 'storage'>): Promise<string[]> {
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
const files = await storage.readdir(backupsFolder);
return {
backups: files
.filter((fn) => isValidBackupName(fn) || fn.startsWith('development-'))
.sort()
.toReversed(),
failedBackups: files.filter((fn) => isFailedBackupName(fn)),
};
return files
.filter(isValidBackupName)
.sort((a, b) => (a.startsWith('uploaded-') !== b.startsWith('uploaded-') ? 1 : a.localeCompare(b)))
.toReversed();
}
export async function uploadBackup(file: Express.Multer.File): Promise<void> {
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
const path = join(backupsFolder, file.originalname);
const fn = file.originalname;
if (!isValidBackupName(fn)) {
throw new BadRequestException('Not a valid backup name!');
}
const path = join(backupsFolder, `uploaded-${fn}`);
try {
await stat(path);

View File

@@ -96,6 +96,7 @@
let uploadProgress = $state(-1);
async function upload() {
try {
const [file] = await openFilePicker({ multiple: false });
const formData = new FormData();
formData.append('file', file);
@@ -112,9 +113,12 @@
const { backups: newList } = await listBackups();
backups = mapBackups(newList);
} catch (error) {
handleError(error, 'Could not upload backup, is it an .sql/.sql.gz file?');
} finally {
uploadProgress = -1;
}
}
</script>
<Stack gap={2} class="mt-4 text-left">