mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 15:42:32 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2d58c2024 |
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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!');
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user