immich/server/src/controllers/maintenance.controller.ts
izzy 94af1bba4d
refactor: maintenanceStatus -> getMaintenanceStatus
refactor: `integrityCheck` -> `detectPriorInstall`
chore: add `v2.4.0` version
refactor: `/backups/list` -> `/backups`
refactor: use sendFile in download route
refactor: use separate backups permissions
chore: correct descriptions
refactor: permit handler that doesn't return promise for sendfile
2025-12-02 16:47:31 +00:00

171 lines
5.7 KiB
TypeScript

import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Next,
Param,
Post,
Res,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import {
MaintenanceAuthDto,
MaintenanceIntegrityResponseDto,
MaintenanceListBackupsResponseDto,
MaintenanceLoginDto,
MaintenanceStatusResponseDto,
MaintenanceUploadBackupDto,
SetMaintenanceModeDto,
} from 'src/dtos/maintenance.dto';
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
import { Auth, Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { LoginDetails } from 'src/services/auth.service';
import { MaintenanceService } from 'src/services/maintenance.service';
import { sendFile } from 'src/utils/file';
import { respondWithCookie } from 'src/utils/response';
import { FilenameParamDto } from 'src/validation';
@ApiTags(ApiTag.Maintenance)
@Controller('admin/maintenance')
export class MaintenanceController {
constructor(
private logger: LoggingRepository,
private service: MaintenanceService,
) {}
@Get('status')
@Endpoint({
summary: 'Get maintenance mode status',
description: 'Fetch information about the currently running maintenance action.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
getMaintenanceStatus(): MaintenanceStatusResponseDto {
return {
action: MaintenanceAction.End,
};
}
@Get('detect-install')
@Endpoint({
summary: 'Detect existing install',
description: 'Collect integrity checks and other heuristics about local data.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
detectPriorInstall(): Promise<MaintenanceIntegrityResponseDto> {
return this.service.detectPriorInstall();
}
@Post('login')
@Endpoint({
summary: 'Log into maintenance mode',
description: 'Login with maintenance token or cookie to receive current information and perform further actions.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
})
maintenanceLogin(@Body() _dto: MaintenanceLoginDto): MaintenanceAuthDto {
throw new BadRequestException('Not in maintenance mode');
}
@Post()
@Endpoint({
summary: 'Set maintenance mode',
description: 'Put Immich into or take it out of maintenance mode',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
})
@Authenticated({ permission: Permission.Maintenance, admin: true })
async setMaintenanceMode(
@Auth() auth: AuthDto,
@Body() dto: SetMaintenanceModeDto,
@GetLoginDetails() loginDetails: LoginDetails,
@Res({ passthrough: true }) res: Response,
): Promise<void> {
if (dto.action !== MaintenanceAction.End) {
const { jwt } = await this.service.startMaintenance(dto, auth.user.name);
return respondWithCookie(res, undefined, {
isSecure: loginDetails.isSecure,
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
});
}
}
@Get('backups')
@Endpoint({
summary: 'List backups',
description: 'Get the list of the successful and failed backups',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
@Authenticated({ permission: Permission.Maintenance, admin: true })
listBackups(): Promise<MaintenanceListBackupsResponseDto> {
return this.service.listBackups();
}
@Get('backups/:filename')
@FileResponse()
@Endpoint({
summary: 'Download backup',
description: 'Downloads the database backup file',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
@Authenticated({ permission: Permission.BackupDownload, admin: true })
async downloadBackup(
@Param() { filename }: FilenameParamDto,
@Res() res: Response,
@Next() next: NextFunction,
): Promise<void> {
await sendFile(res, next, () => this.service.downloadBackup(filename), this.logger);
}
@Delete('backups/:filename')
@Endpoint({
summary: 'Delete backup',
description: 'Delete a backup by its filename',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
@Authenticated({ permission: Permission.BackupDelete, admin: true })
async deleteBackup(@Param() { filename }: FilenameParamDto): Promise<void> {
return this.service.deleteBackup(filename);
}
@Post('backups/restore')
@Endpoint({
summary: 'Start backup restore flow',
description: 'Put Immich into maintenance mode to restore a backup (Immich must not be configured)',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
async startRestoreFlow(
@GetLoginDetails() loginDetails: LoginDetails,
@Res({ passthrough: true }) res: Response,
): Promise<void> {
const { jwt } = await this.service.startRestoreFlow();
return respondWithCookie(res, undefined, {
isSecure: loginDetails.isSecure,
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
});
}
@Post('backups/upload')
@Authenticated({ permission: Permission.BackupUpload, admin: true })
@ApiConsumes('multipart/form-data')
@ApiBody({ description: 'Backup Upload', type: MaintenanceUploadBackupDto })
@Endpoint({
summary: 'Upload database backup',
description: 'Uploads .sql/.sql.gz file to restore backup from',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
@UseInterceptors(FileInterceptor('file'))
uploadBackup(
@UploadedFile()
file: Express.Multer.File,
): Promise<void> {
return this.service.uploadBackup(file);
}
}