refactor(server, web)!: store latest immich version available on the server (#3565)

* refactor: store latest immich version available on the server

* don't store admins acknowledgement

* merge main

* fix: api

* feat: custom interval

* pr feedback

* remove unused code

* update environment-variables

* pr feedback

* ci: fix server tests

* fix: dart number

* pr feedback

* remove proxy

* pr feedback

* feat: make stringToVersion more flexible

* feat(web): disable check

* feat: working version

* remove env

* fix: check if interval exists when updating the interval

* feat: show last check

* fix: tests

* fix: remove availableVersion when updated

* fix merge

* fix: web

* fix e2e tests

* merge main

* merge main

* pr feedback

* pr feedback

* fix: tests

* pr feedback

* pr feedback

* pr feedback

* pr feedback

* pr feedback

* fix: migration

* regenerate api

* fix: typo

* fix: compare versions

* pr feedback

* fix

* pr feedback

* fix: checkIntervalTime on startup

* refactor: websockets and interval logic

* chore: open api

* chore: remove unused code

* fix: use interval instead of cron

* mobile: handle WS event data as json object

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
martin
2023-10-24 17:05:42 +02:00
committed by GitHub
parent 99c6f8fb13
commit 1aae29a0b8
48 changed files with 656 additions and 100 deletions

View File

@@ -8048,6 +8048,9 @@
"map": {
"$ref": "#/components/schemas/SystemConfigMapDto"
},
"newVersionCheck": {
"$ref": "#/components/schemas/SystemConfigNewVersionCheckDto"
},
"oauth": {
"$ref": "#/components/schemas/SystemConfigOAuthDto"
},
@@ -8074,6 +8077,7 @@
"ffmpeg",
"machineLearning",
"map",
"newVersionCheck",
"oauth",
"passwordLogin",
"reverseGeocoding",
@@ -8257,6 +8261,17 @@
],
"type": "object"
},
"SystemConfigNewVersionCheckDto": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"SystemConfigOAuthDto": {
"properties": {
"autoLaunch": {

View File

@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';
import { Optional, ValidateUUID, toBoolean } from '../../domain.util';
import { Optional, toBoolean, ValidateUUID } from '../../domain.util';
export class GetAlbumsDto {
@Optional()

View File

@@ -1,4 +1,4 @@
import { mimeTypes } from '@app/domain';
import { ServerVersion, mimeTypes } from './domain.constant';
describe('mimeTypes', () => {
for (const { mimetype, extension } of [
@@ -188,7 +188,74 @@ describe('mimeTypes', () => {
for (const [ext, v] of Object.entries(mimeTypes.sidecar)) {
it(`should lookup ${ext}`, () => {
expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]);
expect(mimeTypes.lookup(`it.${ext}`)).toEqual(v[0]);
});
}
});
});
describe('ServerVersion', () => {
describe('isNewerThan', () => {
it('should work on patch versions', () => {
expect(new ServerVersion(0, 0, 1).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true);
expect(new ServerVersion(1, 72, 1).isNewerThan(new ServerVersion(1, 72, 0))).toBe(true);
expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(0, 0, 1))).toBe(false);
expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 72, 1))).toBe(false);
});
it('should work on minor versions', () => {
expect(new ServerVersion(0, 1, 0).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true);
expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 71, 0))).toBe(true);
expect(new ServerVersion(1, 72, 0).isNewerThan(new ServerVersion(1, 71, 9))).toBe(true);
expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(0, 1, 0))).toBe(false);
expect(new ServerVersion(1, 71, 0).isNewerThan(new ServerVersion(1, 72, 0))).toBe(false);
expect(new ServerVersion(1, 71, 9).isNewerThan(new ServerVersion(1, 72, 0))).toBe(false);
});
it('should work on major versions', () => {
expect(new ServerVersion(1, 0, 0).isNewerThan(new ServerVersion(0, 0, 0))).toBe(true);
expect(new ServerVersion(2, 0, 0).isNewerThan(new ServerVersion(1, 71, 0))).toBe(true);
expect(new ServerVersion(0, 0, 0).isNewerThan(new ServerVersion(1, 0, 0))).toBe(false);
expect(new ServerVersion(1, 71, 0).isNewerThan(new ServerVersion(2, 0, 0))).toBe(false);
});
it('should work on equal', () => {
for (const version of [
new ServerVersion(0, 0, 0),
new ServerVersion(0, 0, 1),
new ServerVersion(0, 1, 1),
new ServerVersion(0, 1, 0),
new ServerVersion(1, 1, 1),
new ServerVersion(1, 0, 0),
new ServerVersion(1, 72, 1),
new ServerVersion(1, 72, 0),
new ServerVersion(1, 73, 9),
]) {
expect(version.isNewerThan(version)).toBe(false);
}
});
});
describe('fromString', () => {
const tests = [
{ scenario: 'leading v', value: 'v1.72.2', expected: new ServerVersion(1, 72, 2) },
{ scenario: 'uppercase v', value: 'V1.72.2', expected: new ServerVersion(1, 72, 2) },
{ scenario: 'missing v', value: '1.72.2', expected: new ServerVersion(1, 72, 2) },
{ scenario: 'large patch', value: '1.72.123', expected: new ServerVersion(1, 72, 123) },
{ scenario: 'large minor', value: '1.123.0', expected: new ServerVersion(1, 123, 0) },
{ scenario: 'large major', value: '123.0.0', expected: new ServerVersion(123, 0, 0) },
{ scenario: 'major bump', value: 'v2.0.0', expected: new ServerVersion(2, 0, 0) },
];
for (const { scenario, value, expected } of tests) {
it(`should correctly parse ${scenario}`, () => {
const actual = ServerVersion.fromString(value);
expect(actual.major).toEqual(expected.major);
expect(actual.minor).toEqual(expected.minor);
expect(actual.patch).toEqual(expected.patch);
});
}
});

