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