Compare commits

...

6 Commits

Author SHA1 Message Date
github-actions
7c15e11efc chore: version v1.116.1 2024-09-27 15:32:16 +00:00
Alex
03aa346020 fix(mobile): incorrect filename is retrieved during upload (#12990)
* fix(mobile): incorrect filename is retrieve during upload

* use the same convention to get local id

* revert previous change

* pr feedback
2024-09-27 22:28:31 +07:00
martin
3a37fc8bfd feat: no slideshow transition (#12989) 2024-09-27 15:05:07 +00:00
Jason Rasmussen
36ee72cd87 refactor(server): access env via repository (#12987) 2024-09-27 10:28:56 -04:00
Jason Rasmussen
12da250028 refactor: enums (#12988) 2024-09-27 10:28:42 -04:00
Ryan Ribeiro
5b282733fe chore(Brazilian README): fix broken image links and update translation (#12980) 2024-09-27 08:15:25 -04:00
80 changed files with 487 additions and 328 deletions

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.21",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.21",
"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.0", "version": "1.116.1",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.21",
"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",

View File

@@ -1,4 +1,8 @@
[ [
{
"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
View File

@@ -1,12 +1,12 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.116.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.116.1",
"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.20", "version": "2.2.21",
"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.0", "version": "1.116.1",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.116.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.116.0" version = "1.116.1"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 160, "android.injected.version.code" => 161,
"android.injected.version.name" => "1.116.0", "android.injected.version.name" => "1.116.1",
} }
) )
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')

View File

@@ -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.0" version_number: "1.116.1"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,

View File

@@ -4,4 +4,7 @@ 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);
} }

View File

@@ -43,4 +43,17 @@ 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;
}
} }

View File

@@ -15,6 +15,7 @@ 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';
@@ -368,6 +369,7 @@ 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);
@@ -409,6 +411,7 @@ class BackgroundService {
albumService, albumService,
albumMediaRepository, albumMediaRepository,
fileMediaRepository, fileMediaRepository,
assetMediaRepository,
); );
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();

View File

