Compare commits

...

1 Commits

Author SHA1 Message Date
izzy
d2d58c2024 refactor(database restores): use file interceptor (partial impl.) 2026-03-02 09:48:16 +00:00
9 changed files with 30 additions and 43 deletions

View File

@@ -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<void> {
return this.service.uploadBackup(file);
}
@UseInterceptors(FileUploadInterceptor)
uploadDatabaseBackup() {}
}

View File

@@ -29,6 +29,7 @@ export enum UploadFieldName {
ASSET_DATA = 'assetData',
SIDECAR_DATA = 'sidecarData',
PROFILE_DATA = 'file',
BACKUP_DATA = 'backup',
}
class AssetMediaBase {

View File

@@ -490,6 +490,7 @@ export enum MetadataKey {
export enum RouteKey {
Asset = 'assets',
User = 'users',
DatabaseBackup = 'admin/database-backups',
}
export enum CacheControl {

View File

@@ -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<void> {
return this.databaseBackupService.uploadBackup(file);
}
@UseInterceptors(FileUploadInterceptor)
uploadDatabaseBackup() {}
@Get('admin/maintenance/status')
maintenanceStatus(@Req() request: Request): Promise<MaintenanceStatusResponseDto> {

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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<void> {
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!');

View File

@@ -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,

View File

@@ -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<DatabaseBackupUploadDto>({
url: getBaseUrl() + '/admin/database-backups/upload',