Files
immich/server/src/repositories/server-info.repository.ts

127 lines
3.7 KiB
TypeScript

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<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;
};
@Injectable()
export class ServerInfoRepository {
constructor(
private configRepository: ConfigRepository,
private logger: LoggingRepository,
) {
this.logger.setContext(ServerInfoRepository.name);
}
async getGitHubRelease(): Promise<GitHubRelease> {
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<string> {
if (!version) {
const output = await maybeFirstLine(command);
version = commandTransform ? commandTransform(output) : output;
}
return version;
}
async getBuildVersions(): Promise<ServerBuildVersions> {
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;
}
}