mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:06:56 -04:00
Compare commits
10 Commits
148f98e1aa
...
0b253a8a2c
Author | SHA1 | Date | |
---|---|---|---|
|
0b253a8a2c | ||
|
63437529e1 | ||
|
4d20b11f25 | ||
|
1c3603e23b | ||
|
eb3ac09e0d | ||
|
305fc77ebe | ||
|
d46e50213a | ||
|
49486f2d26 | ||
|
eac189a9e5 | ||
|
ae26c3d203 |
@ -91,7 +91,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9
|
image: grafana/grafana:11.2.1-ubuntu@sha256:b90c0fdc482913de7a55fe96539bf9e3c4fbcee835d0c2dffc59152bc3964ff7
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
47
docs/docs/administration/system-integrity.md
Normal file
47
docs/docs/administration/system-integrity.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# System Integrity
|
||||||
|
|
||||||
|
## Folder checks
|
||||||
|
|
||||||
|
:::info
|
||||||
|
The folders considered for these checks include: `upload/`, `library/`, `thumbs/`, `encoded-video/`, `profile/`
|
||||||
|
:::
|
||||||
|
|
||||||
|
When Immich starts, it performs a series of checks in order to validate that it can read and write files to the volume mounts used by the storage system. If it cannot perform all the required operations, it will fail to start. The checks include:
|
||||||
|
|
||||||
|
- Creating an initial hidden file (`.immich`) in each folder
|
||||||
|
- Reading a hidden file (`.immich`) in each folder
|
||||||
|
- Overwriting a hidden file (`.immich`) in each folder
|
||||||
|
|
||||||
|
The checks are designed to catch the following situations:
|
||||||
|
|
||||||
|
- Incorrect permissions (cannot read/write files)
|
||||||
|
- Missing volume mount (`.immich` files should exist, but are missing)
|
||||||
|
|
||||||
|
### Common issues
|
||||||
|
|
||||||
|
:::note
|
||||||
|
`.immich` files serve as markers and help keep track of volume mounts being used by Immich. Except for the situations listed below, they should never be manually created or deleted.
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### Missing `.immich` files
|
||||||
|
|
||||||
|
```
|
||||||
|
Verifying system mount folder checks (enabled=true)
|
||||||
|
...
|
||||||
|
ENOENT: no such file or directory, open 'upload/encoded-video/.immich'
|
||||||
|
```
|
||||||
|
|
||||||
|
The above error messages show that the server has previously (successfully) written `.immich` files to each folder, but now does not detect them. This could be because any of the following:
|
||||||
|
|
||||||
|
- Permission error - unable to read the file, but it exists
|
||||||
|
- File does not exist - volume mount has changed and should be corrected
|
||||||
|
- File does not exist - user manually deleted it and should be manually re-created (`touch .immich`)
|
||||||
|
- File does not exist - user restored from a backup, but did not restore each folder (user should restore all folders or manually create `.immich` in any missing folders)
|
||||||
|
|
||||||
|
### Ignoring the checks
|
||||||
|
|
||||||
|
The checks are designed to catch common problems that we have seen users have in the past, but if you want to disable them you can set the following environment variable:
|
||||||
|
|
||||||
|
```
|
||||||
|
IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
|
||||||
|
```
|
@ -42,6 +42,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||||
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
|
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
|
||||||
|
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
|
||||||
|
|
||||||
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||||
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
||||||
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -183,6 +183,7 @@ Class | Method | HTTP request | Description
|
|||||||
*ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |
|
*ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |
|
||||||
*ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |
|
*ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |
|
||||||
*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme |
|
*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme |
|
||||||
|
*ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history |
|
||||||
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
|
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
|
||||||
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
|
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
|
||||||
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
|
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
|
||||||
@ -400,6 +401,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
||||||
- [ServerStorageResponseDto](doc//ServerStorageResponseDto.md)
|
- [ServerStorageResponseDto](doc//ServerStorageResponseDto.md)
|
||||||
- [ServerThemeDto](doc//ServerThemeDto.md)
|
- [ServerThemeDto](doc//ServerThemeDto.md)
|
||||||
|
- [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md)
|
||||||
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
||||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
- [SessionResponseDto](doc//SessionResponseDto.md)
|
||||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -213,6 +213,7 @@ part 'model/server_ping_response.dart';
|
|||||||
part 'model/server_stats_response_dto.dart';
|
part 'model/server_stats_response_dto.dart';
|
||||||
part 'model/server_storage_response_dto.dart';
|
part 'model/server_storage_response_dto.dart';
|
||||||
part 'model/server_theme_dto.dart';
|
part 'model/server_theme_dto.dart';
|
||||||
|
part 'model/server_version_history_response_dto.dart';
|
||||||
part 'model/server_version_response_dto.dart';
|
part 'model/server_version_response_dto.dart';
|
||||||
part 'model/session_response_dto.dart';
|
part 'model/session_response_dto.dart';
|
||||||
part 'model/shared_link_create_dto.dart';
|
part 'model/shared_link_create_dto.dart';
|
||||||
|
44
mobile/openapi/lib/api/server_api.dart
generated
44
mobile/openapi/lib/api/server_api.dart
generated
@ -418,6 +418,50 @@ class ServerApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /server/version-history' operation and returns the [Response].
|
||||||
|
Future<Response> getVersionHistoryWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/server/version-history';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ServerVersionHistoryResponseDto>?> getVersionHistory() async {
|
||||||
|
final response = await getVersionHistoryWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<ServerVersionHistoryResponseDto>') as List)
|
||||||
|
.cast<ServerVersionHistoryResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /server/ping' operation and returns the [Response].
|
/// Performs an HTTP 'GET /server/ping' operation and returns the [Response].
|
||||||
Future<Response> pingServerWithHttpInfo() async {
|
Future<Response> pingServerWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -480,6 +480,8 @@ class ApiClient {
|
|||||||
return ServerStorageResponseDto.fromJson(value);
|
return ServerStorageResponseDto.fromJson(value);
|
||||||
case 'ServerThemeDto':
|
case 'ServerThemeDto':
|
||||||
return ServerThemeDto.fromJson(value);
|
return ServerThemeDto.fromJson(value);
|
||||||
|
case 'ServerVersionHistoryResponseDto':
|
||||||
|
return ServerVersionHistoryResponseDto.fromJson(value);
|
||||||
case 'ServerVersionResponseDto':
|
case 'ServerVersionResponseDto':
|
||||||
return ServerVersionResponseDto.fromJson(value);
|
return ServerVersionResponseDto.fromJson(value);
|
||||||
case 'SessionResponseDto':
|
case 'SessionResponseDto':
|
||||||
|
115
mobile/openapi/lib/model/server_version_history_response_dto.dart
generated
Normal file
115
mobile/openapi/lib/model/server_version_history_response_dto.dart
generated
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class ServerVersionHistoryResponseDto {
|
||||||
|
/// Returns a new [ServerVersionHistoryResponseDto] instance.
|
||||||
|
ServerVersionHistoryResponseDto({
|
||||||
|
required this.createdAt,
|
||||||
|
required this.id,
|
||||||
|
required this.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
String version;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is ServerVersionHistoryResponseDto &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.id == id &&
|
||||||
|
other.version == version;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(createdAt.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(version.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ServerVersionHistoryResponseDto[createdAt=$createdAt, id=$id, version=$version]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'version'] = this.version;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [ServerVersionHistoryResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static ServerVersionHistoryResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "ServerVersionHistoryResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return ServerVersionHistoryResponseDto(
|
||||||
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
version: mapValueOfType<String>(json, r'version')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ServerVersionHistoryResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <ServerVersionHistoryResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = ServerVersionHistoryResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, ServerVersionHistoryResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, ServerVersionHistoryResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = ServerVersionHistoryResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of ServerVersionHistoryResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<ServerVersionHistoryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<ServerVersionHistoryResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = ServerVersionHistoryResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'createdAt',
|
||||||
|
'id',
|
||||||
|
'version',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -532,10 +532,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_launcher_icons
|
name: flutter_launcher_icons
|
||||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.1"
|
version: "0.14.1"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
@ -90,7 +90,7 @@ dev_dependencies:
|
|||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
build_runner: ^2.4.8
|
build_runner: ^2.4.8
|
||||||
auto_route_generator: ^9.0.0
|
auto_route_generator: ^9.0.0
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.14.0
|
||||||
flutter_native_splash: ^2.3.9
|
flutter_native_splash: ^2.3.9
|
||||||
isar_generator: ^3.1.0+1
|
isar_generator: ^3.1.0+1
|
||||||
integration_test:
|
integration_test:
|
||||||
|
@ -5088,6 +5088,30 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/server/version-history": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getVersionHistory",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ServerVersionHistoryResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/sessions": {
|
"/sessions": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "deleteAllSessions",
|
"operationId": "deleteAllSessions",
|
||||||
@ -11042,6 +11066,26 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ServerVersionHistoryResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"createdAt",
|
||||||
|
"id",
|
||||||
|
"version"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ServerVersionResponseDto": {
|
"ServerVersionResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"major": {
|
"major": {
|
||||||
|
@ -1000,6 +1000,11 @@ export type ServerVersionResponseDto = {
|
|||||||
minor: number;
|
minor: number;
|
||||||
patch: number;
|
patch: number;
|
||||||
};
|
};
|
||||||
|
export type ServerVersionHistoryResponseDto = {
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
export type SessionResponseDto = {
|
export type SessionResponseDto = {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
@ -2667,6 +2672,14 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function getVersionHistory(opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: ServerVersionHistoryResponseDto[];
|
||||||
|
}>("/server/version-history", {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function deleteAllSessions(opts?: Oazapfts.RequestOpts) {
|
export function deleteAllSessions(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchText("/sessions", {
|
return oazapfts.ok(oazapfts.fetchText("/sessions", {
|
||||||
...opts,
|
...opts,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:20240924@sha256:fff4358d435065a626c64a4c015cbfce6ee714b05fabe39aa0d83d8cff3951f2 AS dev
|
FROM ghcr.io/immich-app/base-server-dev:20241001@sha256:bb10832c2567f5625df68bb790523e85a358031ddcb3d7ac98b669f62ed8de27 AS dev
|
||||||
|
|
||||||
RUN apt-get install --no-install-recommends -yqq tini
|
RUN apt-get install --no-install-recommends -yqq tini
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@ -41,7 +41,7 @@ RUN npm run build
|
|||||||
|
|
||||||
|
|
||||||
# prod build
|
# prod build
|
||||||
FROM ghcr.io/immich-app/base-server-prod:20240924@sha256:af3089fe48d7ff162594bd7edfffa56ba4e7014ad10ad69c4ebfd428e39b06ff
|
FROM ghcr.io/immich-app/base-server-prod:20241001@sha256:a9a0745a486e9cbd73fa06b49168e985f8f2c1be0fca9fb0a8e06916246c7087
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
ServerStatsResponseDto,
|
ServerStatsResponseDto,
|
||||||
ServerStorageResponseDto,
|
ServerStorageResponseDto,
|
||||||
ServerThemeDto,
|
ServerThemeDto,
|
||||||
|
ServerVersionHistoryResponseDto,
|
||||||
ServerVersionResponseDto,
|
ServerVersionResponseDto,
|
||||||
} from 'src/dtos/server.dto';
|
} from 'src/dtos/server.dto';
|
||||||
import { Authenticated } from 'src/middleware/auth.guard';
|
import { Authenticated } from 'src/middleware/auth.guard';
|
||||||
@ -46,6 +47,11 @@ export class ServerController {
|
|||||||
return this.versionService.getVersion();
|
return this.versionService.getVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('version-history')
|
||||||
|
getVersionHistory(): Promise<ServerVersionHistoryResponseDto[]> {
|
||||||
|
return this.versionService.getVersionHistory();
|
||||||
|
}
|
||||||
|
|
||||||
@Get('features')
|
@Get('features')
|
||||||
getServerFeatures(): Promise<ServerFeaturesDto> {
|
getServerFeatures(): Promise<ServerFeaturesDto> {
|
||||||
return this.service.getFeatures();
|
return this.service.getFeatures();
|
||||||
|
@ -5,6 +5,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
@ -36,6 +37,7 @@ let instance: StorageCore | null;
|
|||||||
export class StorageCore {
|
export class StorageCore {
|
||||||
private constructor(
|
private constructor(
|
||||||
private assetRepository: IAssetRepository,
|
private assetRepository: IAssetRepository,
|
||||||
|
private configRepository: IConfigRepository,
|
||||||
private cryptoRepository: ICryptoRepository,
|
private cryptoRepository: ICryptoRepository,
|
||||||
private moveRepository: IMoveRepository,
|
private moveRepository: IMoveRepository,
|
||||||
private personRepository: IPersonRepository,
|
private personRepository: IPersonRepository,
|
||||||
@ -46,6 +48,7 @@ export class StorageCore {
|
|||||||
|
|
||||||
static create(
|
static create(
|
||||||
assetRepository: IAssetRepository,
|
assetRepository: IAssetRepository,
|
||||||
|
configRepository: IConfigRepository,
|
||||||
cryptoRepository: ICryptoRepository,
|
cryptoRepository: ICryptoRepository,
|
||||||
moveRepository: IMoveRepository,
|
moveRepository: IMoveRepository,
|
||||||
personRepository: IPersonRepository,
|
personRepository: IPersonRepository,
|
||||||
@ -56,6 +59,7 @@ export class StorageCore {
|
|||||||
if (!instance) {
|
if (!instance) {
|
||||||
instance = new StorageCore(
|
instance = new StorageCore(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
|
configRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
moveRepository,
|
moveRepository,
|
||||||
personRepository,
|
personRepository,
|
||||||
@ -245,7 +249,11 @@ export class StorageCore {
|
|||||||
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
|
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const repos = { metadataRepo: this.systemMetadataRepository, logger: this.logger };
|
const repos = {
|
||||||
|
configRepo: this.configRepository,
|
||||||
|
metadataRepo: this.systemMetadataRepository,
|
||||||
|
logger: this.logger,
|
||||||
|
};
|
||||||
const config = await getConfig(repos, { withCache: true });
|
const config = await getConfig(repos, { withCache: true });
|
||||||
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
|
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
|
||||||
const { checksum } = assetInfo;
|
const { checksum } = assetInfo;
|
||||||
|
@ -68,6 +68,12 @@ export class ServerVersionResponseDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ServerVersionHistoryResponseDto {
|
||||||
|
id!: string;
|
||||||
|
createdAt!: Date;
|
||||||
|
version!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class UsageByUserDto {
|
export class UsageByUserDto {
|
||||||
@ApiProperty({ type: 'string' })
|
@ApiProperty({ type: 'string' })
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
@ -25,6 +25,7 @@ import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
|||||||
import { TagEntity } from 'src/entities/tag.entity';
|
import { TagEntity } from 'src/entities/tag.entity';
|
||||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
|
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
|
||||||
|
|
||||||
export const entities = [
|
export const entities = [
|
||||||
ActivityEntity,
|
ActivityEntity,
|
||||||
@ -54,4 +55,5 @@ export const entities = [
|
|||||||
UserMetadataEntity,
|
UserMetadataEntity,
|
||||||
SessionEntity,
|
SessionEntity,
|
||||||
LibraryEntity,
|
LibraryEntity,
|
||||||
|
VersionHistoryEntity,
|
||||||
];
|
];
|
||||||
|
13
server/src/entities/version-history.entity.ts
Normal file
13
server/src/entities/version-history.entity.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('version_history')
|
||||||
|
export class VersionHistoryEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
version!: string;
|
||||||
|
}
|
@ -3,10 +3,14 @@ import { VectorExtension } from 'src/interfaces/database.interface';
|
|||||||
export const IConfigRepository = 'IConfigRepository';
|
export const IConfigRepository = 'IConfigRepository';
|
||||||
|
|
||||||
export interface EnvData {
|
export interface EnvData {
|
||||||
|
configFile?: string;
|
||||||
database: {
|
database: {
|
||||||
skipMigrations: boolean;
|
skipMigrations: boolean;
|
||||||
vectorExtension: VectorExtension;
|
vectorExtension: VectorExtension;
|
||||||
};
|
};
|
||||||
|
storage: {
|
||||||
|
ignoreMountCheckErrors: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigRepository {
|
export interface IConfigRepository {
|
||||||
|
@ -17,6 +17,7 @@ export enum DatabaseLock {
|
|||||||
Migrations = 200,
|
Migrations = 200,
|
||||||
SystemFileMounts = 300,
|
SystemFileMounts = 300,
|
||||||
StorageTemplateMigration = 420,
|
StorageTemplateMigration = 420,
|
||||||
|
VersionHistory = 500,
|
||||||
CLIPDimSize = 512,
|
CLIPDimSize = 512,
|
||||||
LibraryWatch = 1337,
|
LibraryWatch = 1337,
|
||||||
GetSystemConfig = 69,
|
GetSystemConfig = 69,
|
||||||
|
9
server/src/interfaces/version-history.interface.ts
Normal file
9
server/src/interfaces/version-history.interface.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
|
||||||
|
|
||||||
|
export const IVersionHistoryRepository = 'IVersionHistoryRepository';
|
||||||
|
|
||||||
|
export interface IVersionHistoryRepository {
|
||||||
|
create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity>;
|
||||||
|
getAll(): Promise<VersionHistoryEntity[]>;
|
||||||
|
getLatest(): Promise<VersionHistoryEntity | null>;
|
||||||
|
}
|
14
server/src/migrations/1727797340951-AddVersionHistory.ts
Normal file
14
server/src/migrations/1727797340951-AddVersionHistory.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddVersionHistory1727797340951 implements MigrationInterface {
|
||||||
|
name = 'AddVersionHistory1727797340951'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "version" character varying NOT NULL, CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id"))`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP TABLE "version_history"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,10 +6,14 @@ import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
|
|||||||
export class ConfigRepository implements IConfigRepository {
|
export class ConfigRepository implements IConfigRepository {
|
||||||
getEnv(): EnvData {
|
getEnv(): EnvData {
|
||||||
return {
|
return {
|
||||||
|
configFile: process.env.IMMICH_CONFIG_FILE,
|
||||||
database: {
|
database: {
|
||||||
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
|
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
|
||||||
vectorExtension: getVectorExtension(),
|
vectorExtension: getVectorExtension(),
|
||||||
},
|
},
|
||||||
|
storage: {
|
||||||
|
ignoreMountCheckErrors: process.env.IMMICH_IGNORE_MOUNT_CHECK_ERRORS === 'true',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
|||||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||||
import { ITrashRepository } from 'src/interfaces/trash.interface';
|
import { ITrashRepository } from 'src/interfaces/trash.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||||
import { IViewRepository } from 'src/interfaces/view.interface';
|
import { IViewRepository } from 'src/interfaces/view.interface';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
@ -67,6 +68,7 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos
|
|||||||
import { TagRepository } from 'src/repositories/tag.repository';
|
import { TagRepository } from 'src/repositories/tag.repository';
|
||||||
import { TrashRepository } from 'src/repositories/trash.repository';
|
import { TrashRepository } from 'src/repositories/trash.repository';
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
|
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||||
import { ViewRepository } from 'src/repositories/view-repository';
|
import { ViewRepository } from 'src/repositories/view-repository';
|
||||||
|
|
||||||
export const repositories = [
|
export const repositories = [
|
||||||
@ -104,5 +106,6 @@ export const repositories = [
|
|||||||
{ provide: ITagRepository, useClass: TagRepository },
|
{ provide: ITagRepository, useClass: TagRepository },
|
||||||
{ provide: ITrashRepository, useClass: TrashRepository },
|
{ provide: ITrashRepository, useClass: TrashRepository },
|
||||||
{ provide: IUserRepository, useClass: UserRepository },
|
{ provide: IUserRepository, useClass: UserRepository },
|
||||||
|
{ provide: IVersionHistoryRepository, useClass: VersionHistoryRepository },
|
||||||
{ provide: IViewRepository, useClass: ViewRepository },
|
{ provide: IViewRepository, useClass: ViewRepository },
|
||||||
];
|
];
|
||||||
|
25
server/src/repositories/version-history.repository.ts
Normal file
25
server/src/repositories/version-history.repository.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
|
||||||
|
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||||
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Instrumentation()
|
||||||
|
@Injectable()
|
||||||
|
export class VersionHistoryRepository implements IVersionHistoryRepository {
|
||||||
|
constructor(@InjectRepository(VersionHistoryEntity) private repository: Repository<VersionHistoryEntity>) {}
|
||||||
|
|
||||||
|
async getAll(): Promise<VersionHistoryEntity[]> {
|
||||||
|
return this.repository.find({ order: { createdAt: 'DESC' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLatest(): Promise<VersionHistoryEntity | null> {
|
||||||
|
const results = await this.repository.find({ order: { createdAt: 'DESC' }, take: 1 });
|
||||||
|
return results[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity> {
|
||||||
|
return this.repository.save(version);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetStatus, AssetType } from 'src/enum';
|
import { AssetStatus, AssetType } from 'src/enum';
|
||||||
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -19,6 +20,7 @@ import { partnerStub } from 'test/fixtures/partner.stub';
|
|||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -45,6 +47,7 @@ describe(AssetService.name, () => {
|
|||||||
let sut: AssetService;
|
let sut: AssetService;
|
||||||
let accessMock: IAccessRepositoryMock;
|
let accessMock: IAccessRepositoryMock;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let userMock: Mocked<IUserRepository>;
|
let userMock: Mocked<IUserRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let eventMock: Mocked<IEventRepository>;
|
||||||
@ -66,6 +69,7 @@ describe(AssetService.name, () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accessMock = newAccessRepositoryMock();
|
accessMock = newAccessRepositoryMock();
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
eventMock = newEventRepositoryMock();
|
eventMock = newEventRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
@ -77,6 +81,7 @@ describe(AssetService.name, () => {
|
|||||||
sut = new AssetService(
|
sut = new AssetService(
|
||||||
accessMock,
|
accessMock,
|
||||||
assetMock,
|
assetMock,
|
||||||
|
configMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
systemMock,
|
systemMock,
|
||||||
userMock,
|
userMock,
|
||||||
|
@ -22,6 +22,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { AssetStatus, Permission } from 'src/enum';
|
import { AssetStatus, Permission } from 'src/enum';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IAssetDeleteJob,
|
IAssetDeleteJob,
|
||||||
@ -46,6 +47,7 @@ export class AssetService extends BaseService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@ -54,7 +56,7 @@ export class AssetService extends BaseService {
|
|||||||
@Inject(IStackRepository) private stackRepository: IStackRepository,
|
@Inject(IStackRepository) private stackRepository: IStackRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(AssetService.name);
|
this.logger.setContext(AssetService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { AuthType } from 'src/enum';
|
import { AuthType } from 'src/enum';
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -20,6 +21,7 @@ import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
|||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -57,6 +59,7 @@ const oauthUserWithDefaultQuota = {
|
|||||||
|
|
||||||
describe('AuthService', () => {
|
describe('AuthService', () => {
|
||||||
let sut: AuthService;
|
let sut: AuthService;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let cryptoMock: Mocked<ICryptoRepository>;
|
let cryptoMock: Mocked<ICryptoRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let userMock: Mocked<IUserRepository>;
|
let userMock: Mocked<IUserRepository>;
|
||||||
@ -89,6 +92,7 @@ describe('AuthService', () => {
|
|||||||
}),
|
}),
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
eventMock = newEventRepositoryMock();
|
eventMock = newEventRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
@ -98,7 +102,17 @@ describe('AuthService', () => {
|
|||||||
shareMock = newSharedLinkRepositoryMock();
|
shareMock = newSharedLinkRepositoryMock();
|
||||||
keyMock = newKeyRepositoryMock();
|
keyMock = newKeyRepositoryMock();
|
||||||
|
|
||||||
sut = new AuthService(cryptoMock, eventMock, systemMock, loggerMock, userMock, sessionMock, shareMock, keyMock);
|
sut = new AuthService(
|
||||||
|
configMock,
|
||||||
|
cryptoMock,
|
||||||
|
eventMock,
|
||||||
|
systemMock,
|
||||||
|
loggerMock,
|
||||||
|
userMock,
|
||||||
|
sessionMock,
|
||||||
|
shareMock,
|
||||||
|
keyMock,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
|
@ -31,6 +31,7 @@ import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { AuthType, Permission } from 'src/enum';
|
import { AuthType, Permission } from 'src/enum';
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -72,6 +73,7 @@ export type ValidateRequest = {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService extends BaseService {
|
export class AuthService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@ -81,7 +83,7 @@ export class AuthService extends BaseService {
|
|||||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
||||||
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(AuthService.name);
|
this.logger.setContext(AuthService.name);
|
||||||
|
|
||||||
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
||||||
|
@ -1,32 +1,30 @@
|
|||||||
import { Inject } from '@nestjs/common';
|
import { Inject } from '@nestjs/common';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { getConfig, updateConfig } from 'src/utils/config';
|
import { getConfig, updateConfig } from 'src/utils/config';
|
||||||
|
|
||||||
export class BaseService {
|
export class BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) protected configRepository: IConfigRepository,
|
||||||
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
|
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private get repos() {
|
||||||
|
return {
|
||||||
|
configRepo: this.configRepository,
|
||||||
|
metadataRepo: this.systemMetadataRepository,
|
||||||
|
logger: this.logger,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getConfig(options: { withCache: boolean }) {
|
getConfig(options: { withCache: boolean }) {
|
||||||
return getConfig(
|
return getConfig(this.repos, options);
|
||||||
{
|
|
||||||
metadataRepo: this.systemMetadataRepository,
|
|
||||||
logger: this.logger,
|
|
||||||
},
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConfig(newConfig: SystemConfig) {
|
updateConfig(newConfig: SystemConfig) {
|
||||||
return updateConfig(
|
return updateConfig(this.repos, newConfig);
|
||||||
{
|
|
||||||
metadataRepo: this.systemMetadataRepository,
|
|
||||||
logger: this.logger,
|
|
||||||
},
|
|
||||||
newConfig,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { CliService } from 'src/services/cli.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||||
@ -13,18 +15,20 @@ import { Mocked, describe, it } from 'vitest';
|
|||||||
describe(CliService.name, () => {
|
describe(CliService.name, () => {
|
||||||
let sut: CliService;
|
let sut: CliService;
|
||||||
|
|
||||||
let userMock: Mocked<IUserRepository>;
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let cryptoMock: Mocked<ICryptoRepository>;
|
let cryptoMock: Mocked<ICryptoRepository>;
|
||||||
|
let userMock: Mocked<IUserRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new CliService(cryptoMock, systemMock, userMock, loggerMock);
|
sut = new CliService(configMock, cryptoMock, systemMock, userMock, loggerMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resetAdminPassword', () => {
|
describe('resetAdminPassword', () => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
@ -10,12 +11,13 @@ import { BaseService } from 'src/services/base.service';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class CliService extends BaseService {
|
export class CliService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(CliService.name);
|
this.logger.setContext(CliService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from 'src/interfaces/database.interface';
|
} from 'src/interfaces/database.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { DatabaseService } from 'src/services/database.service';
|
import { DatabaseService } from 'src/services/database.service';
|
||||||
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
@ -60,7 +60,9 @@ describe(DatabaseService.name, () => {
|
|||||||
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
|
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
|
||||||
])('should work with $extensionName', ({ extension, extensionName }) => {
|
])('should work with $extensionName', ({ extension, extensionName }) => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
configMock.getEnv.mockReturnValue({ database: { skipMigrations: false, vectorExtension: extension } });
|
configMock.getEnv.mockReturnValue(
|
||||||
|
mockEnvData({ database: { skipMigrations: false, vectorExtension: extension } }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should start up successfully with ${extension}`, async () => {
|
it(`should start up successfully with ${extension}`, async () => {
|
||||||
@ -244,12 +246,14 @@ describe(DatabaseService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
||||||
configMock.getEnv.mockReturnValue({
|
configMock.getEnv.mockReturnValue(
|
||||||
database: {
|
mockEnvData({
|
||||||
skipMigrations: true,
|
database: {
|
||||||
vectorExtension: DatabaseExtension.VECTORS,
|
skipMigrations: true,
|
||||||
},
|
vectorExtension: DatabaseExtension.VECTORS,
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
@ -257,12 +261,14 @@ describe(DatabaseService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should throw error if pgvector extension could not be created`, async () => {
|
it(`should throw error if pgvector extension could not be created`, async () => {
|
||||||
configMock.getEnv.mockReturnValue({
|
configMock.getEnv.mockReturnValue(
|
||||||
database: {
|
mockEnvData({
|
||||||
skipMigrations: true,
|
database: {
|
||||||
vectorExtension: DatabaseExtension.VECTOR,
|
skipMigrations: true,
|
||||||
},
|
vectorExtension: DatabaseExtension.VECTOR,
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||||
installedVersion: null,
|
installedVersion: null,
|
||||||
availableVersion: minVersionInRange,
|
availableVersion: minVersionInRange,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -8,6 +9,7 @@ import { DuplicateService } from 'src/services/duplicate.service';
|
|||||||
import { SearchService } from 'src/services/search.service';
|
import { SearchService } from 'src/services/search.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -20,6 +22,7 @@ vitest.useFakeTimers();
|
|||||||
describe(SearchService.name, () => {
|
describe(SearchService.name, () => {
|
||||||
let sut: DuplicateService;
|
let sut: DuplicateService;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let searchMock: Mocked<ISearchRepository>;
|
let searchMock: Mocked<ISearchRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
@ -28,13 +31,14 @@ describe(SearchService.name, () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
searchMock = newSearchRepositoryMock();
|
searchMock = newSearchRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
|
|
||||||
sut = new DuplicateService(systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock);
|
sut = new DuplicateService(configMock, systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -4,6 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
|||||||
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
|
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import {
|
import {
|
||||||
IBaseJob,
|
IBaseJob,
|
||||||
@ -24,6 +25,7 @@ import { usePagination } from 'src/utils/pagination';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class DuplicateService extends BaseService {
|
export class DuplicateService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@ -31,7 +33,7 @@ export class DuplicateService extends BaseService {
|
|||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(DuplicateService.name);
|
this.logger.setContext(DuplicateService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { defaults } from 'src/config';
|
import { defaults } from 'src/config';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
@ -18,6 +19,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
|||||||
import { JobService } from 'src/services/job.service';
|
import { JobService } from 'src/services/job.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -37,6 +39,7 @@ const makeMockHandlers = (status: JobStatus) => {
|
|||||||
describe(JobService.name, () => {
|
describe(JobService.name, () => {
|
||||||
let sut: JobService;
|
let sut: JobService;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let personMock: Mocked<IPersonRepository>;
|
let personMock: Mocked<IPersonRepository>;
|
||||||
@ -46,13 +49,14 @@ describe(JobService.name, () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
eventMock = newEventRepositoryMock();
|
eventMock = newEventRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
personMock = newPersonRepositoryMock();
|
personMock = newPersonRepositoryMock();
|
||||||
metricMock = newMetricRepositoryMock();
|
metricMock = newMetricRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
sut = new JobService(assetMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock);
|
sut = new JobService(assetMock, configMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -5,6 +5,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
|||||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||||
import { AssetType, ManualJobName } from 'src/enum';
|
import { AssetType, ManualJobName } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
ConcurrentQueueName,
|
ConcurrentQueueName,
|
||||||
@ -49,6 +50,7 @@ export class JobService extends BaseService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@ -56,7 +58,7 @@ export class JobService extends BaseService {
|
|||||||
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
|
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(JobService.name);
|
this.logger.setContext(JobService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { mapLibrary } from 'src/dtos/library.dto';
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import {
|
import {
|
||||||
@ -26,6 +27,7 @@ import { libraryStub } from 'test/fixtures/library.stub';
|
|||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
@ -43,15 +45,17 @@ describe(LibraryService.name, () => {
|
|||||||
let sut: LibraryService;
|
let sut: LibraryService;
|
||||||
|
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let cryptoMock: Mocked<ICryptoRepository>;
|
let cryptoMock: Mocked<ICryptoRepository>;
|
||||||
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let libraryMock: Mocked<ILibraryRepository>;
|
let libraryMock: Mocked<ILibraryRepository>;
|
||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
libraryMock = newLibraryRepositoryMock();
|
libraryMock = newLibraryRepositoryMock();
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
@ -63,12 +67,13 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
sut = new LibraryService(
|
sut = new LibraryService(
|
||||||
assetMock,
|
assetMock,
|
||||||
systemMock,
|
configMock,
|
||||||
cryptoMock,
|
cryptoMock,
|
||||||
|
databaseMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
libraryMock,
|
libraryMock,
|
||||||
storageMock,
|
storageMock,
|
||||||
databaseMock,
|
systemMock,
|
||||||
loggerMock,
|
loggerMock,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { LibraryEntity } from 'src/entities/library.entity';
|
import { LibraryEntity } from 'src/entities/library.entity';
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.interface';
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
@ -48,15 +49,16 @@ export class LibraryService extends BaseService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(ILibraryRepository) private repository: ILibraryRepository,
|
@Inject(ILibraryRepository) private repository: ILibraryRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(LibraryService.name);
|
this.logger.setContext(LibraryService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +143,13 @@ export class LibraryService extends BaseService {
|
|||||||
const handler = async () => {
|
const handler = async () => {
|
||||||
this.logger.debug(`File add event received for ${path} in library ${library.id}}`);
|
this.logger.debug(`File add event received for ${path} in library ${library.id}}`);
|
||||||
if (matcher(path)) {
|
if (matcher(path)) {
|
||||||
await this.syncFiles(library, [path]);
|
const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
|
||||||
|
if (asset) {
|
||||||
|
await this.syncAssets(library, [asset.id]);
|
||||||
|
}
|
||||||
|
if (matcher(path)) {
|
||||||
|
await this.syncFiles(library, [path]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return handlePromiseError(handler(), this.logger);
|
return handlePromiseError(handler(), this.logger);
|
||||||
@ -604,7 +612,7 @@ export class LibraryService extends BaseService {
|
|||||||
this.logger.log(`Scanning library ${library.id} for removed assets`);
|
this.logger.log(`Scanning library ${library.id} for removed assets`);
|
||||||
|
|
||||||
const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
|
const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
|
||||||
this.assetRepository.getAll(pagination, { libraryId: job.id }),
|
this.assetRepository.getAll(pagination, { libraryId: job.id, withDeleted: true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
let assetCount = 0;
|
let assetCount = 0;
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
VideoCodec,
|
VideoCodec,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -26,6 +27,7 @@ import { faceStub } from 'test/fixtures/face.stub';
|
|||||||
import { probeStub } from 'test/fixtures/media.stub';
|
import { probeStub } from 'test/fixtures/media.stub';
|
||||||
import { personStub } from 'test/fixtures/person.stub';
|
import { personStub } from 'test/fixtures/person.stub';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -39,6 +41,7 @@ import { Mocked } from 'vitest';
|
|||||||
describe(MediaService.name, () => {
|
describe(MediaService.name, () => {
|
||||||
let sut: MediaService;
|
let sut: MediaService;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let mediaMock: Mocked<IMediaRepository>;
|
let mediaMock: Mocked<IMediaRepository>;
|
||||||
let moveMock: Mocked<IMoveRepository>;
|
let moveMock: Mocked<IMoveRepository>;
|
||||||
@ -50,6 +53,7 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
mediaMock = newMediaRepositoryMock();
|
mediaMock = newMediaRepositoryMock();
|
||||||
@ -61,6 +65,7 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
sut = new MediaService(
|
sut = new MediaService(
|
||||||
assetMock,
|
assetMock,
|
||||||
|
configMock,
|
||||||
personMock,
|
personMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
mediaMock,
|
mediaMock,
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
VideoContainer,
|
VideoContainer,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { IAssetRepository, UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import {
|
import {
|
||||||
IBaseJob,
|
IBaseJob,
|
||||||
@ -55,6 +56,7 @@ export class MediaService extends BaseService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||||
@ -64,10 +66,11 @@ export class MediaService extends BaseService {
|
|||||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(MediaService.name);
|
this.logger.setContext(MediaService.name);
|
||||||
this.storageCore = StorageCore.create(
|
this.storageCore = StorageCore.create(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
|
configRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
moveRepository,
|
moveRepository,
|
||||||
personRepository,
|
personRepository,
|
||||||
|
@ -6,6 +6,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
|
|||||||
import { AssetType, SourceType } from 'src/enum';
|
import { AssetType, SourceType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
@ -29,6 +30,7 @@ import { personStub } from 'test/fixtures/person.stub';
|
|||||||
import { tagStub } from 'test/fixtures/tag.stub';
|
import { tagStub } from 'test/fixtures/tag.stub';
|
||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
@ -46,10 +48,14 @@ import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
|||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
describe(MetadataService.name, () => {
|
describe(MetadataService.name, () => {
|
||||||
|
let sut: MetadataService;
|
||||||
|
|
||||||
let albumMock: Mocked<IAlbumRepository>;
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let cryptoRepository: Mocked<ICryptoRepository>;
|
let cryptoRepository: Mocked<ICryptoRepository>;
|
||||||
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let mapMock: Mocked<IMapRepository>;
|
let mapMock: Mocked<IMapRepository>;
|
||||||
let metadataMock: Mocked<IMetadataRepository>;
|
let metadataMock: Mocked<IMetadataRepository>;
|
||||||
@ -57,16 +63,15 @@ describe(MetadataService.name, () => {
|
|||||||
let mediaMock: Mocked<IMediaRepository>;
|
let mediaMock: Mocked<IMediaRepository>;
|
||||||
let personMock: Mocked<IPersonRepository>;
|
let personMock: Mocked<IPersonRepository>;
|
||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let tagMock: Mocked<ITagRepository>;
|
||||||
let userMock: Mocked<IUserRepository>;
|
let userMock: Mocked<IUserRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
let tagMock: Mocked<ITagRepository>;
|
|
||||||
let sut: MetadataService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
albumMock = newAlbumRepositoryMock();
|
albumMock = newAlbumRepositoryMock();
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
cryptoRepository = newCryptoRepositoryMock();
|
cryptoRepository = newCryptoRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
mapMock = newMapRepositoryMock();
|
mapMock = newMapRepositoryMock();
|
||||||
@ -85,9 +90,10 @@ describe(MetadataService.name, () => {
|
|||||||
sut = new MetadataService(
|
sut = new MetadataService(
|
||||||
albumMock,
|
albumMock,
|
||||||
assetMock,
|
assetMock,
|
||||||
eventMock,
|
configMock,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
databaseMock,
|
databaseMock,
|
||||||
|
eventMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
mapMock,
|
mapMock,
|
||||||
mediaMock,
|
mediaMock,
|
||||||
|
@ -15,6 +15,7 @@ import { PersonEntity } from 'src/entities/person.entity';
|
|||||||
import { AssetType, SourceType } from 'src/enum';
|
import { AssetType, SourceType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
@ -103,9 +104,10 @@ export class MetadataService extends BaseService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
@Inject(IMapRepository) private mapRepository: IMapRepository,
|
||||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||||
@ -118,10 +120,11 @@ export class MetadataService extends BaseService {
|
|||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(MetadataService.name);
|
this.logger.setContext(MetadataService.name);
|
||||||
this.storageCore = StorageCore.create(
|
this.storageCore = StorageCore.create(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
|
configRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
moveRepository,
|
moveRepository,
|
||||||
personRepository,
|
personRepository,
|
||||||
|
@ -6,6 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
|||||||
import { AssetFileType, UserMetadataKey } from 'src/enum';
|
import { AssetFileType, UserMetadataKey } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -18,6 +19,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
|||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -66,6 +68,7 @@ const configs = {
|
|||||||
describe(NotificationService.name, () => {
|
describe(NotificationService.name, () => {
|
||||||
let albumMock: Mocked<IAlbumRepository>;
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
@ -77,6 +80,7 @@ describe(NotificationService.name, () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
albumMock = newAlbumRepositoryMock();
|
albumMock = newAlbumRepositoryMock();
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
eventMock = newEventRepositoryMock();
|
eventMock = newEventRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
@ -85,6 +89,7 @@ describe(NotificationService.name, () => {
|
|||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
|
|
||||||
sut = new NotificationService(
|
sut = new NotificationService(
|
||||||
|
configMock,
|
||||||
eventMock,
|
eventMock,
|
||||||
systemMock,
|
systemMock,
|
||||||
notificationMock,
|
notificationMock,
|
||||||
|
@ -5,6 +5,7 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
|||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IEmailJob,
|
IEmailJob,
|
||||||
@ -28,6 +29,7 @@ import { getPreferences } from 'src/utils/preferences';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService extends BaseService {
|
export class NotificationService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
||||||
@ -37,7 +39,7 @@ export class NotificationService extends BaseService {
|
|||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(NotificationService.name);
|
this.logger.setContext(NotificationService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
|
|||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum';
|
import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -23,6 +24,7 @@ import { personStub } from 'test/fixtures/person.stub';
|
|||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -67,6 +69,7 @@ const detectFaceMock: DetectedFaces = {
|
|||||||
describe(PersonService.name, () => {
|
describe(PersonService.name, () => {
|
||||||
let accessMock: IAccessRepositoryMock;
|
let accessMock: IAccessRepositoryMock;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let machineLearningMock: Mocked<IMachineLearningRepository>;
|
let machineLearningMock: Mocked<IMachineLearningRepository>;
|
||||||
@ -82,6 +85,7 @@ describe(PersonService.name, () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accessMock = newAccessRepositoryMock();
|
accessMock = newAccessRepositoryMock();
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
machineLearningMock = newMachineLearningRepositoryMock();
|
machineLearningMock = newMachineLearningRepositoryMock();
|
||||||
@ -92,9 +96,11 @@ describe(PersonService.name, () => {
|
|||||||
searchMock = newSearchRepositoryMock();
|
searchMock = newSearchRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new PersonService(
|
sut = new PersonService(
|
||||||
accessMock,
|
accessMock,
|
||||||
assetMock,
|
assetMock,
|
||||||
|
configMock,
|
||||||
machineLearningMock,
|
machineLearningMock,
|
||||||
moveMock,
|
moveMock,
|
||||||
mediaMock,
|
mediaMock,
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import {
|
import {
|
||||||
IBaseJob,
|
IBaseJob,
|
||||||
@ -70,6 +71,7 @@ export class PersonService extends BaseService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
|
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
|
||||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||||
@ -81,10 +83,11 @@ export class PersonService extends BaseService {
|
|||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(PersonService.name);
|
this.logger.setContext(PersonService.name);
|
||||||
this.storageCore = StorageCore.create(
|
this.storageCore = StorageCore.create(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
|
configRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
moveRepository,
|
moveRepository,
|
||||||
repository,
|
repository,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { SearchSuggestionType } from 'src/dtos/search.dto';
|
import { SearchSuggestionType } from 'src/dtos/search.dto';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
@ -12,6 +13,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
|||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { personStub } from 'test/fixtures/person.stub';
|
import { personStub } from 'test/fixtures/person.stub';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
|
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
|
||||||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||||
@ -25,6 +27,7 @@ vitest.useFakeTimers();
|
|||||||
describe(SearchService.name, () => {
|
describe(SearchService.name, () => {
|
||||||
let sut: SearchService;
|
let sut: SearchService;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let machineMock: Mocked<IMachineLearningRepository>;
|
let machineMock: Mocked<IMachineLearningRepository>;
|
||||||
let personMock: Mocked<IPersonRepository>;
|
let personMock: Mocked<IPersonRepository>;
|
||||||
@ -34,6 +37,7 @@ describe(SearchService.name, () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
machineMock = newMachineLearningRepositoryMock();
|
machineMock = newMachineLearningRepositoryMock();
|
||||||
personMock = newPersonRepositoryMock();
|
personMock = newPersonRepositoryMock();
|
||||||
@ -41,7 +45,16 @@ describe(SearchService.name, () => {
|
|||||||
partnerMock = newPartnerRepositoryMock();
|
partnerMock = newPartnerRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new SearchService(systemMock, machineMock, personMock, searchMock, assetMock, partnerMock, loggerMock);
|
sut = new SearchService(
|
||||||
|
configMock,
|
||||||
|
systemMock,
|
||||||
|
machineMock,
|
||||||
|
personMock,
|
||||||
|
searchMock,
|
||||||
|
assetMock,
|
||||||
|
partnerMock,
|
||||||
|
loggerMock,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetOrder } from 'src/enum';
|
import { AssetOrder } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
@ -30,6 +31,7 @@ import { isSmartSearchEnabled } from 'src/utils/misc';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService extends BaseService {
|
export class SearchService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||||
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||||
@ -38,7 +40,7 @@ export class SearchService extends BaseService {
|
|||||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(SearchService.name);
|
this.logger.setContext(SearchService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
@ -6,6 +7,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { ServerService } from 'src/services/server.service';
|
import { ServerService } from 'src/services/server.service';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
||||||
@ -16,6 +18,7 @@ import { Mocked } from 'vitest';
|
|||||||
|
|
||||||
describe(ServerService.name, () => {
|
describe(ServerService.name, () => {
|
||||||
let sut: ServerService;
|
let sut: ServerService;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let userMock: Mocked<IUserRepository>;
|
let userMock: Mocked<IUserRepository>;
|
||||||
let serverInfoMock: Mocked<IServerInfoRepository>;
|
let serverInfoMock: Mocked<IServerInfoRepository>;
|
||||||
@ -24,6 +27,7 @@ describe(ServerService.name, () => {
|
|||||||
let cryptoMock: Mocked<ICryptoRepository>;
|
let cryptoMock: Mocked<ICryptoRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
serverInfoMock = newServerInfoRepositoryMock();
|
serverInfoMock = newServerInfoRepositoryMock();
|
||||||
@ -31,7 +35,7 @@ describe(ServerService.name, () => {
|
|||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
|
||||||
sut = new ServerService(userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
|
sut = new ServerService(configMock, userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
UsageByUserDto,
|
UsageByUserDto,
|
||||||
} from 'src/dtos/server.dto';
|
} from 'src/dtos/server.dto';
|
||||||
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
@ -23,13 +24,13 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
|||||||
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
|
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { asHumanReadable } from 'src/utils/bytes';
|
import { asHumanReadable } from 'src/utils/bytes';
|
||||||
import { isUsingConfigFile } from 'src/utils/config';
|
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerService extends BaseService {
|
export class ServerService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@ -37,7 +38,7 @@ export class ServerService extends BaseService {
|
|||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(ServerService.name);
|
this.logger.setContext(ServerService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +92,7 @@ export class ServerService extends BaseService {
|
|||||||
async getFeatures(): Promise<ServerFeaturesDto> {
|
async getFeatures(): Promise<ServerFeaturesDto> {
|
||||||
const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
||||||
await this.getConfig({ withCache: false });
|
await this.getConfig({ withCache: false });
|
||||||
|
const { configFile } = this.configRepository.getEnv();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
smartSearch: isSmartSearchEnabled(machineLearning),
|
smartSearch: isSmartSearchEnabled(machineLearning),
|
||||||
@ -105,7 +107,7 @@ export class ServerService extends BaseService {
|
|||||||
oauth: oauth.enabled,
|
oauth: oauth.enabled,
|
||||||
oauthAutoLaunch: oauth.autoLaunch,
|
oauthAutoLaunch: oauth.autoLaunch,
|
||||||
passwordLogin: passwordLogin.enabled,
|
passwordLogin: passwordLogin.enabled,
|
||||||
configFile: isUsingConfigFile(),
|
configFile: !!configFile,
|
||||||
email: notifications.smtp.enabled,
|
email: notifications.smtp.enabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import _ from 'lodash';
|
|||||||
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
|
||||||
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { SharedLinkType } from 'src/enum';
|
import { SharedLinkType } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
@ -13,6 +14,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
|||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
||||||
@ -22,6 +24,7 @@ import { Mocked } from 'vitest';
|
|||||||
describe(SharedLinkService.name, () => {
|
describe(SharedLinkService.name, () => {
|
||||||
let sut: SharedLinkService;
|
let sut: SharedLinkService;
|
||||||
let accessMock: IAccessRepositoryMock;
|
let accessMock: IAccessRepositoryMock;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let cryptoMock: Mocked<ICryptoRepository>;
|
let cryptoMock: Mocked<ICryptoRepository>;
|
||||||
let shareMock: Mocked<ISharedLinkRepository>;
|
let shareMock: Mocked<ISharedLinkRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
@ -29,12 +32,13 @@ describe(SharedLinkService.name, () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accessMock = newAccessRepositoryMock();
|
accessMock = newAccessRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
shareMock = newSharedLinkRepositoryMock();
|
shareMock = newSharedLinkRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
logMock = newLoggerRepositoryMock();
|
logMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new SharedLinkService(accessMock, cryptoMock, logMock, shareMock, systemMock);
|
sut = new SharedLinkService(accessMock, configMock, cryptoMock, logMock, shareMock, systemMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
|
@ -15,6 +15,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
import { Permission, SharedLinkType } from 'src/enum';
|
import { Permission, SharedLinkType } from 'src/enum';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
@ -27,12 +28,13 @@ import { OpenGraphTags } from 'src/utils/misc';
|
|||||||
export class SharedLinkService extends BaseService {
|
export class SharedLinkService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) private access: IAccessRepository,
|
@Inject(IAccessRepository) private access: IAccessRepository,
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(SharedLinkService.name);
|
this.logger.setContext(SharedLinkService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -11,6 +12,7 @@ import { getCLIPModelInfo } from 'src/utils/misc';
|
|||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -22,6 +24,7 @@ import { Mocked } from 'vitest';
|
|||||||
describe(SmartInfoService.name, () => {
|
describe(SmartInfoService.name, () => {
|
||||||
let sut: SmartInfoService;
|
let sut: SmartInfoService;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let searchMock: Mocked<ISearchRepository>;
|
let searchMock: Mocked<ISearchRepository>;
|
||||||
@ -31,13 +34,24 @@ describe(SmartInfoService.name, () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
searchMock = newSearchRepositoryMock();
|
searchMock = newSearchRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
machineMock = newMachineLearningRepositoryMock();
|
machineMock = newMachineLearningRepositoryMock();
|
||||||
databaseMock = newDatabaseRepositoryMock();
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, systemMock, loggerMock);
|
|
||||||
|
sut = new SmartInfoService(
|
||||||
|
assetMock,
|
||||||
|
configMock,
|
||||||
|
databaseMock,
|
||||||
|
jobMock,
|
||||||
|
machineMock,
|
||||||
|
searchMock,
|
||||||
|
systemMock,
|
||||||
|
loggerMock,
|
||||||
|
);
|
||||||
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.interface';
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
@ -26,6 +27,7 @@ import { usePagination } from 'src/utils/pagination';
|
|||||||
export class SmartInfoService extends BaseService {
|
export class SmartInfoService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||||
@ -33,7 +35,7 @@ export class SmartInfoService extends BaseService {
|
|||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(SmartInfoService.name);
|
this.logger.setContext(SmartInfoService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { AssetPathType } from 'src/enum';
|
import { AssetPathType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { JobStatus } from 'src/interfaces/job.interface';
|
import { JobStatus } from 'src/interfaces/job.interface';
|
||||||
@ -19,6 +20,7 @@ import { assetStub } from 'test/fixtures/asset.stub';
|
|||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -33,6 +35,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
let sut: StorageTemplateService;
|
let sut: StorageTemplateService;
|
||||||
let albumMock: Mocked<IAlbumRepository>;
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let cryptoMock: Mocked<ICryptoRepository>;
|
let cryptoMock: Mocked<ICryptoRepository>;
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let moveMock: Mocked<IMoveRepository>;
|
let moveMock: Mocked<IMoveRepository>;
|
||||||
@ -49,6 +52,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
assetMock = newAssetRepositoryMock();
|
assetMock = newAssetRepositoryMock();
|
||||||
albumMock = newAlbumRepositoryMock();
|
albumMock = newAlbumRepositoryMock();
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
databaseMock = newDatabaseRepositoryMock();
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
moveMock = newMoveRepositoryMock();
|
moveMock = newMoveRepositoryMock();
|
||||||
@ -63,6 +67,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
sut = new StorageTemplateService(
|
sut = new StorageTemplateService(
|
||||||
albumMock,
|
albumMock,
|
||||||
assetMock,
|
assetMock,
|
||||||
|
configMock,
|
||||||
systemMock,
|
systemMock,
|
||||||
moveMock,
|
moveMock,
|
||||||
personMock,
|
personMock,
|
||||||
|
@ -18,6 +18,7 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.interface';
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
@ -63,6 +64,7 @@ export class StorageTemplateService extends BaseService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||||
@ -72,10 +74,11 @@ export class StorageTemplateService extends BaseService {
|
|||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(StorageTemplateService.name);
|
this.logger.setContext(StorageTemplateService.name);
|
||||||
this.storageCore = StorageCore.create(
|
this.storageCore = StorageCore.create(
|
||||||
assetRepository,
|
assetRepository,
|
||||||
|
configRepository,
|
||||||
cryptoRepository,
|
cryptoRepository,
|
||||||
moveRepository,
|
moveRepository,
|
||||||
personRepository,
|
personRepository,
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { StorageService } from 'src/services/storage.service';
|
import { StorageService } from 'src/services/storage.service';
|
||||||
|
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||||
@ -12,18 +14,20 @@ import { Mocked } from 'vitest';
|
|||||||
|
|
||||||
describe(StorageService.name, () => {
|
describe(StorageService.name, () => {
|
||||||
let sut: StorageService;
|
let sut: StorageService;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
databaseMock = newDatabaseRepositoryMock();
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
|
|
||||||
sut = new StorageService(databaseMock, storageMock, loggerMock, systemMock);
|
sut = new StorageService(configMock, databaseMock, storageMock, loggerMock, systemMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -52,7 +56,7 @@ describe(StorageService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ mountFiles: true });
|
systemMock.get.mockResolvedValue({ mountFiles: true });
|
||||||
storageMock.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
storageMock.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||||
|
|
||||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount');
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to read');
|
||||||
|
|
||||||
expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled();
|
expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled();
|
||||||
expect(systemMock.set).not.toHaveBeenCalled();
|
expect(systemMock.set).not.toHaveBeenCalled();
|
||||||
@ -62,7 +66,21 @@ describe(StorageService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ mountFiles: true });
|
systemMock.get.mockResolvedValue({ mountFiles: true });
|
||||||
storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||||
|
|
||||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount');
|
await expect(sut.onBootstrap()).rejects.toThrow('Failed to write');
|
||||||
|
|
||||||
|
expect(systemMock.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should startup if checks are disabled', async () => {
|
||||||
|
systemMock.get.mockResolvedValue({ mountFiles: true });
|
||||||
|
configMock.getEnv.mockReturnValue(
|
||||||
|
mockEnvData({
|
||||||
|
storage: { ignoreMountCheckErrors: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
|
||||||
|
|
||||||
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(systemMock.set).not.toHaveBeenCalled();
|
expect(systemMock.set).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
|
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -10,9 +11,12 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { ImmichStartupError } from 'src/utils/events';
|
import { ImmichStartupError } from 'src/utils/events';
|
||||||
|
|
||||||
|
const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StorageService {
|
export class StorageService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
@ -23,30 +27,41 @@ export class StorageService {
|
|||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'app.bootstrap' })
|
||||||
async onBootstrap() {
|
async onBootstrap() {
|
||||||
|
const envData = this.configRepository.getEnv();
|
||||||
|
|
||||||
await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => {
|
await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => {
|
||||||
const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false };
|
const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false };
|
||||||
const enabled = flags.mountFiles ?? false;
|
const enabled = flags.mountFiles ?? false;
|
||||||
|
|
||||||
this.logger.log(`Verifying system mount folder checks (enabled=${enabled})`);
|
this.logger.log(`Verifying system mount folder checks (enabled=${enabled})`);
|
||||||
|
|
||||||
// check each folder exists and is writable
|
try {
|
||||||
for (const folder of Object.values(StorageFolder)) {
|
// check each folder exists and is writable
|
||||||
if (!enabled) {
|
for (const folder of Object.values(StorageFolder)) {
|
||||||
this.logger.log(`Writing initial mount file for the ${folder} folder`);
|
if (!enabled) {
|
||||||
await this.createMountFile(folder);
|
this.logger.log(`Writing initial mount file for the ${folder} folder`);
|
||||||
|
await this.createMountFile(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.verifyReadAccess(folder);
|
||||||
|
await this.verifyWriteAccess(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.verifyReadAccess(folder);
|
if (!flags.mountFiles) {
|
||||||
await this.verifyWriteAccess(folder);
|
flags.mountFiles = true;
|
||||||
}
|
await this.systemMetadata.set(SystemMetadataKey.SYSTEM_FLAGS, flags);
|
||||||
|
this.logger.log('Successfully enabled system mount folders checks');
|
||||||
|
}
|
||||||
|
|
||||||
if (!flags.mountFiles) {
|
this.logger.log('Successfully verified system mount folder checks');
|
||||||
flags.mountFiles = true;
|
} catch (error) {
|
||||||
await this.systemMetadata.set(SystemMetadataKey.SYSTEM_FLAGS, flags);
|
if (envData.storage.ignoreMountCheckErrors) {
|
||||||
this.logger.log('Successfully enabled system mount folders checks');
|
this.logger.error(error);
|
||||||
|
this.logger.warn('Ignoring mount folder errors');
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log('Successfully verified system mount folder checks');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,49 +85,45 @@ export class StorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async verifyReadAccess(folder: StorageFolder) {
|
private async verifyReadAccess(folder: StorageFolder) {
|
||||||
const { filePath } = this.getMountFilePaths(folder);
|
const { internalPath, externalPath } = this.getMountFilePaths(folder);
|
||||||
try {
|
try {
|
||||||
await this.storageRepository.readFile(filePath);
|
await this.storageRepository.readFile(internalPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to read ${filePath}: ${error}`);
|
this.logger.error(`Failed to read ${internalPath}: ${error}`);
|
||||||
this.logger.error(
|
throw new ImmichStartupError(`Failed to read "${externalPath} - ${docsMessage}"`);
|
||||||
`The "${folder}" folder appears to be offline/missing, please make sure the volume is mounted with the correct permissions`,
|
|
||||||
);
|
|
||||||
throw new ImmichStartupError(`Failed to validate folder mount (read from "<MEDIA_LOCATION>/${folder}")`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createMountFile(folder: StorageFolder) {
|
private async createMountFile(folder: StorageFolder) {
|
||||||
const { folderPath, filePath } = this.getMountFilePaths(folder);
|
const { folderPath, internalPath, externalPath } = this.getMountFilePaths(folder);
|
||||||
try {
|
try {
|
||||||
this.storageRepository.mkdirSync(folderPath);
|
this.storageRepository.mkdirSync(folderPath);
|
||||||
await this.storageRepository.createFile(filePath, Buffer.from(`${Date.now()}`));
|
await this.storageRepository.createFile(internalPath, Buffer.from(`${Date.now()}`));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to create ${filePath}: ${error}`);
|
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
||||||
this.logger.error(
|
this.logger.warn('Found existing mount file, skipping creation');
|
||||||
`The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`,
|
return;
|
||||||
);
|
}
|
||||||
throw new ImmichStartupError(`Failed to validate folder mount (write to "<UPLOAD_LOCATION>/${folder}")`);
|
this.logger.error(`Failed to create ${internalPath}: ${error}`);
|
||||||
|
throw new ImmichStartupError(`Failed to create "${externalPath} - ${docsMessage}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyWriteAccess(folder: StorageFolder) {
|
private async verifyWriteAccess(folder: StorageFolder) {
|
||||||
const { filePath } = this.getMountFilePaths(folder);
|
const { internalPath, externalPath } = this.getMountFilePaths(folder);
|
||||||
try {
|
try {
|
||||||
await this.storageRepository.overwriteFile(filePath, Buffer.from(`${Date.now()}`));
|
await this.storageRepository.overwriteFile(internalPath, Buffer.from(`${Date.now()}`));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to write ${filePath}: ${error}`);
|
this.logger.error(`Failed to write ${internalPath}: ${error}`);
|
||||||
this.logger.error(
|
throw new ImmichStartupError(`Failed to write "${externalPath} - ${docsMessage}"`);
|
||||||
`The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`,
|
|
||||||
);
|
|
||||||
throw new ImmichStartupError(`Failed to validate folder mount (write to "<UPLOAD_LOCATION>/${folder}")`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMountFilePaths(folder: StorageFolder) {
|
private getMountFilePaths(folder: StorageFolder) {
|
||||||
const folderPath = StorageCore.getBaseFolder(folder);
|
const folderPath = StorageCore.getBaseFolder(folder);
|
||||||
const filePath = join(folderPath, '.immich');
|
const internalPath = join(folderPath, '.immich');
|
||||||
|
const externalPath = `<UPLOAD_LOCATION>/${folder}/.immich`;
|
||||||
|
|
||||||
return { folderPath, filePath };
|
return { folderPath, internalPath, externalPath };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,13 @@ import {
|
|||||||
VideoCodec,
|
VideoCodec,
|
||||||
VideoContainer,
|
VideoContainer,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { QueueName } from 'src/interfaces/job.interface';
|
import { QueueName } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||||
@ -187,16 +189,18 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
|
|
||||||
describe(SystemConfigService.name, () => {
|
describe(SystemConfigService.name, () => {
|
||||||
let sut: SystemConfigService;
|
let sut: SystemConfigService;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete process.env.IMMICH_CONFIG_FILE;
|
configMock = newConfigRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
|
||||||
eventMock = newEventRepositoryMock();
|
eventMock = newEventRepositoryMock();
|
||||||
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
sut = new SystemConfigService(systemMock, eventMock, loggerMock);
|
|
||||||
|
sut = new SystemConfigService(configMock, eventMock, systemMock, loggerMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -231,8 +235,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should load the config from a json file', async () => {
|
it('should load the config from a json file', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||||
|
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||||
|
|
||||||
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
|
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
|
||||||
@ -241,7 +244,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should log errors with the config file', async () => {
|
it('should log errors with the config file', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||||
|
|
||||||
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
|
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
|
||||||
|
|
||||||
@ -256,7 +259,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should load the config from a yaml file', async () => {
|
it('should load the config from a yaml file', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||||
const partialConfig = `
|
const partialConfig = `
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
crf: 30
|
crf: 30
|
||||||
@ -275,7 +278,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should accept an empty configuration file', async () => {
|
it('should accept an empty configuration file', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||||
|
|
||||||
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
|
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
|
||||||
@ -284,7 +287,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow underscores in the machine learning url', async () => {
|
it('should allow underscores in the machine learning url', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||||
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
|
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||||
|
|
||||||
@ -300,7 +303,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
|
|
||||||
for (const { should, externalDomain, result } of externalDomainTests) {
|
for (const { should, externalDomain, result } of externalDomainTests) {
|
||||||
it(`should normalize an external domain ${should}`, async () => {
|
it(`should normalize an external domain ${should}`, async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||||
const partialConfig = { server: { externalDomain } };
|
const partialConfig = { server: { externalDomain } };
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||||
|
|
||||||
@ -310,7 +313,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('should warn for unknown options in yaml', async () => {
|
it('should warn for unknown options in yaml', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||||
const partialConfig = `
|
const partialConfig = `
|
||||||
unknownOption: true
|
unknownOption: true
|
||||||
`;
|
`;
|
||||||
@ -331,7 +334,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
|
|
||||||
for (const test of tests) {
|
for (const test of tests) {
|
||||||
it(`should ${test.should}`, async () => {
|
it(`should ${test.should}`, async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
|
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
|
||||||
|
|
||||||
if (test.warn) {
|
if (test.warn) {
|
||||||
@ -390,7 +393,7 @@ describe(SystemConfigService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if a config file is in use', async () => {
|
it('should throw an error if a config file is in use', async () => {
|
||||||
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
|
configMock.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||||
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
|
||||||
await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
expect(systemMock.set).not.toHaveBeenCalled();
|
expect(systemMock.set).not.toHaveBeenCalled();
|
||||||
|
@ -15,21 +15,23 @@ import {
|
|||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
|
||||||
import { LogLevel } from 'src/enum';
|
import { LogLevel } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { clearConfigCache, isUsingConfigFile } from 'src/utils/config';
|
import { clearConfigCache } from 'src/utils/config';
|
||||||
import { toPlainObject } from 'src/utils/object';
|
import { toPlainObject } from 'src/utils/object';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SystemConfigService extends BaseService {
|
export class SystemConfigService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(SystemConfigService.name);
|
this.logger.setContext(SystemConfigService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +69,8 @@ export class SystemConfigService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateSystemConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
|
async updateSystemConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
|
||||||
if (isUsingConfigFile()) {
|
const { configFile } = this.configRepository.getEnv();
|
||||||
|
if (configFile) {
|
||||||
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { BadRequestException, InternalServerErrorException, NotFoundException }
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { CacheControl, UserMetadataKey } from 'src/enum';
|
import { CacheControl, UserMetadataKey } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -14,6 +15,7 @@ import { authStub } from 'test/fixtures/auth.stub';
|
|||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
@ -34,6 +36,7 @@ describe(UserService.name, () => {
|
|||||||
let cryptoRepositoryMock: Mocked<ICryptoRepository>;
|
let cryptoRepositoryMock: Mocked<ICryptoRepository>;
|
||||||
|
|
||||||
let albumMock: Mocked<IAlbumRepository>;
|
let albumMock: Mocked<IAlbumRepository>;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
@ -41,14 +44,24 @@ describe(UserService.name, () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
albumMock = newAlbumRepositoryMock();
|
albumMock = newAlbumRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
configMock = newConfigRepositoryMock();
|
||||||
cryptoRepositoryMock = newCryptoRepositoryMock();
|
cryptoRepositoryMock = newCryptoRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
userMock = newUserRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new UserService(albumMock, cryptoRepositoryMock, jobMock, storageMock, systemMock, userMock, loggerMock);
|
sut = new UserService(
|
||||||
|
albumMock,
|
||||||
|
configMock,
|
||||||
|
cryptoRepositoryMock,
|
||||||
|
jobMock,
|
||||||
|
storageMock,
|
||||||
|
systemMock,
|
||||||
|
userMock,
|
||||||
|
loggerMock,
|
||||||
|
);
|
||||||
|
|
||||||
userMock.get.mockImplementation((userId) =>
|
userMock.get.mockImplementation((userId) =>
|
||||||
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),
|
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),
|
||||||
|
@ -12,6 +12,7 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
|
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
@ -26,6 +27,7 @@ import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/uti
|
|||||||
export class UserService extends BaseService {
|
export class UserService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@ -33,7 +35,7 @@ export class UserService extends BaseService {
|
|||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(UserService.name);
|
this.logger.setContext(UserService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||||
import { VersionService } from 'src/services/version.service';
|
import { VersionService } from 'src/services/version.service';
|
||||||
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
||||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
|
||||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||||
|
import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
const mockRelease = (version: string) => ({
|
const mockRelease = (version: string) => ({
|
||||||
@ -26,26 +32,58 @@ const mockRelease = (version: string) => ({
|
|||||||
|
|
||||||
describe(VersionService.name, () => {
|
describe(VersionService.name, () => {
|
||||||
let sut: VersionService;
|
let sut: VersionService;
|
||||||
|
let configMock: Mocked<IConfigRepository>;
|
||||||
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let serverMock: Mocked<IServerInfoRepository>;
|
let serverMock: Mocked<IServerInfoRepository>;
|
||||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
|
let versionMock: Mocked<IVersionHistoryRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
configMock = newConfigRepositoryMock();
|
||||||
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
eventMock = newEventRepositoryMock();
|
eventMock = newEventRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
serverMock = newServerInfoRepositoryMock();
|
serverMock = newServerInfoRepositoryMock();
|
||||||
systemMock = newSystemMetadataRepositoryMock();
|
systemMock = newSystemMetadataRepositoryMock();
|
||||||
|
versionMock = newVersionHistoryRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new VersionService(eventMock, jobMock, serverMock, systemMock, loggerMock);
|
sut = new VersionService(
|
||||||
|
configMock,
|
||||||
|
databaseMock,
|
||||||
|
eventMock,
|
||||||
|
jobMock,
|
||||||
|
serverMock,
|
||||||
|
systemMock,
|
||||||
|
versionMock,
|
||||||
|
loggerMock,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onBootstrap', () => {
|
||||||
|
it('should record a new version', async () => {
|
||||||
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
expect(versionMock.create).toHaveBeenCalledWith({ version: expect.any(String) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip a duplicate version', async () => {
|
||||||
|
versionMock.getLatest.mockResolvedValue({
|
||||||
|
id: 'version-1',
|
||||||
|
createdAt: new Date(),
|
||||||
|
version: serverVersion.toString(),
|
||||||
|
});
|
||||||
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
|
expect(versionMock.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getVersion', () => {
|
describe('getVersion', () => {
|
||||||
it('should respond the server version', () => {
|
it('should respond the server version', () => {
|
||||||
expect(sut.getVersion()).toEqual({
|
expect(sut.getVersion()).toEqual({
|
||||||
@ -56,6 +94,14 @@ describe(VersionService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getVersionHistory', () => {
|
||||||
|
it('should respond the server version history', async () => {
|
||||||
|
const upgrade = { id: 'upgrade-1', createdAt: new Date(), version: '1.0.0' };
|
||||||
|
versionMock.getAll.mockResolvedValue([upgrade]);
|
||||||
|
await expect(sut.getVersionHistory()).resolves.toEqual([upgrade]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('handQueueVersionCheck', () => {
|
describe('handQueueVersionCheck', () => {
|
||||||
it('should queue a version check job', async () => {
|
it('should queue a version check job', async () => {
|
||||||
await expect(sut.handleQueueVersionCheck()).resolves.toBeUndefined();
|
await expect(sut.handleQueueVersionCheck()).resolves.toBeUndefined();
|
||||||
|
@ -6,11 +6,14 @@ import { OnEvent } from 'src/decorators';
|
|||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
|
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
||||||
@ -25,25 +28,41 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class VersionService extends BaseService {
|
export class VersionService extends BaseService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||||
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
|
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
|
@Inject(IVersionHistoryRepository) private versionRepository: IVersionHistoryRepository,
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
super(systemMetadataRepository, logger);
|
super(configRepository, systemMetadataRepository, logger);
|
||||||
this.logger.setContext(VersionService.name);
|
this.logger.setContext(VersionService.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'app.bootstrap' })
|
@OnEvent({ name: 'app.bootstrap' })
|
||||||
async onBootstrap(): Promise<void> {
|
async onBootstrap(): Promise<void> {
|
||||||
await this.handleVersionCheck();
|
await this.handleVersionCheck();
|
||||||
|
|
||||||
|
await this.databaseRepository.withLock(DatabaseLock.VersionHistory, async () => {
|
||||||
|
const latest = await this.versionRepository.getLatest();
|
||||||
|
const current = serverVersion.toString();
|
||||||
|
if (!latest || latest.version !== current) {
|
||||||
|
this.logger.log(`Version has changed, adding ${current} to history`);
|
||||||
|
await this.versionRepository.create({ version: current });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
return ServerVersionResponseDto.fromSemVer(serverVersion);
|
return ServerVersionResponseDto.fromSemVer(serverVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVersionHistory() {
|
||||||
|
return this.versionRepository.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
async handleQueueVersionCheck() {
|
async handleQueueVersionCheck() {
|
||||||
await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} });
|
await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} });
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import * as _ from 'lodash';
|
|||||||
import { SystemConfig, defaults } from 'src/config';
|
import { SystemConfig, defaults } from 'src/config';
|
||||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
@ -15,6 +16,7 @@ import { DeepPartial } from 'typeorm';
|
|||||||
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
||||||
|
|
||||||
type RepoDeps = {
|
type RepoDeps = {
|
||||||
|
configRepo: IConfigRepository;
|
||||||
metadataRepo: ISystemMetadataRepository;
|
metadataRepo: ISystemMetadataRepository;
|
||||||
logger: ILoggerRepository;
|
logger: ILoggerRepository;
|
||||||
};
|
};
|
||||||
@ -28,10 +30,6 @@ export const clearConfigCache = () => {
|
|||||||
lastUpdated = null;
|
lastUpdated = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isUsingConfigFile = () => {
|
|
||||||
return !!process.env.IMMICH_CONFIG_FILE;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getConfig = async (repos: RepoDeps, { withCache }: { withCache: boolean }): Promise<SystemConfig> => {
|
export const getConfig = async (repos: RepoDeps, { withCache }: { withCache: boolean }): Promise<SystemConfig> => {
|
||||||
if (!withCache || !config) {
|
if (!withCache || !config) {
|
||||||
const timestamp = lastUpdated;
|
const timestamp = lastUpdated;
|
||||||
@ -80,11 +78,12 @@ const loadFromFile = async ({ metadataRepo, logger }: RepoDeps, filepath: string
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildConfig = async (repos: RepoDeps) => {
|
const buildConfig = async (repos: RepoDeps) => {
|
||||||
const { metadataRepo, logger } = repos;
|
const { configRepo, metadataRepo, logger } = repos;
|
||||||
|
const { configFile } = configRepo.getEnv();
|
||||||
|
|
||||||
// load partial
|
// load partial
|
||||||
const partial = isUsingConfigFile()
|
const partial = configFile
|
||||||
? await loadFromFile(repos, process.env.IMMICH_CONFIG_FILE as string)
|
? await loadFromFile(repos, configFile)
|
||||||
: await metadataRepo.get(SystemMetadataKey.SYSTEM_CONFIG);
|
: await metadataRepo.get(SystemMetadataKey.SYSTEM_CONFIG);
|
||||||
|
|
||||||
// merge with defaults
|
// merge with defaults
|
||||||
@ -106,7 +105,7 @@ const buildConfig = async (repos: RepoDeps) => {
|
|||||||
// validate full config
|
// validate full config
|
||||||
const errors = await validate(plainToInstance(SystemConfigDto, config));
|
const errors = await validate(plainToInstance(SystemConfigDto, config));
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
if (isUsingConfigFile()) {
|
if (configFile) {
|
||||||
throw new Error(`Invalid value(s) in file: ${errors}`);
|
throw new Error(`Invalid value(s) in file: ${errors}`);
|
||||||
} else {
|
} else {
|
||||||
logger.error('Validation error', errors);
|
logger.error('Validation error', errors);
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||||
import { Mocked, vitest } from 'vitest';
|
import { Mocked, vitest } from 'vitest';
|
||||||
|
|
||||||
|
const envData: EnvData = {
|
||||||
|
database: {
|
||||||
|
skipMigrations: false,
|
||||||
|
vectorExtension: DatabaseExtension.VECTORS,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
ignoreMountCheckErrors: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
|
export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
|
||||||
return {
|
return {
|
||||||
getEnv: vitest.fn().mockReturnValue({
|
getEnv: vitest.fn().mockReturnValue(envData),
|
||||||
database: {
|
|
||||||
skipMigration: false,
|
|
||||||
vectorExtension: DatabaseExtension.VECTORS,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mockEnvData = (config: Partial<EnvData>) => ({ ...envData, ...config });
|
||||||
|
10
server/test/repositories/version-history.repository.mock.ts
Normal file
10
server/test/repositories/version-history.repository.mock.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||||
|
import { Mocked, vitest } from 'vitest';
|
||||||
|
|
||||||
|
export const newVersionHistoryRepositoryMock = (): Mocked<IVersionHistoryRepository> => {
|
||||||
|
return {
|
||||||
|
getAll: vitest.fn().mockResolvedValue([]),
|
||||||
|
getLatest: vitest.fn(),
|
||||||
|
create: vitest.fn(),
|
||||||
|
};
|
||||||
|
};
|
50
web/package-lock.json
generated
50
web/package-lock.json
generated
@ -6129,9 +6129,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier-plugin-svelte": {
|
"node_modules/prettier-plugin-svelte": {
|
||||||
"version": "3.2.6",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.7.tgz",
|
||||||
"integrity": "sha512-Y1XWLw7vXUQQZmgv1JAEiLcErqUniAF2wO7QJsw8BVMvpLET2dI5WpEIEJx1r11iHVdSMzQxivyfrH9On9t2IQ==",
|
"integrity": "sha512-/Dswx/ea0lV34If1eDcG3nulQ63YNr5KPDfMsjbdtpSWOxKKJ7nAc2qlVuYwEvCr4raIuredNoR7K4JCkmTGaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -7085,14 +7085,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-check": {
|
"node_modules/svelte-check": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.3.tgz",
|
||||||
"integrity": "sha512-w2yqcG9ELJe2RJCnAvB7v0OgkHhL3czzz/tVoxGFfO6y4mOrF6QHCDhXijeXzsU7LVKEwWS3Qd9tza4JBuDxqA==",
|
"integrity": "sha512-V2eqOEuNrPi1jGf307opR1JZ+ITP6/7R8ALKSw4Uw3NWp6GfA+fe7tYtEvZc7QHCavYKBizCK4JFwYjbuPCeXQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"chokidar": "^3.4.1",
|
"chokidar": "^4.0.1",
|
||||||
"fdir": "^6.2.0",
|
"fdir": "^6.2.0",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
"sade": "^1.7.4"
|
"sade": "^1.7.4"
|
||||||
@ -7108,10 +7108,26 @@
|
|||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-check/node_modules/chokidar": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte-check/node_modules/fdir": {
|
"node_modules/svelte-check/node_modules/fdir": {
|
||||||
"version": "6.3.0",
|
"version": "6.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz",
|
||||||
"integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
|
"integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@ -7138,6 +7154,20 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-check/node_modules/readdirp": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte-eslint-parser": {
|
"node_modules/svelte-eslint-parser": {
|
||||||
"version": "0.41.1",
|
"version": "0.41.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.1.tgz",
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||||
import { type ServerAboutResponseDto } from '@immich/sdk';
|
import { type ServerAboutResponseDto, type ServerVersionHistoryResponseDto } from '@immich/sdk';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let onClose: () => void;
|
export let onClose: () => void;
|
||||||
|
|
||||||
export let info: ServerAboutResponseDto;
|
export let info: ServerAboutResponseDto;
|
||||||
|
export let versions: ServerVersionHistoryResponseDto[];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Portal>
|
<Portal>
|
||||||
<FullScreenModal title={$t('about')} {onClose}>
|
<FullScreenModal title={$t('about')} {onClose}>
|
||||||
<div
|
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary">
|
||||||
class="immich-scrollbar max-h-[500px] overflow-y-auto flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-desc"
|
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-desc"
|
||||||
>Immich</label
|
>Immich</label
|
||||||
@ -151,6 +151,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="col-span-full">
|
||||||
|
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-history"
|
||||||
|
>{$t('version_history')}</label
|
||||||
|
>
|
||||||
|
<ul id="version-history" class="list-none">
|
||||||
|
{#each versions.slice(0, 5) as item (item.id)}
|
||||||
|
{@const createdAt = DateTime.fromISO(item.createdAt)}
|
||||||
|
<li>
|
||||||
|
<span
|
||||||
|
class="immich-form-label pb-2 text-xs"
|
||||||
|
id="version-history"
|
||||||
|
title={createdAt.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS)}
|
||||||
|
>
|
||||||
|
{$t('version_history_item', {
|
||||||
|
values: {
|
||||||
|
version: item.version,
|
||||||
|
date: createdAt.toLocaleString({
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FullScreenModal>
|
</FullScreenModal>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
@ -4,7 +4,12 @@
|
|||||||
import { requestServerInfo } from '$lib/utils/auth';
|
import { requestServerInfo } from '$lib/utils/auth';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
import {
|
||||||
|
getAboutInfo,
|
||||||
|
getVersionHistory,
|
||||||
|
type ServerAboutResponseDto,
|
||||||
|
type ServerVersionHistoryResponseDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
|
||||||
const { serverVersion, connected } = websocketStore;
|
const { serverVersion, connected } = websocketStore;
|
||||||
|
|
||||||
@ -12,16 +17,17 @@
|
|||||||
|
|
||||||
$: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
|
$: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
|
||||||
|
|
||||||
let aboutInfo: ServerAboutResponseDto;
|
let info: ServerAboutResponseDto;
|
||||||
|
let versions: ServerVersionHistoryResponseDto[] = [];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await requestServerInfo();
|
await requestServerInfo();
|
||||||
aboutInfo = await getAboutInfo();
|
[info, versions] = await Promise.all([getAboutInfo(), getVersionHistory()]);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
|
<ServerAboutModal onClose={() => (isOpen = false)} {info} {versions} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte';
|
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { serverInfo } from '$lib/stores/server-info.store';
|
import { serverInfo } from '$lib/stores/server-info.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
@ -8,18 +7,14 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { getByteUnitString } from '../../../utils/byte-units';
|
import { getByteUnitString } from '../../../utils/byte-units';
|
||||||
import LoadingSpinner from '../loading-spinner.svelte';
|
import LoadingSpinner from '../loading-spinner.svelte';
|
||||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
|
||||||
|
|
||||||
let usageClasses = '';
|
let usageClasses = '';
|
||||||
let isOpen = false;
|
|
||||||
|
|
||||||
$: hasQuota = $user?.quotaSizeInBytes !== null;
|
$: hasQuota = $user?.quotaSizeInBytes !== null;
|
||||||
$: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0;
|
$: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0;
|
||||||
$: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0;
|
$: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0;
|
||||||
$: usedPercentage = Math.min(Math.round((usedBytes / availableBytes) * 100), 100);
|
$: usedPercentage = Math.min(Math.round((usedBytes / availableBytes) * 100), 100);
|
||||||
|
|
||||||
let aboutInfo: ServerAboutResponseDto;
|
|
||||||
|
|
||||||
const onUpdate = () => {
|
const onUpdate = () => {
|
||||||
usageClasses = getUsageClass();
|
usageClasses = getUsageClass();
|
||||||
};
|
};
|
||||||
@ -42,14 +37,9 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await requestServerInfo();
|
await requestServerInfo();
|
||||||
aboutInfo = await getAboutInfo();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
|
||||||
<ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm"
|
class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm"
|
||||||
title={$t('storage_usage', {
|
title={$t('storage_usage', {
|
||||||
|
@ -1278,6 +1278,8 @@
|
|||||||
"version": "Version",
|
"version": "Version",
|
||||||
"version_announcement_closing": "Your friend, Alex",
|
"version_announcement_closing": "Your friend, Alex",
|
||||||
"version_announcement_message": "Hi friend, there is a new version of the application please take your time to visit the <link>release notes</link> and ensure your <code>docker-compose.yml</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your application automatically.",
|
"version_announcement_message": "Hi friend, there is a new version of the application please take your time to visit the <link>release notes</link> and ensure your <code>docker-compose.yml</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your application automatically.",
|
||||||
|
"version_history": "Version History",
|
||||||
|
"version_history_item": "Installed {version} on {date}",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"video_hover_setting": "Play video thumbnail on hover",
|
"video_hover_setting": "Play video thumbnail on hover",
|
||||||
"video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.",
|
"video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user