chore: build metadata (#10612)

feat: build metadata
This commit is contained in:
Jason Rasmussen
2024-06-26 08:25:09 -04:00
committed by GitHub
parent 15c1cd6449
commit 8a445cac07
24 changed files with 905 additions and 18 deletions

View File

@@ -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,
});

View File

@@ -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> {

View File

@@ -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;

View File

@@ -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>;
}

View File

@@ -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 ', '') || '',
};
}
}

View File

@@ -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', () => {

View File

@@ -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);

View File

@@ -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';