feat: upload backups

This commit is contained in:
izzy
2025-11-21 12:52:27 +00:00
parent 3d2d7fa64c
commit 19ba23056c
12 changed files with 297 additions and 13 deletions

View File

@@ -1,5 +1,19 @@
import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import {
BadRequestException,
Body,
Controller,
Delete,
FileTypeValidator,
Get,
Param,
ParseFilePipe,
Post,
Res,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
@@ -8,6 +22,7 @@ import {
MaintenanceListBackupsResponseDto,
MaintenanceLoginDto,
MaintenanceStatusResponseDto,
MaintenanceUploadBackupDto,
SetMaintenanceModeDto,
} from 'src/dtos/maintenance.dto';
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
@@ -104,4 +119,21 @@ export class MaintenanceController {
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
});
}
@Post('backups/upload')
@Authenticated({ permission: Permission.Maintenance, admin: true })
@ApiConsumes('multipart/form-data')
@ApiBody({ description: 'Backup Upload', type: MaintenanceUploadBackupDto })
@Endpoint({
summary: 'Upload asset',
description: 'Uploads a new asset to the server.',
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
})
@UseInterceptors(FileInterceptor('file'))
uploadBackup(
@UploadedFile(new ParseFilePipe({ validators: [new FileTypeValidator({ fileType: 'application/gzip' })] }))
file: Express.Multer.File,
): Promise<void> {
return this.service.uploadBackup(file);
}
}

View File

@@ -1,3 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { MaintenanceAction } from 'src/enum';
import { ValidateEnum, ValidateString } from 'src/validation';
@@ -31,3 +32,8 @@ export class MaintenanceListBackupsResponseDto {
backups!: string[];
failedBackups!: string[];
}
export class MaintenanceUploadBackupDto {
@ApiProperty({ type: 'string', format: 'binary', required: false })
file?: any;
}

View File

@@ -1,4 +1,18 @@
import { Body, Controller, Delete, Get, Param, Post, Req, Res } from '@nestjs/common';
import {
Body,
Controller,
Delete,
FileTypeValidator,
Get,
Param,
ParseFilePipe,
Post,
Req,
Res,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Request, Response } from 'express';
import {
MaintenanceAuthDto,
@@ -62,4 +76,14 @@ export class MaintenanceWorkerController {
async deleteBackup(@Param() { filename }: FilenameParamDto): Promise<void> {
return this.service.deleteBackup(filename);
}
@Post('admin/maintenance/backups/upload')
@MaintenanceRoute()
@UseInterceptors(FileInterceptor('file'))
uploadBackup(
@UploadedFile(new ParseFilePipe({ validators: [new FileTypeValidator({ fileType: 'application/gzip' })] }))
file: Express.Multer.File,
): Promise<void> {
return this.service.uploadBackup(file);
}
}

View File

@@ -20,7 +20,7 @@ import { type ApiService as _ApiService } from 'src/services/api.service';
import { type BaseService as _BaseService } from 'src/services/base.service';
import { type ServerService as _ServerService } from 'src/services/server.service';
import { MaintenanceModeState } from 'src/types';
import { deleteBackup, listBackups, restoreBackup } from 'src/utils/backups';
import { deleteBackup, listBackups, restoreBackup, uploadBackup } from 'src/utils/backups';
import { getConfig } from 'src/utils/config';
import { createMaintenanceLoginUrl } from 'src/utils/maintenance';
import { getExternalDomain } from 'src/utils/misc';
@@ -283,6 +283,10 @@ export class MaintenanceWorkerService {
return deleteBackup(this.backupRepos, filename);
}
async uploadBackup(file: Express.Multer.File): Promise<void> {
return uploadBackup(file);
}
private get backupRepos() {
return {
logger: this.logger,

View File

@@ -4,7 +4,7 @@ import { MaintenanceAuthDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.
import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
import { BaseService } from 'src/services/base.service';
import { MaintenanceModeState } from 'src/types';
import { deleteBackup, listBackups } from 'src/utils/backups';
import { deleteBackup, listBackups, uploadBackup } from 'src/utils/backups';
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
import { getExternalDomain } from 'src/utils/misc';
@@ -83,6 +83,10 @@ export class MaintenanceService extends BaseService {
return deleteBackup(this.backupRepos, filename);
}
async uploadBackup(file: Express.Multer.File): Promise<void> {
return uploadBackup(file);
}
private get backupRepos() {
return {
logger: this.logger,

View File

@@ -1,5 +1,7 @@
import { BadRequestException } from '@nestjs/common';
import { debounce } from 'lodash';
import { DateTime } from 'luxon';
import { stat, writeFile } from 'node:fs/promises';
import path, { join } from 'node:path';
import { PassThrough, Readable, Writable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
@@ -269,6 +271,18 @@ export async function listBackups({
};
}
export async function uploadBackup(file: Express.Multer.File): Promise<void> {
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
const path = join(backupsFolder, file.originalname);
try {
await stat(path);
throw new BadRequestException('File already exists!');
} catch {
await writeFile(path, file.buffer);
}
}
function createSqlProgressStreams(cb: (progress: number) => void) {
const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin');
const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`);