View File

@@ -4,8 +4,7 @@ import { extname } from 'node:path';
import pkg from 'src/../../package.json';
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
const [major, minor, patch] = pkg.version.split('.');
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
export interface IServerVersion {
major: number;
@@ -13,13 +12,49 @@ export interface IServerVersion {
patch: number;
}
export const serverVersion: IServerVersion = {
major: Number(major),
minor: Number(minor),
patch: Number(patch),
};
export class ServerVersion implements IServerVersion {
constructor(
public readonly major: number,
public readonly minor: number,
public readonly patch: number,
) {}
export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`;
toString() {
return `${this.major}.${this.minor}.${this.patch}`;
}
toJSON() {
const { major, minor, patch } = this;
return { major, minor, patch };
}
static fromString(version: string): ServerVersion {
const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
const matchResult = version.match(regex);
if (matchResult) {
const [, major, minor, patch] = matchResult.map(Number);
return new ServerVersion(major, minor, patch);
} else {
throw new Error(`Invalid version format: ${version}`);
}
}
isNewerThan(version: ServerVersion): boolean {
const equalMajor = this.major === version.major;
const equalMinor = this.minor === version.minor;
return (
this.major > version.major ||
(equalMajor && this.minor > version.minor) ||
(equalMajor && equalMinor && this.patch > version.patch)
);
}
}
export const envName = (process.env.NODE_ENV || 'development').toUpperCase();
export const isDev = process.env.NODE_ENV === 'development';
export const serverVersion = ServerVersion.fromString(pkg.version);
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';

View File

@@ -169,7 +169,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
[JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR,
[JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
// Library managment
// Library management
[JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
[JobName.LIBRARY_SCAN]: QueueName.LIBRARY,
[JobName.LIBRARY_DELETE]: QueueName.LIBRARY,

View File

@@ -9,9 +9,13 @@ export enum CommunicationEvent {
PERSON_THUMBNAIL = 'on_person_thumbnail',
SERVER_VERSION = 'on_server_version',
CONFIG_UPDATE = 'on_config_update',
NEW_RELEASE = 'on_new_release',
}
export type Callback = (userId: string) => Promise<void>;
export interface ICommunicationRepository {
send(event: CommunicationEvent, userId: string, data: any): void;
broadcast(event: CommunicationEvent, data: any): void;
addEventListener(event: 'connect', callback: Callback): void;
}

View File

@@ -14,6 +14,7 @@ export * from './move.repository';
export * from './partner.repository';
export * from './person.repository';
export * from './search.repository';
export * from './server-info.repository';
export * from './shared-link.repository';
export * from './smart-info.repository';
export * from './storage.repository';

View File

@@ -85,7 +85,7 @@ export type JobItem =
| { name: JobName.ASSET_DELETION; data: IAssetDeletionJob }
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
// Library Managment
// Library Management
| { name: JobName.LIBRARY_SCAN_ASSET; data: ILibraryFileJob }
| { name: JobName.LIBRARY_SCAN; data: ILibraryRefreshJob }
| { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob }

View File

@@ -0,0 +1,15 @@
export interface GitHubRelease {
id: number;
url: string;
tag_name: string;
name: string;
created_at: string;
published_at: string;
body: string;
}
export const IServerInfoRepository = 'IServerInfoRepository';
export interface IServerInfoRepository {
getGitHubRelease(): Promise<GitHubRelease>;
}

View File

@@ -1,20 +1,36 @@
import { newStorageRepositoryMock, newSystemConfigRepositoryMock, newUserRepositoryMock } from '@test';
import {
newCommunicationRepositoryMock,
newServerInfoRepositoryMock,
newStorageRepositoryMock,
newSystemConfigRepositoryMock,
newUserRepositoryMock,
} from '@test';
import { serverVersion } from '../domain.constant';
import { IStorageRepository, ISystemConfigRepository, IUserRepository } from '../repositories';
import {
ICommunicationRepository,
IServerInfoRepository,
IStorageRepository,
ISystemConfigRepository,
IUserRepository,
} from '../repositories';
import { ServerInfoService } from './server-info.service';
describe(ServerInfoService.name, () => {
let sut: ServerInfoService;
let communicationMock: jest.Mocked<ICommunicationRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let serverInfoMock: jest.Mocked<IServerInfoRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>;
beforeEach(() => {
configMock = newSystemConfigRepositoryMock();
communicationMock = newCommunicationRepositoryMock();
serverInfoMock = newServerInfoRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
sut = new ServerInfoService(configMock, userMock, storageMock);
sut = new ServerInfoService(communicationMock, configMock, userMock, serverInfoMock, storageMock);
});
it('should work', () => {

View File

@@ -1,7 +1,16 @@
import { Inject, Injectable } from '@nestjs/common';
import { mimeTypes, serverVersion } from '../domain.constant';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { DateTime } from 'luxon';
import { ServerVersion, isDev, mimeTypes, serverVersion } from '../domain.constant';
import { asHumanReadable } from '../domain.util';
import { IStorageRepository, ISystemConfigRepository, IUserRepository, UserStatsQueryResponse } from '../repositories';
import {
CommunicationEvent,
ICommunicationRepository,
IServerInfoRepository,
IStorageRepository,
ISystemConfigRepository,
IUserRepository,
UserStatsQueryResponse,
} from '../repositories';
import { StorageCore, StorageFolder } from '../storage';
import { SystemConfigCore } from '../system-config';
import {
@@ -16,14 +25,20 @@ import {
@Injectable()
export class ServerInfoService {
private logger = new Logger(ServerInfoService.name);
private configCore: SystemConfigCore;
private releaseVersion = serverVersion;
private releaseVersionCheckedAt: DateTime | null = null;
constructor(
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {
this.configCore = SystemConfigCore.create(configRepository);
this.communicationRepository.addEventListener('connect', (userId) => this.handleConnect(userId));
}
async getInfo(): Promise<ServerInfoResponseDto> {
@@ -101,4 +116,56 @@ export class ServerInfoService {
sidecar: Object.keys(mimeTypes.sidecar),
};
}
async handleVersionCheck(): Promise<boolean> {
try {
if (isDev) {
return true;
}
const { newVersionCheck } = await this.configCore.getConfig();
if (!newVersionCheck.enabled) {
return true;
}
// check once per hour (max)
if (this.releaseVersionCheckedAt && this.releaseVersionCheckedAt.diffNow().as('minutes') < 60) {
return true;
}
const githubRelease = await this.repository.getGitHubRelease();
const githubVersion = ServerVersion.fromString(githubRelease.tag_name);
const publishedAt = new Date(githubRelease.published_at);
this.releaseVersion = githubVersion;
this.releaseVersionCheckedAt = DateTime.now();
if (githubVersion.isNewerThan(serverVersion)) {
this.logger.log(`Found ${githubVersion.toString()}, released at ${publishedAt.toLocaleString()}`);
this.newReleaseNotification();
}
} catch (error: Error | any) {
this.logger.warn(`Unable to run version check: ${error}`, error?.stack);
}
return true;
}
private async handleConnect(userId: string) {
this.communicationRepository.send(CommunicationEvent.SERVER_VERSION, userId, serverVersion);
this.newReleaseNotification(userId);
}
private newReleaseNotification(userId?: string) {
const event = CommunicationEvent.NEW_RELEASE;
const payload = {
isAvailable: this.releaseVersion.isNewerThan(serverVersion),
checkedAt: this.releaseVersionCheckedAt,
serverVersion,
releaseVersion: this.releaseVersion,
};
userId
? this.communicationRepository.send(event, userId, payload)
: this.communicationRepository.broadcast(event, payload);
}
}

View File

@@ -0,0 +1,6 @@
import { IsBoolean } from 'class-validator';
export class SystemConfigNewVersionCheckDto {
@IsBoolean()
enabled!: boolean;
}

View File

@@ -5,6 +5,7 @@ import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
import { SystemConfigJobDto } from './system-config-job.dto';
import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
import { SystemConfigMapDto } from './system-config-map.dto';
import { SystemConfigNewVersionCheckDto } from './system-config-new-version-check.dto';
import { SystemConfigOAuthDto } from './system-config-oauth.dto';
import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
import { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto';
@@ -29,6 +30,11 @@ export class SystemConfigDto implements SystemConfig {
@IsObject()
map!: SystemConfigMapDto;
@Type(() => SystemConfigNewVersionCheckDto)
@ValidateNested()
@IsObject()
newVersionCheck!: SystemConfigNewVersionCheckDto;
@Type(() => SystemConfigOAuthDto)
@ValidateNested()
@IsObject()

View File

@@ -1,8 +1,8 @@
import {
AudioCodec,
CQMode,
CitiesFile,
Colorspace,
CQMode,
SystemConfig,
SystemConfigEntity,
SystemConfigKey,
@@ -110,6 +110,9 @@ export const defaults = Object.freeze<SystemConfig>({
quality: 80,
colorspace: Colorspace.P3,
},
newVersionCheck: {
enabled: true,
},
trash: {
enabled: true,
days: 30,

View File

@@ -1,8 +1,8 @@
import {
AudioCodec,
CQMode,
CitiesFile,
Colorspace,
CQMode,
SystemConfig,
SystemConfigEntity,
SystemConfigKey,
@@ -15,7 +15,7 @@ import { BadRequestException } from '@nestjs/common';
import { newCommunicationRepositoryMock, newJobRepositoryMock, newSystemConfigRepositoryMock } from '@test';
import { JobName, QueueName } from '../job';
import { ICommunicationRepository, IJobRepository, ISystemConfigRepository } from '../repositories';
import { SystemConfigValidator, defaults } from './system-config.core';
import { defaults, SystemConfigValidator } from './system-config.core';
import { SystemConfigService } from './system-config.service';
const updates: SystemConfigEntity[] = [
@@ -111,6 +111,9 @@ const updatedConfig = Object.freeze<SystemConfig>({
quality: 80,
colorspace: Colorspace.P3,
},
newVersionCheck: {
enabled: true,
},
trash: {
enabled: true,
days: 10,

View File

@@ -1,6 +1,6 @@
import { JobService, SearchService, ServerInfoService, StorageService } from '@app/domain';
import { JobService, ONE_HOUR, SearchService, ServerInfoService, StorageService } from '@app/domain';
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Cron, CronExpression, Interval } from '@nestjs/schedule';
@Injectable()
export class AppService {
@@ -13,6 +13,11 @@ export class AppService {
private serverService: ServerInfoService,
) {}
@Interval(ONE_HOUR.as('milliseconds'))
async onVersionCheck() {
await this.serverService.handleVersionCheck();
}
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
async onNightlyJob() {
await this.jobService.handleNightlyJobs();
@@ -21,6 +26,7 @@ export class AppService {
async init() {
this.storageService.init();
await this.searchService.init();
await this.serverService.handleVersionCheck();
this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`);
}

