Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67453d18ff | ||
|
|
792a87e407 | ||
|
|
6da50626e1 | ||
|
|
6239b3b309 | ||
|
|
b9bc621e2a | ||
|
|
e10bbfa933 | ||
|
|
2dd301e292 | ||
|
|
75edc6de0f | ||
|
|
780c5183e3 | ||
|
|
25a10784eb | ||
|
|
6e1d09fc32 | ||
|
|
73a2063d96 | ||
|
|
deb1e7f41f | ||
|
|
f45f719b9d |
@@ -1,17 +0,0 @@
|
||||
# Deployment checklist for iOS/Android/Server
|
||||
|
||||
[ ] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
|
||||
|
||||
[ ] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
|
||||
|
||||
[ ] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
||||
|
||||
[ ] Up version in [docker/docker-compose.dev.yml](/docker/docker-compose.dev.yml) for `immich_server` service
|
||||
|
||||
[ ] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
||||
|
||||
[ ] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
|
||||
|
||||
[ ] Add changelog to [Android Fastlane F-droid folder](/mobile/android/fastlane/metadata/android/en-US/changelogs)
|
||||
|
||||
All of the version should be the same.
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
immich-server:
|
||||
container_name: immich_server
|
||||
image: ghcr.io/immich-app/immich-server:release
|
||||
entrypoint: [ "/bin/sh", "./start-server.sh" ]
|
||||
entrypoint: ["/bin/sh", "./start-server.sh"]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
env_file:
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: ghcr.io/immich-app/immich-server:release
|
||||
entrypoint: [ "/bin/sh", "./start-microservices.sh" ]
|
||||
entrypoint: ["/bin/sh", "./start-microservices.sh"]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
env_file:
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
immich-web:
|
||||
container_name: immich_web
|
||||
image: ghcr.io/immich-app/immich-web:release
|
||||
entrypoint: [ "/bin/sh", "./entrypoint.sh" ]
|
||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
@@ -63,6 +63,7 @@ services:
|
||||
driver: none
|
||||
volumes:
|
||||
- tsdata:/data
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
|
||||
22
docs/docs/administration/reverse-proxy.md
Normal file
22
docs/docs/administration/reverse-proxy.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Reverse Proxy
|
||||
|
||||
When deploying Immich it is important to understand that a reverse proxy is required in front of the server and web container. The reverse proxy acts as an intermediary between the user and container, forwarding requests to the correct container based on the URL path.
|
||||
|
||||
## Default Reverse Proxy
|
||||
|
||||
Immich provides a default nginx reverse proxy preconfigured to perform the correct routing and set the necessary headers for the server and web container to use. These headers are crucial to redirect to the correct URL and determine the client's IP address.
|
||||
|
||||
## Using a Different Reverse Proxy
|
||||
|
||||
While the reverse proxy provided by Immich works well for basic deployments, some users may want to use a different reverse proxy. Fortunately, Immich is flexible enough to accommodate different reverse proxies. Users can either:
|
||||
|
||||
1. Add another reverse proxy on top of Immich's reverse proxy
|
||||
2. Completely replace the default reverse proxy
|
||||
|
||||
## Adding a Custom Reverse Proxy
|
||||
|
||||
Users can deploy a custom reverse proxy that forwards requests to Immich's reverse proxy. This way, the new reverse proxy can handle TLS termination, load balancing, or other advanced features, while still delegating routing decisions to Immich's reverse proxy. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich.
|
||||
|
||||
## Replacing the Default Reverse Proxy
|
||||
|
||||
Replacing Immich's default reverse proxy is an advanced deployment and support may be limited. When replacing Immich's default proxy it is important to ensure that requests to `/api/*` are routed to the server container and all other requests to the web container. Additionally, the previously mentioned headers should be configured accordingly. You may find our [nginx configuration file](https://github.com/immich-app/immich/blob/main/nginx/templates/default.conf.template) a helpful reference.
|
||||
@@ -36,7 +36,7 @@ platform :android do
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 74,
|
||||
"android.injected.version.name" => "1.51.0",
|
||||
"android.injected.version.name" => "1.51.2",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
* Enter server first for login
|
||||
* Sync assets, albums & users to local database on device
|
||||
* Fixes hero animation on main timeline by
|
||||
* Transparent bottom Android navigation bar
|
||||
* Fix do not crash on malformed asset duration
|
||||
* Gallery viewer fullscreen edge case
|
||||
* Fix Sorted shared album and added share user doesn't reflect change in album view
|
||||
* Allow app to be used offline
|
||||
* No longer wait for background backup in settings
|
||||
* Share album name and adaptive shared album display
|
||||
* Persist album sort order
|
||||
|
||||
@@ -5,17 +5,19 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000306">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="104.812034">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="70.685298">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="47.568445">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.624781">
|
||||
|
||||
<failure message="/opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:229:in `chdir' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:42:in `block (2 levels) in parsing_binding' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/lane.rb:33:in `call' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:45:in `chdir' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/runner.rb:45:in `execute' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/gems/fastlane-2.212.0/bin/fastlane:23:in `<top (required)>' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/bin/fastlane:25:in `load' /opt/homebrew/Cellar/fastlane/2.212.0/libexec/bin/fastlane:25:in `<main>' Google Api Error: Invalid request - The release created has notes in language en-US with length 508, which is too long (max: 500)." />
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.51.0"
|
||||
version_number: "1.51.2"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.50.1
|
||||
- API version: 1.51.1
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
||||
36
mobile/openapi/doc/SearchApi.md
generated
36
mobile/openapi/doc/SearchApi.md
generated
@@ -113,7 +113,7 @@ This endpoint does not need any parameter.
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **search**
|
||||
> SearchResponseDto search()
|
||||
> SearchResponseDto search(q, query, clip, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion)
|
||||
|
||||
|
||||
|
||||
@@ -134,9 +134,23 @@ import 'package:openapi/api.dart';
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
|
||||
final api_instance = SearchApi();
|
||||
final q = q_example; // String |
|
||||
final query = query_example; // String |
|
||||
final clip = true; // bool |
|
||||
final type = type_example; // String |
|
||||
final isFavorite = true; // bool |
|
||||
final exifInfoPeriodCity = exifInfoPeriodCity_example; // String |
|
||||
final exifInfoPeriodState = exifInfoPeriodState_example; // String |
|
||||
final exifInfoPeriodCountry = exifInfoPeriodCountry_example; // String |
|
||||
final exifInfoPeriodMake = exifInfoPeriodMake_example; // String |
|
||||
final exifInfoPeriodModel = exifInfoPeriodModel_example; // String |
|
||||
final smartInfoPeriodObjects = []; // List<String> |
|
||||
final smartInfoPeriodTags = []; // List<String> |
|
||||
final recent = true; // bool |
|
||||
final motion = true; // bool |
|
||||
|
||||
try {
|
||||
final result = api_instance.search();
|
||||
final result = api_instance.search(q, query, clip, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags, recent, motion);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling SearchApi->search: $e\n');
|
||||
@@ -144,7 +158,23 @@ try {
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**q** | **String**| | [optional]
|
||||
**query** | **String**| | [optional]
|
||||
**clip** | **bool**| | [optional]
|
||||
**type** | **String**| | [optional]
|
||||
**isFavorite** | **bool**| | [optional]
|
||||
**exifInfoPeriodCity** | **String**| | [optional]
|
||||
**exifInfoPeriodState** | **String**| | [optional]
|
||||
**exifInfoPeriodCountry** | **String**| | [optional]
|
||||
**exifInfoPeriodMake** | **String**| | [optional]
|
||||
**exifInfoPeriodModel** | **String**| | [optional]
|
||||
**smartInfoPeriodObjects** | [**List<String>**](String.md)| | [optional] [default to const []]
|
||||
**smartInfoPeriodTags** | [**List<String>**](String.md)| | [optional] [default to const []]
|
||||
**recent** | **bool**| | [optional]
|
||||
**motion** | **bool**| | [optional]
|
||||
|
||||
### Return type
|
||||
|
||||
|
||||
109
mobile/openapi/lib/api/search_api.dart
generated
109
mobile/openapi/lib/api/search_api.dart
generated
@@ -110,7 +110,37 @@ class SearchApi {
|
||||
///
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> searchWithHttpInfo() async {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] q:
|
||||
///
|
||||
/// * [String] query:
|
||||
///
|
||||
/// * [bool] clip:
|
||||
///
|
||||
/// * [String] type:
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
///
|
||||
/// * [String] exifInfoPeriodCity:
|
||||
///
|
||||
/// * [String] exifInfoPeriodState:
|
||||
///
|
||||
/// * [String] exifInfoPeriodCountry:
|
||||
///
|
||||
/// * [String] exifInfoPeriodMake:
|
||||
///
|
||||
/// * [String] exifInfoPeriodModel:
|
||||
///
|
||||
/// * [List<String>] smartInfoPeriodObjects:
|
||||
///
|
||||
/// * [List<String>] smartInfoPeriodTags:
|
||||
///
|
||||
/// * [bool] recent:
|
||||
///
|
||||
/// * [bool] motion:
|
||||
Future<Response> searchWithHttpInfo({ String? q, String? query, bool? clip, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/search';
|
||||
|
||||
@@ -121,6 +151,49 @@ class SearchApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (q != null) {
|
||||
queryParams.addAll(_queryParams('', 'q', q));
|
||||
}
|
||||
if (query != null) {
|
||||
queryParams.addAll(_queryParams('', 'query', query));
|
||||
}
|
||||
if (clip != null) {
|
||||
queryParams.addAll(_queryParams('', 'clip', clip));
|
||||
}
|
||||
if (type != null) {
|
||||
queryParams.addAll(_queryParams('', 'type', type));
|
||||
}
|
||||
if (isFavorite != null) {
|
||||
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||
}
|
||||
if (exifInfoPeriodCity != null) {
|
||||
queryParams.addAll(_queryParams('', 'exifInfo.city', exifInfoPeriodCity));
|
||||
}
|
||||
if (exifInfoPeriodState != null) {
|
||||
queryParams.addAll(_queryParams('', 'exifInfo.state', exifInfoPeriodState));
|
||||
}
|
||||
if (exifInfoPeriodCountry != null) {
|
||||
queryParams.addAll(_queryParams('', 'exifInfo.country', exifInfoPeriodCountry));
|
||||
}
|
||||
if (exifInfoPeriodMake != null) {
|
||||
queryParams.addAll(_queryParams('', 'exifInfo.make', exifInfoPeriodMake));
|
||||
}
|
||||
if (exifInfoPeriodModel != null) {
|
||||
queryParams.addAll(_queryParams('', 'exifInfo.model', exifInfoPeriodModel));
|
||||
}
|
||||
if (smartInfoPeriodObjects != null) {
|
||||
queryParams.addAll(_queryParams('multi', 'smartInfo.objects', smartInfoPeriodObjects));
|
||||
}
|
||||
if (smartInfoPeriodTags != null) {
|
||||
queryParams.addAll(_queryParams('multi', 'smartInfo.tags', smartInfoPeriodTags));
|
||||
}
|
||||
if (recent != null) {
|
||||
queryParams.addAll(_queryParams('', 'recent', recent));
|
||||
}
|
||||
if (motion != null) {
|
||||
queryParams.addAll(_queryParams('', 'motion', motion));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
@@ -136,8 +209,38 @@ class SearchApi {
|
||||
}
|
||||
|
||||
///
|
||||
Future<SearchResponseDto?> search() async {
|
||||
final response = await searchWithHttpInfo();
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] q:
|
||||
///
|
||||
/// * [String] query:
|
||||
///
|
||||
/// * [bool] clip:
|
||||
///
|
||||
/// * [String] type:
|
||||
///
|
||||
/// * [bool] isFavorite:
|
||||
///
|
||||
/// * [String] exifInfoPeriodCity:
|
||||
///
|
||||
/// * [String] exifInfoPeriodState:
|
||||
///
|
||||
/// * [String] exifInfoPeriodCountry:
|
||||
///
|
||||
/// * [String] exifInfoPeriodMake:
|
||||
///
|
||||
/// * [String] exifInfoPeriodModel:
|
||||
///
|
||||
/// * [List<String>] smartInfoPeriodObjects:
|
||||
///
|
||||
/// * [List<String>] smartInfoPeriodTags:
|
||||
///
|
||||
/// * [bool] recent:
|
||||
///
|
||||
/// * [bool] motion:
|
||||
Future<SearchResponseDto?> search({ String? q, String? query, bool? clip, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, bool? recent, bool? motion, }) async {
|
||||
final response = await searchWithHttpInfo( q: q, query: query, clip: clip, type: type, isFavorite: isFavorite, exifInfoPeriodCity: exifInfoPeriodCity, exifInfoPeriodState: exifInfoPeriodState, exifInfoPeriodCountry: exifInfoPeriodCountry, exifInfoPeriodMake: exifInfoPeriodMake, exifInfoPeriodModel: exifInfoPeriodModel, smartInfoPeriodObjects: smartInfoPeriodObjects, smartInfoPeriodTags: smartInfoPeriodTags, recent: recent, motion: motion, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
2
mobile/openapi/test/search_api_test.dart
generated
2
mobile/openapi/test/search_api_test.dart
generated
@@ -33,7 +33,7 @@ void main() {
|
||||
|
||||
//
|
||||
//
|
||||
//Future<SearchResponseDto> search() async
|
||||
//Future<SearchResponseDto> search({ String q, String query, bool clip, String type, bool isFavorite, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags, bool recent, bool motion }) async
|
||||
test('test search', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.51.0+74
|
||||
version: 1.51.2+74
|
||||
isar_version: &isar_version 3.0.5
|
||||
|
||||
environment:
|
||||
|
||||
@@ -3,6 +3,14 @@ map $http_upgrade $connection_upgrade {
|
||||
'' close;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_proto $forwarded_protocol {
|
||||
default $scheme;
|
||||
|
||||
# Only allow the values 'http' and 'https' for the X-Forwarded-Proto header.
|
||||
http http;
|
||||
https https;
|
||||
}
|
||||
|
||||
upstream server {
|
||||
server ${IMMICH_SERVER_HOST};
|
||||
keepalive 2;
|
||||
@@ -43,13 +51,12 @@ server {
|
||||
proxy_force_ranges on;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $forwarded_protocol;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
rewrite /api/(.*) /$1 break;
|
||||
|
||||
@@ -64,13 +71,12 @@ server {
|
||||
proxy_force_ranges on;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Proto $forwarded_protocol;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_pass ${IMMICH_WEB_SCHEME}web;
|
||||
}
|
||||
|
||||
@@ -279,17 +279,20 @@ export class AssetService {
|
||||
/**
|
||||
* Serve file viewer on the web
|
||||
*/
|
||||
if (query.isWeb) {
|
||||
if (query.isWeb && asset.mimeType != 'image/gif') {
|
||||
res.set({
|
||||
'Content-Type': 'image/jpeg',
|
||||
});
|
||||
|
||||
if (!asset.resizePath) {
|
||||
Logger.error('Error serving IMAGE asset for web', 'ServeFile');
|
||||
throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile');
|
||||
}
|
||||
|
||||
if (await processETag(asset.resizePath, res, headers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
|
||||
fileReadStream = createReadStream(asset.resizePath);
|
||||
|
||||
@@ -299,7 +302,7 @@ export class AssetService {
|
||||
/**
|
||||
* Serve thumbnail image for both web and mobile app
|
||||
*/
|
||||
if (!query.isThumb && allowOriginalFile) {
|
||||
if ((!query.isThumb && allowOriginalFile) || (query.isWeb && asset.mimeType === 'image/gif')) {
|
||||
res.set({
|
||||
'Content-Type': asset.mimeType,
|
||||
});
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import { serverVersion } from '../../constants/server_version.constant';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ServerPingResponse } from './response-dto/server-ping-response.dto';
|
||||
import { ServerVersionReponseDto } from './response-dto/server-version-response.dto';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
|
||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||
|
||||
@ApiTags('Server Info')
|
||||
@Controller('server-info')
|
||||
export class ServerInfoController {
|
||||
constructor(private readonly serverInfoService: ServerInfoService) {}
|
||||
|
||||
@Get()
|
||||
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
return await this.serverInfoService.getServerInfo();
|
||||
}
|
||||
|
||||
@Get('/ping')
|
||||
async pingServer(): Promise<ServerPingResponse> {
|
||||
return new ServerPingResponse('pong');
|
||||
}
|
||||
|
||||
@Get('/version')
|
||||
async getServerVersion(): Promise<ServerVersionReponseDto> {
|
||||
return serverVersion;
|
||||
}
|
||||
|
||||
@Authenticated({ admin: true })
|
||||
@Get('/stats')
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
return await this.serverInfoService.getStats();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
import { ServerInfoController } from './server-info.controller';
|
||||
import { UserEntity } from '@app/infra';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserEntity])],
|
||||
controllers: [ServerInfoController],
|
||||
providers: [ServerInfoService],
|
||||
})
|
||||
export class ServerInfoModule {}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ServerInfoResponseDto } from './response-dto/server-info-response.dto';
|
||||
import diskusage from 'diskusage';
|
||||
import { ServerStatsResponseDto } from './response-dto/server-stats-response.dto';
|
||||
import { UsageByUserDto } from './response-dto/usage-by-user-response.dto';
|
||||
import { UserEntity } from '@app/infra';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { asHumanReadable } from '../../utils/human-readable.util';
|
||||
|
||||
@Injectable()
|
||||
export class ServerInfoService {
|
||||
constructor(
|
||||
@InjectRepository(UserEntity)
|
||||
private userRepository: Repository<UserEntity>,
|
||||
) {}
|
||||
|
||||
async getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
const diskInfo = await diskusage.check(APP_UPLOAD_LOCATION);
|
||||
|
||||
const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2);
|
||||
|
||||
const serverInfo = new ServerInfoResponseDto();
|
||||
serverInfo.diskAvailable = asHumanReadable(diskInfo.available);
|
||||
serverInfo.diskSize = asHumanReadable(diskInfo.total);
|
||||
serverInfo.diskUse = asHumanReadable(diskInfo.total - diskInfo.free);
|
||||
serverInfo.diskAvailableRaw = diskInfo.available;
|
||||
serverInfo.diskSizeRaw = diskInfo.total;
|
||||
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||
serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
type UserStatsQueryResponse = {
|
||||
userId: string;
|
||||
userFirstName: string;
|
||||
userLastName: string;
|
||||
photos: string;
|
||||
videos: string;
|
||||
usage: string;
|
||||
};
|
||||
|
||||
const userStatsQueryResponse: UserStatsQueryResponse[] = await this.userRepository
|
||||
.createQueryBuilder('users')
|
||||
.select('users.id', 'userId')
|
||||
.addSelect('users.firstName', 'userFirstName')
|
||||
.addSelect('users.lastName', 'userLastName')
|
||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
|
||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
|
||||
.addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage')
|
||||
.leftJoin('users.assets', 'assets')
|
||||
.leftJoin('assets.exifInfo', 'exif')
|
||||
.groupBy('users.id')
|
||||
.orderBy('users.createdAt', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
const usageByUser = userStatsQueryResponse.map((userStats) => {
|
||||
const usage = new UsageByUserDto();
|
||||
usage.userId = userStats.userId;
|
||||
usage.userFirstName = userStats.userFirstName;
|
||||
usage.userLastName = userStats.userLastName;
|
||||
usage.photos = Number(userStats.photos);
|
||||
usage.videos = Number(userStats.videos);
|
||||
usage.usage = Number(userStats.usage);
|
||||
|
||||
return usage;
|
||||
});
|
||||
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
usageByUser.forEach((user) => {
|
||||
serverStats.photos += user.photos;
|
||||
serverStats.videos += user.videos;
|
||||
serverStats.usage += user.usage;
|
||||
});
|
||||
serverStats.usageByUser = usageByUser;
|
||||
|
||||
return serverStats;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { immichAppConfig } from '@app/common/config';
|
||||
import { Module, OnModuleInit } from '@nestjs/common';
|
||||
import { AssetModule } from './api-v1/asset/asset.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
||||
import { AlbumModule } from './api-v1/album/album.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
JobController,
|
||||
OAuthController,
|
||||
SearchController,
|
||||
ServerInfoController,
|
||||
ShareController,
|
||||
SystemConfigController,
|
||||
UserController,
|
||||
@@ -34,8 +34,6 @@ import { AuthGuard } from './middlewares/auth.guard';
|
||||
|
||||
AssetModule,
|
||||
|
||||
ServerInfoModule,
|
||||
|
||||
AlbumModule,
|
||||
|
||||
ScheduleModule.forRoot(),
|
||||
@@ -52,6 +50,7 @@ import { AuthGuard } from './middlewares/auth.guard';
|
||||
JobController,
|
||||
OAuthController,
|
||||
SearchController,
|
||||
ServerInfoController,
|
||||
ShareController,
|
||||
SystemConfigController,
|
||||
UserController,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { APP_UPLOAD_LOCATION } from '@app/domain/domain.constant';
|
||||
import { BadRequestException, Logger, UnauthorizedException } from '@nestjs/common';
|
||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||
import { createHash, randomUUID } from 'crypto';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { APP_UPLOAD_LOCATION } from '@app/domain/domain.constant';
|
||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
|
||||
import { Request } from 'express';
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './device-info.controller';
|
||||
export * from './job.controller';
|
||||
export * from './oauth.controller';
|
||||
export * from './search.controller';
|
||||
export * from './server-info.controller';
|
||||
export * from './share.controller';
|
||||
export * from './system-config.controller';
|
||||
export * from './user.controller';
|
||||
|
||||
@@ -20,7 +20,7 @@ export class SearchController {
|
||||
@Get()
|
||||
async search(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Query(new ValidationPipe({ transform: true })) dto: SearchDto | any,
|
||||
@Query(new ValidationPipe({ transform: true })) dto: SearchDto,
|
||||
): Promise<SearchResponseDto> {
|
||||
return this.searchService.search(authUser, dto);
|
||||
}
|
||||
|
||||
37
server/apps/immich/src/controllers/server-info.controller.ts
Normal file
37
server/apps/immich/src/controllers/server-info.controller.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
ServerInfoResponseDto,
|
||||
ServerInfoService,
|
||||
ServerPingResponse,
|
||||
ServerStatsResponseDto,
|
||||
ServerVersionReponseDto,
|
||||
} from '@app/domain';
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||
|
||||
@ApiTags('Server Info')
|
||||
@Controller('server-info')
|
||||
export class ServerInfoController {
|
||||
constructor(private readonly service: ServerInfoService) {}
|
||||
|
||||
@Get()
|
||||
getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
return this.service.getInfo();
|
||||
}
|
||||
|
||||
@Get('/ping')
|
||||
pingServer(): ServerPingResponse {
|
||||
return this.service.ping();
|
||||
}
|
||||
|
||||
@Get('/version')
|
||||
getServerVersion(): ServerVersionReponseDto {
|
||||
return this.service.getVersion();
|
||||
}
|
||||
|
||||
@Authenticated({ admin: true })
|
||||
@Get('/stats')
|
||||
getStats(): Promise<ServerStatsResponseDto> {
|
||||
return this.service.getStats();
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,11 @@ import cookieParser from 'cookie-parser';
|
||||
import { writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
import { SERVER_VERSION } from './constants/server_version.constant';
|
||||
import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
||||
import { json } from 'body-parser';
|
||||
import { patchOpenAPI } from './utils/patch-open-api.util';
|
||||
import { getLogLevels, MACHINE_LEARNING_ENABLED } from '@app/common';
|
||||
import { IMMICH_ACCESS_COOKIE, SearchService } from '@app/domain';
|
||||
import { SERVER_VERSION, IMMICH_ACCESS_COOKIE, SearchService } from '@app/domain';
|
||||
|
||||
const logger = new Logger('ImmichServer');
|
||||
|
||||
@@ -20,7 +19,7 @@ async function bootstrap() {
|
||||
logger: getLogLevels(),
|
||||
});
|
||||
|
||||
app.set('trust proxy');
|
||||
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
|
||||
app.set('etag', 'strong');
|
||||
app.use(cookieParser());
|
||||
app.use(json({ limit: '10mb' }));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AssetEntity } from '@app/infra';
|
||||
import { BadRequestException, Injectable, InternalServerErrorException, Logger, StreamableFile } from '@nestjs/common';
|
||||
import archiver from 'archiver';
|
||||
import { extname } from 'path';
|
||||
import { asHumanReadable, HumanReadableSize } from '../../utils/human-readable.util';
|
||||
import { asHumanReadable, HumanReadableSize } from '@app/domain';
|
||||
|
||||
export interface DownloadArchive {
|
||||
stream: StreamableFile;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { SERVER_VERSION } from 'apps/immich/src/constants/server_version.constant';
|
||||
import { SERVER_VERSION } from '@app/domain';
|
||||
import { getLogLevels } from '@app/common';
|
||||
import { RedisIoAdapter } from '../../immich/src/middlewares/redis-io.adapter.middleware';
|
||||
import { MicroservicesModule } from './microservices.module';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common/constants';
|
||||
import { AssetEntity, AssetType } from '@app/infra';
|
||||
import {
|
||||
APP_UPLOAD_LOCATION,
|
||||
IAssetJob,
|
||||
IAssetRepository,
|
||||
IBaseJob,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
SystemConfigService,
|
||||
WithoutProperty,
|
||||
} from '@app/domain';
|
||||
import { AssetEntity, AssetType } from '@app/infra';
|
||||
import { Process, Processor } from '@nestjs/bull';
|
||||
import { Inject, Logger } from '@nestjs/common';
|
||||
import { Job } from 'bull';
|
||||
|
||||
@@ -620,7 +620,132 @@
|
||||
"get": {
|
||||
"operationId": "search",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "q",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "query",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "clip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"IMAGE",
|
||||
"VIDEO",
|
||||
"AUDIO",
|
||||
"OTHER"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isFavorite",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exifInfo.city",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exifInfo.state",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exifInfo.country",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exifInfo.make",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exifInfo.model",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "smartInfo.objects",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "smartInfo.tags",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "recent",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "motion",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -709,6 +834,102 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info": {
|
||||
"get": {
|
||||
"operationId": "getServerInfo",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerInfoResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/ping": {
|
||||
"get": {
|
||||
"operationId": "pingServer",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerPingResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/version": {
|
||||
"get": {
|
||||
"operationId": "getServerVersion",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerVersionReponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/stats": {
|
||||
"get": {
|
||||
"operationId": "getStats",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerStatsResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/share": {
|
||||
"get": {
|
||||
"operationId": "getAllSharedLinks",
|
||||
@@ -3145,108 +3366,12 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info": {
|
||||
"get": {
|
||||
"operationId": "getServerInfo",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerInfoResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/ping": {
|
||||
"get": {
|
||||
"operationId": "pingServer",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerPingResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/version": {
|
||||
"get": {
|
||||
"operationId": "getServerVersion",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerVersionReponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/server-info/stats": {
|
||||
"get": {
|
||||
"operationId": "getStats",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ServerStatsResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Server Info"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.51.0",
|
||||
"version": "1.51.2",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -4205,6 +4330,148 @@
|
||||
"items"
|
||||
]
|
||||
},
|
||||
"ServerInfoResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"diskSizeRaw": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"diskUseRaw": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"diskAvailableRaw": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"diskUsagePercentage": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"diskSize": {
|
||||
"type": "string"
|
||||
},
|
||||
"diskUse": {
|
||||
"type": "string"
|
||||
},
|
||||
"diskAvailable": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"diskSizeRaw",
|
||||
"diskUseRaw",
|
||||
"diskAvailableRaw",
|
||||
"diskUsagePercentage",
|
||||
"diskSize",
|
||||
"diskUse",
|
||||
"diskAvailable"
|
||||
]
|
||||
},
|
||||
"ServerPingResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"res": {
|
||||
"type": "string",
|
||||
"readOnly": true,
|
||||
"example": "pong"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"res"
|
||||
]
|
||||
},
|
||||
"ServerVersionReponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"major": {
|
||||
"type": "integer"
|
||||
},
|
||||
"minor": {
|
||||
"type": "integer"
|
||||
},
|
||||
"patch": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"major",
|
||||
"minor",
|
||||
"patch"
|
||||
]
|
||||
},
|
||||
"UsageByUserDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userFirstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"userLastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"photos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"videos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"usage": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"userId",
|
||||
"userFirstName",
|
||||
"userLastName",
|
||||
"photos",
|
||||
"videos",
|
||||
"usage"
|
||||
]
|
||||
},
|
||||
"ServerStatsResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"photos": {
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
},
|
||||
"videos": {
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
},
|
||||
"usage": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"format": "int64"
|
||||
},
|
||||
"usageByUser": {
|
||||
"default": [],
|
||||
"title": "Array of usage for each user",
|
||||
"example": [
|
||||
{
|
||||
"photos": 1,
|
||||
"videos": 1,
|
||||
"diskUsageRaw": 1
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UsageByUserDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"photos",
|
||||
"videos",
|
||||
"usage",
|
||||
"usageByUser"
|
||||
]
|
||||
},
|
||||
"SharedLinkType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -5146,148 +5413,6 @@
|
||||
"required": [
|
||||
"albumId"
|
||||
]
|
||||
},
|
||||
"ServerInfoResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"diskSizeRaw": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"diskUseRaw": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"diskAvailableRaw": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"diskUsagePercentage": {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
},
|
||||
"diskSize": {
|
||||
"type": "string"
|
||||
},
|
||||
"diskUse": {
|
||||
"type": "string"
|
||||
},
|
||||
"diskAvailable": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"diskSizeRaw",
|
||||
"diskUseRaw",
|
||||
"diskAvailableRaw",
|
||||
"diskUsagePercentage",
|
||||
"diskSize",
|
||||
"diskUse",
|
||||
"diskAvailable"
|
||||
]
|
||||
},
|
||||
"ServerPingResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"res": {
|
||||
"type": "string",
|
||||
"readOnly": true,
|
||||
"example": "pong"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"res"
|
||||
]
|
||||
},
|
||||
"ServerVersionReponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"major": {
|
||||
"type": "integer"
|
||||
},
|
||||
"minor": {
|
||||
"type": "integer"
|
||||
},
|
||||
"patch": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"major",
|
||||
"minor",
|
||||
"patch"
|
||||
]
|
||||
},
|
||||
"UsageByUserDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userFirstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"userLastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"photos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"videos": {
|
||||
"type": "integer"
|
||||
},
|
||||
"usage": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"userId",
|
||||
"userFirstName",
|
||||
"userLastName",
|
||||
"photos",
|
||||
"videos",
|
||||
"usage"
|
||||
]
|
||||
},
|
||||
"ServerStatsResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"photos": {
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
},
|
||||
"videos": {
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
},
|
||||
"usage": {
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"format": "int64"
|
||||
},
|
||||
"usageByUser": {
|
||||
"default": [],
|
||||
"title": "Array of usage for each user",
|
||||
"example": [
|
||||
{
|
||||
"photos": 1,
|
||||
"videos": 1,
|
||||
"diskUsageRaw": 1
|
||||
}
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UsageByUserDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"photos",
|
||||
"videos",
|
||||
"usage",
|
||||
"usageByUser"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
export * from './upload_location.constant';
|
||||
|
||||
export const MACHINE_LEARNING_URL = process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003';
|
||||
export const MACHINE_LEARNING_ENABLED = MACHINE_LEARNING_URL !== 'false';
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const APP_UPLOAD_LOCATION = './upload';
|
||||
@@ -1,4 +1,4 @@
|
||||
import pkg from 'package.json';
|
||||
import pkg from '../../../package.json';
|
||||
|
||||
const [major, minor, patch] = pkg.version.split('.');
|
||||
|
||||
@@ -15,3 +15,5 @@ export const serverVersion: IServerVersion = {
|
||||
};
|
||||
|
||||
export const SERVER_VERSION = `${serverVersion.major}.${serverVersion.minor}.${serverVersion.patch}`;
|
||||
|
||||
export const APP_UPLOAD_LOCATION = './upload';
|
||||
@@ -7,6 +7,7 @@ import { JobService } from './job';
|
||||
import { MediaService } from './media';
|
||||
import { OAuthService } from './oauth';
|
||||
import { SearchService } from './search';
|
||||
import { ServerInfoService } from './server-info';
|
||||
import { ShareService } from './share';
|
||||
import { SmartInfoService } from './smart-info';
|
||||
import { StorageService } from './storage';
|
||||
@@ -22,6 +23,7 @@ const providers: Provider[] = [
|
||||
JobService,
|
||||
MediaService,
|
||||
OAuthService,
|
||||
ServerInfoService,
|
||||
SmartInfoService,
|
||||
StorageService,
|
||||
StorageTemplateService,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { basename, extname } from 'node:path';
|
||||
|
||||
export function getFileNameWithoutExtension(path: string): string {
|
||||
return basename(path, extname(path));
|
||||
}
|
||||
|
||||
const KiB = Math.pow(1024, 1);
|
||||
const MiB = Math.pow(1024, 2);
|
||||
const GiB = Math.pow(1024, 3);
|
||||
@@ -5,11 +5,14 @@ export * from './auth';
|
||||
export * from './communication';
|
||||
export * from './crypto';
|
||||
export * from './device-info';
|
||||
export * from './domain.constant';
|
||||
export * from './domain.module';
|
||||
export * from './domain.util';
|
||||
export * from './job';
|
||||
export * from './media';
|
||||
export * from './oauth';
|
||||
export * from './search';
|
||||
export * from './server-info';
|
||||
export * from './share';
|
||||
export * from './smart-info';
|
||||
export * from './storage';
|
||||
@@ -18,4 +21,3 @@ export * from './system-config';
|
||||
export * from './tag';
|
||||
export * from './user';
|
||||
export * from './user-token';
|
||||
export * from './util';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
||||
import { AssetType } from '@app/infra/db/entities';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { join } from 'path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { IAssetRepository, mapAsset, WithoutProperty } from '../asset';
|
||||
import { CommunicationEvent, ICommunicationRepository } from '../communication';
|
||||
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||
import { IAssetJob, IBaseJob, IJobRepository, JobName } from '../job';
|
||||
import { IStorageRepository } from '../storage';
|
||||
import { IMediaRepository } from './media.repository';
|
||||
|
||||
@@ -173,12 +173,23 @@ describe(SearchService.name, () => {
|
||||
});
|
||||
|
||||
describe('handleIndexAssets', () => {
|
||||
it('should call done, even when there are no assets', async () => {
|
||||
assetMock.getAll.mockResolvedValue([]);
|
||||
|
||||
await sut.handleIndexAssets();
|
||||
|
||||
expect(searchMock.importAssets).toHaveBeenCalledWith([], true);
|
||||
});
|
||||
|
||||
it('should index all the assets', async () => {
|
||||
assetMock.getAll.mockResolvedValue([assetEntityStub.image]);
|
||||
|
||||
await sut.handleIndexAssets();
|
||||
|
||||
expect(searchMock.importAssets).toHaveBeenCalledWith([assetEntityStub.image], true);
|
||||
expect(searchMock.importAssets.mock.calls).toEqual([
|
||||
[[assetEntityStub.image], false],
|
||||
[[], true],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should log an error', async () => {
|
||||
|
||||
@@ -145,7 +145,14 @@ export class SearchService {
|
||||
// TODO: do this in batches based on searchIndexVersion
|
||||
const assets = this.patchAssets(await this.assetRepository.getAll({ isVisible: true }));
|
||||
this.logger.log(`Indexing ${assets.length} assets`);
|
||||
await this.searchRepository.importAssets(assets, true);
|
||||
|
||||
const chunkSize = 1000;
|
||||
for (let i = 0; i < assets.length; i += chunkSize) {
|
||||
await this.searchRepository.importAssets(assets.slice(i, i + chunkSize), false);
|
||||
}
|
||||
|
||||
await this.searchRepository.importAssets([], true);
|
||||
|
||||
this.logger.debug('Finished re-indexing all assets');
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Unable to index all assets`, error?.stack);
|
||||
|
||||
2
server/libs/domain/src/server-info/index.ts
Normal file
2
server/libs/domain/src/server-info/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './response-dto';
|
||||
export * from './server-info.service';
|
||||
5
server/libs/domain/src/server-info/response-dto/index.ts
Normal file
5
server/libs/domain/src/server-info/response-dto/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './server-info-response.dto';
|
||||
export * from './server-ping-response.dto';
|
||||
export * from './server-stats-response.dto';
|
||||
export * from './server-version-response.dto';
|
||||
export * from './usage-by-user-response.dto';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IServerVersion } from 'apps/immich/src/constants/server_version.constant';
|
||||
import { IServerVersion } from '@app/domain';
|
||||
|
||||
export class ServerVersionReponseDto implements IServerVersion {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
209
server/libs/domain/src/server-info/server-info.service.spec.ts
Normal file
209
server/libs/domain/src/server-info/server-info.service.spec.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { newStorageRepositoryMock, newUserRepositoryMock } from '../../test';
|
||||
import { serverVersion } from '../domain.constant';
|
||||
import { IStorageRepository } from '../storage';
|
||||
import { IUserRepository } from '../user';
|
||||
import { ServerInfoService } from './server-info.service';
|
||||
|
||||
describe(ServerInfoService.name, () => {
|
||||
let sut: ServerInfoService;
|
||||
let storageMock: jest.Mocked<IStorageRepository>;
|
||||
let userMock: jest.Mocked<IUserRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
|
||||
sut = new ServerInfoService(userMock, storageMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getInfo', () => {
|
||||
it('should return the disk space as B', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({ free: 200, available: 300, total: 500 });
|
||||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '300 B',
|
||||
diskAvailableRaw: 300,
|
||||
diskSize: '500 B',
|
||||
diskSizeRaw: 500,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '300 B',
|
||||
diskUseRaw: 300,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||
});
|
||||
|
||||
it('should return the disk space as KiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({ free: 200_000, available: 300_000, total: 500_000 });
|
||||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '293.0 KiB',
|
||||
diskAvailableRaw: 300000,
|
||||
diskSize: '488.3 KiB',
|
||||
diskSizeRaw: 500000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '293.0 KiB',
|
||||
diskUseRaw: 300000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||
});
|
||||
|
||||
it('should return the disk space as MiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({ free: 200_000_000, available: 300_000_000, total: 500_000_000 });
|
||||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '286.1 MiB',
|
||||
diskAvailableRaw: 300000000,
|
||||
diskSize: '476.8 MiB',
|
||||
diskSizeRaw: 500000000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '286.1 MiB',
|
||||
diskUseRaw: 300000000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||
});
|
||||
|
||||
it('should return the disk space as GiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({
|
||||
free: 200_000_000_000,
|
||||
available: 300_000_000_000,
|
||||
total: 500_000_000_000,
|
||||
});
|
||||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '279.4 GiB',
|
||||
diskAvailableRaw: 300000000000,
|
||||
diskSize: '465.7 GiB',
|
||||
diskSizeRaw: 500000000000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '279.4 GiB',
|
||||
diskUseRaw: 300000000000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||
});
|
||||
|
||||
it('should return the disk space as TiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({
|
||||
free: 200_000_000_000_000,
|
||||
available: 300_000_000_000_000,
|
||||
total: 500_000_000_000_000,
|
||||
});
|
||||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '272.8 TiB',
|
||||
diskAvailableRaw: 300000000000000,
|
||||
diskSize: '454.7 TiB',
|
||||
diskSizeRaw: 500000000000000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '272.8 TiB',
|
||||
diskUseRaw: 300000000000000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||
});
|
||||
|
||||
it('should return the disk space as PiB', async () => {
|
||||
storageMock.checkDiskUsage.mockResolvedValue({
|
||||
free: 200_000_000_000_000_000,
|
||||
available: 300_000_000_000_000_000,
|
||||
total: 500_000_000_000_000_000,
|
||||
});
|
||||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '266.5 PiB',
|
||||
diskAvailableRaw: 300000000000000000,
|
||||
diskSize: '444.1 PiB',
|
||||
diskSizeRaw: 500000000000000000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '266.5 PiB',
|
||||
diskUseRaw: 300000000000000000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('./upload');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ping', () => {
|
||||
it('should respond with pong', () => {
|
||||
expect(sut.ping()).toEqual({ res: 'pong' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersion', () => {
|
||||
it('should respond the server version', () => {
|
||||
expect(sut.getVersion()).toEqual(serverVersion);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should total up usage by user', async () => {
|
||||
userMock.getUserStats.mockResolvedValue([
|
||||
{
|
||||
userId: 'user1',
|
||||
userFirstName: '1',
|
||||
userLastName: 'User',
|
||||
photos: 10,
|
||||
videos: 11,
|
||||
usage: 12345,
|
||||
},
|
||||
{
|
||||
userId: 'user2',
|
||||
userFirstName: '2',
|
||||
userLastName: 'User',
|
||||
photos: 10,
|
||||
videos: 20,
|
||||
usage: 123456,
|
||||
},
|
||||
{
|
||||
userId: 'user3',
|
||||
userFirstName: '3',
|
||||
userLastName: 'User',
|
||||
photos: 100,
|
||||
videos: 0,
|
||||
usage: 987654,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.getStats()).resolves.toEqual({
|
||||
photos: 120,
|
||||
videos: 31,
|
||||
usage: 1123455,
|
||||
usageByUser: [
|
||||
{
|
||||
photos: 10,
|
||||
usage: 12345,
|
||||
userFirstName: '1',
|
||||
userId: 'user1',
|
||||
userLastName: 'User',
|
||||
videos: 11,
|
||||
},
|
||||
{
|
||||
photos: 10,
|
||||
usage: 123456,
|
||||
userFirstName: '2',
|
||||
userId: 'user2',
|
||||
userLastName: 'User',
|
||||
videos: 20,
|
||||
},
|
||||
{
|
||||
photos: 100,
|
||||
usage: 987654,
|
||||
userFirstName: '3',
|
||||
userId: 'user3',
|
||||
userLastName: 'User',
|
||||
videos: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(userMock.getUserStats).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
60
server/libs/domain/src/server-info/server-info.service.ts
Normal file
60
server/libs/domain/src/server-info/server-info.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { APP_UPLOAD_LOCATION, serverVersion } from '../domain.constant';
|
||||
import { asHumanReadable } from '../domain.util';
|
||||
import { IStorageRepository } from '../storage';
|
||||
import { IUserRepository, UserStatsQueryResponse } from '../user';
|
||||
import { ServerInfoResponseDto, ServerPingResponse, ServerStatsResponseDto, UsageByUserDto } from './response-dto';
|
||||
|
||||
@Injectable()
|
||||
export class ServerInfoService {
|
||||
constructor(
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
) {}
|
||||
|
||||
async getInfo(): Promise<ServerInfoResponseDto> {
|
||||
const diskInfo = await this.storageRepository.checkDiskUsage(APP_UPLOAD_LOCATION);
|
||||
|
||||
const usagePercentage = (((diskInfo.total - diskInfo.free) / diskInfo.total) * 100).toFixed(2);
|
||||
|
||||
const serverInfo = new ServerInfoResponseDto();
|
||||
serverInfo.diskAvailable = asHumanReadable(diskInfo.available);
|
||||
serverInfo.diskSize = asHumanReadable(diskInfo.total);
|
||||
serverInfo.diskUse = asHumanReadable(diskInfo.total - diskInfo.free);
|
||||
serverInfo.diskAvailableRaw = diskInfo.available;
|
||||
serverInfo.diskSizeRaw = diskInfo.total;
|
||||
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||
serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
ping(): ServerPingResponse {
|
||||
return new ServerPingResponse('pong');
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
return serverVersion;
|
||||
}
|
||||
|
||||
async getStats(): Promise<ServerStatsResponseDto> {
|
||||
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
|
||||
for (const user of userStats) {
|
||||
const usage = new UsageByUserDto();
|
||||
usage.userId = user.userId;
|
||||
usage.userFirstName = user.userFirstName;
|
||||
usage.userLastName = user.userLastName;
|
||||
usage.photos = user.photos;
|
||||
usage.videos = user.videos;
|
||||
usage.usage = user.usage;
|
||||
|
||||
serverStats.photos += usage.photos;
|
||||
serverStats.videos += usage.videos;
|
||||
serverStats.usage += usage.usage;
|
||||
serverStats.usageByUser.push(usage);
|
||||
}
|
||||
|
||||
return serverStats;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
||||
import {
|
||||
IStorageRepository,
|
||||
ISystemConfigRepository,
|
||||
@@ -15,6 +14,7 @@ import handlebar from 'handlebars';
|
||||
import * as luxon from 'luxon';
|
||||
import path from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||
|
||||
export class StorageTemplateCore {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
||||
import { AssetEntity, SystemConfig } from '@app/infra/db/entities';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { IAssetRepository } from '../asset/asset.repository';
|
||||
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||
import { IStorageRepository } from '../storage/storage.repository';
|
||||
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
|
||||
import { StorageTemplateCore } from './storage-template.core';
|
||||
|
||||
@@ -6,6 +6,12 @@ export interface ImmichReadStream {
|
||||
length: number;
|
||||
}
|
||||
|
||||
export interface DiskUsage {
|
||||
available: number;
|
||||
free: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const IStorageRepository = 'IStorageRepository';
|
||||
|
||||
export interface IStorageRepository {
|
||||
@@ -16,4 +22,5 @@ export interface IStorageRepository {
|
||||
moveFile(source: string, target: string): Promise<void>;
|
||||
checkFileExists(filepath: string): Promise<boolean>;
|
||||
mkdirSync(filepath: string): void;
|
||||
checkDiskUsage(folder: string): Promise<DiskUsage>;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,15 @@ export interface UserListFilter {
|
||||
excludeId?: string;
|
||||
}
|
||||
|
||||
export interface UserStatsQueryResponse {
|
||||
userId: string;
|
||||
userFirstName: string;
|
||||
userLastName: string;
|
||||
photos: number;
|
||||
videos: number;
|
||||
usage: number;
|
||||
}
|
||||
|
||||
export const IUserRepository = 'IUserRepository';
|
||||
|
||||
export interface IUserRepository {
|
||||
@@ -13,6 +22,7 @@ export interface IUserRepository {
|
||||
getByOAuthId(oauthId: string): Promise<UserEntity | null>;
|
||||
getDeletedUsers(): Promise<UserEntity[]>;
|
||||
getList(filter?: UserListFilter): Promise<UserEntity[]>;
|
||||
getUserStats(): Promise<UserStatsQueryResponse[]>;
|
||||
create(user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
||||
|
||||
@@ -3,12 +3,12 @@ import { BadRequestException, Inject, Injectable, Logger, NotFoundException } fr
|
||||
import { randomBytes } from 'crypto';
|
||||
import { ReadStream } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { APP_UPLOAD_LOCATION } from '@app/common';
|
||||
import { IAlbumRepository } from '../album/album.repository';
|
||||
import { IKeyRepository } from '../api-key/api-key.repository';
|
||||
import { IAssetRepository } from '../asset/asset.repository';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { ICryptoRepository } from '../crypto/crypto.repository';
|
||||
import { APP_UPLOAD_LOCATION } from '../domain.constant';
|
||||
import { IJobRepository, IUserDeletionJob, JobName } from '../job';
|
||||
import { IStorageRepository } from '../storage/storage.repository';
|
||||
import { IUserTokenRepository } from '../user-token/user-token.repository';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { basename, extname } from 'node:path';
|
||||
|
||||
export function getFileNameWithoutExtension(path: string): string {
|
||||
return basename(path, extname(path));
|
||||
}
|
||||
@@ -9,5 +9,6 @@ export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
|
||||
moveFile: jest.fn(),
|
||||
checkFileExists: jest.fn(),
|
||||
mkdirSync: jest.fn(),
|
||||
checkDiskUsage: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export const newUserRepositoryMock = (): jest.Mocked<IUserRepository> => {
|
||||
getAdmin: jest.fn(),
|
||||
getByEmail: jest.fn(),
|
||||
getByOAuthId: jest.fn(),
|
||||
getUserStats: jest.fn(),
|
||||
getList: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UserEntity } from '../entities';
|
||||
import { IUserRepository, UserListFilter } from '@app/domain';
|
||||
import { IUserRepository, UserListFilter, UserStatsQueryResponse } from '@app/domain';
|
||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { IsNull, Not, Repository } from 'typeorm';
|
||||
@@ -76,4 +76,28 @@ export class UserRepository implements IUserRepository {
|
||||
async restore(user: UserEntity): Promise<UserEntity> {
|
||||
return this.userRepository.recover(user);
|
||||
}
|
||||
|
||||
async getUserStats(): Promise<UserStatsQueryResponse[]> {
|
||||
const stats = await this.userRepository
|
||||
.createQueryBuilder('users')
|
||||
.select('users.id', 'userId')
|
||||
.addSelect('users.firstName', 'userFirstName')
|
||||
.addSelect('users.lastName', 'userLastName')
|
||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos')
|
||||
.addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos')
|
||||
.addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage')
|
||||
.leftJoin('users.assets', 'assets')
|
||||
.leftJoin('assets.exifInfo', 'exif')
|
||||
.groupBy('users.id')
|
||||
.orderBy('users.createdAt', 'ASC')
|
||||
.getRawMany();
|
||||
|
||||
for (const stat of stats) {
|
||||
stat.photos = Number(stat.photos);
|
||||
stat.videos = Number(stat.videos);
|
||||
stat.usage = Number(stat.usage);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@app/domain';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import _, { Dictionary } from 'lodash';
|
||||
import { filter, firstValueFrom, from, map, mergeMap, toArray } from 'rxjs';
|
||||
import { catchError, filter, firstValueFrom, from, map, mergeMap, of, toArray } from 'rxjs';
|
||||
import { Client } from 'typesense';
|
||||
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
|
||||
import { DocumentSchema, SearchResponse } from 'typesense/lib/Typesense/Documents';
|
||||
@@ -148,7 +148,7 @@ export class TypesenseRepository implements ISearchRepository {
|
||||
|
||||
const common = {
|
||||
q: '*',
|
||||
filter_by: `ownerId:${userId}`,
|
||||
filter_by: this.buildFilterBy('ownerId', userId, true),
|
||||
per_page: 100,
|
||||
};
|
||||
|
||||
@@ -157,8 +157,8 @@ export class TypesenseRepository implements ISearchRepository {
|
||||
const { facet_counts: facets } = await asset$.search({
|
||||
...common,
|
||||
query_by: 'exifInfo.imageName',
|
||||
facet_by: this.getFacetFieldNames(SearchCollection.ASSETS),
|
||||
max_facet_values: 50,
|
||||
facet_by: 'exifInfo.city,smartInfo.objects',
|
||||
max_facet_values: 12,
|
||||
});
|
||||
|
||||
return firstValueFrom(
|
||||
@@ -166,23 +166,31 @@ export class TypesenseRepository implements ISearchRepository {
|
||||
mergeMap(
|
||||
(facet) =>
|
||||
from(facet.counts).pipe(
|
||||
mergeMap(
|
||||
(count) =>
|
||||
from(
|
||||
asset$.search({
|
||||
...common,
|
||||
query_by: 'exifInfo.imageName',
|
||||
filter_by: `${facet.field_name}:${count.value}`,
|
||||
}),
|
||||
).pipe(
|
||||
map((result) => ({
|
||||
value: count.value,
|
||||
data: result.hits?.[0]?.document as AssetEntity,
|
||||
})),
|
||||
filter((item) => !!item.data),
|
||||
),
|
||||
5,
|
||||
),
|
||||
mergeMap((count) => {
|
||||
const config = {
|
||||
...common,
|
||||
query_by: 'exifInfo.imageName',
|
||||
filter_by: [
|
||||
this.buildFilterBy('ownerId', userId, true),
|
||||
this.buildFilterBy(facet.field_name, count.value, true),
|
||||
].join(' && '),
|
||||
per_page: 1,
|
||||
};
|
||||
|
||||
this.logger.verbose(`Explore subquery: "filter_by:${config.filter_by}" (count:${count.count})`);
|
||||
|
||||
return from(asset$.search(config)).pipe(
|
||||
catchError((error: any) => {
|
||||
this.logger.warn(`Explore subquery error: ${error}`, error?.stack);
|
||||
return of({ hits: [] });
|
||||
}),
|
||||
map((result) => ({
|
||||
value: count.value,
|
||||
data: result.hits?.[0]?.document as AssetEntity,
|
||||
})),
|
||||
filter((item) => !!item.data),
|
||||
);
|
||||
}, 5),
|
||||
toArray(),
|
||||
map((items) => ({
|
||||
fieldName: facet.field_name as string,
|
||||
@@ -208,7 +216,7 @@ export class TypesenseRepository implements ISearchRepository {
|
||||
await this.client
|
||||
.collections(schemaMap[collection].name)
|
||||
.documents()
|
||||
.delete({ filter_by: `id: [${ids.join(',')}]` });
|
||||
.delete({ filter_by: this.buildFilterBy('id', ids, true) });
|
||||
}
|
||||
|
||||
async searchAlbums(query: string, filters: SearchFilter): Promise<SearchResult<AlbumEntity>> {
|
||||
@@ -350,38 +358,61 @@ export class TypesenseRepository implements ISearchRepository {
|
||||
|
||||
private getAlbumFilters(filters: SearchFilter) {
|
||||
const { userId } = filters;
|
||||
const _filters = [`ownerId:${userId}`];
|
||||
|
||||
const _filters = [this.buildFilterBy('ownerId', userId, true)];
|
||||
|
||||
if (filters.id) {
|
||||
_filters.push(`id:=${filters.id}`);
|
||||
_filters.push(this.buildFilterBy('id', filters.id, true));
|
||||
}
|
||||
|
||||
for (const item of albumSchema.fields || []) {
|
||||
let value = filters[item.name as keyof SearchFilter];
|
||||
if (Array.isArray(value)) {
|
||||
value = `[${value.join(',')}]`;
|
||||
}
|
||||
const value = filters[item.name as keyof SearchFilter];
|
||||
if (item.facet && value !== undefined) {
|
||||
_filters.push(`${item.name}:${value}`);
|
||||
_filters.push(this.buildFilterBy(item.name, value));
|
||||
}
|
||||
}
|
||||
|
||||
return _filters.join(' && ');
|
||||
const result = _filters.join(' && ');
|
||||
|
||||
this.logger.debug(`Album filters are: ${result}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getAssetFilters(filters: SearchFilter) {
|
||||
const _filters = [`ownerId:${filters.userId}`];
|
||||
const { userId } = filters;
|
||||
const _filters = [this.buildFilterBy('ownerId', userId, true)];
|
||||
|
||||
if (filters.id) {
|
||||
_filters.push(`id:=${filters.id}`);
|
||||
_filters.push(this.buildFilterBy('id', filters.id, true));
|
||||
}
|
||||
|
||||
for (const item of assetSchema.fields || []) {
|
||||
let value = filters[item.name as keyof SearchFilter];
|
||||
if (Array.isArray(value)) {
|
||||
value = `[${value.join(',')}]`;
|
||||
}
|
||||
const value = filters[item.name as keyof SearchFilter];
|
||||
if (item.facet && value !== undefined) {
|
||||
_filters.push(`${item.name}:${value}`);
|
||||
_filters.push(this.buildFilterBy(item.name, value));
|
||||
}
|
||||
}
|
||||
return _filters.join(' && ');
|
||||
|
||||
const result = _filters.join(' && ');
|
||||
|
||||
this.logger.debug(`Asset filters are: ${result}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private buildFilterBy(key: string, values: boolean | string | string[], exact?: boolean) {
|
||||
const token = exact ? ':=' : ':';
|
||||
|
||||
const _values = (Array.isArray(values) ? values : [values]).map((value) => {
|
||||
if (typeof value === 'boolean' || value === 'true' || value === 'false') {
|
||||
return value;
|
||||
}
|
||||
return '`' + value + '`';
|
||||
});
|
||||
|
||||
const value = _values.length > 1 ? `[${_values.join(',')}]` : _values[0];
|
||||
|
||||
return `${key}${token}${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ImmichReadStream, IStorageRepository } from '@app/domain';
|
||||
import { DiskUsage, ImmichReadStream, IStorageRepository } from '@app/domain';
|
||||
import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import mv from 'mv';
|
||||
import { promisify } from 'node:util';
|
||||
import diskUsage from 'diskusage';
|
||||
import path from 'path';
|
||||
|
||||
const moveFile = promisify<string, string, mv.Options>(mv);
|
||||
@@ -66,4 +67,8 @@ export class FilesystemProvider implements IStorageRepository {
|
||||
mkdirSync(filepath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
checkDiskUsage(folder: string): Promise<DiskUsage> {
|
||||
return diskUsage.check(folder);
|
||||
}
|
||||
}
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.51.0",
|
||||
"version": "1.51.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.50.1",
|
||||
"version": "1.51.0",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.51.0",
|
||||
"version": "1.51.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -129,7 +129,7 @@
|
||||
"rootDir": ".",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s",
|
||||
@@ -137,10 +137,6 @@
|
||||
],
|
||||
"coverageDirectory": "./coverage",
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"lines": 17,
|
||||
"statements": 17
|
||||
},
|
||||
"./libs/domain/": {
|
||||
"branches": 80,
|
||||
"functions": 85,
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
"paths": {
|
||||
"@app/common": ["libs/common/src"],
|
||||
"@app/common/*": ["libs/common/src/*"],
|
||||
"@app/storage": ["libs/storage/src"],
|
||||
"@app/storage/*": ["libs/storage/src/*"],
|
||||
"@app/infra": ["libs/infra/src"],
|
||||
"@app/infra/*": ["libs/infra/src/*"],
|
||||
"@app/domain": ["libs/domain/src"],
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
export PUBLIC_IMMICH_SERVER_URL=$IMMICH_SERVER_URL
|
||||
export PUBLIC_IMMICH_API_URL_EXTERNAL=$IMMICH_API_URL_EXTERNAL
|
||||
|
||||
export PROTOCOL_HEADER=X-Forwarded-Proto
|
||||
|
||||
if [ "$(id -u)" -eq 0 ] && [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups node /usr/src/app/build/index.js
|
||||
else
|
||||
|
||||
128
web/src/api/open-api/api.ts
generated
128
web/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.50.1
|
||||
* The version of the OpenAPI document: 1.51.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
@@ -6761,10 +6761,24 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [q]
|
||||
* @param {string} [query]
|
||||
* @param {boolean} [clip]
|
||||
* @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type]
|
||||
* @param {boolean} [isFavorite]
|
||||
* @param {string} [exifInfoCity]
|
||||
* @param {string} [exifInfoState]
|
||||
* @param {string} [exifInfoCountry]
|
||||
* @param {string} [exifInfoMake]
|
||||
* @param {string} [exifInfoModel]
|
||||
* @param {Array<string>} [smartInfoObjects]
|
||||
* @param {Array<string>} [smartInfoTags]
|
||||
* @param {boolean} [recent]
|
||||
* @param {boolean} [motion]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
search: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
search: async (q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/search`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@@ -6783,6 +6797,62 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
if (q !== undefined) {
|
||||
localVarQueryParameter['q'] = q;
|
||||
}
|
||||
|
||||
if (query !== undefined) {
|
||||
localVarQueryParameter['query'] = query;
|
||||
}
|
||||
|
||||
if (clip !== undefined) {
|
||||
localVarQueryParameter['clip'] = clip;
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
localVarQueryParameter['type'] = type;
|
||||
}
|
||||
|
||||
if (isFavorite !== undefined) {
|
||||
localVarQueryParameter['isFavorite'] = isFavorite;
|
||||
}
|
||||
|
||||
if (exifInfoCity !== undefined) {
|
||||
localVarQueryParameter['exifInfo.city'] = exifInfoCity;
|
||||
}
|
||||
|
||||
if (exifInfoState !== undefined) {
|
||||
localVarQueryParameter['exifInfo.state'] = exifInfoState;
|
||||
}
|
||||
|
||||
if (exifInfoCountry !== undefined) {
|
||||
localVarQueryParameter['exifInfo.country'] = exifInfoCountry;
|
||||
}
|
||||
|
||||
if (exifInfoMake !== undefined) {
|
||||
localVarQueryParameter['exifInfo.make'] = exifInfoMake;
|
||||
}
|
||||
|
||||
if (exifInfoModel !== undefined) {
|
||||
localVarQueryParameter['exifInfo.model'] = exifInfoModel;
|
||||
}
|
||||
|
||||
if (smartInfoObjects) {
|
||||
localVarQueryParameter['smartInfo.objects'] = smartInfoObjects;
|
||||
}
|
||||
|
||||
if (smartInfoTags) {
|
||||
localVarQueryParameter['smartInfo.tags'] = smartInfoTags;
|
||||
}
|
||||
|
||||
if (recent !== undefined) {
|
||||
localVarQueryParameter['recent'] = recent;
|
||||
}
|
||||
|
||||
if (motion !== undefined) {
|
||||
localVarQueryParameter['motion'] = motion;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
@@ -6824,11 +6894,25 @@ export const SearchApiFp = function(configuration?: Configuration) {
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [q]
|
||||
* @param {string} [query]
|
||||
* @param {boolean} [clip]
|
||||
* @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type]
|
||||
* @param {boolean} [isFavorite]
|
||||
* @param {string} [exifInfoCity]
|
||||
* @param {string} [exifInfoState]
|
||||
* @param {string} [exifInfoCountry]
|
||||
* @param {string} [exifInfoMake]
|
||||
* @param {string} [exifInfoModel]
|
||||
* @param {Array<string>} [smartInfoObjects]
|
||||
* @param {Array<string>} [smartInfoTags]
|
||||
* @param {boolean} [recent]
|
||||
* @param {boolean} [motion]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async search(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.search(options);
|
||||
async search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
@@ -6859,11 +6943,25 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [q]
|
||||
* @param {string} [query]
|
||||
* @param {boolean} [clip]
|
||||
* @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type]
|
||||
* @param {boolean} [isFavorite]
|
||||
* @param {string} [exifInfoCity]
|
||||
* @param {string} [exifInfoState]
|
||||
* @param {string} [exifInfoCountry]
|
||||
* @param {string} [exifInfoMake]
|
||||
* @param {string} [exifInfoModel]
|
||||
* @param {Array<string>} [smartInfoObjects]
|
||||
* @param {Array<string>} [smartInfoTags]
|
||||
* @param {boolean} [recent]
|
||||
* @param {boolean} [motion]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
search(options?: any): AxiosPromise<SearchResponseDto> {
|
||||
return localVarFp.search(options).then((request) => request(axios, basePath));
|
||||
search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: any): AxiosPromise<SearchResponseDto> {
|
||||
return localVarFp.search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -6897,12 +6995,26 @@ export class SearchApi extends BaseAPI {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [q]
|
||||
* @param {string} [query]
|
||||
* @param {boolean} [clip]
|
||||
* @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type]
|
||||
* @param {boolean} [isFavorite]
|
||||
* @param {string} [exifInfoCity]
|
||||
* @param {string} [exifInfoState]
|
||||
* @param {string} [exifInfoCountry]
|
||||
* @param {string} [exifInfoMake]
|
||||
* @param {string} [exifInfoModel]
|
||||
* @param {Array<string>} [smartInfoObjects]
|
||||
* @param {Array<string>} [smartInfoTags]
|
||||
* @param {boolean} [recent]
|
||||
* @param {boolean} [motion]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof SearchApi
|
||||
*/
|
||||
public search(options?: AxiosRequestConfig) {
|
||||
return SearchApiFp(this.configuration).search(options).then((request) => request(this.axios, this.basePath));
|
||||
public search(q?: string, query?: string, clip?: boolean, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, recent?: boolean, motion?: boolean, options?: AxiosRequestConfig) {
|
||||
return SearchApiFp(this.configuration).search(q, query, clip, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, recent, motion, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.50.1
|
||||
* The version of the OpenAPI document: 1.51.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.50.1
|
||||
* The version of the OpenAPI document: 1.51.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.50.1
|
||||
* The version of the OpenAPI document: 1.51.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.50.1
|
||||
* The version of the OpenAPI document: 1.51.1
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data)
|
||||
]);
|
||||
|
||||
selectedPreset = templateOptions.presetOptions[0];
|
||||
selectedPreset = savedConfig.template;
|
||||
}
|
||||
|
||||
const getSupportDateTimeFormat = async () => {
|
||||
|
||||
@@ -9,7 +9,23 @@ export const load = (async ({ locals, parent, url }) => {
|
||||
|
||||
const term = url.searchParams.get('q') || url.searchParams.get('query') || undefined;
|
||||
|
||||
const { data: results } = await locals.api.searchApi.search({ params: url.searchParams });
|
||||
const { data: results } = await locals.api.searchApi.search(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ params: url.searchParams }
|
||||
);
|
||||
|
||||
return {
|
||||
user,
|
||||
|
||||
Reference in New Issue
Block a user