Compare commits
1 Commits
v1.116.2
...
fix/scroll
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e628e6a807 |
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.22",
|
"version": "2.2.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.22",
|
"version": "2.2.20",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.22",
|
"version": "2.2.20",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin
|
|||||||
|
|
||||||
### How does smart search work?
|
### How does smart search work?
|
||||||
|
|
||||||
Immich uses CLIP models. An ML model converts each image to an "embedding", which is essentially a string of numbers that semantically encodes what is in the image. The same is done for the text that you enter when you do a search, and that text embedding is then compared with those of the images to find similar ones. As such, there are no "tags", "labels", or "descriptions" generated that you can look at. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
|
Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
|
||||||
|
|
||||||
### How does facial recognition work?
|
### How does facial recognition work?
|
||||||
|
|
||||||
|
|||||||
8
docs/static/archived-versions.json
vendored
8
docs/static/archived-versions.json
vendored
@@ -1,12 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"label": "v1.116.2",
|
|
||||||
"url": "https://v1.116.2.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "v1.116.1",
|
|
||||||
"url": "https://v1.116.1.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "v1.116.0",
|
"label": "v1.116.0",
|
||||||
"url": "https://v1.116.0.archive.immich.app"
|
"url": "https://v1.116.0.archive.immich.app"
|
||||||
|
|||||||
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"../cli": {
|
"../cli": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.22",
|
"version": "2.2.20",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.116.2"
|
version = "1.116.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 161,
|
"android.injected.version.code" => 160,
|
||||||
"android.injected.version.name" => "1.116.2",
|
"android.injected.version.name" => "1.116.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
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')
|
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')
|
||||||
|
|||||||
@@ -401,7 +401,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 177;
|
CURRENT_PROJECT_VERSION = 176;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -543,7 +543,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 177;
|
CURRENT_PROJECT_VERSION = 176;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -571,7 +571,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 177;
|
CURRENT_PROJECT_VERSION = 176;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -58,11 +58,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.116.1</string>
|
<string>1.116.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>177</string>
|
<string>176</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Release"
|
desc "iOS Release"
|
||||||
lane :release do
|
lane :release do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.116.2"
|
version_number: "1.116.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -4,7 +4,4 @@ abstract interface class IAssetMediaRepository {
|
|||||||
Future<List<String>> deleteAll(List<String> ids);
|
Future<List<String>> deleteAll(List<String> ids);
|
||||||
|
|
||||||
Future<Asset?> get(String id);
|
Future<Asset?> get(String id);
|
||||||
|
|
||||||
/// Obtaining the correct original filename of the asset
|
|
||||||
Future<String?> getOriginalFilename(String id);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,17 +43,4 @@ class AssetMediaRepository implements IAssetMediaRepository {
|
|||||||
asset.local = local;
|
asset.local = local;
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<String?> getOriginalFilename(String id) async {
|
|
||||||
final entity = await AssetEntity.fromId(id);
|
|
||||||
|
|
||||||
if (entity == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// titleAsync gets the correct original filename for some assets on iOS
|
|
||||||
// otherwise using the `entity.title` would return a random GUID
|
|
||||||
return await entity.titleAsync;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
|||||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
|
||||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
@@ -369,7 +368,6 @@ class BackgroundService {
|
|||||||
BackupRepository backupAlbumRepository = BackupRepository(db);
|
BackupRepository backupAlbumRepository = BackupRepository(db);
|
||||||
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||||
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
||||||
AssetMediaRepository assetMediaRepository = AssetMediaRepository();
|
|
||||||
UserRepository userRepository = UserRepository(db);
|
UserRepository userRepository = UserRepository(db);
|
||||||
UserApiRepository userApiRepository =
|
UserApiRepository userApiRepository =
|
||||||
UserApiRepository(apiService.usersApi);
|
UserApiRepository(apiService.usersApi);
|
||||||
@@ -411,7 +409,6 @@ class BackgroundService {
|
|||||||
albumService,
|
albumService,
|
||||||
albumMediaRepository,
|
albumMediaRepository,
|
||||||
fileMediaRepository,
|
fileMediaRepository,
|
||||||
assetMediaRepository,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
|
|||||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
|
||||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||||
@@ -22,7 +21,6 @@ import 'package:immich_mobile/providers/api.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
@@ -42,7 +40,6 @@ final backupServiceProvider = Provider(
|
|||||||
ref.watch(albumServiceProvider),
|
ref.watch(albumServiceProvider),
|
||||||
ref.watch(albumMediaRepositoryProvider),
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
ref.watch(fileMediaRepositoryProvider),
|
ref.watch(fileMediaRepositoryProvider),
|
||||||
ref.watch(assetMediaRepositoryProvider),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,7 +52,6 @@ class BackupService {
|
|||||||
final AlbumService _albumService;
|
final AlbumService _albumService;
|
||||||
final IAlbumMediaRepository _albumMediaRepository;
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
final IFileMediaRepository _fileMediaRepository;
|
final IFileMediaRepository _fileMediaRepository;
|
||||||
final IAssetMediaRepository _assetMediaRepository;
|
|
||||||
|
|
||||||
BackupService(
|
BackupService(
|
||||||
this._apiService,
|
this._apiService,
|
||||||
@@ -64,7 +60,6 @@ class BackupService {
|
|||||||
this._albumService,
|
this._albumService,
|
||||||
this._albumMediaRepository,
|
this._albumMediaRepository,
|
||||||
this._fileMediaRepository,
|
this._fileMediaRepository,
|
||||||
this._assetMediaRepository,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<String>?> getDeviceBackupAsset() async {
|
Future<List<String>?> getDeviceBackupAsset() async {
|
||||||
@@ -334,9 +329,7 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
String? originalFileName =
|
String originalFileName = asset.fileName;
|
||||||
await _assetMediaRepository.getOriginalFilename(asset.localId!);
|
|
||||||
originalFileName ??= asset.fileName;
|
|
||||||
|
|
||||||
if (asset.local!.isLivePhoto) {
|
if (asset.local!.isLivePhoto) {
|
||||||
if (livePhotoFile == null) {
|
if (livePhotoFile == null) {
|
||||||
|
|||||||
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:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.116.2
|
- API version: 1.116.0
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.116.2+161
|
version: 1.116.0+160
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|||||||
@@ -7409,7 +7409,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
|||||||
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.116.2
|
* 1.116.0
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,82 +9,76 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../design/immich-logo-stacked-light.svg" width="150" title="Immich Logo">
|
<img src="design/immich-logo.svg" width="150" title="Login com URL customizada">
|
||||||
</p>
|
</p>
|
||||||
<h3 align="center">Solução self-hosted de alta performance para backup de fotos e vídeos</h3>
|
<h3 align="center">Immich - Solução self-hosted de alta performance para backup de fotos e vídeos</h3>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://immich.app">
|
<a href="https://immich.app">
|
||||||
<img src="../design/immich-screenshots.png" title="Captura de tela princial">
|
<img src="design/immich-screenshots.png" title="Captura de tela princial">
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<a href="../README.md">English</a>
|
||||||
<a href="../README.md">English</a>
|
<a href="README_ca_ES.md">Català</a>
|
||||||
<a href="README_ca_ES.md">Català</a>
|
<a href="README_es_ES.md">Español</a>
|
||||||
<a href="README_es_ES.md">Español</a>
|
<a href="README_fr_FR.md">Français</a>
|
||||||
<a href="README_fr_FR.md">Français</a>
|
<a href="README_it_IT.md">Italiano</a>
|
||||||
<a href="README_it_IT.md">Italiano</a>
|
<a href="README_ja_JP.md">日本語</a>
|
||||||
<a href="README_ja_JP.md">日本語</a>
|
<a href="README_ko_KR.md">한국어</a>
|
||||||
<a href="README_ko_KR.md">한국어</a>
|
<a href="README_de_DE.md">Deutsch</a>
|
||||||
<a href="README_de_DE.md">Deutsch</a>
|
<a href="README_nl_NL.md">Nederlands</a>
|
||||||
<a href="README_nl_NL.md">Nederlands</a>
|
<a href="README_tr_TR.md">Türkçe</a>
|
||||||
<a href="README_tr_TR.md">Türkçe</a>
|
<a href="README_zh_CN.md">中文</a>
|
||||||
<a href="README_zh_CN.md">中文</a>
|
<a href="README_ru_RU.md">Русский</a>
|
||||||
<a href="README_ru_RU.md">Русский</a>
|
<a href="README_sv_SE.md">Svenska</a>
|
||||||
<a href="README_sv_SE.md">Svenska</a>
|
<a href="README_ar_JO.md">العربية</a>
|
||||||
<a href="README_ar_JO.md">العربية</a>
|
|
||||||
<a href="README_vi_VN.md">Tiếng Việt</a>
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Avisos
|
## Avisos
|
||||||
|
|
||||||
- ⚠️ Este projeto está sob **desenvolvimento constante**.
|
- ⚠️ Este projeto está sob **desenvolvimento constante**.
|
||||||
- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a
|
- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a compatibilidade com versões anteriores).
|
||||||
compatibilidade com versões anteriores).
|
- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e vídeos.**
|
||||||
- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e
|
- ⚠️ Sempre siga o plano [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup para as suas mídias preciosas!
|
||||||
vídeos.**
|
|
||||||
- ⚠️ Sempre siga o plano
|
|
||||||
[3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup
|
|
||||||
para as suas mídias preciosas!
|
|
||||||
|
|
||||||
> [!NOTE]
|
## Conteúdo
|
||||||
> Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
|
|
||||||
|
|
||||||
## Links
|
- [Documentação Oficial](https://immich.app/docs)
|
||||||
|
|
||||||
- [Documentação](https://immich.app/docs)
|
|
||||||
- [Sobre](https://immich.app/docs/overview/introduction)
|
|
||||||
- [Instalação](https://immich.app/docs/install/requirements)
|
|
||||||
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
|
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
|
||||||
- [Demonstração](#demonstração)
|
- [Demonstração](#demo)
|
||||||
- [Funcionalidades](#funcionalidades)
|
- [Recursos](#features)
|
||||||
- [Traduções](https://immich.app/docs/developer/translations)
|
- [Introdução](https://immich.app/docs/overview/introduction)
|
||||||
|
- [Instalação](https://immich.app/docs/install/requirements)
|
||||||
- [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project)
|
- [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project)
|
||||||
|
|
||||||
|
## Documentação
|
||||||
|
|
||||||
|
Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
|
||||||
|
|
||||||
## Demonstração
|
## Demonstração
|
||||||
|
|
||||||
Acesse a demonstração [aqui](https://demo.immich.app). A demonstração está
|
Você pode acessar a demonstração web em https://demo.immich.app
|
||||||
hospedada no Nível Gratuito da Oracle VM em Amsterdam com um processador 2.4Ghz
|
|
||||||
quad-core ARM64 e 24GB de RAM.
|
|
||||||
|
|
||||||
No aplicativo para dispositivos móveis, você pode usar
|
No aplicativo para dispositivos móveis, você pode usar `https://demo.immich.app/api` no campo `Server Endpoint URL`
|
||||||
`https://demo.immich.app/api` no campo `Server Endpoint URL`
|
|
||||||
|
|
||||||
### Credenciais de login
|
```bash title="Credenciais de Demonstração"
|
||||||
|
Credenciais de Demonstração
|
||||||
|
email: demo@immich.app
|
||||||
|
senha: demo
|
||||||
|
```
|
||||||
|
|
||||||
| Email | Senha |
|
```
|
||||||
| --------------- | ----- |
|
Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||||
| demo@immich.app | demo |
|
```
|
||||||
|
|
||||||
## Atividades
|
## Atividades
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Funcionalidades
|
## Recursos
|
||||||
|
|
||||||
| Funcionalidades | Aplicativo Móvel | Web |
|
|
||||||
| :-------------------------------------------------- | ---------------- | --- |
|
| Recursos | Aplicativo Móvel | Web |
|
||||||
|
|:----------------------------------------------------|------------------|-----|
|
||||||
| Fazer upload e visualizar fotos e vídeos | Sim | Sim |
|
| Fazer upload e visualizar fotos e vídeos | Sim | Sim |
|
||||||
| Backup automático ao abrir o aplicativo | Sim | N/A |
|
| Backup automático ao abrir o aplicativo | Sim | N/A |
|
||||||
| Prevenir a duplicação de arquivos | Sim | Sim |
|
| Prevenir a duplicação de arquivos | Sim | Sim |
|
||||||
@@ -94,17 +88,17 @@ No aplicativo para dispositivos móveis, você pode usar
|
|||||||
| Criação de álbuns e álbuns compartilhados | Sim | Sim |
|
| Criação de álbuns e álbuns compartilhados | Sim | Sim |
|
||||||
| Barra de rolagem arrastável | Sim | Sim |
|
| Barra de rolagem arrastável | Sim | Sim |
|
||||||
| Suporta formatos RAW | Sim | Sim |
|
| Suporta formatos RAW | Sim | Sim |
|
||||||
| Visualização de metadados (EXIF, mapa) | Sim | Sim |
|
| Visualização de metadados (EXIF, map) | Sim | Sim |
|
||||||
| Pesquisar por metadados, objetos, rostos, e CLIP | Sim | Sim |
|
| Pesquisar por metadados, objetos, rostos, and CLIP | Sim | Sim |
|
||||||
| Funções administrativas (gerenciamento de usuários) | Não | Sim |
|
| Funções administrativas (gerenciamento de usuários) | Não | Sim |
|
||||||
| Backup em segundo plano | Sim | N/A |
|
| Backup em segundo plano | Sim | N/A |
|
||||||
| Rolagem virtual | Sim | Sim |
|
| Virtual scroll | Sim | Sim |
|
||||||
| Suporte OAuth | Sim | Sim |
|
| Suporte OAuth | Sim | Sim |
|
||||||
| Chaves de API | N/A | Sim |
|
| Chaves de API | N/A | Sim |
|
||||||
| Backup e reprodução de LivePhoto/MotionPhoto | Sim | Sim |
|
| Backup e visualização de LivePhoto/MotionPhoto | Sim | Sim |
|
||||||
| Visualização de imagens 360º | Não | Sim |
|
| Visualização de imagens 360º | Não | Sim |
|
||||||
| Estrutura de armazenamento definida pelo usuário | Sim | Sim |
|
| Estrutura de armazenamento definida pelo usuário | Sim | Sim |
|
||||||
| Compartilhar com o público | Sim | Sim |
|
| Compartilhar com o público | Não | Sim |
|
||||||
| Arquivo e Favoritos | Sim | Sim |
|
| Arquivo e Favoritos | Sim | Sim |
|
||||||
| Mapa Global | Sim | Sim |
|
| Mapa Global | Sim | Sim |
|
||||||
| Compartilhamento com parceiro | Sim | Sim |
|
| Compartilhamento com parceiro | Sim | Sim |
|
||||||
@@ -114,29 +108,6 @@ No aplicativo para dispositivos móveis, você pode usar
|
|||||||
| Galeria em modo apenas leitura | Sim | Sim |
|
| Galeria em modo apenas leitura | Sim | Sim |
|
||||||
| Empilhamento de fotos | Sim | Sim |
|
| Empilhamento de fotos | Sim | Sim |
|
||||||
|
|
||||||
## Traduções
|
|
||||||
|
|
||||||
Leia mais sobre as traduções
|
|
||||||
[aqui](https://immich.app/docs/developer/translations).
|
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/immich/">
|
|
||||||
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Status da tradução" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Atividade do repositório
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Histórico de estrelas
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#immich-app/immich&Date">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
|
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
|
|
||||||
<img alt="Gráfico de histórico de estrelas" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
## Contribuidores
|
## Contribuidores
|
||||||
|
|
||||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||||
|
|||||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^10.0.1",
|
"@nestjs/bullmq": "^10.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -7,20 +7,83 @@ import { RedisOptions } from 'ioredis';
|
|||||||
import Joi, { Root } from 'joi';
|
import Joi, { Root } from 'joi';
|
||||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||||
import { ImmichHeader } from 'src/dtos/auth.dto';
|
import { ImmichHeader } from 'src/dtos/auth.dto';
|
||||||
import {
|
|
||||||
AudioCodec,
|
|
||||||
Colorspace,
|
|
||||||
CQMode,
|
|
||||||
ImageFormat,
|
|
||||||
LogLevel,
|
|
||||||
ToneMapping,
|
|
||||||
TranscodeHWAccel,
|
|
||||||
TranscodePolicy,
|
|
||||||
VideoCodec,
|
|
||||||
VideoContainer,
|
|
||||||
} from 'src/enum';
|
|
||||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||||
|
|
||||||
|
export enum TranscodePolicy {
|
||||||
|
ALL = 'all',
|
||||||
|
OPTIMAL = 'optimal',
|
||||||
|
BITRATE = 'bitrate',
|
||||||
|
REQUIRED = 'required',
|
||||||
|
DISABLED = 'disabled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TranscodeTarget {
|
||||||
|
NONE,
|
||||||
|
AUDIO,
|
||||||
|
VIDEO,
|
||||||
|
ALL,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VideoCodec {
|
||||||
|
H264 = 'h264',
|
||||||
|
HEVC = 'hevc',
|
||||||
|
VP9 = 'vp9',
|
||||||
|
AV1 = 'av1',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AudioCodec {
|
||||||
|
MP3 = 'mp3',
|
||||||
|
AAC = 'aac',
|
||||||
|
LIBOPUS = 'libopus',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VideoContainer {
|
||||||
|
MOV = 'mov',
|
||||||
|
MP4 = 'mp4',
|
||||||
|
OGG = 'ogg',
|
||||||
|
WEBM = 'webm',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TranscodeHWAccel {
|
||||||
|
NVENC = 'nvenc',
|
||||||
|
QSV = 'qsv',
|
||||||
|
VAAPI = 'vaapi',
|
||||||
|
RKMPP = 'rkmpp',
|
||||||
|
DISABLED = 'disabled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ToneMapping {
|
||||||
|
HABLE = 'hable',
|
||||||
|
MOBIUS = 'mobius',
|
||||||
|
REINHARD = 'reinhard',
|
||||||
|
DISABLED = 'disabled',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CQMode {
|
||||||
|
AUTO = 'auto',
|
||||||
|
CQP = 'cqp',
|
||||||
|
ICQ = 'icq',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Colorspace {
|
||||||
|
SRGB = 'srgb',
|
||||||
|
P3 = 'p3',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ImageFormat {
|
||||||
|
JPEG = 'jpeg',
|
||||||
|
WEBP = 'webp',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
VERBOSE = 'verbose',
|
||||||
|
DEBUG = 'debug',
|
||||||
|
LOG = 'log',
|
||||||
|
WARN = 'warn',
|
||||||
|
ERROR = 'error',
|
||||||
|
FATAL = 'fatal',
|
||||||
|
}
|
||||||
|
|
||||||
export interface SystemConfig {
|
export interface SystemConfig {
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
crf: number;
|
crf: number;
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ export const resourcePaths = {
|
|||||||
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
||||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||||
|
|
||||||
|
export enum AuthType {
|
||||||
|
PASSWORD = 'password',
|
||||||
|
OAUTH = 'oauth',
|
||||||
|
}
|
||||||
|
|
||||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
||||||
|
|
||||||
export const FACE_THUMBNAIL_SIZE = 250;
|
export const FACE_THUMBNAIL_SIZE = 250;
|
||||||
|
|||||||
@@ -33,17 +33,16 @@ import {
|
|||||||
UploadFieldName,
|
UploadFieldName,
|
||||||
} from 'src/dtos/asset-media.dto';
|
} from 'src/dtos/asset-media.dto';
|
||||||
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
|
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
|
||||||
import { RouteKey } from 'src/enum';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
||||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||||
import { FileUploadInterceptor, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor, Route, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
|
||||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
import { sendFile } from 'src/utils/file';
|
import { sendFile } from 'src/utils/file';
|
||||||
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Assets')
|
@ApiTags('Assets')
|
||||||
@Controller(RouteKey.ASSET)
|
@Controller(Route.ASSET)
|
||||||
export class AssetMediaController {
|
export class AssetMediaController {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import {
|
|||||||
} from 'src/dtos/asset.dto';
|
} from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MemoryLaneDto } from 'src/dtos/search.dto';
|
import { MemoryLaneDto } from 'src/dtos/search.dto';
|
||||||
import { RouteKey } from 'src/enum';
|
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { Route } from 'src/middleware/file-upload.interceptor';
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Assets')
|
@ApiTags('Assets')
|
||||||
@Controller(RouteKey.ASSET)
|
@Controller(Route.ASSET)
|
||||||
export class AssetController {
|
export class AssetController {
|
||||||
constructor(private service: AssetService) {}
|
constructor(private service: AssetService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
|
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { AuthType } from 'src/constants';
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
ChangePasswordDto,
|
ChangePasswordDto,
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
ValidateAccessTokenResponseDto,
|
ValidateAccessTokenResponseDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||||
import { AuthType } from 'src/enum';
|
|
||||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
|
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { AuthType } from 'src/constants';
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
ImmichCookie,
|
ImmichCookie,
|
||||||
@@ -10,7 +11,6 @@ import {
|
|||||||
OAuthConfigDto,
|
OAuthConfigDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||||
import { AuthType } from 'src/enum';
|
|
||||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { respondWithCookie } from 'src/utils/response';
|
import { respondWithCookie } from 'src/utils/response';
|
||||||
|
|||||||
@@ -21,16 +21,15 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
|||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||||
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
||||||
import { RouteKey } from 'src/enum';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
import { sendFile } from 'src/utils/file';
|
import { sendFile } from 'src/utils/file';
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Users')
|
@ApiTags('Users')
|
||||||
@Controller(RouteKey.USER)
|
@Controller(Route.USER)
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(
|
constructor(
|
||||||
private service: UserService,
|
private service: UserService,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
|
import { ImageFormat } from 'src/config';
|
||||||
import { APP_MEDIA_LOCATION } from 'src/constants';
|
import { APP_MEDIA_LOCATION } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
|
import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.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 } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.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';
|
||||||
@@ -14,6 +16,14 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
|
|
||||||
|
export enum StorageFolder {
|
||||||
|
ENCODED_VIDEO = 'encoded-video',
|
||||||
|
LIBRARY = 'library',
|
||||||
|
UPLOAD = 'upload',
|
||||||
|
PROFILE = 'profile',
|
||||||
|
THUMBNAILS = 'thumbs',
|
||||||
|
}
|
||||||
|
|
||||||
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
|
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
|
||||||
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
|
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
|
|||||||
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||||
import { MetadataKey } from 'src/enum';
|
|
||||||
import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface';
|
import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface';
|
||||||
|
import { Metadata } from 'src/middleware/auth.guard';
|
||||||
import { setUnion } from 'src/utils/set';
|
import { setUnion } from 'src/utils/set';
|
||||||
|
|
||||||
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
||||||
@@ -141,7 +141,7 @@ export type EmitConfig = {
|
|||||||
/** lower value has higher priority, defaults to 0 */
|
/** lower value has higher priority, defaults to 0 */
|
||||||
priority?: number;
|
priority?: number;
|
||||||
};
|
};
|
||||||
export const OnEmit = (config: EmitConfig) => SetMetadata(MetadataKey.ON_EMIT_CONFIG, config);
|
export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config);
|
||||||
|
|
||||||
type LifecycleRelease = 'NEXT_RELEASE' | string;
|
type LifecycleRelease = 'NEXT_RELEASE' | string;
|
||||||
type LifecycleMetadata = {
|
type LifecycleMetadata = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
|
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
|
||||||
import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum';
|
import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
|
||||||
|
import { EntityType } from 'src/enum';
|
||||||
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
|
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
|
||||||
|
|||||||
@@ -18,20 +18,20 @@ import {
|
|||||||
ValidatorConstraint,
|
ValidatorConstraint,
|
||||||
ValidatorConstraintInterface,
|
ValidatorConstraintInterface,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { SystemConfig } from 'src/config';
|
|
||||||
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
|
||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
CQMode,
|
CQMode,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
SystemConfig,
|
||||||
ToneMapping,
|
ToneMapping,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
VideoContainer,
|
VideoContainer,
|
||||||
} from 'src/enum';
|
} from 'src/config';
|
||||||
|
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
|
||||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||||
import { ValidateBoolean, validateCronExpression } from 'src/validation';
|
import { ValidateBoolean, validateCronExpression } from 'src/validation';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PathType } from 'src/enum';
|
|
||||||
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
|
|
||||||
@Entity('move_history')
|
@Entity('move_history')
|
||||||
@@ -22,3 +21,21 @@ export class MoveEntity {
|
|||||||
@Column({ type: 'varchar' })
|
@Column({ type: 'varchar' })
|
||||||
newPath!: string;
|
newPath!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AssetPathType {
|
||||||
|
ORIGINAL = 'original',
|
||||||
|
PREVIEW = 'preview',
|
||||||
|
THUMBNAIL = 'thumbnail',
|
||||||
|
ENCODED_VIDEO = 'encoded_video',
|
||||||
|
SIDECAR = 'sidecar',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PersonPathType {
|
||||||
|
FACE = 'face',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserPathType {
|
||||||
|
PROFILE = 'profile',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathType = AssetPathType | PersonPathType | UserPathType;
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
export enum AuthType {
|
|
||||||
PASSWORD = 'password',
|
|
||||||
OAUTH = 'oauth',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AssetType {
|
export enum AssetType {
|
||||||
IMAGE = 'IMAGE',
|
IMAGE = 'IMAGE',
|
||||||
VIDEO = 'VIDEO',
|
VIDEO = 'VIDEO',
|
||||||
@@ -153,14 +148,6 @@ export enum SharedLinkType {
|
|||||||
INDIVIDUAL = 'INDIVIDUAL',
|
INDIVIDUAL = 'INDIVIDUAL',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StorageFolder {
|
|
||||||
ENCODED_VIDEO = 'encoded-video',
|
|
||||||
LIBRARY = 'library',
|
|
||||||
UPLOAD = 'upload',
|
|
||||||
PROFILE = 'profile',
|
|
||||||
THUMBNAILS = 'thumbs',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SystemMetadataKey {
|
export enum SystemMetadataKey {
|
||||||
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
|
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
|
||||||
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
|
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
|
||||||
@@ -211,120 +198,3 @@ export enum ManualJobName {
|
|||||||
TAG_CLEANUP = 'tag-cleanup',
|
TAG_CLEANUP = 'tag-cleanup',
|
||||||
USER_CLEANUP = 'user-cleanup',
|
USER_CLEANUP = 'user-cleanup',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AssetPathType {
|
|
||||||
ORIGINAL = 'original',
|
|
||||||
PREVIEW = 'preview',
|
|
||||||
THUMBNAIL = 'thumbnail',
|
|
||||||
ENCODED_VIDEO = 'encoded_video',
|
|
||||||
SIDECAR = 'sidecar',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PersonPathType {
|
|
||||||
FACE = 'face',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum UserPathType {
|
|
||||||
PROFILE = 'profile',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PathType = AssetPathType | PersonPathType | UserPathType;
|
|
||||||
|
|
||||||
export enum TranscodePolicy {
|
|
||||||
ALL = 'all',
|
|
||||||
OPTIMAL = 'optimal',
|
|
||||||
BITRATE = 'bitrate',
|
|
||||||
REQUIRED = 'required',
|
|
||||||
DISABLED = 'disabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TranscodeTarget {
|
|
||||||
NONE,
|
|
||||||
AUDIO,
|
|
||||||
VIDEO,
|
|
||||||
ALL,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum VideoCodec {
|
|
||||||
H264 = 'h264',
|
|
||||||
HEVC = 'hevc',
|
|
||||||
VP9 = 'vp9',
|
|
||||||
AV1 = 'av1',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AudioCodec {
|
|
||||||
MP3 = 'mp3',
|
|
||||||
AAC = 'aac',
|
|
||||||
LIBOPUS = 'libopus',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum VideoContainer {
|
|
||||||
MOV = 'mov',
|
|
||||||
MP4 = 'mp4',
|
|
||||||
OGG = 'ogg',
|
|
||||||
WEBM = 'webm',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum TranscodeHWAccel {
|
|
||||||
NVENC = 'nvenc',
|
|
||||||
QSV = 'qsv',
|
|
||||||
VAAPI = 'vaapi',
|
|
||||||
RKMPP = 'rkmpp',
|
|
||||||
DISABLED = 'disabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ToneMapping {
|
|
||||||
HABLE = 'hable',
|
|
||||||
MOBIUS = 'mobius',
|
|
||||||
REINHARD = 'reinhard',
|
|
||||||
DISABLED = 'disabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CQMode {
|
|
||||||
AUTO = 'auto',
|
|
||||||
CQP = 'cqp',
|
|
||||||
ICQ = 'icq',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum Colorspace {
|
|
||||||
SRGB = 'srgb',
|
|
||||||
P3 = 'p3',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ImageFormat {
|
|
||||||
JPEG = 'jpeg',
|
|
||||||
WEBP = 'webp',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LogLevel {
|
|
||||||
VERBOSE = 'verbose',
|
|
||||||
DEBUG = 'debug',
|
|
||||||
LOG = 'log',
|
|
||||||
WARN = 'warn',
|
|
||||||
ERROR = 'error',
|
|
||||||
FATAL = 'fatal',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MetadataKey {
|
|
||||||
AUTH_ROUTE = 'auth_route',
|
|
||||||
ADMIN_ROUTE = 'admin_route',
|
|
||||||
SHARED_ROUTE = 'shared_route',
|
|
||||||
API_KEY_SECURITY = 'api_key',
|
|
||||||
ON_EMIT_CONFIG = 'on_emit_config',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RouteKey {
|
|
||||||
ASSET = 'assets',
|
|
||||||
USER = 'users',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CacheControl {
|
|
||||||
PRIVATE_WITH_CACHE = 'private_with_cache',
|
|
||||||
PRIVATE_WITHOUT_CACHE = 'private_without_cache',
|
|
||||||
NONE = 'none',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PaginationMode {
|
|
||||||
LIMIT_OFFSET = 'limit-offset',
|
|
||||||
SKIP_TAKE = 'skip-take',
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { VectorExtension } from 'src/interfaces/database.interface';
|
|
||||||
|
|
||||||
export const IConfigRepository = 'IConfigRepository';
|
|
||||||
|
|
||||||
export interface EnvData {
|
|
||||||
database: {
|
|
||||||
skipMigrations: boolean;
|
|
||||||
vectorExtension: VectorExtension;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IConfigRepository {
|
|
||||||
getEnv(): EnvData;
|
|
||||||
}
|
|
||||||
@@ -116,7 +116,7 @@ export enum JobName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
||||||
export const JOBS_LIBRARY_PAGINATION_SIZE = 10_000;
|
export const JOBS_LIBRARY_PAGINATION_SIZE = 100_000;
|
||||||
|
|
||||||
export interface IBaseJob {
|
export interface IBaseJob {
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LogLevel } from 'src/enum';
|
import { LogLevel } from 'src/config';
|
||||||
|
|
||||||
export const ILoggerRepository = 'ILoggerRepository';
|
export const ILoggerRepository = 'ILoggerRepository';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/enum';
|
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config';
|
||||||
|
|
||||||
export const IMediaRepository = 'IMediaRepository';
|
export const IMediaRepository = 'IMediaRepository';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { MoveEntity } from 'src/entities/move.entity';
|
import { MoveEntity, PathType } from 'src/entities/move.entity';
|
||||||
import { PathType } from 'src/enum';
|
|
||||||
|
|
||||||
export const IMoveRepository = 'IMoveRepository';
|
export const IMoveRepository = 'IMoveRepository';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CommandFactory } from 'nest-commander';
|
|||||||
import { fork } from 'node:child_process';
|
import { fork } from 'node:child_process';
|
||||||
import { Worker } from 'node:worker_threads';
|
import { Worker } from 'node:worker_threads';
|
||||||
import { ImmichAdminModule } from 'src/app.module';
|
import { ImmichAdminModule } from 'src/app.module';
|
||||||
import { LogLevel } from 'src/enum';
|
import { LogLevel } from 'src/config';
|
||||||
import { getWorkers } from 'src/utils/workers';
|
import { getWorkers } from 'src/utils/workers';
|
||||||
const immichApp = process.argv[2] || process.env.IMMICH_APP;
|
const immichApp = process.argv[2] || process.env.IMMICH_APP;
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,19 @@ import { Reflector } from '@nestjs/core';
|
|||||||
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
|
import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
|
||||||
import { MetadataKey, Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
|
|
||||||
|
export enum Metadata {
|
||||||
|
AUTH_ROUTE = 'auth_route',
|
||||||
|
ADMIN_ROUTE = 'admin_route',
|
||||||
|
SHARED_ROUTE = 'shared_route',
|
||||||
|
API_KEY_SECURITY = 'api_key',
|
||||||
|
ON_EMIT_CONFIG = 'on_emit_config',
|
||||||
|
}
|
||||||
|
|
||||||
type AdminRoute = { admin?: true };
|
type AdminRoute = { admin?: true };
|
||||||
type SharedLinkRoute = { sharedLink?: true };
|
type SharedLinkRoute = { sharedLink?: true };
|
||||||
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
|
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
|
||||||
@@ -24,8 +32,8 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
|
|||||||
const decorators: MethodDecorator[] = [
|
const decorators: MethodDecorator[] = [
|
||||||
ApiBearerAuth(),
|
ApiBearerAuth(),
|
||||||
ApiCookieAuth(),
|
ApiCookieAuth(),
|
||||||
ApiSecurity(MetadataKey.API_KEY_SECURITY),
|
ApiSecurity(Metadata.API_KEY_SECURITY),
|
||||||
SetMetadata(MetadataKey.AUTH_ROUTE, options || {}),
|
SetMetadata(Metadata.AUTH_ROUTE, options || {}),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ((options as SharedLinkRoute)?.sharedLink) {
|
if ((options as SharedLinkRoute)?.sharedLink) {
|
||||||
@@ -77,7 +85,7 @@ export class AuthGuard implements CanActivate {
|
|||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const targets = [context.getHandler()];
|
const targets = [context.getHandler()];
|
||||||
|
|
||||||
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AUTH_ROUTE, targets);
|
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(Metadata.AUTH_ROUTE, targets);
|
||||||
if (!options) {
|
if (!options) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import multer, { StorageEngine, diskStorage } from 'multer';
|
|||||||
import { createHash, randomUUID } from 'node:crypto';
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||||
import { RouteKey } from 'src/enum';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||||
import { AssetMediaService, UploadFile } from 'src/services/asset-media.service';
|
import { AssetMediaService, UploadFile } from 'src/services/asset-media.service';
|
||||||
@@ -29,6 +28,11 @@ export function getFiles(files: UploadFiles) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Route {
|
||||||
|
ASSET = 'assets',
|
||||||
|
USER = 'users',
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImmichFile extends Express.Multer.File {
|
export interface ImmichFile extends Express.Multer.File {
|
||||||
/** sha1 hash of file */
|
/** sha1 hash of file */
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -111,7 +115,7 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||||||
const context_ = context.switchToHttp();
|
const context_ = context.switchToHttp();
|
||||||
const route = this.reflect.get<string>(PATH_METADATA, context.getClass());
|
const route = this.reflect.get<string>(PATH_METADATA, context.getClass());
|
||||||
|
|
||||||
const handler: RequestHandler | null = this.getHandler(route as RouteKey);
|
const handler: RequestHandler | null = this.getHandler(route as Route);
|
||||||
if (handler) {
|
if (handler) {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
|
const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
|
||||||
@@ -172,13 +176,13 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHandler(route: RouteKey) {
|
private getHandler(route: Route) {
|
||||||
switch (route) {
|
switch (route) {
|
||||||
case RouteKey.ASSET: {
|
case Route.ASSET: {
|
||||||
return this.handlers.assetUpload;
|
return this.handlers.assetUpload;
|
||||||
}
|
}
|
||||||
|
|
||||||
case RouteKey.USER: {
|
case Route.USER: {
|
||||||
return this.handlers.userProfile;
|
return this.handlers.userProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||||
import { AssetFileType, AssetOrder, AssetStatus, AssetType, PaginationMode } from 'src/enum';
|
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
||||||
import {
|
import {
|
||||||
AssetBuilderOptions,
|
AssetBuilderOptions,
|
||||||
AssetCreate,
|
AssetCreate,
|
||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||||
import { searchAssetBuilder } from 'src/utils/database';
|
import { searchAssetBuilder } from 'src/utils/database';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||||
import {
|
import {
|
||||||
Brackets,
|
Brackets,
|
||||||
FindOptionsOrder,
|
FindOptionsOrder,
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { getVectorExtension } from 'src/database.config';
|
|
||||||
import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ConfigRepository implements IConfigRepository {
|
|
||||||
getEnv(): EnvData {
|
|
||||||
return {
|
|
||||||
database: {
|
|
||||||
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
|
|
||||||
vectorExtension: getVectorExtension(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
import { IAuditRepository } from 'src/interfaces/audit.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';
|
||||||
@@ -40,7 +39,6 @@ import { AlbumRepository } from 'src/repositories/album.repository';
|
|||||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
|
||||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
@@ -76,7 +74,6 @@ export const repositories = [
|
|||||||
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
||||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||||
{ provide: IAuditRepository, useClass: AuditRepository },
|
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||||
{ provide: IConfigRepository, useClass: ConfigRepository },
|
|
||||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||||
{ provide: IEventRepository, useClass: EventRepository },
|
{ provide: IEventRepository, useClass: EventRepository },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
||||||
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
import { LogLevel } from 'src/enum';
|
import { LogLevel } from 'src/config';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { LogColor } from 'src/utils/logger';
|
import { LogColor } from 'src/utils/logger';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
|
|||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { Colorspace } from 'src/enum';
|
import { Colorspace } from 'src/config';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import {
|
import {
|
||||||
IMediaRepository,
|
IMediaRepository,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { MoveEntity } from 'src/entities/move.entity';
|
import { MoveEntity, PathType } from 'src/entities/move.entity';
|
||||||
import { PathType } from 'src/enum';
|
|
||||||
import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface';
|
import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
|||||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { PaginationMode, SourceType } from 'src/enum';
|
import { SourceType } from 'src/enum';
|
||||||
import {
|
import {
|
||||||
AssetFaceId,
|
AssetFaceId,
|
||||||
DeleteAllFacesOptions,
|
DeleteAllFacesOptions,
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
UpdateFacesData,
|
UpdateFacesData,
|
||||||
} from 'src/interfaces/person.interface';
|
} from 'src/interfaces/person.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||||
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
|
|||||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||||
import { AssetType, PaginationMode } from 'src/enum';
|
import { AssetType } from 'src/enum';
|
||||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from 'src/interfaces/search.interface';
|
} from 'src/interfaces/search.interface';
|
||||||
import { asVector, searchAssetBuilder } from 'src/utils/database';
|
import { asVector, searchAssetBuilder } from 'src/utils/database';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Paginated, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
|
||||||
import { isValidInteger } from 'src/validation';
|
import { isValidInteger } from 'src/validation';
|
||||||
import { Repository, SelectQueryBuilder } from 'typeorm';
|
import { Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos
|
|||||||
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetStatus, AssetType, CacheControl } from 'src/enum';
|
import { AssetStatus, AssetType } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.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';
|
||||||
@@ -12,7 +12,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { fileStub } from 'test/fixtures/file.stub';
|
import { fileStub } from 'test/fixtures/file.stub';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { extname } from 'node:path';
|
import { extname } from 'node:path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import {
|
import {
|
||||||
AssetBulkUploadCheckResponseDto,
|
AssetBulkUploadCheckResponseDto,
|
||||||
AssetMediaResponseDto,
|
AssetMediaResponseDto,
|
||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
} from 'src/dtos/asset-media.dto';
|
} from 'src/dtos/asset-media.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
|
import { AssetStatus, AssetType, 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 { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
@@ -37,7 +37,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { fromChecksum } from 'src/utils/request';
|
import { fromChecksum } from 'src/utils/request';
|
||||||
import { QueryFailedError } from 'typeorm';
|
import { QueryFailedError } from 'typeorm';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import {
|
import {
|
||||||
AuditDeletesDto,
|
AuditDeletesDto,
|
||||||
AuditDeletesResponseDto,
|
AuditDeletesResponseDto,
|
||||||
@@ -12,15 +12,8 @@ import {
|
|||||||
PathEntityType,
|
PathEntityType,
|
||||||
} from 'src/dtos/audit.dto';
|
} from 'src/dtos/audit.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
|
||||||
AssetFileType,
|
import { AssetFileType, DatabaseAction, Permission } from 'src/enum';
|
||||||
AssetPathType,
|
|
||||||
DatabaseAction,
|
|
||||||
Permission,
|
|
||||||
PersonPathType,
|
|
||||||
StorageFolder,
|
|
||||||
UserPathType,
|
|
||||||
} 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 { IAuditRepository } from 'src/interfaces/audit.interface';
|
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||||
import { Issuer, generators } from 'openid-client';
|
import { Issuer, generators } from 'openid-client';
|
||||||
|
import { AuthType } from 'src/constants';
|
||||||
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
|
||||||
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 { AuthType } from 'src/enum';
|
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.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';
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { DateTime } from 'luxon';
|
|||||||
import { IncomingHttpHeaders } from 'node:http';
|
import { IncomingHttpHeaders } from 'node:http';
|
||||||
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
import { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { UserCore } from 'src/cores/user.core';
|
import { UserCore } from 'src/cores/user.core';
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
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 { Permission } from 'src/enum';
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
import { IKeyRepository } from 'src/interfaces/api-key.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';
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import {
|
|
||||||
DatabaseExtension,
|
|
||||||
EXTENSION_NAMES,
|
|
||||||
IDatabaseRepository,
|
|
||||||
VectorExtension,
|
|
||||||
} 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 { 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';
|
||||||
|
|
||||||
describe(DatabaseService.name, () => {
|
describe(DatabaseService.name, () => {
|
||||||
let sut: DatabaseService;
|
let sut: DatabaseService;
|
||||||
let configMock: Mocked<IConfigRepository>;
|
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
let extensionRange: string;
|
let extensionRange: string;
|
||||||
@@ -24,11 +16,9 @@ describe(DatabaseService.name, () => {
|
|||||||
let versionAboveRange: string;
|
let versionAboveRange: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
configMock = newConfigRepositoryMock();
|
|
||||||
databaseMock = newDatabaseRepositoryMock();
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
sut = new DatabaseService(databaseMock, loggerMock);
|
||||||
sut = new DatabaseService(configMock, databaseMock, loggerMock);
|
|
||||||
|
|
||||||
extensionRange = '0.2.x';
|
extensionRange = '0.2.x';
|
||||||
databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);
|
databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);
|
||||||
@@ -43,6 +33,11 @@ describe(DatabaseService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env.DB_SKIP_MIGRATIONS;
|
||||||
|
delete process.env.DB_VECTOR_EXTENSION;
|
||||||
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
@@ -55,12 +50,12 @@ describe(DatabaseService.name, () => {
|
|||||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
|
describe.each([
|
||||||
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
|
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
|
||||||
{ 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 } });
|
process.env.DB_VECTOR_EXTENSION = extensionName;
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should start up successfully with ${extension}`, async () => {
|
it(`should start up successfully with ${extension}`, async () => {
|
||||||
@@ -241,28 +236,18 @@ describe(DatabaseService.name, () => {
|
|||||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
||||||
configMock.getEnv.mockReturnValue({
|
process.env.DB_SKIP_MIGRATIONS = 'true';
|
||||||
database: {
|
|
||||||
skipMigrations: true,
|
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||||
vectorExtension: DatabaseExtension.VECTORS,
|
|
||||||
},
|
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
|
||||||
|
|
||||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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({
|
process.env.DB_VECTOR_EXTENSION = 'pgvector';
|
||||||
database: {
|
|
||||||
skipMigrations: true,
|
|
||||||
vectorExtension: DatabaseExtension.VECTOR,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||||
installedVersion: null,
|
installedVersion: null,
|
||||||
availableVersion: minVersionInRange,
|
availableVersion: minVersionInRange,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
import { getVectorExtension } from 'src/database.config';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
|
||||||
import {
|
import {
|
||||||
DatabaseExtension,
|
DatabaseExtension,
|
||||||
DatabaseLock,
|
DatabaseLock,
|
||||||
@@ -67,7 +67,6 @@ export class DatabaseService {
|
|||||||
private reconnection?: NodeJS.Timeout;
|
private reconnection?: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
@@ -86,8 +85,7 @@ export class DatabaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
|
await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
|
||||||
const envData = this.configRepository.getEnv();
|
const extension = getVectorExtension();
|
||||||
const extension = envData.database.vectorExtension;
|
|
||||||
const name = EXTENSION_NAMES[extension];
|
const name = EXTENSION_NAMES[extension];
|
||||||
const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
|
const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
|
||||||
|
|
||||||
@@ -118,8 +116,7 @@ export class DatabaseService {
|
|||||||
|
|
||||||
await this.checkReindexing();
|
await this.checkReindexing();
|
||||||
|
|
||||||
const { database } = this.configRepository.getEnv();
|
if (process.env.DB_SKIP_MIGRATIONS !== 'true') {
|
||||||
if (!database.skipMigrations) {
|
|
||||||
await this.databaseRepository.runMigrations();
|
await this.databaseRepository.runMigrations();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { Stats } from 'node:fs';
|
import { Stats } from 'node:fs';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
|
||||||
import {
|
import {
|
||||||
AssetFileType,
|
|
||||||
AssetType,
|
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
@@ -10,7 +7,9 @@ import {
|
|||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
} from 'src/enum';
|
} from 'src/config';
|
||||||
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
|
import { AssetFileType, AssetType } from 'src/enum';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.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';
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
|
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { GeneratedImageType, StorageCore } from 'src/cores/storage.core';
|
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import {
|
import {
|
||||||
AssetFileType,
|
|
||||||
AssetPathType,
|
|
||||||
AssetType,
|
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
StorageFolder,
|
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
TranscodeTarget,
|
TranscodeTarget,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
VideoContainer,
|
VideoContainer,
|
||||||
} from 'src/enum';
|
} from 'src/config';
|
||||||
|
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
|
import { AssetPathType } from 'src/entities/move.entity';
|
||||||
|
import { AssetFileType, AssetType } from 'src/enum';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||||
|
import { Colorspace } from 'src/config';
|
||||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
|
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, SourceType, SystemMetadataKey } from 'src/enum';
|
import { SourceType, SystemMetadataKey } from 'src/enum';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.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';
|
||||||
@@ -15,7 +16,7 @@ import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.inter
|
|||||||
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 { PersonService } from 'src/services/person.service';
|
import { PersonService } from 'src/services/person.service';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
import { faceStub } from 'test/fixtures/face.stub';
|
import { faceStub } from 'test/fixtures/face.stub';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { ImageFormat } from 'src/config';
|
||||||
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
@@ -22,16 +23,9 @@ import {
|
|||||||
} from 'src/dtos/person.dto';
|
} from 'src/dtos/person.dto';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
|
import { PersonPathType } from 'src/entities/move.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import {
|
import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum';
|
||||||
AssetType,
|
|
||||||
CacheControl,
|
|
||||||
ImageFormat,
|
|
||||||
Permission,
|
|
||||||
PersonPathType,
|
|
||||||
SourceType,
|
|
||||||
SystemMetadataKey,
|
|
||||||
} 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 { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
@@ -57,7 +51,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 { checkAccess, requireAccess } from 'src/utils/access';
|
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
|
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
|
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
|
||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
ServerStorageResponseDto,
|
ServerStorageResponseDto,
|
||||||
UsageByUserDto,
|
UsageByUserDto,
|
||||||
} from 'src/dtos/server.dto';
|
} from 'src/dtos/server.dto';
|
||||||
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
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';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Stats } from 'node:fs';
|
|||||||
import { SystemConfig, defaults } from 'src/config';
|
import { SystemConfig, defaults } from 'src/config';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType } from 'src/enum';
|
import { AssetPathType } from 'src/entities/move.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 { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import {
|
|||||||
supportedWeekTokens,
|
supportedWeekTokens,
|
||||||
supportedYearTokens,
|
supportedYearTokens,
|
||||||
} from 'src/constants';
|
} from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
import { AssetPathType } from 'src/entities/move.entity';
|
||||||
|
import { AssetType } 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 { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
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';
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { defaults, SystemConfig } from 'src/config';
|
|
||||||
import {
|
import {
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
|
||||||
CQMode,
|
CQMode,
|
||||||
|
Colorspace,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
SystemMetadataKey,
|
SystemConfig,
|
||||||
ToneMapping,
|
ToneMapping,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
VideoContainer,
|
VideoContainer,
|
||||||
} from 'src/enum';
|
defaults,
|
||||||
|
} from 'src/config';
|
||||||
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
|
import { IEventRepository, ServerEvent } 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';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { instanceToPlain } from 'class-transformer';
|
import { instanceToPlain } from 'class-transformer';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { SystemConfig, defaults } from 'src/config';
|
import { LogLevel, SystemConfig, defaults } from 'src/config';
|
||||||
import {
|
import {
|
||||||
supportedDayTokens,
|
supportedDayTokens,
|
||||||
supportedHourTokens,
|
supportedHourTokens,
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnEmit, OnServerEvent } from 'src/decorators';
|
import { OnEmit, OnServerEvent } 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 { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
|
import { ArgOf, ClientEvent, IEventRepository, ServerEvent } 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';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { CacheControl, UserMetadataKey } from 'src/enum';
|
import { UserMetadataKey } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.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';
|
||||||
@@ -9,7 +9,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 { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
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';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nes
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
|
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
@@ -11,7 +11,7 @@ import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
|||||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
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 { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
|
import { UserMetadataKey } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.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';
|
||||||
@@ -19,7 +19,7 @@ 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 { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ModuleRef, Reflector } from '@nestjs/core';
|
import { ModuleRef, Reflector } from '@nestjs/core';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { EmitConfig } from 'src/decorators';
|
import { EmitConfig } from 'src/decorators';
|
||||||
import { MetadataKey } from 'src/enum';
|
|
||||||
import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface';
|
import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
|
import { Metadata } from 'src/middleware/auth.guard';
|
||||||
import { services } from 'src/services';
|
import { services } from 'src/services';
|
||||||
|
|
||||||
type Item<T extends EmitEvent> = {
|
type Item<T extends EmitEvent> = {
|
||||||
@@ -35,7 +35,7 @@ export const setupEventHandlers = (moduleRef: ModuleRef) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = reflector.get<EmitConfig>(MetadataKey.ON_EMIT_CONFIG, handler);
|
const options = reflector.get<EmitConfig>(Metadata.ON_EMIT_CONFIG, handler);
|
||||||
if (!options) {
|
if (!options) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { NextFunction, Response } from 'express';
|
|||||||
import { access, constants } from 'node:fs/promises';
|
import { access, constants } from 'node:fs/promises';
|
||||||
import { basename, extname, isAbsolute } from 'node:path';
|
import { basename, extname, isAbsolute } from 'node:path';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import { CacheControl } from 'src/enum';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
||||||
import { isConnectionAborted } from 'src/utils/misc';
|
import { isConnectionAborted } from 'src/utils/misc';
|
||||||
@@ -20,6 +19,12 @@ export function getLivePhotoMotionFilename(stillName: string, motionName: string
|
|||||||
return getFileNameWithoutExtension(stillName) + extname(motionName);
|
return getFileNameWithoutExtension(stillName) + extname(motionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CacheControl {
|
||||||
|
PRIVATE_WITH_CACHE = 'private_with_cache',
|
||||||
|
PRIVATE_WITHOUT_CACHE = 'private_without_cache',
|
||||||
|
NONE = 'none',
|
||||||
|
}
|
||||||
|
|
||||||
export class ImmichFileResponse {
|
export class ImmichFileResponse {
|
||||||
public readonly path!: string;
|
public readonly path!: string;
|
||||||
public readonly contentType!: string;
|
public readonly contentType!: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/config';
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/enum';
|
|
||||||
import {
|
import {
|
||||||
AudioStreamInfo,
|
AudioStreamInfo,
|
||||||
BitrateDistribution,
|
BitrateDistribution,
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import path from 'node:path';
|
|||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants';
|
import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants';
|
||||||
import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
|
import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
|
||||||
import { MetadataKey } from 'src/enum';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import { Metadata } from 'src/middleware/auth.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns a list of strings representing the keys of the object in dot notation
|
* @returns a list of strings representing the keys of the object in dot notation
|
||||||
@@ -210,7 +210,7 @@ export const useSwagger = (app: INestApplication, force = false) => {
|
|||||||
in: 'header',
|
in: 'header',
|
||||||
name: ImmichHeader.API_KEY,
|
name: ImmichHeader.API_KEY,
|
||||||
},
|
},
|
||||||
MetadataKey.API_KEY_SECURITY,
|
Metadata.API_KEY_SECURITY,
|
||||||
)
|
)
|
||||||
.addServer('/api')
|
.addServer('/api')
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { PaginationMode } from 'src/enum';
|
|
||||||
import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
|
import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
export interface PaginationOptions {
|
export interface PaginationOptions {
|
||||||
@@ -7,6 +6,11 @@ export interface PaginationOptions {
|
|||||||
skip?: number;
|
skip?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PaginationMode {
|
||||||
|
LIMIT_OFFSET = 'limit-offset',
|
||||||
|
SKIP_TAKE = 'skip-take',
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaginatedBuilderOptions {
|
export interface PaginatedBuilderOptions {
|
||||||
take: number;
|
take: number;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
|
||||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
|
||||||
import { Mocked, vitest } from 'vitest';
|
|
||||||
|
|
||||||
export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
|
|
||||||
return {
|
|
||||||
getEnv: vitest.fn().mockReturnValue({
|
|
||||||
database: {
|
|
||||||
skipMigration: false,
|
|
||||||
vectorExtension: DatabaseExtension.VECTORS,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.7.8",
|
"@formatjs/icu-messageformat-parser": "^2.7.8",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.116.2",
|
"version": "1.116.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ export const contextMenuNavigation: Action<HTMLElement, Options> = (node, option
|
|||||||
};
|
};
|
||||||
|
|
||||||
const moveSelection = async (direction: 'up' | 'down', event: KeyboardEvent) => {
|
const moveSelection = async (direction: 'up' | 'down', event: KeyboardEvent) => {
|
||||||
const { selectionChanged, container, openDropdown } = options;
|
const { selectionChanged, container, openDropdown, isOpen } = options;
|
||||||
|
if (!isOpen) {
|
||||||
|
// reset the scroll position before opening the menu
|
||||||
|
container?.scrollTo({ top: 0 });
|
||||||
|
}
|
||||||
if (openDropdown) {
|
if (openDropdown) {
|
||||||
openDropdown(event);
|
openDropdown(event);
|
||||||
await tick();
|
await tick();
|
||||||
|
|||||||
@@ -109,7 +109,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if isOwned}
|
{#if isOwned}
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}>
|
<ButtonContextMenu
|
||||||
|
icon={mdiDotsVertical}
|
||||||
|
size="20"
|
||||||
|
title={$t('options')}
|
||||||
|
direction="right"
|
||||||
|
align="top-left"
|
||||||
|
>
|
||||||
{#if role === AlbumUserRole.Viewer}
|
{#if role === AlbumUserRole.Viewer}
|
||||||
<MenuOption onClick={() => handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} />
|
<MenuOption onClick={() => handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -186,13 +186,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||||
<div class="mr-4">
|
<div class="mr-4">
|
||||||
<ButtonContextMenu
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('comment_options')} size="16">
|
||||||
icon={mdiDotsVertical}
|
|
||||||
title={$t('comment_options')}
|
|
||||||
align="top-right"
|
|
||||||
direction="left"
|
|
||||||
size="16"
|
|
||||||
>
|
|
||||||
<MenuOption
|
<MenuOption
|
||||||
activeColor="bg-red-200"
|
activeColor="bg-red-200"
|
||||||
icon={mdiDeleteOutline}
|
icon={mdiDeleteOutline}
|
||||||
@@ -239,13 +233,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||||
<div class="mr-4">
|
<div class="mr-4">
|
||||||
<ButtonContextMenu
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('reaction_options')} size="16">
|
||||||
icon={mdiDotsVertical}
|
|
||||||
title={$t('reaction_options')}
|
|
||||||
align="top-right"
|
|
||||||
direction="left"
|
|
||||||
size="16"
|
|
||||||
>
|
|
||||||
<MenuOption
|
<MenuOption
|
||||||
activeColor="bg-red-200"
|
activeColor="bg-red-200"
|
||||||
icon={mdiDeleteOutline}
|
icon={mdiDeleteOutline}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<DeleteAction {asset} {onAction} />
|
<DeleteAction {asset} {onAction} />
|
||||||
|
|
||||||
<ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}>
|
<ButtonContextMenu color="opaque" title={$t('more')} icon={mdiDotsVertical}>
|
||||||
{#if showSlideshow}
|
{#if showSlideshow}
|
||||||
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -67,7 +67,6 @@
|
|||||||
stopProgress: stopSlideshowProgress,
|
stopProgress: stopSlideshowProgress,
|
||||||
slideshowNavigation,
|
slideshowNavigation,
|
||||||
slideshowState,
|
slideshowState,
|
||||||
slideshowTransition,
|
|
||||||
} = slideshowStore;
|
} = slideshowStore;
|
||||||
|
|
||||||
let appearsInAlbums: AlbumResponseDto[] = [];
|
let appearsInAlbums: AlbumResponseDto[] = [];
|
||||||
@@ -83,14 +82,13 @@
|
|||||||
let numberOfComments: number;
|
let numberOfComments: number;
|
||||||
let fullscreenElement: Element;
|
let fullscreenElement: Element;
|
||||||
let unsubscribes: (() => void)[] = [];
|
let unsubscribes: (() => void)[] = [];
|
||||||
let selectedEditType: string = '';
|
|
||||||
let stack: StackResponseDto | null = null;
|
|
||||||
|
|
||||||
let zoomToggle = () => void 0;
|
let zoomToggle = () => void 0;
|
||||||
let copyImage: () => Promise<void>;
|
let copyImage: () => Promise<void>;
|
||||||
|
|
||||||
$: isFullScreen = fullscreenElement !== null;
|
$: isFullScreen = fullscreenElement !== null;
|
||||||
|
|
||||||
|
let stack: StackResponseDto | null = null;
|
||||||
|
|
||||||
const refreshStack = async () => {
|
const refreshStack = async () => {
|
||||||
if (isSharedLink()) {
|
if (isSharedLink()) {
|
||||||
return;
|
return;
|
||||||
@@ -392,9 +390,11 @@
|
|||||||
onAction?.(action);
|
onAction?.(action);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateSelectedEditType = (type: string) => {
|
let selectedEditType: string = '';
|
||||||
|
|
||||||
|
function handleUpdateSelectedEditType(type: string) {
|
||||||
selectedEditType = type;
|
selectedEditType = type;
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document bind:fullscreenElement />
|
<svelte:document bind:fullscreenElement />
|
||||||
@@ -508,7 +508,6 @@
|
|||||||
onNextAsset={() => navigateAsset('next')}
|
onNextAsset={() => navigateAsset('next')}
|
||||||
on:close={closeViewer}
|
on:close={closeViewer}
|
||||||
{sharedLink}
|
{sharedLink}
|
||||||
haveFadeTransition={$slideshowState === SlideshowState.None || $slideshowTransition}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -67,6 +67,8 @@
|
|||||||
size="20"
|
size="20"
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
title={$t('show_person_options')}
|
title={$t('show_person_options')}
|
||||||
|
direction="right"
|
||||||
|
align="top-left"
|
||||||
>
|
>
|
||||||
<MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
|
<MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
|
||||||
<MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
|
<MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
|
||||||
|
|||||||
@@ -237,7 +237,7 @@
|
|||||||
|
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={handleUpdate} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={handleUpdate} />
|
||||||
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
/**
|
/**
|
||||||
* The alignment of the context menu relative to the button.
|
* The alignment of the context menu relative to the button.
|
||||||
*/
|
*/
|
||||||
export let align: Align = 'top-left';
|
export let align: Align = 'top-right';
|
||||||
/**
|
/**
|
||||||
* The direction in which the context menu should open.
|
* The direction in which the context menu should open.
|
||||||
*/
|
*/
|
||||||
export let direction: 'left' | 'right' = 'right';
|
export let direction: 'left' | 'right' = 'left';
|
||||||
export let color: Color = 'transparent';
|
export let color: Color = 'transparent';
|
||||||
export let size: string | undefined = undefined;
|
export let size: string | undefined = undefined;
|
||||||
export let padding: Padding | undefined = undefined;
|
export let padding: Padding | undefined = undefined;
|
||||||
|
|||||||
@@ -18,25 +18,26 @@
|
|||||||
let left: number;
|
let left: number;
|
||||||
let top: number;
|
let top: number;
|
||||||
|
|
||||||
// We need to bind clientHeight since the bounding box may return a height
|
|
||||||
// of zero when starting the 'slide' animation.
|
|
||||||
let height: number;
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (menuElement) {
|
if (menuElement) {
|
||||||
const rect = menuElement.getBoundingClientRect();
|
const rect = menuElement.getBoundingClientRect();
|
||||||
const directionWidth = direction === 'left' ? rect.width : 0;
|
const directionWidth = direction === 'left' ? rect.width : 0;
|
||||||
const menuHeight = Math.min(menuElement.clientHeight, height) || 0;
|
const menuHeight = menuElement.clientHeight || 0;
|
||||||
|
|
||||||
left = Math.min(window.innerWidth - rect.width, x - directionWidth);
|
const calcLeft = Math.min(window.innerWidth - rect.width, x - directionWidth);
|
||||||
|
left = Math.max(0, calcLeft);
|
||||||
top = Math.min(window.innerHeight - menuHeight, y);
|
top = Math.min(window.innerHeight - menuHeight, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:clientHeight={height}
|
class="fixed z-10 overflow-hidden rounded-lg duration-[250ms] ease-in {isVisible
|
||||||
class="fixed z-10 min-w-[200px] w-max max-w-[300px] overflow-hidden rounded-lg shadow-lg"
|
? 'shadow-lg transition-shadow'
|
||||||
|
: 'shadow-none transition-none'}"
|
||||||
|
class:shadow-none={!isVisible}
|
||||||
|
class:shadow-lg={isVisible}
|
||||||
|
class:transition-none={!isVisible}
|
||||||
style:left="{left}px"
|
style:left="{left}px"
|
||||||
style:top="{top}px"
|
style:top="{top}px"
|
||||||
transition:slide={{ duration: 250, easing: quintOut }}
|
transition:slide={{ duration: 250, easing: quintOut }}
|
||||||
@@ -48,9 +49,9 @@
|
|||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
aria-labelledby={ariaLabelledBy}
|
aria-labelledby={ariaLabelledBy}
|
||||||
bind:this={menuElement}
|
bind:this={menuElement}
|
||||||
class:max-h-[100vh]={isVisible}
|
class="flex flex-col transition-all duration-[250ms] ease-in-out outline-none immich-scrollbar bg-slate-100 relative min-w-[200px] max-w-[200px] sm:max-w-[256px] rounded-lg {isVisible
|
||||||
class:max-h-0={!isVisible}
|
? 'translate-x-0 max-h-dvh overflow-y-auto'
|
||||||
class="flex flex-col transition-all duration-[250ms] ease-in-out outline-none"
|
: `${direction === 'left' ? 'translate-x-28' : '-translate-x-28'} max-h-0 overflow-y-hidden`}"
|
||||||
role="menu"
|
role="menu"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,7 +33,9 @@
|
|||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<Icon path={icon} ariaHidden={true} size="18" />
|
<div class="flex-none">
|
||||||
|
<Icon path={icon} ariaHidden={true} size="18" />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
{text}
|
{text}
|
||||||
|
|||||||
@@ -107,6 +107,8 @@
|
|||||||
size="24"
|
size="24"
|
||||||
padding="3"
|
padding="3"
|
||||||
hideContent
|
hideContent
|
||||||
|
direction="right"
|
||||||
|
align="top-left"
|
||||||
>
|
>
|
||||||
<SharedLinkEdit menuItem {onEdit} />
|
<SharedLinkEdit menuItem {onEdit} />
|
||||||
<SharedLinkCopy menuItem {link} />
|
<SharedLinkCopy menuItem {link} />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
import SettingDropdown from './shared-components/settings/setting-dropdown.svelte';
|
import SettingDropdown from './shared-components/settings/setting-dropdown.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook, slideshowTransition } = slideshowStore;
|
const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook } = slideshowStore;
|
||||||
|
|
||||||
export let onClose = () => {};
|
export let onClose = () => {};
|
||||||
|
|
||||||
@@ -65,7 +65,6 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SettingSwitch title={$t('show_progress_bar')} bind:checked={$showProgressBar} />
|
<SettingSwitch title={$t('show_progress_bar')} bind:checked={$showProgressBar} />
|
||||||
<SettingSwitch title={$t('show_slideshow_transition')} bind:checked={$slideshowTransition} />
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label={$t('duration')}
|
label={$t('duration')}
|
||||||
|
|||||||
@@ -1144,7 +1144,6 @@
|
|||||||
"show_person_options": "Show person options",
|
"show_person_options": "Show person options",
|
||||||
"show_progress_bar": "Show Progress Bar",
|
"show_progress_bar": "Show Progress Bar",
|
||||||
"show_search_options": "Show search options",
|
"show_search_options": "Show search options",
|
||||||
"show_slideshow_transition": "Show slideshow transition",
|
|
||||||
"show_supporter_badge": "Supporter badge",
|
"show_supporter_badge": "Supporter badge",
|
||||||
"show_supporter_badge_description": "Show a supporter badge",
|
"show_supporter_badge_description": "Show a supporter badge",
|
||||||
"shuffle": "Shuffle",
|
"shuffle": "Shuffle",
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ function createSlideshowStore() {
|
|||||||
|
|
||||||
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
|
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
|
||||||
const slideshowDelay = persisted<number>('slideshow-delay', 5, {});
|
const slideshowDelay = persisted<number>('slideshow-delay', 5, {});
|
||||||
const slideshowTransition = persisted<boolean>('slideshow-transition', true);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
restartProgress: {
|
restartProgress: {
|
||||||
@@ -68,7 +67,6 @@ function createSlideshowStore() {
|
|||||||
slideshowState,
|
slideshowState,
|
||||||
slideshowDelay,
|
slideshowDelay,
|
||||||
showProgressBar,
|
showProgressBar,
|
||||||
slideshowTransition,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<AddToAlbum shared />
|
<AddToAlbum shared />
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
|
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
{#if $isMultiSelectState}
|
{#if $isMultiSelectState}
|
||||||
<AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}>
|
<AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}>
|
||||||
<CreateSharedLink />
|
<CreateSharedLink />
|
||||||
<ButtonContextMenu icon={mdiPlus} title={$t('add')}>
|
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
|
||||||
<AddToAlbum />
|
<AddToAlbum />
|
||||||
<AddToAlbum shared />
|
<AddToAlbum shared />
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
|
|||||||
@@ -385,7 +385,7 @@
|
|||||||
<AddToAlbum shared />
|
<AddToAlbum shared />
|
||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
|
<DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
|
||||||
<MenuOption
|
<MenuOption
|
||||||
icon={mdiAccountMultipleCheckOutline}
|
icon={mdiAccountMultipleCheckOutline}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@
|
|||||||
</ButtonContextMenu>
|
</ButtonContextMenu>
|
||||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} />
|
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} />
|
||||||
|
|
||||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
|
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
|
|||||||
@@ -285,14 +285,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class=" text-ellipsis px-4 text-sm">
|
<td class=" text-ellipsis px-4 text-sm">
|
||||||
<ButtonContextMenu
|
<ButtonContextMenu color="primary" size="16" icon={mdiDotsVertical} title={$t('library_options')}>
|
||||||
align="top-right"
|
|
||||||
direction="left"
|
|
||||||
color="primary"
|
|
||||||
size="16"
|
|
||||||
icon={mdiDotsVertical}
|
|
||||||
title={$t('library_options')}
|
|
||||||
>
|
|
||||||
<MenuOption onClick={() => onScanClicked(library)} text={$t('scan_library')} />
|
<MenuOption onClick={() => onScanClicked(library)} text={$t('scan_library')} />
|
||||||
<hr />
|
<hr />
|
||||||
<MenuOption onClick={() => onRenameClicked(index)} text={$t('rename')} />
|
<MenuOption onClick={() => onRenameClicked(index)} text={$t('rename')} />
|
||||||
|
|||||||
Reference in New Issue
Block a user