@@ -12,6 +12,7 @@ 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';
@@ -21,6 +22,7 @@ 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';
@@ -40,6 +42,7 @@ final backupServiceProvider = Provider(
ref.watch(albumServiceProvider), ref.watch(albumServiceProvider),
ref.watch(albumMediaRepositoryProvider), ref.watch(albumMediaRepositoryProvider),
ref.watch(fileMediaRepositoryProvider), ref.watch(fileMediaRepositoryProvider),
ref.watch(assetMediaRepositoryProvider),
), ),
); );
@@ -52,6 +55,7 @@ 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,
@@ -60,6 +64,7 @@ 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 {
@@ -329,7 +334,9 @@ class BackupService {
} }
if (file != null) { if (file != null) {
String originalFileName = asset.fileName; String? originalFileName =
await _assetMediaRepository.getOriginalFilename(asset.localId!);
originalFileName ??= asset.fileName;
if (asset.local!.isLivePhoto) { if (asset.local!.isLivePhoto) {
if (livePhotoFile == null) { if (livePhotoFile == null) {

View File

@@ -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.0 - API version: 1.116.1
- Generator version: 7.8.0 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -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.0+160 version: 1.116.1+161
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'

View File

@@ -7409,7 +7409,7 @@
"info": { "info": {
"title": "Immich", "title": "Immich",
"description": "Immich API", "description": "Immich API",
"version": "1.116.0", "version": "1.116.1",
"contact": {} "contact": {}
}, },
"tags": [], "tags": [],

View File

@@ -1,12 +1,12 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.116.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.116.1",
"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"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.116.1",
"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",

View File

@@ -1,6 +1,6 @@
/** /**
* Immich * Immich
* 1.116.0 * 1.116.1
* 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
*/ */

View File

@@ -1,84 +1,90 @@
<p align="center"> <p align="center">
<br/> <br/>
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="Licença: AGPLv3"></a> <a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="Licença: AGPLv3"></a>
<a href="https://discord.immich.app"> <a href="https://discord.immich.app">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/> <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a> </a>
<br/> <br/>
<br/> <br/>
</p> </p>
<p align="center"> <p align="center">
<img src="design/immich-logo.svg" width="150" title="Login com URL customizada"> <img src="../design/immich-logo-stacked-light.svg" width="150" title="Immich Logo">
</p> </p>
<h3 align="center">Immich - Solução self-hosted de alta performance para backup de fotos e vídeos</h3> <h3 align="center">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_ca_ES.md">Català</a> <a href="../README.md">English</a>
<a href="README_es_ES.md">Español</a> <a href="README_ca_ES.md">Català</a>
<a href="README_fr_FR.md">Français</a> <a href="README_es_ES.md">Español</a>
<a href="README_it_IT.md">Italiano</a> <a href="README_fr_FR.md">Français</a>
<a href="README_ja_JP.md">日本語</a> <a href="README_it_IT.md">Italiano</a>
<a href="README_ko_KR.md">한국어</a> <a href="README_ja_JP.md">日本語</a>
<a href="README_de_DE.md">Deutsch</a> <a href="README_ko_KR.md">한국어</a>
<a href="README_nl_NL.md">Nederlands</a> <a href="README_de_DE.md">Deutsch</a>
<a href="README_tr_TR.md">Türkçe</a> <a href="README_nl_NL.md">Nederlands</a>
<a href="README_zh_CN.md">中文</a> <a href="README_tr_TR.md">Türkçe</a>
<a href="README_ru_RU.md">Русский</a> <a href="README_zh_CN.md">中文</a>
<a href="README_sv_SE.md">Svenska</a> <a href="README_ru_RU.md">Русский</a>
<a href="README_ar_JO.md">العربية</a> <a href="README_sv_SE.md">Svenska</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 compatibilidade com versões anteriores). - ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a
- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e vídeos.** compatibilidade com versões anteriores).
- ⚠️ 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! - ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e
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!
## Conteúdo > [!NOTE]
> Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
- [Documentação Oficial](https://immich.app/docs) ## Links
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
- [Demonstração](#demo) - [Documentação](https://immich.app/docs)
- [Recursos](#features) - [Sobre](https://immich.app/docs/overview/introduction)
- [Introdução](https://immich.app/docs/overview/introduction)
- [Instalação](https://immich.app/docs/install/requirements) - [Instalação](https://immich.app/docs/install/requirements)
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
- [Demonstração](#demonstração)
- [Funcionalidades](#funcionalidades)
- [Traduções](https://immich.app/docs/developer/translations)
- [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
Você pode acessar a demonstração web em https://demo.immich.app Acesse a demonstração [aqui](https://demo.immich.app). A demonstração está
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 `https://demo.immich.app/api` no campo `Server Endpoint URL` No aplicativo para dispositivos móveis, você pode usar
`https://demo.immich.app/api` no campo `Server Endpoint URL`
```bash title="Credenciais de Demonstração" ### Credenciais de login
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
![Atividades](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de Analytics do Repobeats") ![Atividades](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de Analytics do Repobeats")
## Recursos ## Funcionalidades
| 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 |
@@ -88,17 +94,17 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR
| 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, map) | Sim | Sim | | Visualização de metadados (EXIF, mapa) | Sim | Sim |
| Pesquisar por metadados, objetos, rostos, and CLIP | Sim | Sim | | Pesquisar por metadados, objetos, rostos, e 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 |
| Virtual scroll | Sim | Sim | | Rolagem virtual | Sim | Sim |
| Suporte OAuth | Sim | Sim | | Suporte OAuth | Sim | Sim |
| Chaves de API | N/A | Sim | | Chaves de API | N/A | Sim |
| Backup e visualização de LivePhoto/MotionPhoto | Sim | Sim | | Backup e reproduçã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 | Não | Sim | | Compartilhar com o público | Sim | 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 |
@@ -108,6 +114,29 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR
| 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
![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de análise de atividade Repobeats")
## 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">

View File

@@ -1,12 +1,12 @@
{ {
"name": "immich", "name": "immich",
"version": "1.116.0", "version": "1.116.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich", "name": "immich",
"version": "1.116.0", "version": "1.116.1",
"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",

View File

@@ -1,6 +1,6 @@
{ {
"name": "immich", "name": "immich",
"version": "1.116.0", "version": "1.116.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@@ -7,83 +7,20 @@ 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;

View File

@@ -54,11 +54,6 @@ 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;

View File

@@ -33,16 +33,17 @@ 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, Route, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor'; import { FileUploadInterceptor, 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(Route.ASSET) @Controller(RouteKey.ASSET)
export class AssetMediaController { export class AssetMediaController {
constructor( constructor(
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,

View File

@@ -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(Route.ASSET) @Controller(RouteKey.ASSET)
export class AssetController { export class AssetController {
constructor(private service: AssetService) {} constructor(private service: AssetService) {}

View File

@@ -1,7 +1,6 @@
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,
@@ -13,6 +12,7 @@ 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';

View File

@@ -1,7 +1,6 @@
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,
@@ -11,6 +10,7 @@ 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';

View File

@@ -21,15 +21,16 @@ 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, Route } from 'src/middleware/file-upload.interceptor'; import { FileUploadInterceptor } 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(Route.USER) @Controller(RouteKey.USER)
export class UserController { export class UserController {
constructor( constructor(
private service: UserService, private service: UserService,

View File

@@ -1,12 +1,10 @@
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 } from 'src/enum'; import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { 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';
@@ -16,14 +14,6 @@ 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));

View File

@@ -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(Metadata.ON_EMIT_CONFIG, config); export const OnEmit = (config: EmitConfig) => SetMetadata(MetadataKey.ON_EMIT_CONFIG, config);
type LifecycleRelease = 'NEXT_RELEASE' | string; type LifecycleRelease = 'NEXT_RELEASE' | string;
type LifecycleMetadata = { type LifecycleMetadata = {

View File

@@ -1,8 +1,7 @@
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, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum';
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 });

View File

@@ -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/config'; } from 'src/enum';
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';

View File

@@ -1,3 +1,4 @@
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')
@@ -21,21 +22,3 @@ 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;

View File

@@ -1,3 +1,8 @@
export enum AuthType {
PASSWORD = 'password',
OAUTH = 'oauth',
}
export enum AssetType { export enum AssetType {
IMAGE = 'IMAGE', IMAGE = 'IMAGE',
VIDEO = 'VIDEO', VIDEO = 'VIDEO',
@@ -148,6 +153,14 @@ 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',
@@ -198,3 +211,120 @@ 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',
}

View File

@@ -0,0 +1,14 @@
import { VectorExtension } from 'src/interfaces/database.interface';
export const IConfigRepository = 'IConfigRepository';
export interface EnvData {
database: {
skipMigrations: boolean;
vectorExtension: VectorExtension;
};
}
export interface IConfigRepository {
getEnv(): EnvData;
}

View File

@@ -1,4 +1,4 @@
import { LogLevel } from 'src/config'; import { LogLevel } from 'src/enum';
export const ILoggerRepository = 'ILoggerRepository'; export const ILoggerRepository = 'ILoggerRepository';

View File

@@ -1,5 +1,5 @@
import { Writable } from 'node:stream'; import { Writable } from 'node:stream';
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config'; import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/enum';
export const IMediaRepository = 'IMediaRepository'; export const IMediaRepository = 'IMediaRepository';

View File

@@ -1,4 +1,5 @@
import { MoveEntity, PathType } from 'src/entities/move.entity'; import { MoveEntity } from 'src/entities/move.entity';
import { PathType } from 'src/enum';
export const IMoveRepository = 'IMoveRepository'; export const IMoveRepository = 'IMoveRepository';

View File

@@ -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/config'; import { LogLevel } from 'src/enum';
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;

View File

@@ -11,19 +11,11 @@ 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 { Permission } from 'src/enum'; import { MetadataKey, 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);
@@ -32,8 +24,8 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
const decorators: MethodDecorator[] = [ const decorators: MethodDecorator[] = [
ApiBearerAuth(), ApiBearerAuth(),
ApiCookieAuth(), ApiCookieAuth(),
ApiSecurity(Metadata.API_KEY_SECURITY), ApiSecurity(MetadataKey.API_KEY_SECURITY),
SetMetadata(Metadata.AUTH_ROUTE, options || {}), SetMetadata(MetadataKey.AUTH_ROUTE, options || {}),
]; ];
if ((options as SharedLinkRoute)?.sharedLink) { if ((options as SharedLinkRoute)?.sharedLink) {
@@ -85,7 +77,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>(Metadata.AUTH_ROUTE, targets); const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AUTH_ROUTE, targets);
if (!options) { if (!options) {
return true; return true;
} }

View File

@@ -7,6 +7,7 @@ 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';
@@ -28,11 +29,6 @@ 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;
@@ -115,7 +111,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 Route); const handler: RequestHandler | null = this.getHandler(route as RouteKey);
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());
@@ -176,13 +172,13 @@ export class FileUploadInterceptor implements NestInterceptor {
return false; return false;
} }
private getHandler(route: Route) { private getHandler(route: RouteKey) {
switch (route) { switch (route) {
case Route.ASSET: { case RouteKey.ASSET: {
return this.handlers.assetUpload; return this.handlers.assetUpload;
} }
case Route.USER: { case RouteKey.USER: {
return this.handlers.userProfile; return this.handlers.userProfile;
} }

View File

@@ -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 } from 'src/enum'; import { AssetFileType, AssetOrder, AssetStatus, AssetType, PaginationMode } 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, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
import { import {
Brackets, Brackets,
FindOptionsOrder, FindOptionsOrder,

View File

@@ -0,0 +1,15 @@
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(),
},
};
}
}

View File

@@ -5,6 +5,7 @@ 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';
@@ -39,6 +40,7 @@ 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';
@@ -74,6 +76,7 @@ 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 },

View File

@@ -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/config'; import { LogLevel } from 'src/enum';
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';

View File

@@ -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/config'; import { Colorspace } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { import {
IMediaRepository, IMediaRepository,

View File

@@ -1,7 +1,8 @@
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, PathType } from 'src/entities/move.entity'; import { MoveEntity } 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';

View File

@@ -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 { SourceType } from 'src/enum'; import { PaginationMode, 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, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; import { Paginated, 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()

View File

@@ -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 } from 'src/enum'; import { AssetType, PaginationMode } 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, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination'; import { Paginated, 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';

View File

@@ -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 } from 'src/enum'; import { AssetStatus, AssetType, CacheControl } 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 { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { 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';

View File

@@ -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, StorageFolder } from 'src/cores/storage.core'; import { StorageCore } 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, Permission } from 'src/enum'; import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } 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 { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { 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';

View File

@@ -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, StorageFolder } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { import {
AuditDeletesDto, AuditDeletesDto,
AuditDeletesResponseDto, AuditDeletesResponseDto,
@@ -12,8 +12,15 @@ 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 { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; import {
import { AssetFileType, DatabaseAction, Permission } from 'src/enum'; AssetFileType,
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';

View File

@@ -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';

View File

@@ -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 { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; import { 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 { Permission } from 'src/enum'; import { AuthType, Permission } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { 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';

View File

@@ -1,12 +1,20 @@
import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface'; import { IConfigRepository } from 'src/interfaces/config.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;
@@ -16,9 +24,11 @@ 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);
@@ -33,11 +43,6 @@ 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();
}); });
@@ -50,12 +55,12 @@ describe(DatabaseService.name, () => {
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1); expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
}); });
describe.each([ describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
{ 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(() => {
process.env.DB_VECTOR_EXTENSION = extensionName; configMock.getEnv.mockReturnValue({ database: { skipMigrations: false, vectorExtension: extension } });
}); });
it(`should start up successfully with ${extension}`, async () => { it(`should start up successfully with ${extension}`, async () => {
@@ -236,18 +241,28 @@ 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 () => {
process.env.DB_SKIP_MIGRATIONS = 'true'; configMock.getEnv.mockReturnValue({
database: {
await expect(sut.onBootstrap()).resolves.toBeUndefined(); skipMigrations: true,
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 () => {
process.env.DB_VECTOR_EXTENSION = 'pgvector'; configMock.getEnv.mockReturnValue({
database: {
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTOR,
},
});
databaseMock.getExtensionVersion.mockResolvedValue({ databaseMock.getExtensionVersion.mockResolvedValue({
installedVersion: null, installedVersion: null,
availableVersion: minVersionInRange, availableVersion: minVersionInRange,

View File

@@ -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,6 +67,7 @@ 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,
) { ) {
@@ -85,7 +86,8 @@ export class DatabaseService {
} }
await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => { await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
const extension = getVectorExtension(); const envData = this.configRepository.getEnv();
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);
@@ -116,7 +118,8 @@ export class DatabaseService {
await this.checkReindexing(); await this.checkReindexing();
if (process.env.DB_SKIP_MIGRATIONS !== 'true') { const { database } = this.configRepository.getEnv();
if (!database.skipMigrations) {
await this.databaseRepository.runMigrations(); await this.databaseRepository.runMigrations();
} }
}); });

View File

@@ -1,5 +1,8 @@
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,
@@ -7,9 +10,7 @@ import {
TranscodeHWAccel, TranscodeHWAccel,
TranscodePolicy, TranscodePolicy,
VideoCodec, VideoCodec,
} from 'src/config'; } from 'src/enum';
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';

View File

@@ -1,21 +1,23 @@
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/config'; } from 'src/enum';
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 {

View File

@@ -1,9 +1,8 @@
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 { SourceType, SystemMetadataKey } from 'src/enum'; import { CacheControl, Colorspace, 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';
@@ -16,7 +15,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 { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { 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';

View File

@@ -1,5 +1,4 @@
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';
@@ -23,9 +22,16 @@ 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 { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum'; import {
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';
@@ -51,7 +57,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 { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { 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';

View File

@@ -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, StorageFolder } 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';
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 { SystemMetadataKey } from 'src/enum'; import { StorageFolder, 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';

View File

@@ -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/entities/move.entity'; import { AssetPathType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';

View File

@@ -13,12 +13,11 @@ import {
supportedWeekTokens, supportedWeekTokens,
supportedYearTokens, supportedYearTokens,
} from 'src/constants'; } from 'src/constants';
import { StorageCore, StorageFolder } 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';
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 } from 'src/entities/move.entity'; import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
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';

View File

@@ -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, StorageFolder } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { OnEmit } from 'src/decorators'; import { OnEmit } from 'src/decorators';
import { SystemMetadataKey } from 'src/enum'; import { StorageFolder, 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';

View File

@@ -1,19 +1,18 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { defaults, SystemConfig } from 'src/config';
import { import {
AudioCodec, AudioCodec,
CQMode,
Colorspace, Colorspace,
CQMode,
ImageFormat, ImageFormat,
LogLevel, LogLevel,
SystemConfig, SystemMetadataKey,
ToneMapping, ToneMapping,
TranscodeHWAccel, TranscodeHWAccel,
TranscodePolicy, TranscodePolicy,
VideoCodec, VideoCodec,
VideoContainer, VideoContainer,
defaults, } from 'src/enum';
} 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';

View File

@@ -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 { LogLevel, SystemConfig, defaults } from 'src/config'; import { SystemConfig, defaults } from 'src/config';
import { import {
supportedDayTokens, supportedDayTokens,
supportedHourTokens, supportedHourTokens,
@@ -15,6 +15,7 @@ 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';

View File

@@ -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 { UserMetadataKey } from 'src/enum'; import { CacheControl, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { 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 { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { 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';

View File

@@ -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, StorageFolder } 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';
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 { UserMetadataKey } from 'src/enum'; import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { 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 { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { ImmichFileResponse } from 'src/utils/file';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
@Injectable() @Injectable()

View File

@@ -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>(Metadata.ON_EMIT_CONFIG, handler); const options = reflector.get<EmitConfig>(MetadataKey.ON_EMIT_CONFIG, handler);
if (!options) { if (!options) {
continue; continue;
} }

View File

@@ -3,6 +3,7 @@ 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';
@@ -19,12 +20,6 @@ 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;

View File

@@ -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,

View File

@@ -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,
}, },
Metadata.API_KEY_SECURITY, MetadataKey.API_KEY_SECURITY,
) )
.addServer('/api') .addServer('/api')
.build(); .build();

View File

@@ -1,4 +1,5 @@
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 {
@@ -6,11 +7,6 @@ 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;

View File

@@ -0,0 +1,14 @@
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
View File

@@ -1,12 +1,12 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.116.0", "version": "1.116.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-web", "name": "immich-web",
"version": "1.116.0", "version": "1.116.1",
"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.0", "version": "1.116.1",
"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"

View File

@@ -1,6 +1,6 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.116.0", "version": "1.116.1",
"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",

View File

@@ -67,6 +67,7 @@
stopProgress: stopSlideshowProgress, stopProgress: stopSlideshowProgress,
slideshowNavigation, slideshowNavigation,
slideshowState, slideshowState,
slideshowTransition,
} = slideshowStore; } = slideshowStore;
let appearsInAlbums: AlbumResponseDto[] = []; let appearsInAlbums: AlbumResponseDto[] = [];
@@ -82,13 +83,14 @@
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;
@@ -390,11 +392,9 @@
onAction?.(action); onAction?.(action);
}; };
let selectedEditType: string = ''; const handleUpdateSelectedEditType = (type: string) => {
function handleUpdateSelectedEditType(type: string) {
selectedEditType = type; selectedEditType = type;
} };
</script> </script>
<svelte:document bind:fullscreenElement /> <svelte:document bind:fullscreenElement />
@@ -508,6 +508,7 @@
onNextAsset={() => navigateAsset('next')} onNextAsset={() => navigateAsset('next')}
on:close={closeViewer} on:close={closeViewer}
{sharedLink} {sharedLink}
haveFadeTransition={$slideshowState === SlideshowState.None || $slideshowTransition}
/> />
{/if} {/if}
{:else} {:else}

View File

@@ -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 } = slideshowStore; const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook, slideshowTransition } = slideshowStore;
export let onClose = () => {}; export let onClose = () => {};
@@ -65,6 +65,7 @@
}} }}
/> />
<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')}

View File

@@ -1144,6 +1144,7 @@
"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",

View File

@@ -38,6 +38,7 @@ 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: {
@@ -67,6 +68,7 @@ function createSlideshowStore() {
slideshowState, slideshowState,
slideshowDelay, slideshowDelay,
showProgressBar, showProgressBar,
slideshowTransition,
}; };
} }