Files
immich/server/src/services/maintenance.service.ts
2025-11-24 16:18:33 +00:00

119 lines
3.5 KiB
TypeScript

import { BadRequestException, Injectable } from '@nestjs/common';
import { basename, join } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent } from 'src/decorators';
import { MaintenanceAuthDto, MaintenanceIntegrityResponseDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum';
import { BaseService } from 'src/services/base.service';
import { MaintenanceModeState } from 'src/types';
import { deleteBackup, isValidBackupName, listBackups, uploadBackup } from 'src/utils/backups';
import {
createMaintenanceLoginUrl,
generateMaintenanceSecret,
integrityCheck,
signMaintenanceJwt,
} from 'src/utils/maintenance';
import { getExternalDomain } from 'src/utils/misc';
/**
* This service is available outside of maintenance mode to manage maintenance mode
*/
@Injectable()
export class MaintenanceService extends BaseService {
getMaintenanceMode(): Promise<MaintenanceModeState> {
return this.systemMetadataRepository
.get(SystemMetadataKey.MaintenanceMode)
.then((state) => state ?? { isMaintenanceMode: false });
}
integrityCheck(): Promise<MaintenanceIntegrityResponseDto> {
return integrityCheck(this.storageRepository);
}
async startMaintenance(action: SetMaintenanceModeDto, username: string): Promise<{ jwt: string }> {
const secret = generateMaintenanceSecret();
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, {
isMaintenanceMode: true,
secret,
action,
});
await this.eventRepository.emit('AppRestart', { isMaintenanceMode: true });
return {
jwt: await signMaintenanceJwt(secret, {
username,
}),
};
}
async startRestoreFlow(): Promise<{ jwt: string }> {
const adminUser = await this.userRepository.getAdmin();
if (adminUser) {
throw new BadRequestException('The server already has an admin');
}
return this.startMaintenance(
{
action: MaintenanceAction.RestoreDatabase,
},
'admin',
);
}
@OnEvent({ name: 'AppRestart', server: true })
onRestart(): void {
this.appRepository.exitApp();
}
async createLoginUrl(auth: MaintenanceAuthDto, secret?: string): Promise<string> {
const { server } = await this.getConfig({ withCache: true });
const baseUrl = getExternalDomain(server);
if (!secret) {
const state = await this.getMaintenanceMode();
if (!state.isMaintenanceMode) {
throw new Error('Not in maintenance mode');
}
secret = state.secret;
}
return await createMaintenanceLoginUrl(baseUrl, auth, secret);
}
/**
* Backups
*/
async listBackups(): Promise<{ backups: string[] }> {
return { backups: await listBackups(this.backupRepos) };
}
async deleteBackup(filename: string): Promise<void> {
return deleteBackup(this.backupRepos, basename(filename));
}
async uploadBackup(file: Express.Multer.File): Promise<void> {
return uploadBackup(this.backupRepos, file);
}
getBackupPath(filename: string): string {
if (!isValidBackupName(filename)) {
throw new BadRequestException('Invalid backup name!');
}
return join(StorageCore.getBaseFolder(StorageFolder.Backups), basename(filename));
}
private get backupRepos() {
return {
logger: this.logger,
storage: this.storageRepository,
config: this.configRepository,
process: this.processRepository,
database: this.databaseRepository,
};
}
}