mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 17:25:11 +03:00
feat: system integrity check in restore flow
This commit is contained in:
@@ -17,6 +17,7 @@ import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
MaintenanceAuthDto,
|
||||
MaintenanceIntegrityResponseDto,
|
||||
MaintenanceListBackupsResponseDto,
|
||||
MaintenanceLoginDto,
|
||||
MaintenanceStatusResponseDto,
|
||||
@@ -25,15 +26,20 @@ import {
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { MaintenanceService } from 'src/services/maintenance.service';
|
||||
import { integrityCheck } from 'src/utils/maintenance';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
import { FilenameParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags(ApiTag.Maintenance)
|
||||
@Controller('admin/maintenance')
|
||||
export class MaintenanceController {
|
||||
constructor(private service: MaintenanceService) {}
|
||||
constructor(
|
||||
private service: MaintenanceService,
|
||||
private storageRepository: StorageRepository,
|
||||
) {}
|
||||
|
||||
@Get('status')
|
||||
@Endpoint({
|
||||
@@ -47,6 +53,16 @@ export class MaintenanceController {
|
||||
};
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@Endpoint({
|
||||
summary: 'Log into maintenance mode',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { MaintenanceAction } from 'src/enum';
|
||||
import { MaintenanceAction, StorageFolder } from 'src/enum';
|
||||
import { ValidateEnum, ValidateString } from 'src/validation';
|
||||
|
||||
export class SetMaintenanceModeDto {
|
||||
@@ -28,6 +28,22 @@ export class MaintenanceStatusResponseDto {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class MaintenanceIntegrityResponseDto {
|
||||
storageIntegrity!: Record<
|
||||
StorageFolder,
|
||||
{
|
||||
readable: boolean;
|
||||
writable: boolean;
|
||||
}
|
||||
>;
|
||||
storageHeuristics!: Record<
|
||||
StorageFolder,
|
||||
{
|
||||
files: number;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export class MaintenanceListBackupsResponseDto {
|
||||
backups!: string[];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Request, Response } from 'express';
|
||||
import {
|
||||
MaintenanceAuthDto,
|
||||
MaintenanceIntegrityResponseDto,
|
||||
MaintenanceListBackupsResponseDto,
|
||||
MaintenanceLoginDto,
|
||||
MaintenanceStatusResponseDto,
|
||||
@@ -13,13 +14,18 @@ import { ImmichCookie } from 'src/enum';
|
||||
import { MaintenanceRoute } from 'src/maintenance/maintenance-auth.guard';
|
||||
import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service';
|
||||
import { GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { integrityCheck } from 'src/utils/maintenance';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
import { FilenameParamDto } from 'src/validation';
|
||||
|
||||
@Controller()
|
||||
export class MaintenanceWorkerController {
|
||||
constructor(private service: MaintenanceWorkerService) {}
|
||||
constructor(
|
||||
private service: MaintenanceWorkerService,
|
||||
private storageRepository: StorageRepository,
|
||||
) {}
|
||||
|
||||
@Get('server/config')
|
||||
getServerConfig(): ServerConfigDto {
|
||||
@@ -31,6 +37,11 @@ export class MaintenanceWorkerController {
|
||||
return this.service.status(request.cookies[ImmichCookie.MaintenanceToken]);
|
||||
}
|
||||
|
||||
@Get('admin/maintenance/integrity')
|
||||
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
|
||||
return integrityCheck(this.storageRepository);
|
||||
}
|
||||
|
||||
@Post('admin/maintenance/login')
|
||||
async maintenanceLogin(
|
||||
@Req() request: Request,
|
||||
|
||||
@@ -2,10 +2,14 @@ import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import Redis from 'ioredis';
|
||||
import { SignJWT } from 'jose';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { join } from 'node:path';
|
||||
import { Server as SocketIO } from 'socket.io';
|
||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { MaintenanceAuthDto, MaintenanceIntegrityResponseDto } from 'src/dtos/maintenance.dto';
|
||||
import { StorageFolder } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
|
||||
export function sendOneShotAppRestart(state: AppRestartEvent): void {
|
||||
const server = new SocketIO();
|
||||
@@ -72,3 +76,47 @@ export async function signMaintenanceJwt(secret: string, data: MaintenanceAuthDt
|
||||
export function generateMaintenanceSecret(): string {
|
||||
return randomBytes(64).toString('hex');
|
||||
}
|
||||
|
||||
export async function integrityCheck(storageRepository: StorageRepository): Promise<MaintenanceIntegrityResponseDto> {
|
||||
return {
|
||||
storageIntegrity: Object.fromEntries(
|
||||
await Promise.all(
|
||||
Object.values(StorageFolder).map(async (folder) => {
|
||||
const path = join(StorageCore.getBaseFolder(folder), '.immich');
|
||||
|
||||
try {
|
||||
await storageRepository.readFile(path);
|
||||
|
||||
try {
|
||||
await storageRepository.overwriteFile(path, Buffer.from(`${Date.now()}`));
|
||||
return [folder, { readable: true, writable: true }];
|
||||
} catch {
|
||||
return [folder, { readable: true, writable: false }];
|
||||
}
|
||||
} catch {
|
||||
return [folder, { readable: false, writable: false }];
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
storageHeuristics: Object.fromEntries(
|
||||
await Promise.all(
|
||||
Object.values(StorageFolder).map(async (folder) => {
|
||||
const path = StorageCore.getBaseFolder(folder);
|
||||
const files = await storageRepository.readdir(path);
|
||||
|
||||
try {
|
||||
return [
|
||||
folder,
|
||||
{
|
||||
files: files.filter((fn) => fn !== '.immich').length,
|
||||
},
|
||||
];
|
||||
} catch {
|
||||
return [folder, { files: 0 }];
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user