import { 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 { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; export interface GitHubRelease { id: number; url: string; tag_name: string; name: string; created_at: string; published_at: string; body: string; } export interface ServerBuildVersions { nodejs: string; ffmpeg: string; libvips: string; exiftool: string; imagemagick: string; } const exec = promisify(execCallback); const maybeFirstLine = async (command: string): Promise => { 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; }; @Injectable() export class ServerInfoRepository { constructor( private configRepository: ConfigRepository, private logger: LoggingRepository, ) { this.logger.setContext(ServerInfoRepository.name); } async getGitHubRelease(): Promise { try { const response = await fetch('https://api.github.com/repos/immich-app/immich/releases/latest'); if (!response.ok) { throw new Error(`GitHub API request failed with status ${response.status}: ${await response.text()}`); } return response.json(); } catch (error) { throw new Error(`Failed to fetch GitHub release: ${error}`); } } buildVersions?: ServerBuildVersions; private async retrieveVersionFallback( command: string, commandTransform?: (output: string) => string, version?: string, ): Promise { if (!version) { const output = await maybeFirstLine(command); version = commandTransform ? commandTransform(output) : output; } return version; } async getBuildVersions(): Promise { if (!this.buildVersions) { const { nodeVersion, resourcePaths } = this.configRepository.getEnv(); const lockfile: BuildLockfile | undefined = await readFile(resourcePaths.lockFile) .then((buffer) => JSON.parse(buffer.toString())) .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`)); const [nodejsVersion, ffmpegVersion, magickVersion, exiftoolVersion] = await Promise.all([ this.retrieveVersionFallback('node --version', undefined, nodeVersion), this.retrieveVersionFallback( 'ffmpeg -version', (output) => output.replaceAll('ffmpeg version ', ''), getLockfileVersion('ffmpeg', lockfile), ), this.retrieveVersionFallback( 'magick --version', (output) => output.replaceAll('Version: ImageMagick ', ''), getLockfileVersion('imagemagick', lockfile), ), exiftool.version(), ]); const libvipsVersion = getLockfileVersion('libvips', lockfile) || sharp.versions.vips; this.buildVersions = { nodejs: nodejsVersion, exiftool: exiftoolVersion, ffmpeg: ffmpegVersion, libvips: libvipsVersion, imagemagick: magickVersion, }; } return this.buildVersions; } }