2025-11-21 12:52:27 +00:00
|
|
|
import {
|
|
|
|
|
BadRequestException,
|
|
|
|
|
Body,
|
|
|
|
|
Controller,
|
|
|
|
|
Delete,
|
|
|
|
|
Get,
|
|
|
|
|
Param,
|
|
|
|
|
Post,
|
|
|
|
|
Res,
|
|
|
|
|
UploadedFile,
|
|
|
|
|
UseInterceptors,
|
|
|
|
|
} from '@nestjs/common';
|
|
|
|
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
|
|
|
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
2025-11-17 17:15:44 +00:00
|
|
|
import { Response } from 'express';
|
|
|
|
|
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
|
|
|
|
import { AuthDto } from 'src/dtos/auth.dto';
|
2025-11-19 15:54:44 +00:00
|
|
|
import {
|
|
|
|
|
MaintenanceAuthDto,
|
2025-11-21 16:37:28 +00:00
|
|
|
MaintenanceIntegrityResponseDto,
|
2025-11-20 15:31:35 +00:00
|
|
|
MaintenanceListBackupsResponseDto,
|
2025-11-19 15:54:44 +00:00
|
|
|
MaintenanceLoginDto,
|
|
|
|
|
MaintenanceStatusResponseDto,
|
2025-11-21 12:52:27 +00:00
|
|
|
MaintenanceUploadBackupDto,
|
2025-11-19 15:54:44 +00:00
|
|
|
SetMaintenanceModeDto,
|
|
|
|
|
} from 'src/dtos/maintenance.dto';
|
2025-11-17 17:15:44 +00:00
|
|
|
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
2025-11-21 14:47:11 +00:00
|
|
|
import { Auth, Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard';
|
2025-11-21 16:37:28 +00:00
|
|
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
2025-11-17 17:15:44 +00:00
|
|
|
import { LoginDetails } from 'src/services/auth.service';
|
|
|
|
|
import { MaintenanceService } from 'src/services/maintenance.service';
|
2025-11-21 16:37:28 +00:00
|
|
|
import { integrityCheck } from 'src/utils/maintenance';
|
2025-11-17 17:15:44 +00:00
|
|
|
import { respondWithCookie } from 'src/utils/response';
|
2025-11-20 15:31:35 +00:00
|
|
|
import { FilenameParamDto } from 'src/validation';
|
2025-11-17 17:15:44 +00:00
|
|
|
|
|
|
|
|
@ApiTags(ApiTag.Maintenance)
|
|
|
|
|
@Controller('admin/maintenance')
|
|
|
|
|
export class MaintenanceController {
|
2025-11-21 16:37:28 +00:00
|
|
|
constructor(
|
|
|
|
|
private service: MaintenanceService,
|
|
|
|
|
private storageRepository: StorageRepository,
|
|
|
|
|
) {}
|
2025-11-17 17:15:44 +00:00
|
|
|
|
2025-11-20 16:08:16 +00:00
|
|
|
@Get('status')
|
2025-11-19 15:54:44 +00:00
|
|
|
@Endpoint({
|
|
|
|
|
summary: 'Get maintenance mode status',
|
|
|
|
|
description: 'Fetch information about the currently running maintenance action.',
|
|
|
|
|
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
|
|
|
|
})
|
|
|
|
|
maintenanceStatus(): MaintenanceStatusResponseDto {
|
2025-11-20 15:24:48 +00:00
|
|
|
return {
|
|
|
|
|
action: MaintenanceAction.End,
|
|
|
|
|
};
|
2025-11-19 15:54:44 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-21 16:37:28 +00:00
|
|
|
@Get('integrity')
|
|
|
|
|
@Endpoint({
|
|
|
|
|
summary: 'Get integrity and heuristics',
|
|
|
|
|
description: 'Collect integrity checks and other heuristics about local data.',
|
|
|
|
|
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
|
|
|
|
})
|
|
|
|
|
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
|
|
|
|
|
return integrityCheck(this.storageRepository);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 17:15:44 +00:00
|
|
|
@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> {
|
2025-11-18 17:28:03 +00:00
|
|
|
if (dto.action !== MaintenanceAction.End) {
|
|
|
|
|
const { jwt } = await this.service.startMaintenance(dto, auth.user.name);
|
2025-11-17 17:15:44 +00:00
|
|
|
return respondWithCookie(res, undefined, {
|
|
|
|
|
isSecure: loginDetails.isSecure,
|
|
|
|
|
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-20 15:31:35 +00:00
|
|
|
|
2025-11-20 16:08:16 +00:00
|
|
|
@Get('backups/list')
|
2025-11-20 15:31:35 +00:00
|
|
|
@Endpoint({
|
|
|
|
|
summary: 'List backups',
|
|
|
|
|
description: 'Get the list of the successful and failed backups',
|
|
|
|
|
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
|
|
|
|
})
|
|
|
|
|
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
|
|
|
|
listBackups(): Promise<MaintenanceListBackupsResponseDto> {
|
|
|
|
|
return this.service.listBackups();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-21 14:47:11 +00:00
|
|
|
@Get('backups/:filename')
|
|
|
|
|
@FileResponse()
|
|
|
|
|
@Endpoint({
|
|
|
|
|
summary: 'Download backup',
|
|
|
|
|
description: 'Downloads the database backup file',
|
|
|
|
|
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
|
|
|
|
})
|
|
|
|
|
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
2025-11-21 18:02:27 +00:00
|
|
|
downloadBackup(@Param() { filename }: FilenameParamDto, @Res() res: Response) {
|
2025-11-21 14:47:11 +00:00
|
|
|
res.header('Content-Disposition', 'attachment');
|
|
|
|
|
res.sendFile(this.service.getBackupPath(filename));
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-20 16:08:16 +00:00
|
|
|
@Delete('backups/:filename')
|
2025-11-20 15:31:35 +00:00
|
|
|
@Endpoint({
|
|
|
|
|
summary: 'Delete backup',
|
|
|
|
|
description: 'Delete a backup by its filename',
|
|
|
|
|
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
|
|
|
|
})
|
|
|
|
|
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
|
|
|
|
async deleteBackup(@Param() { filename }: FilenameParamDto): Promise<void> {
|
|
|
|
|
return this.service.deleteBackup(filename);
|
|
|
|
|
}
|
2025-11-20 18:31:57 +00:00
|
|
|
|
|
|
|
|
@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('v9.9.9').alpha('v9.9.9'),
|
|
|
|
|
})
|
|
|
|
|
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 }],
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-21 12:52:27 +00:00
|
|
|
|
|
|
|
|
@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(
|
2025-11-21 14:58:38 +00:00
|
|
|
@UploadedFile()
|
2025-11-21 12:52:27 +00:00
|
|
|
file: Express.Multer.File,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
return this.service.uploadBackup(file);
|
|
|
|
|
}
|
2025-11-17 17:15:44 +00:00
|
|
|
}
|