From d2d58c2024501bf7f0b6667dd7599fd67fe234a6 Mon Sep 17 00:00:00 2001 From: izzy Date: Mon, 2 Mar 2026 09:48:16 +0000 Subject: [PATCH] refactor(database restores): use file interceptor (partial impl.) --- .../controllers/database-backup.controller.ts | 13 +++------- server/src/dtos/asset-media.dto.ts | 1 + server/src/enum.ts | 1 + .../maintenance-worker.controller.ts | 25 +++---------------- .../src/middleware/file-upload.interceptor.ts | 6 +++++ server/src/services/asset-media.service.ts | 11 ++++++++ .../src/services/database-backup.service.ts | 13 +--------- server/src/utils/mime-types.ts | 1 + .../lib/services/database-backups.service.ts | 2 +- 9 files changed, 30 insertions(+), 43 deletions(-) diff --git a/server/src/controllers/database-backup.controller.ts b/server/src/controllers/database-backup.controller.ts index 737c8f3958..5f5ae52850 100644 --- a/server/src/controllers/database-backup.controller.ts +++ b/server/src/controllers/database-backup.controller.ts @@ -1,5 +1,4 @@ -import { Body, Controller, Delete, Get, Next, Param, Post, Res, UploadedFile, UseInterceptors } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; +import { Body, Controller, Delete, Get, Next, Param, Post, Res, UseInterceptors } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; @@ -10,6 +9,7 @@ import { } from 'src/dtos/database-backup.dto'; import { ApiTag, ImmichCookie, Permission } from 'src/enum'; import { Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard'; +import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoginDetails } from 'src/services/auth.service'; import { DatabaseBackupService } from 'src/services/database-backup.service'; @@ -91,11 +91,6 @@ export class DatabaseBackupController { description: 'Uploads .sql/.sql.gz file to restore backup from', history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), }) - @UseInterceptors(FileInterceptor('file')) - uploadDatabaseBackup( - @UploadedFile() - file: Express.Multer.File, - ): Promise { - return this.service.uploadBackup(file); - } + @UseInterceptors(FileUploadInterceptor) + uploadDatabaseBackup() {} } diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index 4655850379..c410d43831 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -29,6 +29,7 @@ export enum UploadFieldName { ASSET_DATA = 'assetData', SIDECAR_DATA = 'sidecarData', PROFILE_DATA = 'file', + BACKUP_DATA = 'backup', } class AssetMediaBase { diff --git a/server/src/enum.ts b/server/src/enum.ts index 2aa9bd2aa6..ec582a240f 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -490,6 +490,7 @@ export enum MetadataKey { export enum RouteKey { Asset = 'assets', User = 'users', + DatabaseBackup = 'admin/database-backups', } export enum CacheControl { diff --git a/server/src/maintenance/maintenance-worker.controller.ts b/server/src/maintenance/maintenance-worker.controller.ts index 162fa27257..b1fe156180 100644 --- a/server/src/maintenance/maintenance-worker.controller.ts +++ b/server/src/maintenance/maintenance-worker.controller.ts @@ -1,17 +1,4 @@ -import { - Body, - Controller, - Delete, - Get, - Next, - Param, - Post, - Req, - Res, - UploadedFile, - UseInterceptors, -} from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; +import { Body, Controller, Delete, Get, Next, Param, Post, Req, Res, UseInterceptors } from '@nestjs/common'; import { NextFunction, Request, Response } from 'express'; import { MaintenanceAuthDto, @@ -34,6 +21,7 @@ import { FilenameParamDto } from 'src/validation'; import type { DatabaseBackupController as _DatabaseBackupController } from 'src/controllers/database-backup.controller'; import type { ServerController as _ServerController } from 'src/controllers/server.controller'; import { DatabaseBackupDeleteDto, DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; +import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { DatabaseBackupService } from 'src/services/database-backup.service'; @Controller() @@ -93,13 +81,8 @@ export class MaintenanceWorkerController { */ @Post('admin/database-backups/upload') @MaintenanceRoute() - @UseInterceptors(FileInterceptor('file')) - uploadDatabaseBackup( - @UploadedFile() - file: Express.Multer.File, - ): Promise { - return this.databaseBackupService.uploadBackup(file); - } + @UseInterceptors(FileUploadInterceptor) + uploadDatabaseBackup() {} @Get('admin/maintenance/status') maintenanceStatus(@Req() request: Request): Promise { diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index 6dfd11ee4b..5dd919f115 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -48,6 +48,7 @@ export class FileUploadInterceptor implements NestInterceptor { private handlers: { userProfile: RequestHandler; assetUpload: RequestHandler; + databaseBackup: RequestHandler; }; private defaultStorage: StorageEngine; @@ -77,6 +78,7 @@ export class FileUploadInterceptor implements NestInterceptor { { name: UploadFieldName.ASSET_DATA, maxCount: 1 }, { name: UploadFieldName.SIDECAR_DATA, maxCount: 1 }, ]), + databaseBackup: instance.single(UploadFieldName.BACKUP_DATA), }; } @@ -165,6 +167,10 @@ export class FileUploadInterceptor implements NestInterceptor { return this.handlers.userProfile; } + case RouteKey.DatabaseBackup: { + return this.handlers.databaseBackup; + } + default: { return null; } diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 020bda4df7..e9f7f5e716 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -86,6 +86,13 @@ export class AssetMediaService extends BaseService { } break; } + + case UploadFieldName.BACKUP_DATA: { + if (mimeTypes.isBackup(filename)) { + return true; + } + break; + } } this.logger.error(`Unsupported file type ${filename}`); @@ -101,6 +108,7 @@ export class AssetMediaService extends BaseService { [UploadFieldName.ASSET_DATA]: extension, [UploadFieldName.SIDECAR_DATA]: '.xmp', [UploadFieldName.PROFILE_DATA]: extension, + [UploadFieldName.BACKUP_DATA]: extension === '.gz' ? '.sql.gz' : extension, }; return sanitize(`${file.uuid}${lookup[fieldName]}`); @@ -113,6 +121,9 @@ export class AssetMediaService extends BaseService { if (fieldName === UploadFieldName.PROFILE_DATA) { folder = StorageCore.getFolderLocation(StorageFolder.Profile, auth.user.id); } + if (fieldName === UploadFieldName.BACKUP_DATA) { + folder = StorageCore.getFolderLocation(StorageFolder.Backups, `uploaded-${file.originalName}`); + } this.storageRepository.mkdirSync(folder); diff --git a/server/src/services/database-backup.service.ts b/server/src/services/database-backup.service.ts index 3c964c950c..ea9401ef1a 100644 --- a/server/src/services/database-backup.service.ts +++ b/server/src/services/database-backup.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable, Optional } from '@nestjs/common'; import { debounce } from 'lodash'; import { DateTime } from 'luxon'; -import path, { basename } from 'node:path'; +import path from 'node:path'; import { PassThrough, Readable, Writable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import semver from 'semver'; @@ -252,17 +252,6 @@ export class DatabaseBackupService { return backupFilePath; } - async uploadBackup(file: Express.Multer.File): Promise { - const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); - const fn = basename(file.originalname); - if (!isValidDatabaseBackupName(fn)) { - throw new BadRequestException('Invalid backup name!'); - } - - const filePath = path.join(backupsFolder, `uploaded-${fn}`); - await this.storageRepository.createOrOverwriteFile(filePath, file.buffer); - } - downloadBackup(fileName: string): ImmichFileResponse { if (!isValidDatabaseBackupName(fileName)) { throw new BadRequestException('Invalid backup name!'); diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 43421e7937..1e9055bc53 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -149,6 +149,7 @@ export const mimeTypes = { isProfile: (filename: string) => isType(filename, profile), isSidecar: (filename: string) => isType(filename, sidecar), isVideo: (filename: string) => isType(filename, video), + isBackup: (filename: string) => filename.endsWith('.sql') || filename.endsWith('.sql.gz'), canBeTransparent: (filename: string) => transparentCapableExtensions.has(extname(filename).toLowerCase()), isRaw: (filename: string) => isType(filename, raw), lookup, diff --git a/web/src/lib/services/database-backups.service.ts b/web/src/lib/services/database-backups.service.ts index 900a0ddb80..243a07ac98 100644 --- a/web/src/lib/services/database-backups.service.ts +++ b/web/src/lib/services/database-backups.service.ts @@ -102,7 +102,7 @@ export const handleUploadDatabaseBackup = async () => { try { const [file] = await openFilePicker({ multiple: false }); const formData = new FormData(); - formData.append('file', file); + formData.append('backup', file); await uploadRequest({ url: getBaseUrl() + '/admin/database-backups/upload',