mirror of
https://github.com/immich-app/immich.git
synced 2025-12-15 06:24:23 +03:00
feat: synchronised status, restore db action
This commit is contained in:
@@ -16604,6 +16604,9 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ManualJobName": {
|
||||
|
||||
@@ -146,6 +146,10 @@ export class MaintenanceModule {
|
||||
this.maintenanceWorkerService.authenticate(client.request.headers),
|
||||
);
|
||||
|
||||
this.maintenanceWebsocketRepository.setStatusUpdateFn((status) =>
|
||||
this.maintenanceEphemeralStateRepository.setStatus(status),
|
||||
);
|
||||
|
||||
await this.maintenanceWorkerService.logSecret();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ export class MaintenanceController {
|
||||
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||
})
|
||||
maintenanceStatus(): MaintenanceStatusResponseDto {
|
||||
return {};
|
||||
return {
|
||||
action: MaintenanceAction.End,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
|
||||
@@ -19,7 +19,7 @@ export class MaintenanceAuthDto {
|
||||
}
|
||||
|
||||
export class MaintenanceStatusResponseDto {
|
||||
action?: MaintenanceAction;
|
||||
action!: MaintenanceAction;
|
||||
|
||||
progress?: number;
|
||||
task?: string;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MaintenanceStatusResponseDto } from 'src/dtos/maintenance.dto';
|
||||
import { MaintenanceAction } from 'src/enum';
|
||||
|
||||
@Injectable()
|
||||
export class MaintenanceEphemeralStateRepository {
|
||||
#secret: string = null!;
|
||||
#state: MaintenanceStatusResponseDto = {};
|
||||
#state: MaintenanceStatusResponseDto = {
|
||||
action: MaintenanceAction.Start,
|
||||
};
|
||||
|
||||
setSecret(secret: string) {
|
||||
this.#secret = secret;
|
||||
@@ -14,15 +17,15 @@ export class MaintenanceEphemeralStateRepository {
|
||||
return this.#secret;
|
||||
}
|
||||
|
||||
setState(state: MaintenanceStatusResponseDto) {
|
||||
setStatus(state: MaintenanceStatusResponseDto) {
|
||||
this.#state = state;
|
||||
}
|
||||
|
||||
getState(): MaintenanceStatusResponseDto {
|
||||
getStatus(): MaintenanceStatusResponseDto {
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
getPublicState(): MaintenanceStatusResponseDto {
|
||||
getPublicStatus(): MaintenanceStatusResponseDto {
|
||||
const state = structuredClone(this.#state);
|
||||
|
||||
if (state.error) {
|
||||
|
||||
@@ -7,19 +7,23 @@ import {
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||
import { MaintenanceAuthDto, MaintenanceStatusResponseDto } from 'src/dtos/maintenance.dto';
|
||||
import { AppRepository } from 'src/repositories/app.repository';
|
||||
import { AppRestartEvent, ArgsOf } from 'src/repositories/event.repository';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export const serverEvents = ['AppRestart'] as const;
|
||||
export type ServerEvents = (typeof serverEvents)[number];
|
||||
interface ServerEventMap {
|
||||
AppRestart: [AppRestartEvent];
|
||||
MaintenanceStatus: [MaintenanceStatusResponseDto];
|
||||
}
|
||||
|
||||
export interface ClientEventMap {
|
||||
interface ClientEventMap {
|
||||
AppRestartV1: [AppRestartEvent];
|
||||
MaintenanceStatusV1: [MaintenanceStatusResponseDto];
|
||||
}
|
||||
|
||||
type AuthFn = (client: Socket) => Promise<MaintenanceAuthDto>;
|
||||
type StatusUpdateFn = (status: MaintenanceStatusResponseDto) => void;
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: true,
|
||||
@@ -29,9 +33,10 @@ type AuthFn = (client: Socket) => Promise<MaintenanceAuthDto>;
|
||||
@Injectable()
|
||||
export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
|
||||
private authFn?: AuthFn;
|
||||
private statusUpdateFn?: StatusUpdateFn;
|
||||
|
||||
@WebSocketServer()
|
||||
private websocketServer?: Server;
|
||||
private server?: Server;
|
||||
|
||||
constructor(
|
||||
private logger: LoggingRepository,
|
||||
@@ -40,18 +45,23 @@ export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGa
|
||||
this.logger.setContext(MaintenanceWebsocketRepository.name);
|
||||
}
|
||||
|
||||
afterInit(websocketServer: Server) {
|
||||
afterInit(server: Server) {
|
||||
this.logger.log('Initialized websocket server');
|
||||
websocketServer.on('AppRestart', () => this.appRepository.exitApp());
|
||||
server.on('AppRestart', () => this.appRepository.exitApp());
|
||||
server.on('MaintenanceStatus', (status) => this.statusUpdateFn?.(status));
|
||||
}
|
||||
|
||||
clientSend<T extends keyof ClientEventMap>(event: T, room: string, ...data: ClientEventMap[T]) {
|
||||
this.server?.to(room).emit(event, ...data);
|
||||
}
|
||||
|
||||
clientBroadcast<T extends keyof ClientEventMap>(event: T, ...data: ClientEventMap[T]) {
|
||||
this.websocketServer?.emit(event, ...data);
|
||||
this.server?.emit(event, ...data);
|
||||
}
|
||||
|
||||
serverSend<T extends ServerEvents>(event: T, ...args: ArgsOf<T>): void {
|
||||
serverSend<T extends keyof ServerEventMap>(event: T, ...args: ServerEventMap[T]): void {
|
||||
this.logger.debug(`Server event: ${event} (send)`);
|
||||
this.websocketServer?.serverSideEmit(event, ...args);
|
||||
this.server?.serverSideEmit(event, ...args);
|
||||
}
|
||||
|
||||
async handleConnection(client: Socket) {
|
||||
@@ -73,4 +83,8 @@ export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGa
|
||||
setAuthFn(fn: (client: Socket) => Promise<MaintenanceAuthDto>) {
|
||||
this.authFn = fn;
|
||||
}
|
||||
|
||||
setStatusUpdateFn(fn: (status: MaintenanceStatusResponseDto) => void) {
|
||||
this.statusUpdateFn = fn;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
SetMaintenanceModeDto,
|
||||
} from 'src/dtos/maintenance.dto';
|
||||
import { ServerConfigDto } from 'src/dtos/server.dto';
|
||||
import { ImmichCookie, MaintenanceAction } from 'src/enum';
|
||||
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';
|
||||
@@ -46,8 +46,6 @@ export class MaintenanceWorkerController {
|
||||
@Post('admin/maintenance')
|
||||
@MaintenanceRoute()
|
||||
async setMaintenanceMode(@Body() dto: SetMaintenanceModeDto): Promise<void> {
|
||||
if (dto.action === MaintenanceAction.End) {
|
||||
await this.service.endMaintenance();
|
||||
}
|
||||
await this.service.runAction(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { NextFunction, Request, Response } from 'express';
|
||||
import { jwtVerify } from 'jose';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
import { MaintenanceAuthDto, MaintenanceStatusResponseDto } from 'src/dtos/maintenance.dto';
|
||||
import { MaintenanceAuthDto, MaintenanceStatusResponseDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
|
||||
import { ServerConfigDto } from 'src/dtos/server.dto';
|
||||
import { ImmichCookie, SystemMetadataKey } from 'src/enum';
|
||||
import { DatabaseLock, ImmichCookie, MaintenanceAction, SystemMetadataKey } from 'src/enum';
|
||||
import { MaintenanceEphemeralStateRepository } from 'src/maintenance/maintenance-ephemeral-state.repository';
|
||||
import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository';
|
||||
import { AppRepository } from 'src/repositories/app.repository';
|
||||
@@ -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 } from 'src/utils/backups';
|
||||
import { deleteBackup, listBackups, restoreBackup } from 'src/utils/backups';
|
||||
import { getConfig } from 'src/utils/config';
|
||||
import { createMaintenanceLoginUrl } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
@@ -35,7 +35,7 @@ export class MaintenanceWorkerService {
|
||||
private appRepository: AppRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
private systemMetadataRepository: SystemMetadataRepository,
|
||||
private maintenanceWorkerRepository: MaintenanceWebsocketRepository,
|
||||
private maintenanceWebsocketRepository: MaintenanceWebsocketRepository,
|
||||
private maintenanceEphemeralStateRepository: MaintenanceEphemeralStateRepository,
|
||||
private storageRepository: StorageRepository,
|
||||
private processRepository: ProcessRepository,
|
||||
@@ -130,6 +130,17 @@ export class MaintenanceWorkerService {
|
||||
return '/usr/src/app/upload';
|
||||
}
|
||||
|
||||
setStatus(status: MaintenanceStatusResponseDto): void {
|
||||
this.maintenanceEphemeralStateRepository.setStatus(status);
|
||||
this.maintenanceWebsocketRepository.serverSend('MaintenanceStatus', status);
|
||||
this.maintenanceWebsocketRepository.clientSend('MaintenanceStatusV1', 'private', status);
|
||||
this.maintenanceWebsocketRepository.clientSend(
|
||||
'MaintenanceStatusV1',
|
||||
'public',
|
||||
this.maintenanceEphemeralStateRepository.getPublicStatus(),
|
||||
);
|
||||
}
|
||||
|
||||
async logSecret(): Promise<void> {
|
||||
const { server } = await this.getConfig({ withCache: true });
|
||||
|
||||
@@ -153,9 +164,9 @@ export class MaintenanceWorkerService {
|
||||
async status(potentiallyJwt?: string): Promise<MaintenanceStatusResponseDto> {
|
||||
try {
|
||||
await this.login(potentiallyJwt);
|
||||
return this.maintenanceEphemeralStateRepository.getState();
|
||||
return this.maintenanceEphemeralStateRepository.getStatus();
|
||||
} catch {
|
||||
return this.maintenanceEphemeralStateRepository.getPublicState();
|
||||
return this.maintenanceEphemeralStateRepository.getPublicStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,13 +185,53 @@ export class MaintenanceWorkerService {
|
||||
}
|
||||
}
|
||||
|
||||
async runAction(action: SetMaintenanceModeDto) {
|
||||
switch (action.action) {
|
||||
case MaintenanceAction.Start:
|
||||
return;
|
||||
case MaintenanceAction.End:
|
||||
return this.endMaintenance();
|
||||
case MaintenanceAction.RestoreDatabase:
|
||||
if (!action.restoreBackupFilename) return;
|
||||
}
|
||||
|
||||
const lock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation);
|
||||
if (!lock) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Running maintenance action ${action.action}`);
|
||||
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, {
|
||||
isMaintenanceMode: true,
|
||||
secret: this.maintenanceEphemeralStateRepository.getSecret(),
|
||||
action: {
|
||||
action: MaintenanceAction.Start,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
switch (action.action) {
|
||||
case MaintenanceAction.RestoreDatabase:
|
||||
await this.restoreBackup(action.restoreBackupFilename);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Encountered error running action: ${error}`);
|
||||
this.setStatus({
|
||||
action: action.action,
|
||||
error: '' + error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async endMaintenance(): Promise<void> {
|
||||
const state: MaintenanceModeState = { isMaintenanceMode: false as const };
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state);
|
||||
|
||||
// => corresponds to notification.service.ts#onAppRestart
|
||||
this.maintenanceWorkerRepository.clientBroadcast('AppRestartV1', state);
|
||||
this.maintenanceWorkerRepository.serverSend('AppRestart', state);
|
||||
this.maintenanceWebsocketRepository.clientBroadcast('AppRestartV1', state);
|
||||
this.maintenanceWebsocketRepository.serverSend('AppRestart', state);
|
||||
this.appRepository.exitApp();
|
||||
}
|
||||
|
||||
@@ -188,6 +239,31 @@ export class MaintenanceWorkerService {
|
||||
* Backups
|
||||
*/
|
||||
|
||||
private async restoreBackup(filename: string): Promise<void> {
|
||||
this.setStatus({
|
||||
action: MaintenanceAction.RestoreDatabase,
|
||||
progress: 0,
|
||||
});
|
||||
|
||||
await restoreBackup(this.backupRepos, filename, (task, progress) =>
|
||||
this.setStatus({
|
||||
action: MaintenanceAction.RestoreDatabase,
|
||||
progress,
|
||||
task,
|
||||
}),
|
||||
);
|
||||
|
||||
this.setStatus({
|
||||
action: MaintenanceAction.End,
|
||||
});
|
||||
|
||||
// => corresponds to notification.service.ts#onAppRestart
|
||||
const state: MaintenanceModeState = { isMaintenanceMode: false };
|
||||
this.maintenanceWebsocketRepository.clientBroadcast('AppRestartV1', state);
|
||||
this.maintenanceWebsocketRepository.serverSend('AppRestart', state);
|
||||
this.appRepository.exitApp();
|
||||
}
|
||||
|
||||
async listBackups(): Promise<Record<'backups' | 'failedBackups', string[]>> {
|
||||
return listBackups(this.backupRepos);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user