mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
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:
parent
99c6f8fb13
commit
1aae29a0b8
19
cli/src/api/open-api/api.ts
generated
19
cli/src/api/open-api/api.ts
generated
@ -3283,6 +3283,12 @@ export interface SystemConfigDto {
|
||||
* @memberof SystemConfigDto
|
||||
*/
|
||||
'map': SystemConfigMapDto;
|
||||
/**
|
||||
*
|
||||
* @type {SystemConfigNewVersionCheckDto}
|
||||
* @memberof SystemConfigDto
|
||||
*/
|
||||
'newVersionCheck': SystemConfigNewVersionCheckDto;
|
||||
/**
|
||||
*
|
||||
* @type {SystemConfigOAuthDto}
|
||||
@ -3572,6 +3578,19 @@ export interface SystemConfigMapDto {
|
||||
*/
|
||||
'tileUrl': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SystemConfigNewVersionCheckDto
|
||||
*/
|
||||
export interface SystemConfigNewVersionCheckDto {
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SystemConfigNewVersionCheckDto
|
||||
*/
|
||||
'enabled': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
@ -175,9 +173,8 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
.where((c) => c.action == PendingAction.assetDelete)
|
||||
.toList();
|
||||
if (deleteChanges.isNotEmpty) {
|
||||
List<String> remoteIds = deleteChanges
|
||||
.map((a) => jsonDecode(a.value.toString()).toString())
|
||||
.toList();
|
||||
List<String> remoteIds =
|
||||
deleteChanges.map((a) => a.value.toString()).toList();
|
||||
ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges
|
||||
@ -188,21 +185,20 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
}
|
||||
|
||||
_handleOnUploadSuccess(dynamic data) {
|
||||
final jsonString = jsonDecode(data.toString());
|
||||
final dto = AssetResponseDto.fromJson(jsonString);
|
||||
final dto = AssetResponseDto.fromJson(data);
|
||||
if (dto != null) {
|
||||
final newAsset = Asset.remote(dto);
|
||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||
}
|
||||
}
|
||||
|
||||
_handleOnConfigUpdate(dynamic data) {
|
||||
_handleOnConfigUpdate(dynamic _) {
|
||||
ref.read(serverInfoProvider.notifier).getServerFeatures();
|
||||
ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||
}
|
||||
|
||||
// Refresh updated assets
|
||||
_handleServerUpdates(dynamic data) {
|
||||
_handleServerUpdates(dynamic _) {
|
||||
ref.read(assetProvider.notifier).getAllAsset();
|
||||
}
|
||||
|
||||
|
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
@ -130,6 +130,7 @@ doc/SystemConfigFFmpegDto.md
|
||||
doc/SystemConfigJobDto.md
|
||||
doc/SystemConfigMachineLearningDto.md
|
||||
doc/SystemConfigMapDto.md
|
||||
doc/SystemConfigNewVersionCheckDto.md
|
||||
doc/SystemConfigOAuthDto.md
|
||||
doc/SystemConfigPasswordLoginDto.md
|
||||
doc/SystemConfigReverseGeocodingDto.md
|
||||
@ -298,6 +299,7 @@ lib/model/system_config_f_fmpeg_dto.dart
|
||||
lib/model/system_config_job_dto.dart
|
||||
lib/model/system_config_machine_learning_dto.dart
|
||||
lib/model/system_config_map_dto.dart
|
||||
lib/model/system_config_new_version_check_dto.dart
|
||||
lib/model/system_config_o_auth_dto.dart
|
||||
lib/model/system_config_password_login_dto.dart
|
||||
lib/model/system_config_reverse_geocoding_dto.dart
|
||||
@ -453,6 +455,7 @@ test/system_config_f_fmpeg_dto_test.dart
|
||||
test/system_config_job_dto_test.dart
|
||||
test/system_config_machine_learning_dto_test.dart
|
||||
test/system_config_map_dto_test.dart
|
||||
test/system_config_new_version_check_dto_test.dart
|
||||
test/system_config_o_auth_dto_test.dart
|
||||
test/system_config_password_login_dto_test.dart
|
||||
test/system_config_reverse_geocoding_dto_test.dart
|
||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigDto.md
generated
BIN
mobile/openapi/doc/SystemConfigDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigNewVersionCheckDto.md
generated
Normal file
BIN
mobile/openapi/doc/SystemConfigNewVersionCheckDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_new_version_check_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/system_config_new_version_check_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/system_config_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_new_version_check_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/system_config_new_version_check_dto_test.dart
generated
Normal file
Binary file not shown.
@ -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": {
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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 }
|
||||
|
15
server/src/domain/repositories/server-info.repository.ts
Normal file
15
server/src/domain/repositories/server-info.repository.ts
Normal 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>;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { IsBoolean } from 'class-validator';
|
||||
|
||||
export class SystemConfigNewVersionCheckDto {
|
||||
@IsBoolean()
|
||||
enabled!: boolean;
|
||||
}
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)}`);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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}] `);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 },
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
12
server/src/infra/repositories/server-info.repository.ts
Normal file
12
server/src/infra/repositories/server-info.repository.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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}] `);
|
||||
}
|
||||
|
@ -4,5 +4,6 @@ export const newCommunicationRepositoryMock = (): jest.Mocked<ICommunicationRepo
|
||||
return {
|
||||
send: jest.fn(),
|
||||
broadcast: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
@ -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';
|
||||
|
7
server/test/repositories/system-info.repository.mock.ts
Normal file
7
server/test/repositories/system-info.repository.mock.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { IServerInfoRepository } from '@app/domain';
|
||||
|
||||
export const newServerInfoRepositoryMock = (): jest.Mocked<IServerInfoRepository> => {
|
||||
return {
|
||||
getGitHubRelease: jest.fn(),
|
||||
};
|
||||
};
|
19
web/src/api/open-api/api.ts
generated
19
web/src/api/open-api/api.ts
generated
@ -3283,6 +3283,12 @@ export interface SystemConfigDto {
|
||||
* @memberof SystemConfigDto
|
||||
*/
|
||||
'map': SystemConfigMapDto;
|
||||
/**
|
||||
*
|
||||
* @type {SystemConfigNewVersionCheckDto}
|
||||
* @memberof SystemConfigDto
|
||||
*/
|
||||
'newVersionCheck': SystemConfigNewVersionCheckDto;
|
||||
/**
|
||||
*
|
||||
* @type {SystemConfigOAuthDto}
|
||||
@ -3572,6 +3578,19 @@ export interface SystemConfigMapDto {
|
||||
*/
|
||||
'tileUrl': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SystemConfigNewVersionCheckDto
|
||||
*/
|
||||
export interface SystemConfigNewVersionCheckDto {
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SystemConfigNewVersionCheckDto
|
||||
*/
|
||||
'enabled': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigNewVersionCheckDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
|
||||
export let newVersionCheckConfig: SystemConfigNewVersionCheckDto; // this is the config that is being edited
|
||||
|
||||
let savedConfig: SystemConfigNewVersionCheckDto;
|
||||
let defaultConfig: SystemConfigNewVersionCheckDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.newVersionCheck),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.newVersionCheck),
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
newVersionCheck: newVersionCheckConfig,
|
||||
},
|
||||
});
|
||||
|
||||
newVersionCheckConfig = { ...result.data.newVersionCheck };
|
||||
savedConfig = { ...result.data.newVersionCheck };
|
||||
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
newVersionCheckConfig = { ...resetConfig.newVersionCheck };
|
||||
savedConfig = { ...resetConfig.newVersionCheck };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
newVersionCheckConfig = { ...configs.newVersionCheck };
|
||||
defaultConfig = { ...configs.newVersionCheck };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ml-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="Enable period requests to GitHub to check for new releases"
|
||||
bind:checked={newVersionCheckConfig.enabled}
|
||||
/>
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
@ -1,43 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { getGithubVersion } from '$lib/utils/get-github-version';
|
||||
import { onMount } from 'svelte';
|
||||
import FullScreenModal from './full-screen-modal.svelte';
|
||||
import type { ServerVersionResponseDto } from '@api';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
export let serverVersion: ServerVersionResponseDto;
|
||||
import FullScreenModal from './full-screen-modal.svelte';
|
||||
|
||||
let showModal = false;
|
||||
let githubVersion: string;
|
||||
$: serverVersionName = semverToName(serverVersion);
|
||||
|
||||
function semverToName({ major, minor, patch }: ServerVersionResponseDto) {
|
||||
return `v${major}.${minor}.${patch}`;
|
||||
}
|
||||
const { onRelease } = websocketStore;
|
||||
|
||||
function onAcknowledge() {
|
||||
// Store server version to prevent the notification
|
||||
// from showing again.
|
||||
localStorage.setItem('appVersion', githubVersion);
|
||||
const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
|
||||
|
||||
$: releaseVersion = $onRelease && semverToName($onRelease.releaseVersion);
|
||||
$: serverVersion = $onRelease && semverToName($onRelease.serverVersion);
|
||||
$: $onRelease?.isAvailable && handleRelease();
|
||||
|
||||
const onAcknowledge = () => {
|
||||
localStorage.setItem('appVersion', releaseVersion);
|
||||
showModal = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const handleRelease = () => {
|
||||
try {
|
||||
githubVersion = await getGithubVersion();
|
||||
if (localStorage.getItem('appVersion') === githubVersion) {
|
||||
// Updated version has already been acknowledged.
|
||||
if (localStorage.getItem('appVersion') === releaseVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (githubVersion !== serverVersionName) {
|
||||
showModal = true;
|
||||
}
|
||||
} catch (err) {
|
||||
// Only log any errors that occur.
|
||||
console.error('Error [VersionAnnouncementBox]:', err);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if showModal}
|
||||
@ -63,9 +55,9 @@
|
||||
<div class="mt-4 font-medium">Your friend, Alex</div>
|
||||
|
||||
<div class="font-sm mt-8">
|
||||
<code>Server Version: {serverVersionName}</code>
|
||||
<code>Server Version: {serverVersion}</code>
|
||||
<br />
|
||||
<code>Latest Version: {githubVersion}</code>
|
||||
<code>Latest Version: {releaseVersion}</code>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-right">
|
||||
|
@ -3,6 +3,13 @@ import { io } from 'socket.io-client';
|
||||
import { writable } from 'svelte/store';
|
||||
import { loadConfig } from './server-config.store';
|
||||
|
||||
export interface ReleaseEvent {
|
||||
isAvailable: boolean;
|
||||
checkedAt: Date;
|
||||
serverVersion: ServerVersionResponseDto;
|
||||
releaseVersion: ServerVersionResponseDto;
|
||||
}
|
||||
|
||||
export const websocketStore = {
|
||||
onUploadSuccess: writable<AssetResponseDto>(),
|
||||
onAssetDelete: writable<string>(),
|
||||
@ -10,6 +17,7 @@ export const websocketStore = {
|
||||
onPersonThumbnail: writable<string>(),
|
||||
serverVersion: writable<ServerVersionResponseDto>(),
|
||||
connected: writable<boolean>(false),
|
||||
onRelease: writable<ReleaseEvent>(),
|
||||
};
|
||||
|
||||
export const openWebsocketConnection = () => {
|
||||
@ -24,12 +32,13 @@ export const openWebsocketConnection = () => {
|
||||
websocket
|
||||
.on('connect', () => websocketStore.connected.set(true))
|
||||
.on('disconnect', () => websocketStore.connected.set(false))
|
||||
// .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(JSON.parse(data) as AssetResponseDto))
|
||||
.on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(JSON.parse(data) as string))
|
||||
.on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(JSON.parse(data) as string[]))
|
||||
.on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(JSON.parse(data) as string))
|
||||
.on('on_server_version', (data) => websocketStore.serverVersion.set(JSON.parse(data) as ServerVersionResponseDto))
|
||||
// .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(data))
|
||||
.on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(data))
|
||||
.on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(data))
|
||||
.on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(data))
|
||||
.on('on_server_version', (data) => websocketStore.serverVersion.set(data))
|
||||
.on('on_config_update', () => loadConfig())
|
||||
.on('on_new_release', (data) => websocketStore.onRelease.set(data))
|
||||
.on('error', (e) => console.log('Websocket Error', e));
|
||||
|
||||
return () => websocket?.close();
|
||||
|
@ -1,15 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
type GithubRelease = {
|
||||
tag_name: string;
|
||||
};
|
||||
|
||||
export const getGithubVersion = async (): Promise<string> => {
|
||||
const { data } = await axios.get<GithubRelease>('https://api.github.com/repos/immich-app/immich/releases/latest', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
});
|
||||
|
||||
return data.tag_name;
|
||||
};
|
@ -1,7 +1,5 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ locals: { api, user } }) => {
|
||||
const { data: serverVersion } = await api.serverInfoApi.getServerVersion();
|
||||
|
||||
return { serverVersion, user };
|
||||
export const load = (async ({ locals: { user } }) => {
|
||||
return { user };
|
||||
}) satisfies LayoutServerLoad;
|
||||
|
@ -108,7 +108,7 @@
|
||||
<NotificationList />
|
||||
|
||||
{#if data.user?.isAdmin}
|
||||
<VersionAnnouncementBox serverVersion={data.serverVersion} />
|
||||
<VersionAnnouncementBox />
|
||||
{/if}
|
||||
|
||||
{#if $page.route.id?.includes('(user)')}
|
||||
|
@ -21,6 +21,7 @@
|
||||
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
||||
import Download from 'svelte-material-icons/Download.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@ -109,6 +110,10 @@
|
||||
<TrashSettings disabled={$featureFlags.configFile} trashConfig={configs.trash} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Version Check" subtitle="Enable/disable the new version notification">
|
||||
<NewVersionCheckSettings newVersionCheckConfig={configs.newVersionCheck} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
title="Video Transcoding Settings"
|
||||
subtitle="Manage the resolution and encoding information of the video files"
|
||||
|
Loading…
Reference in New Issue
Block a user