mirror of
https://github.com/immich-app/immich.git
synced 2025-01-13 15:35:15 +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
|
* @memberof SystemConfigDto
|
||||||
*/
|
*/
|
||||||
'map': SystemConfigMapDto;
|
'map': SystemConfigMapDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {SystemConfigNewVersionCheckDto}
|
||||||
|
* @memberof SystemConfigDto
|
||||||
|
*/
|
||||||
|
'newVersionCheck': SystemConfigNewVersionCheckDto;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SystemConfigOAuthDto}
|
* @type {SystemConfigOAuthDto}
|
||||||
@ -3572,6 +3578,19 @@ export interface SystemConfigMapDto {
|
|||||||
*/
|
*/
|
||||||
'tileUrl': string;
|
'tileUrl': string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface SystemConfigNewVersionCheckDto
|
||||||
|
*/
|
||||||
|
export interface SystemConfigNewVersionCheckDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof SystemConfigNewVersionCheckDto
|
||||||
|
*/
|
||||||
|
'enabled': boolean;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.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)
|
.where((c) => c.action == PendingAction.assetDelete)
|
||||||
.toList();
|
.toList();
|
||||||
if (deleteChanges.isNotEmpty) {
|
if (deleteChanges.isNotEmpty) {
|
||||||
List<String> remoteIds = deleteChanges
|
List<String> remoteIds =
|
||||||
.map((a) => jsonDecode(a.value.toString()).toString())
|
deleteChanges.map((a) => a.value.toString()).toList();
|
||||||
.toList();
|
|
||||||
ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
pendingChanges: state.pendingChanges
|
pendingChanges: state.pendingChanges
|
||||||
@ -188,21 +185,20 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleOnUploadSuccess(dynamic data) {
|
_handleOnUploadSuccess(dynamic data) {
|
||||||
final jsonString = jsonDecode(data.toString());
|
final dto = AssetResponseDto.fromJson(data);
|
||||||
final dto = AssetResponseDto.fromJson(jsonString);
|
|
||||||
if (dto != null) {
|
if (dto != null) {
|
||||||
final newAsset = Asset.remote(dto);
|
final newAsset = Asset.remote(dto);
|
||||||
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleOnConfigUpdate(dynamic data) {
|
_handleOnConfigUpdate(dynamic _) {
|
||||||
ref.read(serverInfoProvider.notifier).getServerFeatures();
|
ref.read(serverInfoProvider.notifier).getServerFeatures();
|
||||||
ref.read(serverInfoProvider.notifier).getServerConfig();
|
ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh updated assets
|
// Refresh updated assets
|
||||||
_handleServerUpdates(dynamic data) {
|
_handleServerUpdates(dynamic _) {
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
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/SystemConfigJobDto.md
|
||||||
doc/SystemConfigMachineLearningDto.md
|
doc/SystemConfigMachineLearningDto.md
|
||||||
doc/SystemConfigMapDto.md
|
doc/SystemConfigMapDto.md
|
||||||
|
doc/SystemConfigNewVersionCheckDto.md
|
||||||
doc/SystemConfigOAuthDto.md
|
doc/SystemConfigOAuthDto.md
|
||||||
doc/SystemConfigPasswordLoginDto.md
|
doc/SystemConfigPasswordLoginDto.md
|
||||||
doc/SystemConfigReverseGeocodingDto.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_job_dto.dart
|
||||||
lib/model/system_config_machine_learning_dto.dart
|
lib/model/system_config_machine_learning_dto.dart
|
||||||
lib/model/system_config_map_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_o_auth_dto.dart
|
||||||
lib/model/system_config_password_login_dto.dart
|
lib/model/system_config_password_login_dto.dart
|
||||||
lib/model/system_config_reverse_geocoding_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_job_dto_test.dart
|
||||||
test/system_config_machine_learning_dto_test.dart
|
test/system_config_machine_learning_dto_test.dart
|
||||||
test/system_config_map_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_o_auth_dto_test.dart
|
||||||
test/system_config_password_login_dto_test.dart
|
test/system_config_password_login_dto_test.dart
|
||||||
test/system_config_reverse_geocoding_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": {
|
"map": {
|
||||||
"$ref": "#/components/schemas/SystemConfigMapDto"
|
"$ref": "#/components/schemas/SystemConfigMapDto"
|
||||||
},
|
},
|
||||||
|
"newVersionCheck": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigNewVersionCheckDto"
|
||||||
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"$ref": "#/components/schemas/SystemConfigOAuthDto"
|
"$ref": "#/components/schemas/SystemConfigOAuthDto"
|
||||||
},
|
},
|
||||||
@ -8074,6 +8077,7 @@
|
|||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"machineLearning",
|
"machineLearning",
|
||||||
"map",
|
"map",
|
||||||
|
"newVersionCheck",
|
||||||
"oauth",
|
"oauth",
|
||||||
"passwordLogin",
|
"passwordLogin",
|
||||||
"reverseGeocoding",
|
"reverseGeocoding",
|
||||||
@ -8257,6 +8261,17 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SystemConfigNewVersionCheckDto": {
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SystemConfigOAuthDto": {
|
"SystemConfigOAuthDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"autoLaunch": {
|
"autoLaunch": {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean } from 'class-validator';
|
import { IsBoolean } from 'class-validator';
|
||||||
import { Optional, ValidateUUID, toBoolean } from '../../domain.util';
|
import { Optional, toBoolean, ValidateUUID } from '../../domain.util';
|
||||||
|
|
||||||
export class GetAlbumsDto {
|
export class GetAlbumsDto {
|
||||||
@Optional()
|
@Optional()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { mimeTypes } from '@app/domain';
|
import { ServerVersion, mimeTypes } from './domain.constant';
|
||||||
|
|
||||||
describe('mimeTypes', () => {
|
describe('mimeTypes', () => {
|
||||||
for (const { mimetype, extension } of [
|
for (const { mimetype, extension } of [
|
||||||
@ -188,7 +188,74 @@ describe('mimeTypes', () => {
|
|||||||
|
|
||||||
for (const [ext, v] of Object.entries(mimeTypes.sidecar)) {
|
for (const [ext, v] of Object.entries(mimeTypes.sidecar)) {
|
||||||
it(`should lookup ${ext}`, () => {
|
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';
|
import pkg from 'src/../../package.json';
|
||||||
|
|
||||||
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||||
|
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
||||||
const [major, minor, patch] = pkg.version.split('.');
|
|
||||||
|
|
||||||
export interface IServerVersion {
|
export interface IServerVersion {
|
||||||
major: number;
|
major: number;
|
||||||
@ -13,13 +12,49 @@ export interface IServerVersion {
|
|||||||
patch: number;
|
patch: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serverVersion: IServerVersion = {
|
export class ServerVersion implements IServerVersion {
|
||||||
major: Number(major),
|
constructor(
|
||||||
minor: Number(minor),
|
public readonly major: number,
|
||||||
patch: Number(patch),
|
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';
|
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_DISCOVERY]: QueueName.SIDECAR,
|
||||||
[JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
|
[JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
|
||||||
|
|
||||||
// Library managment
|
// Library management
|
||||||
[JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
|
[JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
|
||||||
[JobName.LIBRARY_SCAN]: QueueName.LIBRARY,
|
[JobName.LIBRARY_SCAN]: QueueName.LIBRARY,
|
||||||
[JobName.LIBRARY_DELETE]: QueueName.LIBRARY,
|
[JobName.LIBRARY_DELETE]: QueueName.LIBRARY,
|
||||||
|
@ -9,9 +9,13 @@ export enum CommunicationEvent {
|
|||||||
PERSON_THUMBNAIL = 'on_person_thumbnail',
|
PERSON_THUMBNAIL = 'on_person_thumbnail',
|
||||||
SERVER_VERSION = 'on_server_version',
|
SERVER_VERSION = 'on_server_version',
|
||||||
CONFIG_UPDATE = 'on_config_update',
|
CONFIG_UPDATE = 'on_config_update',
|
||||||
|
NEW_RELEASE = 'on_new_release',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Callback = (userId: string) => Promise<void>;
|
||||||
|
|
||||||
export interface ICommunicationRepository {
|
export interface ICommunicationRepository {
|
||||||
send(event: CommunicationEvent, userId: string, data: any): void;
|
send(event: CommunicationEvent, userId: string, data: any): void;
|
||||||
broadcast(event: CommunicationEvent, 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 './partner.repository';
|
||||||
export * from './person.repository';
|
export * from './person.repository';
|
||||||
export * from './search.repository';
|
export * from './search.repository';
|
||||||
|
export * from './server-info.repository';
|
||||||
export * from './shared-link.repository';
|
export * from './shared-link.repository';
|
||||||
export * from './smart-info.repository';
|
export * from './smart-info.repository';
|
||||||
export * from './storage.repository';
|
export * from './storage.repository';
|
||||||
|
@ -85,7 +85,7 @@ export type JobItem =
|
|||||||
| { name: JobName.ASSET_DELETION; data: IAssetDeletionJob }
|
| { name: JobName.ASSET_DELETION; data: IAssetDeletionJob }
|
||||||
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
|
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
|
||||||
|
|
||||||
// Library Managment
|
// Library Management
|
||||||
| { name: JobName.LIBRARY_SCAN_ASSET; data: ILibraryFileJob }
|
| { name: JobName.LIBRARY_SCAN_ASSET; data: ILibraryFileJob }
|
||||||
| { name: JobName.LIBRARY_SCAN; data: ILibraryRefreshJob }
|
| { name: JobName.LIBRARY_SCAN; data: ILibraryRefreshJob }
|
||||||
| { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob }
|
| { 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 { serverVersion } from '../domain.constant';
|
||||||
import { IStorageRepository, ISystemConfigRepository, IUserRepository } from '../repositories';
|
import {
|
||||||
|
ICommunicationRepository,
|
||||||
|
IServerInfoRepository,
|
||||||
|
IStorageRepository,
|
||||||
|
ISystemConfigRepository,
|
||||||
|
IUserRepository,
|
||||||
|
} from '../repositories';
|
||||||
import { ServerInfoService } from './server-info.service';
|
import { ServerInfoService } from './server-info.service';
|
||||||
|
|
||||||
describe(ServerInfoService.name, () => {
|
describe(ServerInfoService.name, () => {
|
||||||
let sut: ServerInfoService;
|
let sut: ServerInfoService;
|
||||||
|
let communicationMock: jest.Mocked<ICommunicationRepository>;
|
||||||
let configMock: jest.Mocked<ISystemConfigRepository>;
|
let configMock: jest.Mocked<ISystemConfigRepository>;
|
||||||
|
let serverInfoMock: jest.Mocked<IServerInfoRepository>;
|
||||||
let storageMock: jest.Mocked<IStorageRepository>;
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
let userMock: jest.Mocked<IUserRepository>;
|
let userMock: jest.Mocked<IUserRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
configMock = newSystemConfigRepositoryMock();
|
configMock = newSystemConfigRepositoryMock();
|
||||||
|
communicationMock = newCommunicationRepositoryMock();
|
||||||
|
serverInfoMock = newServerInfoRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
|
|
||||||
sut = new ServerInfoService(configMock, userMock, storageMock);
|
sut = new ServerInfoService(communicationMock, configMock, userMock, serverInfoMock, storageMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { mimeTypes, serverVersion } from '../domain.constant';
|
import { DateTime } from 'luxon';
|
||||||
|
import { ServerVersion, isDev, mimeTypes, serverVersion } from '../domain.constant';
|
||||||
import { asHumanReadable } from '../domain.util';
|
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 { StorageCore, StorageFolder } from '../storage';
|
||||||
import { SystemConfigCore } from '../system-config';
|
import { SystemConfigCore } from '../system-config';
|
||||||
import {
|
import {
|
||||||
@ -16,14 +25,20 @@ import {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerInfoService {
|
export class ServerInfoService {
|
||||||
|
private logger = new Logger(ServerInfoService.name);
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
|
private releaseVersion = serverVersion;
|
||||||
|
private releaseVersionCheckedAt: DateTime | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
|
||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
|
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
) {
|
) {
|
||||||
this.configCore = SystemConfigCore.create(configRepository);
|
this.configCore = SystemConfigCore.create(configRepository);
|
||||||
|
this.communicationRepository.addEventListener('connect', (userId) => this.handleConnect(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInfo(): Promise<ServerInfoResponseDto> {
|
async getInfo(): Promise<ServerInfoResponseDto> {
|
||||||
@ -101,4 +116,56 @@ export class ServerInfoService {
|
|||||||
sidecar: Object.keys(mimeTypes.sidecar),
|
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 { SystemConfigJobDto } from './system-config-job.dto';
|
||||||
import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
|
import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto';
|
||||||
import { SystemConfigMapDto } from './system-config-map.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 { SystemConfigOAuthDto } from './system-config-oauth.dto';
|
||||||
import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
|
import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
|
||||||
import { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto';
|
import { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto';
|
||||||
@ -29,6 +30,11 @@ export class SystemConfigDto implements SystemConfig {
|
|||||||
@IsObject()
|
@IsObject()
|
||||||
map!: SystemConfigMapDto;
|
map!: SystemConfigMapDto;
|
||||||
|
|
||||||
|
@Type(() => SystemConfigNewVersionCheckDto)
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
newVersionCheck!: SystemConfigNewVersionCheckDto;
|
||||||
|
|
||||||
@Type(() => SystemConfigOAuthDto)
|
@Type(() => SystemConfigOAuthDto)
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
CQMode,
|
|
||||||
CitiesFile,
|
CitiesFile,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
|
CQMode,
|
||||||
SystemConfig,
|
SystemConfig,
|
||||||
SystemConfigEntity,
|
SystemConfigEntity,
|
||||||
SystemConfigKey,
|
SystemConfigKey,
|
||||||
@ -110,6 +110,9 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
},
|
},
|
||||||
|
newVersionCheck: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
trash: {
|
trash: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
days: 30,
|
days: 30,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
CQMode,
|
|
||||||
CitiesFile,
|
CitiesFile,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
|
CQMode,
|
||||||
SystemConfig,
|
SystemConfig,
|
||||||
SystemConfigEntity,
|
SystemConfigEntity,
|
||||||
SystemConfigKey,
|
SystemConfigKey,
|
||||||
@ -15,7 +15,7 @@ import { BadRequestException } from '@nestjs/common';
|
|||||||
import { newCommunicationRepositoryMock, newJobRepositoryMock, newSystemConfigRepositoryMock } from '@test';
|
import { newCommunicationRepositoryMock, newJobRepositoryMock, newSystemConfigRepositoryMock } from '@test';
|
||||||
import { JobName, QueueName } from '../job';
|
import { JobName, QueueName } from '../job';
|
||||||
import { ICommunicationRepository, IJobRepository, ISystemConfigRepository } from '../repositories';
|
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';
|
import { SystemConfigService } from './system-config.service';
|
||||||
|
|
||||||
const updates: SystemConfigEntity[] = [
|
const updates: SystemConfigEntity[] = [
|
||||||
@ -111,6 +111,9 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
},
|
},
|
||||||
|
newVersionCheck: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
trash: {
|
trash: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
days: 10,
|
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 { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
import { Cron, CronExpression, Interval } from '@nestjs/schedule';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
@ -13,6 +13,11 @@ export class AppService {
|
|||||||
private serverService: ServerInfoService,
|
private serverService: ServerInfoService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Interval(ONE_HOUR.as('milliseconds'))
|
||||||
|
async onVersionCheck() {
|
||||||
|
await this.serverService.handleVersionCheck();
|
||||||
|
}
|
||||||
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||||
async onNightlyJob() {
|
async onNightlyJob() {
|
||||||
await this.jobService.handleNightlyJobs();
|
await this.jobService.handleNightlyJobs();
|
||||||
@ -21,6 +26,7 @@ export class AppService {
|
|||||||
async init() {
|
async init() {
|
||||||
this.storageService.init();
|
this.storageService.init();
|
||||||
await this.searchService.init();
|
await this.searchService.init();
|
||||||
|
await this.serverService.handleVersionCheck();
|
||||||
this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`);
|
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_HEADER,
|
||||||
IMMICH_API_KEY_NAME,
|
IMMICH_API_KEY_NAME,
|
||||||
ImmichReadStream,
|
ImmichReadStream,
|
||||||
SERVER_VERSION,
|
serverVersion,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { INestApplication, StreamableFile } from '@nestjs/common';
|
import { INestApplication, StreamableFile } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
@ -91,7 +91,7 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => {
|
|||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle('Immich')
|
.setTitle('Immich')
|
||||||
.setDescription('Immich API')
|
.setDescription('Immich API')
|
||||||
.setVersion(SERVER_VERSION)
|
.setVersion(serverVersion.toString())
|
||||||
.addBearerAuth({
|
.addBearerAuth({
|
||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: 'Bearer',
|
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 { RedisIoAdapter } from '@app/infra';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
@ -9,9 +9,7 @@ import { AppModule } from './app.module';
|
|||||||
import { useSwagger } from './app.utils';
|
import { useSwagger } from './app.utils';
|
||||||
|
|
||||||
const logger = new Logger('ImmichServer');
|
const logger = new Logger('ImmichServer');
|
||||||
const envName = (process.env.NODE_ENV || 'development').toUpperCase();
|
|
||||||
const port = Number(process.env.SERVER_PORT) || 3001;
|
const port = Number(process.env.SERVER_PORT) || 3001;
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, { logger: getLogLevels() });
|
const app = await NestFactory.create<NestExpressApplication>(AppModule, { logger: getLogLevels() });
|
||||||
@ -29,5 +27,5 @@ export async function bootstrap() {
|
|||||||
const server = await app.listen(port);
|
const server = await app.listen(port);
|
||||||
server.requestTimeout = 30 * 60 * 1000;
|
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_ENABLED = 'reverseGeocoding.enabled',
|
||||||
REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride',
|
REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride',
|
||||||
|
|
||||||
|
NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled',
|
||||||
|
|
||||||
OAUTH_ENABLED = 'oauth.enabled',
|
OAUTH_ENABLED = 'oauth.enabled',
|
||||||
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
|
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
|
||||||
OAUTH_CLIENT_ID = 'oauth.clientId',
|
OAUTH_CLIENT_ID = 'oauth.clientId',
|
||||||
@ -219,6 +221,9 @@ export interface SystemConfig {
|
|||||||
quality: number;
|
quality: number;
|
||||||
colorspace: Colorspace;
|
colorspace: Colorspace;
|
||||||
};
|
};
|
||||||
|
newVersionCheck: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
trash: {
|
trash: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
days: number;
|
days: number;
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
IPartnerRepository,
|
IPartnerRepository,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
ISearchRepository,
|
ISearchRepository,
|
||||||
|
IServerInfoRepository,
|
||||||
ISharedLinkRepository,
|
ISharedLinkRepository,
|
||||||
ISmartInfoRepository,
|
ISmartInfoRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
@ -48,6 +49,7 @@ import {
|
|||||||
MoveRepository,
|
MoveRepository,
|
||||||
PartnerRepository,
|
PartnerRepository,
|
||||||
PersonRepository,
|
PersonRepository,
|
||||||
|
ServerInfoRepository,
|
||||||
SharedLinkRepository,
|
SharedLinkRepository,
|
||||||
SmartInfoRepository,
|
SmartInfoRepository,
|
||||||
SystemConfigRepository,
|
SystemConfigRepository,
|
||||||
@ -73,6 +75,7 @@ const providers: Provider[] = [
|
|||||||
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
||||||
{ provide: IPersonRepository, useClass: PersonRepository },
|
{ provide: IPersonRepository, useClass: PersonRepository },
|
||||||
{ provide: ISearchRepository, useClass: TypesenseRepository },
|
{ provide: ISearchRepository, useClass: TypesenseRepository },
|
||||||
|
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||||
{ provide: ISmartInfoRepository, useClass: SmartInfoRepository },
|
{ provide: ISmartInfoRepository, useClass: SmartInfoRepository },
|
||||||
{ provide: IStorageRepository, useClass: FilesystemProvider },
|
{ 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 { Logger } from '@nestjs/common';
|
||||||
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
|
||||||
import { Server, Socket } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
@ -6,18 +6,25 @@ import { Server, Socket } from 'socket.io';
|
|||||||
@WebSocketGateway({ cors: true })
|
@WebSocketGateway({ cors: true })
|
||||||
export class CommunicationRepository implements OnGatewayConnection, OnGatewayDisconnect, ICommunicationRepository {
|
export class CommunicationRepository implements OnGatewayConnection, OnGatewayDisconnect, ICommunicationRepository {
|
||||||
private logger = new Logger(CommunicationRepository.name);
|
private logger = new Logger(CommunicationRepository.name);
|
||||||
|
private onConnectCallbacks: Callback[] = [];
|
||||||
|
|
||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
@WebSocketServer() server!: Server;
|
@WebSocketServer() server!: Server;
|
||||||
|
|
||||||
|
addEventListener(event: 'connect', callback: Callback) {
|
||||||
|
this.onConnectCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
async handleConnection(client: Socket) {
|
async handleConnection(client: Socket) {
|
||||||
try {
|
try {
|
||||||
this.logger.log(`New websocket connection: ${client.id}`);
|
this.logger.log(`New websocket connection: ${client.id}`);
|
||||||
const user = await this.authService.validate(client.request.headers, {});
|
const user = await this.authService.validate(client.request.headers, {});
|
||||||
if (user) {
|
if (user) {
|
||||||
await client.join(user.id);
|
await client.join(user.id);
|
||||||
this.send(CommunicationEvent.SERVER_VERSION, user.id, serverVersion);
|
for (const callback of this.onConnectCallbacks) {
|
||||||
|
await callback(user.id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
client.emit('error', 'unauthorized');
|
client.emit('error', 'unauthorized');
|
||||||
client.disconnect();
|
client.disconnect();
|
||||||
@ -34,7 +41,7 @@ export class CommunicationRepository implements OnGatewayConnection, OnGatewayDi
|
|||||||
}
|
}
|
||||||
|
|
||||||
send(event: CommunicationEvent, userId: string, data: any) {
|
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) {
|
broadcast(event: CommunicationEvent, data: any) {
|
||||||
|
@ -14,6 +14,7 @@ export * from './metadata.repository';
|
|||||||
export * from './move.repository';
|
export * from './move.repository';
|
||||||
export * from './partner.repository';
|
export * from './partner.repository';
|
||||||
export * from './person.repository';
|
export * from './person.repository';
|
||||||
|
export * from './server-info.repository';
|
||||||
export * from './shared-link.repository';
|
export * from './shared-link.repository';
|
||||||
export * from './smart-info.repository';
|
export * from './smart-info.repository';
|
||||||
export * from './system-config.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,
|
MetadataService,
|
||||||
PersonService,
|
PersonService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
ServerInfoService,
|
||||||
SmartInfoService,
|
SmartInfoService,
|
||||||
StorageService,
|
StorageService,
|
||||||
StorageTemplateService,
|
StorageTemplateService,
|
||||||
@ -23,19 +24,20 @@ export class AppService {
|
|||||||
private logger = new Logger(AppService.name);
|
private logger = new Logger(AppService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private jobService: JobService,
|
private auditService: AuditService,
|
||||||
private assetService: AssetService,
|
private assetService: AssetService,
|
||||||
|
private jobService: JobService,
|
||||||
|
private libraryService: LibraryService,
|
||||||
private mediaService: MediaService,
|
private mediaService: MediaService,
|
||||||
private metadataService: MetadataService,
|
private metadataService: MetadataService,
|
||||||
private personService: PersonService,
|
private personService: PersonService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
|
private serverInfoService: ServerInfoService,
|
||||||
private smartInfoService: SmartInfoService,
|
private smartInfoService: SmartInfoService,
|
||||||
private storageTemplateService: StorageTemplateService,
|
private storageTemplateService: StorageTemplateService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private systemConfigService: SystemConfigService,
|
private systemConfigService: SystemConfigService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private auditService: AuditService,
|
|
||||||
private libraryService: LibraryService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
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 { RedisIoAdapter } from '@app/infra';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
@ -7,7 +7,6 @@ import { MicroservicesModule } from './microservices.module';
|
|||||||
|
|
||||||
const logger = new Logger('ImmichMicroservice');
|
const logger = new Logger('ImmichMicroservice');
|
||||||
const port = Number(process.env.MICROSERVICES_PORT) || 3002;
|
const port = Number(process.env.MICROSERVICES_PORT) || 3002;
|
||||||
const envName = (process.env.NODE_ENV || 'development').toUpperCase();
|
|
||||||
|
|
||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
const app = await NestFactory.create(MicroservicesModule, { logger: getLogLevels() });
|
const app = await NestFactory.create(MicroservicesModule, { logger: getLogLevels() });
|
||||||
@ -17,5 +16,5 @@ export async function bootstrap() {
|
|||||||
await app.get(AppService).init();
|
await app.get(AppService).init();
|
||||||
await app.listen(port);
|
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 {
|
return {
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
broadcast: 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 './smart-info.repository.mock';
|
||||||
export * from './storage.repository.mock';
|
export * from './storage.repository.mock';
|
||||||
export * from './system-config.repository.mock';
|
export * from './system-config.repository.mock';
|
||||||
|
export * from './system-info.repository.mock';
|
||||||
export * from './tag.repository.mock';
|
export * from './tag.repository.mock';
|
||||||
export * from './user-token.repository.mock';
|
export * from './user-token.repository.mock';
|
||||||
export * from './user.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
|
* @memberof SystemConfigDto
|
||||||
*/
|
*/
|
||||||
'map': SystemConfigMapDto;
|
'map': SystemConfigMapDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {SystemConfigNewVersionCheckDto}
|
||||||
|
* @memberof SystemConfigDto
|
||||||
|
*/
|
||||||
|
'newVersionCheck': SystemConfigNewVersionCheckDto;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {SystemConfigOAuthDto}
|
* @type {SystemConfigOAuthDto}
|
||||||
@ -3572,6 +3578,19 @@ export interface SystemConfigMapDto {
|
|||||||
*/
|
*/
|
||||||
'tileUrl': string;
|
'tileUrl': string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface SystemConfigNewVersionCheckDto
|
||||||
|
*/
|
||||||
|
export interface SystemConfigNewVersionCheckDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof SystemConfigNewVersionCheckDto
|
||||||
|
*/
|
||||||
|
'enabled': boolean;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @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">
|
<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 type { ServerVersionResponseDto } from '@api';
|
||||||
|
import { websocketStore } from '$lib/stores/websocket';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
import FullScreenModal from './full-screen-modal.svelte';
|
||||||
export let serverVersion: ServerVersionResponseDto;
|
|
||||||
|
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
let githubVersion: string;
|
|
||||||
$: serverVersionName = semverToName(serverVersion);
|
|
||||||
|
|
||||||
function semverToName({ major, minor, patch }: ServerVersionResponseDto) {
|
const { onRelease } = websocketStore;
|
||||||
return `v${major}.${minor}.${patch}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onAcknowledge() {
|
const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
|
||||||
// Store server version to prevent the notification
|
|
||||||
// from showing again.
|
$: releaseVersion = $onRelease && semverToName($onRelease.releaseVersion);
|
||||||
localStorage.setItem('appVersion', githubVersion);
|
$: serverVersion = $onRelease && semverToName($onRelease.serverVersion);
|
||||||
|
$: $onRelease?.isAvailable && handleRelease();
|
||||||
|
|
||||||
|
const onAcknowledge = () => {
|
||||||
|
localStorage.setItem('appVersion', releaseVersion);
|
||||||
showModal = false;
|
showModal = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
const handleRelease = () => {
|
||||||
try {
|
try {
|
||||||
githubVersion = await getGithubVersion();
|
if (localStorage.getItem('appVersion') === releaseVersion) {
|
||||||
if (localStorage.getItem('appVersion') === githubVersion) {
|
|
||||||
// Updated version has already been acknowledged.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (githubVersion !== serverVersionName) {
|
showModal = true;
|
||||||
showModal = true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Only log any errors that occur.
|
|
||||||
console.error('Error [VersionAnnouncementBox]:', err);
|
console.error('Error [VersionAnnouncementBox]:', err);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showModal}
|
{#if showModal}
|
||||||
@ -63,9 +55,9 @@
|
|||||||
<div class="mt-4 font-medium">Your friend, Alex</div>
|
<div class="mt-4 font-medium">Your friend, Alex</div>
|
||||||
|
|
||||||
<div class="font-sm mt-8">
|
<div class="font-sm mt-8">
|
||||||
<code>Server Version: {serverVersionName}</code>
|
<code>Server Version: {serverVersion}</code>
|
||||||
<br />
|
<br />
|
||||||
<code>Latest Version: {githubVersion}</code>
|
<code>Latest Version: {releaseVersion}</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 text-right">
|
<div class="mt-8 text-right">
|
||||||
|
@ -3,6 +3,13 @@ import { io } from 'socket.io-client';
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { loadConfig } from './server-config.store';
|
import { loadConfig } from './server-config.store';
|
||||||
|
|
||||||
|
export interface ReleaseEvent {
|
||||||
|
isAvailable: boolean;
|
||||||
|
checkedAt: Date;
|
||||||
|
serverVersion: ServerVersionResponseDto;
|
||||||
|
releaseVersion: ServerVersionResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
export const websocketStore = {
|
export const websocketStore = {
|
||||||
onUploadSuccess: writable<AssetResponseDto>(),
|
onUploadSuccess: writable<AssetResponseDto>(),
|
||||||
onAssetDelete: writable<string>(),
|
onAssetDelete: writable<string>(),
|
||||||
@ -10,6 +17,7 @@ export const websocketStore = {
|
|||||||
onPersonThumbnail: writable<string>(),
|
onPersonThumbnail: writable<string>(),
|
||||||
serverVersion: writable<ServerVersionResponseDto>(),
|
serverVersion: writable<ServerVersionResponseDto>(),
|
||||||
connected: writable<boolean>(false),
|
connected: writable<boolean>(false),
|
||||||
|
onRelease: writable<ReleaseEvent>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openWebsocketConnection = () => {
|
export const openWebsocketConnection = () => {
|
||||||
@ -24,12 +32,13 @@ export const openWebsocketConnection = () => {
|
|||||||
websocket
|
websocket
|
||||||
.on('connect', () => websocketStore.connected.set(true))
|
.on('connect', () => websocketStore.connected.set(true))
|
||||||
.on('disconnect', () => websocketStore.connected.set(false))
|
.on('disconnect', () => websocketStore.connected.set(false))
|
||||||
// .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(JSON.parse(data) as AssetResponseDto))
|
// .on('on_upload_success', (data) => websocketStore.onUploadSuccess.set(data))
|
||||||
.on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(JSON.parse(data) as string))
|
.on('on_asset_delete', (data) => websocketStore.onAssetDelete.set(data))
|
||||||
.on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(JSON.parse(data) as string[]))
|
.on('on_asset_trash', (data) => websocketStore.onAssetTrash.set(data))
|
||||||
.on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(JSON.parse(data) as string))
|
.on('on_person_thumbnail', (data) => websocketStore.onPersonThumbnail.set(data))
|
||||||
.on('on_server_version', (data) => websocketStore.serverVersion.set(JSON.parse(data) as ServerVersionResponseDto))
|
.on('on_server_version', (data) => websocketStore.serverVersion.set(data))
|
||||||
.on('on_config_update', () => loadConfig())
|
.on('on_config_update', () => loadConfig())
|
||||||
|
.on('on_new_release', (data) => websocketStore.onRelease.set(data))
|
||||||
.on('error', (e) => console.log('Websocket Error', e));
|
.on('error', (e) => console.log('Websocket Error', e));
|
||||||
|
|
||||||
return () => websocket?.close();
|
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';
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ locals: { api, user } }) => {
|
export const load = (async ({ locals: { user } }) => {
|
||||||
const { data: serverVersion } = await api.serverInfoApi.getServerVersion();
|
return { user };
|
||||||
|
|
||||||
return { serverVersion, user };
|
|
||||||
}) satisfies LayoutServerLoad;
|
}) satisfies LayoutServerLoad;
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
<NotificationList />
|
<NotificationList />
|
||||||
|
|
||||||
{#if data.user?.isAdmin}
|
{#if data.user?.isAdmin}
|
||||||
<VersionAnnouncementBox serverVersion={data.serverVersion} />
|
<VersionAnnouncementBox />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $page.route.id?.includes('(user)')}
|
{#if $page.route.id?.includes('(user)')}
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
||||||
import Download from 'svelte-material-icons/Download.svelte';
|
import Download from 'svelte-material-icons/Download.svelte';
|
||||||
import type { PageData } from './$types';
|
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;
|
export let data: PageData;
|
||||||
|
|
||||||
@ -109,6 +110,10 @@
|
|||||||
<TrashSettings disabled={$featureFlags.configFile} trashConfig={configs.trash} />
|
<TrashSettings disabled={$featureFlags.configFile} trashConfig={configs.trash} />
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
|
<SettingAccordion title="Version Check" subtitle="Enable/disable the new version notification">
|
||||||
|
<NewVersionCheckSettings newVersionCheckConfig={configs.newVersionCheck} />
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
title="Video Transcoding Settings"
|
title="Video Transcoding Settings"
|
||||||
subtitle="Manage the resolution and encoding information of the video files"
|
subtitle="Manage the resolution and encoding information of the video files"
|
||||||
|
Loading…
Reference in New Issue
Block a user