View File

@@ -3,7 +3,7 @@ import {
IMMICH_API_KEY_HEADER,
IMMICH_API_KEY_NAME,
ImmichReadStream,
SERVER_VERSION,
serverVersion,
} from '@app/domain';
import { INestApplication, StreamableFile } from '@nestjs/common';
import {
@@ -91,7 +91,7 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => {
const config = new DocumentBuilder()
.setTitle('Immich')
.setDescription('Immich API')
.setVersion(SERVER_VERSION)
.setVersion(serverVersion.toString())
.addBearerAuth({
type: 'http',
scheme: 'Bearer',

View File

@@ -1,4 +1,4 @@
import { getLogLevels, SERVER_VERSION } from '@app/domain';
import { envName, getLogLevels, isDev, serverVersion } from '@app/domain';
import { RedisIoAdapter } from '@app/infra';
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
@@ -9,9 +9,7 @@ import { AppModule } from './app.module';
import { useSwagger } from './app.utils';
const logger = new Logger('ImmichServer');
const envName = (process.env.NODE_ENV || 'development').toUpperCase();
const port = Number(process.env.SERVER_PORT) || 3001;
const isDev = process.env.NODE_ENV === 'development';
export async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { logger: getLogLevels() });
@@ -29,5 +27,5 @@ export async function bootstrap() {
const server = await app.listen(port);
server.requestTimeout = 30 * 60 * 1000;
logger.log(`Immich Server is listening on ${await app.getUrl()} [v${SERVER_VERSION}] [${envName}] `);
logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
}

View File

@@ -67,6 +67,8 @@ export enum SystemConfigKey {
REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled',
REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride',
NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled',
OAUTH_ENABLED = 'oauth.enabled',
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
OAUTH_CLIENT_ID = 'oauth.clientId',
@@ -219,6 +221,9 @@ export interface SystemConfig {
quality: number;
colorspace: Colorspace;
};
newVersionCheck: {
enabled: boolean;
};
trash: {
enabled: boolean;
days: number;

View File

@@ -15,6 +15,7 @@ import {
IPartnerRepository,
IPersonRepository,
ISearchRepository,
IServerInfoRepository,
ISharedLinkRepository,
ISmartInfoRepository,
IStorageRepository,
@@ -48,6 +49,7 @@ import {
MoveRepository,
PartnerRepository,
PersonRepository,
ServerInfoRepository,
SharedLinkRepository,
SmartInfoRepository,
SystemConfigRepository,
@@ -73,6 +75,7 @@ const providers: Provider[] = [
{ provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository },
{ provide: ISearchRepository, useClass: TypesenseRepository },
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: ISmartInfoRepository, useClass: SmartInfoRepository },
{ provide: IStorageRepository, useClass: FilesystemProvider },

View File

@@ -1,4 +1,4 @@
import { AuthService, CommunicationEvent, ICommunicationRepository, serverVersion } from '@app/domain';
import { AuthService, Callback, CommunicationEvent, ICommunicationRepository } from '@app/domain';
import { Logger } from '@nestjs/common';
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@@ -6,18 +6,25 @@ import { Server, Socket } from 'socket.io';
@WebSocketGateway({ cors: true })
export class CommunicationRepository implements OnGatewayConnection, OnGatewayDisconnect, ICommunicationRepository {
private logger = new Logger(CommunicationRepository.name);
private onConnectCallbacks: Callback[] = [];
constructor(private authService: AuthService) {}
@WebSocketServer() server!: Server;
addEventListener(event: 'connect', callback: Callback) {
this.onConnectCallbacks.push(callback);
}
async handleConnection(client: Socket) {
try {
this.logger.log(`New websocket connection: ${client.id}`);
const user = await this.authService.validate(client.request.headers, {});
if (user) {
await client.join(user.id);
this.send(CommunicationEvent.SERVER_VERSION, user.id, serverVersion);
for (const callback of this.onConnectCallbacks) {
await callback(user.id);
}
} else {
client.emit('error', 'unauthorized');
client.disconnect();
@@ -34,7 +41,7 @@ export class CommunicationRepository implements OnGatewayConnection, OnGatewayDi
}
send(event: CommunicationEvent, userId: string, data: any) {
this.server.to(userId).emit(event, JSON.stringify(data));
this.server.to(userId).emit(event, data);
}
broadcast(event: CommunicationEvent, data: any) {

View File

@@ -14,6 +14,7 @@ export * from './metadata.repository';
export * from './move.repository';
export * from './partner.repository';
export * from './person.repository';
export * from './server-info.repository';
export * from './shared-link.repository';
export * from './smart-info.repository';
export * from './system-config.repository';

View File

@@ -0,0 +1,12 @@
import { GitHubRelease, IServerInfoRepository } from '@app/domain';
import { Injectable } from '@nestjs/common';
import axios from 'axios';
@Injectable()
export class ServerInfoRepository implements IServerInfoRepository {
getGitHubRelease(): Promise<GitHubRelease> {
return axios
.get<GitHubRelease>('https://api.github.com/repos/immich-app/immich/releases/latest')
.then((response) => response.data);
}
}

View File

@@ -9,6 +9,7 @@ import {
MetadataService,
PersonService,
SearchService,
ServerInfoService,
SmartInfoService,
StorageService,
StorageTemplateService,
@@ -23,19 +24,20 @@ export class AppService {
private logger = new Logger(AppService.name);
constructor(
private jobService: JobService,
private auditService: AuditService,
private assetService: AssetService,
private jobService: JobService,
private libraryService: LibraryService,
private mediaService: MediaService,
private metadataService: MetadataService,
private personService: PersonService,
private searchService: SearchService,
private serverInfoService: ServerInfoService,
private smartInfoService: SmartInfoService,
private storageTemplateService: StorageTemplateService,
private storageService: StorageService,
private systemConfigService: SystemConfigService,
private userService: UserService,
private auditService: AuditService,
private libraryService: LibraryService,
) {}
async init() {

View File

@@ -1,4 +1,4 @@
import { getLogLevels, SERVER_VERSION } from '@app/domain';
import { envName, getLogLevels, serverVersion } from '@app/domain';
import { RedisIoAdapter } from '@app/infra';
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
@@ -7,7 +7,6 @@ import { MicroservicesModule } from './microservices.module';
const logger = new Logger('ImmichMicroservice');
const port = Number(process.env.MICROSERVICES_PORT) || 3002;
const envName = (process.env.NODE_ENV || 'development').toUpperCase();
export async function bootstrap() {
const app = await NestFactory.create(MicroservicesModule, { logger: getLogLevels() });
@@ -17,5 +16,5 @@ export async function bootstrap() {
await app.get(AppService).init();
await app.listen(port);
logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${SERVER_VERSION}] [${envName}] `);
logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
}

View File

@@ -4,5 +4,6 @@ export const newCommunicationRepositoryMock = (): jest.Mocked<ICommunicationRepo
return {
send: jest.fn(),
broadcast: jest.fn(),
addEventListener: jest.fn(),
};
};

View File

@@ -18,6 +18,7 @@ export * from './shared-link.repository.mock';
export * from './smart-info.repository.mock';
export * from './storage.repository.mock';
export * from './system-config.repository.mock';
export * from './system-info.repository.mock';
export * from './tag.repository.mock';
export * from './user-token.repository.mock';
export * from './user.repository.mock';

View File

@@ -0,0 +1,7 @@
import { IServerInfoRepository } from '@app/domain';
export const newServerInfoRepositoryMock = (): jest.Mocked<IServerInfoRepository> => {
return {
getGitHubRelease: jest.fn(),
};
};