mirror of
https://github.com/immich-app/immich.git
synced 2025-12-20 09:15:35 +03:00
@@ -429,3 +429,15 @@ export const clsConfig: ClsModuleOptions = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getBuildMetadata = () => ({
|
||||
build: process.env.IMMICH_BUILD,
|
||||
buildUrl: process.env.IMMICH_BUILD_URL,
|
||||
buildImage: process.env.IMMICH_BUILD_IMAGE,
|
||||
buildImageUrl: process.env.IMMICH_BUILD_IMAGE_URL,
|
||||
repository: process.env.IMMICH_REPOSITORY,
|
||||
repositoryUrl: process.env.IMMICH_REPOSITORY_URL,
|
||||
sourceRef: process.env.IMMICH_SOURCE_REF,
|
||||
sourceCommit: process.env.IMMICH_SOURCE_COMMIT,
|
||||
sourceUrl: process.env.IMMICH_SOURCE_URL,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ServerAboutResponseDto,
|
||||
ServerConfigDto,
|
||||
ServerFeaturesDto,
|
||||
ServerMediaTypesResponseDto,
|
||||
@@ -22,6 +23,12 @@ export class ServerInfoController {
|
||||
private versionService: VersionService,
|
||||
) {}
|
||||
|
||||
@Get('about')
|
||||
@Authenticated()
|
||||
getAboutInfo(): Promise<ServerAboutResponseDto> {
|
||||
return this.service.getAboutInfo();
|
||||
}
|
||||
|
||||
@Get('storage')
|
||||
@Authenticated()
|
||||
getStorage(): Promise<ServerStorageResponseDto> {
|
||||
|
||||
@@ -7,6 +7,29 @@ export class ServerPingResponse {
|
||||
res!: string;
|
||||
}
|
||||
|
||||
export class ServerAboutResponseDto {
|
||||
version!: string;
|
||||
versionUrl!: string;
|
||||
|
||||
repository?: string;
|
||||
repositoryUrl?: string;
|
||||
|
||||
sourceRef?: string;
|
||||
sourceCommit?: string;
|
||||
sourceUrl?: string;
|
||||
|
||||
build?: string;
|
||||
buildUrl?: string;
|
||||
buildImage?: string;
|
||||
buildImageUrl?: string;
|
||||
|
||||
nodejs?: string;
|
||||
ffmpeg?: string;
|
||||
imagemagick?: string;
|
||||
libvips?: string;
|
||||
exiftool?: string;
|
||||
}
|
||||
|
||||
export class ServerStorageResponseDto {
|
||||
diskSize!: string;
|
||||
diskUse!: string;
|
||||
|
||||
@@ -8,8 +8,17 @@ export interface GitHubRelease {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ServerBuildVersions {
|
||||
nodejs: string;
|
||||
ffmpeg: string;
|
||||
libvips: string;
|
||||
exiftool: string;
|
||||
imagemagick: string;
|
||||
}
|
||||
|
||||
export const IServerInfoRepository = 'IServerInfoRepository';
|
||||
|
||||
export interface IServerInfoRepository {
|
||||
getGitHubRelease(): Promise<GitHubRelease>;
|
||||
getBuildVersions(): Promise<ServerBuildVersions>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GitHubRelease, IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { exiftool } from 'exiftool-vendored';
|
||||
import { exec as execCallback } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
const maybeFirstLine = async (command: string): Promise<string> => {
|
||||
try {
|
||||
const { stdout } = await exec(command);
|
||||
return stdout.trim().split('\n')[0] || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
type BuildLockfile = {
|
||||
sources: Array<{ name: string; version: string }>;
|
||||
packages: Array<{ name: string; version: string }>;
|
||||
};
|
||||
|
||||
const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
|
||||
if (!lockfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = [...(lockfile.sources || []), ...(lockfile?.packages || [])];
|
||||
const item = items.find((item) => item.name === name);
|
||||
return item?.version;
|
||||
};
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class ServerInfoRepository implements IServerInfoRepository {
|
||||
constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
|
||||
this.logger.setContext(ServerInfoRepository.name);
|
||||
}
|
||||
|
||||
async getGitHubRelease(): Promise<GitHubRelease> {
|
||||
try {
|
||||
const response = await fetch('https://api.github.com/repos/immich-app/immich/releases/latest');
|
||||
@@ -18,4 +53,25 @@ export class ServerInfoRepository implements IServerInfoRepository {
|
||||
throw new Error(`Failed to fetch GitHub release: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getBuildVersions(): Promise<ServerBuildVersions> {
|
||||
const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([
|
||||
maybeFirstLine('node --version'),
|
||||
maybeFirstLine('ffmpeg -version'),
|
||||
maybeFirstLine('convert --version'),
|
||||
]);
|
||||
|
||||
const lockfile = await readFile('build-lock.json')
|
||||
.then((buffer) => JSON.parse(buffer.toString()))
|
||||
.catch(() => this.logger.warn('Failed to read build-lock.json'));
|
||||
|
||||
return {
|
||||
nodejs: nodejsOutput || process.env.NODE_VERSION || '',
|
||||
exiftool: await exiftool.version(),
|
||||
ffmpeg: getLockfileVersion('ffmpeg', lockfile) || ffmpegOutput.replaceAll('ffmpeg version', '') || '',
|
||||
libvips: getLockfileVersion('libvips', lockfile) || sharp.versions.vips,
|
||||
imagemagick:
|
||||
getLockfileVersion('imagemagick', lockfile) || magickOutput.replaceAll('Version: ImageMagick ', '') || '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { ServerInfoService } from 'src/services/server-info.service';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
@@ -13,16 +15,18 @@ describe(ServerInfoService.name, () => {
|
||||
let sut: ServerInfoService;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let serverInfoMock: Mocked<IServerInfoRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
serverInfoMock = newServerInfoRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new ServerInfoService(userMock, storageMock, systemMock, loggerMock);
|
||||
sut = new ServerInfoService(userMock, storageMock, systemMock, serverInfoMock, loggerMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { getBuildMetadata } from 'src/config';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import {
|
||||
ServerAboutResponseDto,
|
||||
ServerConfigDto,
|
||||
ServerFeaturesDto,
|
||||
ServerMediaTypesResponseDto,
|
||||
@@ -12,6 +15,7 @@ import {
|
||||
} from 'src/dtos/server-info.dto';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
|
||||
@@ -27,6 +31,7 @@ export class ServerInfoService {
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(ServerInfoService.name);
|
||||
@@ -42,6 +47,19 @@ export class ServerInfoService {
|
||||
}
|
||||
}
|
||||
|
||||
async getAboutInfo(): Promise<ServerAboutResponseDto> {
|
||||
const version = serverVersion.toString();
|
||||
const buildMetadata = getBuildMetadata();
|
||||
const buildVersions = await this.serverInfoRepository.getBuildVersions();
|
||||
|
||||
return {
|
||||
version,
|
||||
versionUrl: `https://github.com/immich-app/immich/releases/tag/${version}`,
|
||||
...buildMetadata,
|
||||
...buildVersions,
|
||||
};
|
||||
}
|
||||
|
||||
async getStorage(): Promise<ServerStorageResponseDto> {
|
||||
const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
|
||||
const diskInfo = await this.storageRepository.checkDiskUsage(libraryBase);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { VersionService } from 'src/services/version.service';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/system-info.repository.mock';
|
||||
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user