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
@@ -1,5 +1,4 @@
import { Body, Controller, Delete, Get, Next, Param, Post, Res, UploadedFile, UseInterceptors } from '@nestjs/common'; import { Body, Controller, Delete, Get, Next, Param, Post, Res, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { Endpoint, HistoryBuilder } from 'src/decorators'; import { Endpoint, HistoryBuilder } from 'src/decorators';
@@ -10,6 +9,7 @@ import {
} from 'src/dtos/database-backup.dto'; } from 'src/dtos/database-backup.dto';
import { ApiTag, ImmichCookie, Permission } from 'src/enum'; import { ApiTag, ImmichCookie, Permission } from 'src/enum';
import { Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard'; 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 { LoggingRepository } from 'src/repositories/logging.repository';
import { LoginDetails } from 'src/services/auth.service'; import { LoginDetails } from 'src/services/auth.service';
import { DatabaseBackupService } from 'src/services/database-backup.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', description: 'Uploads .sql/.sql.gz file to restore backup from',
history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'),
}) })
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileUploadInterceptor)
uploadDatabaseBackup( uploadDatabaseBackup() {}
@UploadedFile()
file: Express.Multer.File,
): Promise<void> {
return this.service.uploadBackup(file);
}
} }
+1
View File
@@ -29,6 +29,7 @@ export enum UploadFieldName {
ASSET_DATA = 'assetData', ASSET_DATA = 'assetData',
SIDECAR_DATA = 'sidecarData', SIDECAR_DATA = 'sidecarData',
PROFILE_DATA = 'file', PROFILE_DATA = 'file',
BACKUP_DATA = 'backup',
} }
class AssetMediaBase { class AssetMediaBase {
+1
View File
@@ -490,6 +490,7 @@ export enum MetadataKey {
export enum RouteKey { export enum RouteKey {
Asset = 'assets', Asset = 'assets',
User = 'users', User = 'users',
DatabaseBackup = 'admin/database-backups',
} }
export enum CacheControl { export enum CacheControl {
@@ -1,17 +1,4 @@
import { import { Body, Controller, Delete, Get, Next, Param, Post, Req, Res, UseInterceptors } from '@nestjs/common';
Body,
Controller,
Delete,
Get,
Next,
Param,
Post,
Req,
Res,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { import {
MaintenanceAuthDto, MaintenanceAuthDto,
@@ -34,6 +21,7 @@ import { FilenameParamDto } from 'src/validation';
import type { DatabaseBackupController as _DatabaseBackupController } from 'src/controllers/database-backup.controller'; import type { DatabaseBackupController as _DatabaseBackupController } from 'src/controllers/database-backup.controller';
import type { ServerController as _ServerController } from 'src/controllers/server.controller'; import type { ServerController as _ServerController } from 'src/controllers/server.controller';
import { DatabaseBackupDeleteDto, DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; 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'; import { DatabaseBackupService } from 'src/services/database-backup.service';
@Controller() @Controller()
@@ -93,13 +81,8 @@ export class MaintenanceWorkerController {
*/ */
@Post('admin/database-backups/upload') @Post('admin/database-backups/upload')
@MaintenanceRoute() @MaintenanceRoute()
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileUploadInterceptor)
uploadDatabaseBackup( uploadDatabaseBackup() {}
@UploadedFile()
file: Express.Multer.File,
): Promise<void> {
return this.databaseBackupService.uploadBackup(file);
}
@Get('admin/maintenance/status') @Get('admin/maintenance/status')
maintenanceStatus(@Req() request: Request): Promise<MaintenanceStatusResponseDto> { maintenanceStatus(@Req() request: Request): Promise<MaintenanceStatusResponseDto> {
@@ -48,6 +48,7 @@ export class FileUploadInterceptor implements NestInterceptor {
private handlers: { private handlers: {
userProfile: RequestHandler; userProfile: RequestHandler;
assetUpload: RequestHandler; assetUpload: RequestHandler;
databaseBackup: RequestHandler;
}; };
private defaultStorage: StorageEngine; private defaultStorage: StorageEngine;
@@ -77,6 +78,7 @@ export class FileUploadInterceptor implements NestInterceptor {
{ name: UploadFieldName.ASSET_DATA, maxCount: 1 }, { name: UploadFieldName.ASSET_DATA, maxCount: 1 },
{ name: UploadFieldName.SIDECAR_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; return this.handlers.userProfile;
} }
case RouteKey.DatabaseBackup: {
return this.handlers.databaseBackup;
}
default: { default: {
return null; return null;
} }
@@ -86,6 +86,13 @@ export class AssetMediaService extends BaseService {
} }
break; break;
} }
case UploadFieldName.BACKUP_DATA: {
if (mimeTypes.isBackup(filename)) {
return true;
}
break;
}
} }
this.logger.error(`Unsupported file type ${filename}`); this.logger.error(`Unsupported file type ${filename}`);
@@ -101,6 +108,7 @@ export class AssetMediaService extends BaseService {
[UploadFieldName.ASSET_DATA]: extension, [UploadFieldName.ASSET_DATA]: extension,
[UploadFieldName.SIDECAR_DATA]: '.xmp', [UploadFieldName.SIDECAR_DATA]: '.xmp',
[UploadFieldName.PROFILE_DATA]: extension, [UploadFieldName.PROFILE_DATA]: extension,
[UploadFieldName.BACKUP_DATA]: extension === '.gz' ? '.sql.gz' : extension,
}; };
return sanitize(`${file.uuid}${lookup[fieldName]}`); return sanitize(`${file.uuid}${lookup[fieldName]}`);
@@ -113,6 +121,9 @@ export class AssetMediaService extends BaseService {
if (fieldName === UploadFieldName.PROFILE_DATA) { if (fieldName === UploadFieldName.PROFILE_DATA) {
folder = StorageCore.getFolderLocation(StorageFolder.Profile, auth.user.id); 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); this.storageRepository.mkdirSync(folder);
+1 -12
View File
@@ -1,7 +1,7 @@
import { BadRequestException, Injectable, Optional } from '@nestjs/common'; import { BadRequestException, Injectable, Optional } from '@nestjs/common';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import path, { basename } from 'node:path'; import path from 'node:path';
import { PassThrough, Readable, Writable } from 'node:stream'; import { PassThrough, Readable, Writable } from 'node:stream';
import { pipeline } from 'node:stream/promises'; import { pipeline } from 'node:stream/promises';
import semver from 'semver'; import semver from 'semver';
@@ -252,17 +252,6 @@ export class DatabaseBackupService {
return backupFilePath; 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 { downloadBackup(fileName: string): ImmichFileResponse {
if (!isValidDatabaseBackupName(fileName)) { if (!isValidDatabaseBackupName(fileName)) {
throw new BadRequestException('Invalid backup name!'); throw new BadRequestException('Invalid backup name!');
+1
View File
@@ -149,6 +149,7 @@ export const mimeTypes = {
isProfile: (filename: string) => isType(filename, profile), isProfile: (filename: string) => isType(filename, profile),
isSidecar: (filename: string) => isType(filename, sidecar), isSidecar: (filename: string) => isType(filename, sidecar),
isVideo: (filename: string) => isType(filename, video), isVideo: (filename: string) => isType(filename, video),
isBackup: (filename: string) => filename.endsWith('.sql') || filename.endsWith('.sql.gz'),
canBeTransparent: (filename: string) => transparentCapableExtensions.has(extname(filename).toLowerCase()), canBeTransparent: (filename: string) => transparentCapableExtensions.has(extname(filename).toLowerCase()),
isRaw: (filename: string) => isType(filename, raw), isRaw: (filename: string) => isType(filename, raw),
lookup, lookup,
@@ -102,7 +102,7 @@ export const handleUploadDatabaseBackup = async () => {
try { try {
const [file] = await openFilePicker({ multiple: false }); const [file] = await openFilePicker({ multiple: false });
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('backup', file);
await uploadRequest<DatabaseBackupUploadDto>({ await uploadRequest<DatabaseBackupUploadDto>({
url: getBaseUrl() + '/admin/database-backups/upload', url: getBaseUrl() + '/admin/database-backups/upload',