Compare commits

..

1 Commits

Author SHA1 Message Date
ben-basten
e628e6a807 feat(web): scrollable context menus 2024-09-26 23:13:46 -04:00
99 changed files with 377 additions and 542 deletions

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.22",
"version": "2.2.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.22",
"version": "2.2.20",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -52,7 +52,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.116.2",
"version": "1.116.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.22",
"version": "2.2.20",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -187,7 +187,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin
### How does smart search work?
Immich uses CLIP models. An ML model converts each image to an "embedding", which is essentially a string of numbers that semantically encodes what is in the image. The same is done for the text that you enter when you do a search, and that text embedding is then compared with those of the images to find similar ones. As such, there are no "tags", "labels", or "descriptions" generated that you can look at. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
### How does facial recognition work?

View File

@@ -1,12 +1,4 @@
[
{
"label": "v1.116.2",
"url": "https://v1.116.2.archive.immich.app"
},
{
"label": "v1.116.1",
"url": "https://v1.116.1.archive.immich.app"
},
{
"label": "v1.116.0",
"url": "https://v1.116.0.archive.immich.app"

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.116.2",
"version": "1.116.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.116.2",
"version": "1.116.0",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -45,7 +45,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.22",
"version": "2.2.20",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -92,7 +92,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.116.2",
"version": "1.116.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

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

View File

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

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 161,
"android.injected.version.name" => "1.116.2",
"android.injected.version.code" => 160,
"android.injected.version.name" => "1.116.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -401,7 +401,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177;
CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177;
CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -571,7 +571,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177;
CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -58,11 +58,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.116.1</string>
<string>1.116.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>177</string>
<string>176</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
version_number: "1.116.2"
version_number: "1.116.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -4,7 +4,4 @@ abstract interface class IAssetMediaRepository {
Future<List<String>> deleteAll(List<String> ids);
Future<Asset?> get(String id);
/// Obtaining the correct original filename of the asset
Future<String?> getOriginalFilename(String id);
}

View File

@@ -43,17 +43,4 @@ class AssetMediaRepository implements IAssetMediaRepository {
asset.local = local;
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,7 +15,6 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.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/album_media.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
@@ -369,7 +368,6 @@ class BackgroundService {
BackupRepository backupAlbumRepository = BackupRepository(db);
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
FileMediaRepository fileMediaRepository = FileMediaRepository();
AssetMediaRepository assetMediaRepository = AssetMediaRepository();
UserRepository userRepository = UserRepository(db);
UserApiRepository userApiRepository =
UserApiRepository(apiService.usersApi);
@@ -411,7 +409,6 @@ class BackgroundService {
albumService,
albumMediaRepository,
fileMediaRepository,
assetMediaRepository,
);
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();

View File

@@ -12,7 +12,6 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.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/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -22,7 +21,6 @@ import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/db.provider.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/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
@@ -42,7 +40,6 @@ final backupServiceProvider = Provider(
ref.watch(albumServiceProvider),
ref.watch(albumMediaRepositoryProvider),
ref.watch(fileMediaRepositoryProvider),
ref.watch(assetMediaRepositoryProvider),
),
);
@@ -55,7 +52,6 @@ class BackupService {
final AlbumService _albumService;
final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository;
final IAssetMediaRepository _assetMediaRepository;
BackupService(
this._apiService,
@@ -64,7 +60,6 @@ class BackupService {
this._albumService,
this._albumMediaRepository,
this._fileMediaRepository,
this._assetMediaRepository,
);
Future<List<String>?> getDeviceBackupAsset() async {
@@ -334,9 +329,7 @@ class BackupService {
}
if (file != null) {
String? originalFileName =
await _assetMediaRepository.getOriginalFilename(asset.localId!);
originalFileName ??= asset.fileName;
String originalFileName = asset.fileName;
if (asset.local!.isLivePhoto) {
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:
- API version: 1.116.2
- API version: 1.116.0
- Generator version: 7.8.0
- 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
publish_to: 'none'
version: 1.116.2+161
version: 1.116.0+160
environment:
sdk: '>=3.3.0 <4.0.0'

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.116.2",
"version": "1.116.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.116.2",
"version": "1.116.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.116.2",
"version": "1.116.0",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.116.2
* 1.116.0
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,90 +1,84 @@
<p align="center">
<br/>
<p align="center">
<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://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"/>
</a>
<br/>
<br/>
<br/>
<br/>
</p>
<p align="center">
<img src="../design/immich-logo-stacked-light.svg" width="150" title="Immich Logo">
<img src="design/immich-logo.svg" width="150" title="Login com URL customizada">
</p>
<h3 align="center">Solução self-hosted de alta performance para backup de fotos e vídeos</h3>
<h3 align="center">Immich - Solução self-hosted de alta performance para backup de fotos e vídeos</h3>
<br/>
<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>
<br/>
<p align="center">
<a href="../README.md">English</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_ru_RU.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>
<a href="../README.md">English</a>
<a href="README_ca_ES.md">Català</a>
<a href="README_es_ES.md">Español</a>
<a href="README_fr_FR.md">Français</a>
<a href="README_it_IT.md">Italiano</a>
<a href="README_ja_JP.md">日本語</a>
<a href="README_ko_KR.md">한국어</a>
<a href="README_de_DE.md">Deutsch</a>
<a href="README_nl_NL.md">Nederlands</a>
<a href="README_tr_TR.md">Türkçe</a>
<a href="README_zh_CN.md">中文</a>
<a href="README_ru_RU.md">Русский</a>
<a href="README_sv_SE.md">Svenska</a>
<a href="README_ar_JO.md">العربية</a>
</p>
## Avisos
- ⚠️ Este projeto está sob **desenvolvimento constante**.
- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a
compatibilidade com versões anteriores).
- ⚠️ **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!
- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a compatibilidade com versões anteriores).
- ⚠️ **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!
> [!NOTE]
> Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
## Conteúdo
## Links
- [Documentação](https://immich.app/docs)
- [Sobre](https://immich.app/docs/overview/introduction)
- [Instalação](https://immich.app/docs/install/requirements)
- [Documentação Oficial](https://immich.app/docs)
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
- [Demonstração](#demonstração)
- [Funcionalidades](#funcionalidades)
- [Traduções](https://immich.app/docs/developer/translations)
- [Demonstração](#demo)
- [Recursos](#features)
- [Introdução](https://immich.app/docs/overview/introduction)
- [Instalação](https://immich.app/docs/install/requirements)
- [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project)
## Documentação
Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/.
## Demonstração
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.
Você pode acessar a demonstração web em https://demo.immich.app
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`
### Credenciais de login
```bash title="Credenciais de Demonstração"
Credenciais de Demonstração
email: demo@immich.app
senha: demo
```
| Email | Senha |
| --------------- | ----- |
| demo@immich.app | demo |
```
Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
```
## Atividades
![Atividades](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de Analytics do Repobeats")
## Funcionalidades
## Recursos
| Funcionalidades | Aplicativo Móvel | Web |
| :-------------------------------------------------- | ---------------- | --- |
| Recursos | Aplicativo Móvel | Web |
|:----------------------------------------------------|------------------|-----|
| Fazer upload e visualizar fotos e vídeos | Sim | Sim |
| Backup automático ao abrir o aplicativo | Sim | N/A |
| Prevenir a duplicação de arquivos | Sim | Sim |
@@ -94,17 +88,17 @@ No aplicativo para dispositivos móveis, você pode usar
| Criação de álbuns e álbuns compartilhados | Sim | Sim |
| Barra de rolagem arrastável | Sim | Sim |
| Suporta formatos RAW | Sim | Sim |
| Visualização de metadados (EXIF, mapa) | Sim | Sim |
| Pesquisar por metadados, objetos, rostos, e CLIP | Sim | Sim |
| Visualização de metadados (EXIF, map) | Sim | Sim |
| Pesquisar por metadados, objetos, rostos, and CLIP | Sim | Sim |
| Funções administrativas (gerenciamento de usuários) | Não | Sim |
| Backup em segundo plano | Sim | N/A |
| Rolagem virtual | Sim | Sim |
| Virtual scroll | Sim | Sim |
| Suporte OAuth | Sim | Sim |
| Chaves de API | N/A | Sim |
| Backup e reprodução de LivePhoto/MotionPhoto | Sim | Sim |
| Backup e visualização de LivePhoto/MotionPhoto | Sim | Sim |
| Visualização de imagens 360º | Não | Sim |
| Estrutura de armazenamento definida pelo usuário | Sim | Sim |
| Compartilhar com o público | Sim | Sim |
| Compartilhar com o público | Não | Sim |
| Arquivo e Favoritos | Sim | Sim |
| Mapa Global | Sim | Sim |
| Compartilhamento com parceiro | Sim | Sim |
@@ -114,29 +108,6 @@ No aplicativo para dispositivos móveis, você pode usar
| Galeria em modo apenas leitura | Sim | Sim |
| 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
<a href="https://github.com/alextran1502/immich/graphs/contributors">

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.116.2",
"version": "1.116.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.116.2",
"version": "1.116.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@nestjs/bullmq": "^10.0.1",

View File

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

View File

@@ -7,20 +7,83 @@ import { RedisOptions } from 'ioredis';
import Joi, { Root } from 'joi';
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
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';
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 {
ffmpeg: {
crf: number;

View File

@@ -54,6 +54,11 @@ export const resourcePaths = {
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
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 FACE_THUMBNAIL_SIZE = 250;

View File

@@ -33,17 +33,16 @@ import {
UploadFieldName,
} from 'src/dtos/asset-media.dto';
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
import { RouteKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
import { FileUploadInterceptor, Route, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
import { AssetMediaService } from 'src/services/asset-media.service';
import { sendFile } from 'src/utils/file';
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
@ApiTags('Assets')
@Controller(RouteKey.ASSET)
@Controller(Route.ASSET)
export class AssetMediaController {
constructor(
@Inject(ILoggerRepository) private logger: ILoggerRepository,

View File

@@ -14,13 +14,13 @@ import {
} from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryLaneDto } from 'src/dtos/search.dto';
import { RouteKey } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { Route } from 'src/middleware/file-upload.interceptor';
import { AssetService } from 'src/services/asset.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Assets')
@Controller(RouteKey.ASSET)
@Controller(Route.ASSET)
export class AssetController {
constructor(private service: AssetService) {}

View File

@@ -1,6 +1,7 @@
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { AuthType } from 'src/constants';
import {
AuthDto,
ChangePasswordDto,
@@ -12,7 +13,6 @@ import {
ValidateAccessTokenResponseDto,
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto } from 'src/dtos/user.dto';
import { AuthType } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';

View File

@@ -1,6 +1,7 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { AuthType } from 'src/constants';
import {
AuthDto,
ImmichCookie,
@@ -10,7 +11,6 @@ import {
OAuthConfigDto,
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto } from 'src/dtos/user.dto';
import { AuthType } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service';
import { respondWithCookie } from 'src/utils/response';

View File

@@ -21,16 +21,15 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
import { RouteKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
import { UserService } from 'src/services/user.service';
import { sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Users')
@Controller(RouteKey.USER)
@Controller(Route.USER)
export class UserController {
constructor(
private service: UserService,

View File

@@ -1,10 +1,12 @@
import { randomUUID } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
import { ImageFormat } from 'src/config';
import { APP_MEDIA_LOCATION } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
import { AssetFileType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@@ -14,6 +16,14 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { 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 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 _ from 'lodash';
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 { Metadata } from 'src/middleware/auth.guard';
import { setUnion } from 'src/utils/set';
// 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 */
priority?: number;
};
export const OnEmit = (config: EmitConfig) => SetMetadata(MetadataKey.ON_EMIT_CONFIG, config);
export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config);
type LifecycleRelease = 'NEXT_RELEASE' | string;
type LifecycleMetadata = {

View File

@@ -1,7 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum';
import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
import { EntityType } from 'src/enum';
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });

View File

@@ -18,20 +18,20 @@ import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { SystemConfig } from 'src/config';
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
import {
AudioCodec,
CQMode,
Colorspace,
ImageFormat,
LogLevel,
SystemConfig,
ToneMapping,
TranscodeHWAccel,
TranscodePolicy,
VideoCodec,
VideoContainer,
} from 'src/enum';
} from 'src/config';
import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto';
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
import { ValidateBoolean, validateCronExpression } from 'src/validation';

View File

@@ -1,4 +1,3 @@
import { PathType } from 'src/enum';
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
@Entity('move_history')
@@ -22,3 +21,21 @@ export class MoveEntity {
@Column({ type: 'varchar' })
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,8 +1,3 @@
export enum AuthType {
PASSWORD = 'password',
OAUTH = 'oauth',
}
export enum AssetType {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
@@ -153,14 +148,6 @@ export enum SharedLinkType {
INDIVIDUAL = 'INDIVIDUAL',
}
export enum StorageFolder {
ENCODED_VIDEO = 'encoded-video',
LIBRARY = 'library',
UPLOAD = 'upload',
PROFILE = 'profile',
THUMBNAILS = 'thumbs',
}
export enum SystemMetadataKey {
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
@@ -211,120 +198,3 @@ export enum ManualJobName {
TAG_CLEANUP = 'tag-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

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

View File

@@ -116,7 +116,7 @@ export enum JobName {
}
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
export const JOBS_LIBRARY_PAGINATION_SIZE = 10_000;
export const JOBS_LIBRARY_PAGINATION_SIZE = 100_000;
export interface IBaseJob {
force?: boolean;

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import { CommandFactory } from 'nest-commander';
import { fork } from 'node:child_process';
import { Worker } from 'node:worker_threads';
import { ImmichAdminModule } from 'src/app.module';
import { LogLevel } from 'src/enum';
import { LogLevel } from 'src/config';
import { getWorkers } from 'src/utils/workers';
const immichApp = process.argv[2] || process.env.IMMICH_APP;

View File

@@ -11,11 +11,19 @@ import { Reflector } from '@nestjs/core';
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
import { Request } from 'express';
import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto';
import { MetadataKey, Permission } from 'src/enum';
import { Permission } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthService, LoginDetails } from 'src/services/auth.service';
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 SharedLinkRoute = { sharedLink?: true };
type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute);
@@ -24,8 +32,8 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
const decorators: MethodDecorator[] = [
ApiBearerAuth(),
ApiCookieAuth(),
ApiSecurity(MetadataKey.API_KEY_SECURITY),
SetMetadata(MetadataKey.AUTH_ROUTE, options || {}),
ApiSecurity(Metadata.API_KEY_SECURITY),
SetMetadata(Metadata.AUTH_ROUTE, options || {}),
];
if ((options as SharedLinkRoute)?.sharedLink) {
@@ -77,7 +85,7 @@ export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const targets = [context.getHandler()];
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AUTH_ROUTE, targets);
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(Metadata.AUTH_ROUTE, targets);
if (!options) {
return true;
}

View File

@@ -7,7 +7,6 @@ import multer, { StorageEngine, diskStorage } from 'multer';
import { createHash, randomUUID } from 'node:crypto';
import { Observable } from 'rxjs';
import { UploadFieldName } from 'src/dtos/asset-media.dto';
import { RouteKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AuthRequest } from 'src/middleware/auth.guard';
import { AssetMediaService, UploadFile } from 'src/services/asset-media.service';
@@ -29,6 +28,11 @@ export function getFiles(files: UploadFiles) {
};
}
export enum Route {
ASSET = 'assets',
USER = 'users',
}
export interface ImmichFile extends Express.Multer.File {
/** sha1 hash of file */
uuid: string;
@@ -111,7 +115,7 @@ export class FileUploadInterceptor implements NestInterceptor {
const context_ = context.switchToHttp();
const route = this.reflect.get<string>(PATH_METADATA, context.getClass());
const handler: RequestHandler | null = this.getHandler(route as RouteKey);
const handler: RequestHandler | null = this.getHandler(route as Route);
if (handler) {
await new Promise<void>((resolve, reject) => {
const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
@@ -172,13 +176,13 @@ export class FileUploadInterceptor implements NestInterceptor {
return false;
}
private getHandler(route: RouteKey) {
private getHandler(route: Route) {
switch (route) {
case RouteKey.ASSET: {
case Route.ASSET: {
return this.handlers.assetUpload;
}
case RouteKey.USER: {
case Route.USER: {
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 { ExifEntity } from 'src/entities/exif.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { AssetFileType, AssetOrder, AssetStatus, AssetType, PaginationMode } from 'src/enum';
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
import {
AssetBuilderOptions,
AssetCreate,
@@ -30,7 +30,7 @@ import {
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
import { searchAssetBuilder } from 'src/utils/database';
import { Instrumentation } from 'src/utils/instrumentation';
import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
import {
Brackets,
FindOptionsOrder,

View File

@@ -1,15 +0,0 @@
import { Injectable } from '@nestjs/common';
import { getVectorExtension } from 'src/database.config';
import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
@Injectable()
export class ConfigRepository implements IConfigRepository {
getEnv(): EnvData {
return {
database: {
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
vectorExtension: getVectorExtension(),
},
};
}
}

View File

@@ -5,7 +5,6 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
@@ -40,7 +39,6 @@ import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
import { CryptoRepository } from 'src/repositories/crypto.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
import { EventRepository } from 'src/repositories/event.repository';
@@ -76,7 +74,6 @@ export const repositories = [
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
{ provide: IAssetRepository, useClass: AssetRepository },
{ provide: IAuditRepository, useClass: AuditRepository },
{ provide: IConfigRepository, useClass: ConfigRepository },
{ provide: ICryptoRepository, useClass: CryptoRepository },
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
{ provide: IEventRepository, useClass: EventRepository },

View File

@@ -1,7 +1,7 @@
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
import { ClsService } from 'nestjs-cls';
import { LogLevel } from 'src/enum';
import { LogLevel } from 'src/config';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { LogColor } from 'src/utils/logger';

View File

@@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
import { Writable } from 'node:stream';
import { promisify } from 'node:util';
import sharp from 'sharp';
import { Colorspace } from 'src/enum';
import { Colorspace } from 'src/config';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
IMediaRepository,

View File

@@ -1,8 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
import { MoveEntity } from 'src/entities/move.entity';
import { PathType } from 'src/enum';
import { MoveEntity, PathType } from 'src/entities/move.entity';
import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface';
import { Instrumentation } from 'src/utils/instrumentation';
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 { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { PaginationMode, SourceType } from 'src/enum';
import { SourceType } from 'src/enum';
import {
AssetFaceId,
DeleteAllFacesOptions,
@@ -19,7 +19,7 @@ import {
UpdateFacesData,
} from 'src/interfaces/person.interface';
import { Instrumentation } from 'src/utils/instrumentation';
import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
@Instrumentation()

View File

@@ -8,7 +8,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
import { AssetType, PaginationMode } from 'src/enum';
import { AssetType } from 'src/enum';
import { DatabaseExtension } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
@@ -23,7 +23,7 @@ import {
} from 'src/interfaces/search.interface';
import { asVector, searchAssetBuilder } from 'src/utils/database';
import { Instrumentation } from 'src/utils/instrumentation';
import { Paginated, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
import { isValidInteger } from 'src/validation';
import { 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 { AssetFileEntity } from 'src/entities/asset-files.entity';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, AssetType, CacheControl } from 'src/enum';
import { AssetStatus, AssetType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.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 { IUserRepository } from 'src/interfaces/user.interface';
import { AssetMediaService } from 'src/services/asset-media.service';
import { ImmichFileResponse } from 'src/utils/file';
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub';

View File

@@ -7,7 +7,7 @@ import {
} from '@nestjs/common';
import { extname } from 'node:path';
import sanitize from 'sanitize-filename';
import { StorageCore } from 'src/cores/storage.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import {
AssetBulkUploadCheckResponseDto,
AssetMediaResponseDto,
@@ -27,7 +27,7 @@ import {
} from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
import { AssetStatus, AssetType, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.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 { requireAccess, requireUploadAccess } from 'src/utils/access';
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
import { ImmichFileResponse } from 'src/utils/file';
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types';
import { fromChecksum } from 'src/utils/request';
import { QueryFailedError } from 'typeorm';

View File

@@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import { resolve } from 'node:path';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import {
AuditDeletesDto,
AuditDeletesResponseDto,
@@ -12,15 +12,8 @@ import {
PathEntityType,
} from 'src/dtos/audit.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
AssetFileType,
AssetPathType,
DatabaseAction,
Permission,
PersonPathType,
StorageFolder,
UserPathType,
} from 'src/enum';
import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
import { AssetFileType, DatabaseAction, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface';

View File

@@ -1,9 +1,9 @@
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import { Issuer, generators } from 'openid-client';
import { AuthType } from 'src/constants';
import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';

View File

@@ -12,7 +12,7 @@ import { DateTime } from 'luxon';
import { IncomingHttpHeaders } from 'node:http';
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
import { SystemConfig } from 'src/config';
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
import { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.core';
import {
@@ -31,7 +31,7 @@ import {
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum';
import { Permission } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';

View File

@@ -1,20 +1,12 @@
import { IConfigRepository } from 'src/interfaces/config.interface';
import {
DatabaseExtension,
EXTENSION_NAMES,
IDatabaseRepository,
VectorExtension,
} from 'src/interfaces/database.interface';
import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { DatabaseService } from 'src/services/database.service';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { Mocked } from 'vitest';
describe(DatabaseService.name, () => {
let sut: DatabaseService;
let configMock: Mocked<IConfigRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let extensionRange: string;
@@ -24,11 +16,9 @@ describe(DatabaseService.name, () => {
let versionAboveRange: string;
beforeEach(() => {
configMock = newConfigRepositoryMock();
databaseMock = newDatabaseRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new DatabaseService(configMock, databaseMock, loggerMock);
sut = new DatabaseService(databaseMock, loggerMock);
extensionRange = '0.2.x';
databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);
@@ -43,6 +33,11 @@ describe(DatabaseService.name, () => {
});
});
afterEach(() => {
delete process.env.DB_SKIP_MIGRATIONS;
delete process.env.DB_VECTOR_EXTENSION;
});
it('should work', () => {
expect(sut).toBeDefined();
});
@@ -55,12 +50,12 @@ describe(DatabaseService.name, () => {
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
});
describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
describe.each([
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
])('should work with $extensionName', ({ extension, extensionName }) => {
beforeEach(() => {
configMock.getEnv.mockReturnValue({ database: { skipMigrations: false, vectorExtension: extension } });
process.env.DB_VECTOR_EXTENSION = extensionName;
});
it(`should start up successfully with ${extension}`, async () => {
@@ -241,28 +236,18 @@ describe(DatabaseService.name, () => {
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
expect(loggerMock.fatal).not.toHaveBeenCalled();
});
});
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
configMock.getEnv.mockReturnValue({
database: {
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTORS,
},
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
process.env.DB_SKIP_MIGRATIONS = 'true';
await expect(sut.onBootstrap()).resolves.toBeUndefined();
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 () => {
configMock.getEnv.mockReturnValue({
database: {
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTOR,
},
});
process.env.DB_VECTOR_EXTENSION = 'pgvector';
databaseMock.getExtensionVersion.mockResolvedValue({
installedVersion: null,
availableVersion: minVersionInRange,

View File

@@ -1,8 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { Duration } from 'luxon';
import semver from 'semver';
import { getVectorExtension } from 'src/database.config';
import { OnEmit } from 'src/decorators';
import { IConfigRepository } from 'src/interfaces/config.interface';
import {
DatabaseExtension,
DatabaseLock,
@@ -67,7 +67,6 @@ export class DatabaseService {
private reconnection?: NodeJS.Timeout;
constructor(
@Inject(IConfigRepository) private configRepository: IConfigRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
) {
@@ -86,8 +85,7 @@ export class DatabaseService {
}
await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => {
const envData = this.configRepository.getEnv();
const extension = envData.database.vectorExtension;
const extension = getVectorExtension();
const name = EXTENSION_NAMES[extension];
const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
@@ -118,8 +116,7 @@ export class DatabaseService {
await this.checkReindexing();
const { database } = this.configRepository.getEnv();
if (!database.skipMigrations) {
if (process.env.DB_SKIP_MIGRATIONS !== 'true') {
await this.databaseRepository.runMigrations();
}
});

View File

@@ -1,8 +1,5 @@
import { Stats } from 'node:fs';
import { ExifEntity } from 'src/entities/exif.entity';
import {
AssetFileType,
AssetType,
AudioCodec,
Colorspace,
ImageFormat,
@@ -10,7 +7,9 @@ import {
TranscodeHWAccel,
TranscodePolicy,
VideoCodec,
} from 'src/enum';
} from 'src/config';
import { ExifEntity } from 'src/entities/exif.entity';
import { AssetFileType, AssetType } from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';

View File

@@ -1,23 +1,21 @@
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
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 {
AssetFileType,
AssetPathType,
AssetType,
AudioCodec,
Colorspace,
ImageFormat,
StorageFolder,
TranscodeHWAccel,
TranscodePolicy,
TranscodeTarget,
VideoCodec,
VideoContainer,
} from 'src/enum';
} from 'src/config';
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType } from 'src/entities/move.entity';
import { AssetFileType, AssetType } from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import {

View File

@@ -1,8 +1,9 @@
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { Colorspace } from 'src/config';
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { CacheControl, Colorspace, SourceType, SystemMetadataKey } from 'src/enum';
import { SourceType, SystemMetadataKey } from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@@ -15,7 +16,7 @@ import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.inter
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { PersonService } from 'src/services/person.service';
import { ImmichFileResponse } from 'src/utils/file';
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { faceStub } from 'test/fixtures/face.stub';

View File

@@ -1,4 +1,5 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { ImageFormat } from 'src/config';
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
@@ -22,16 +23,9 @@ import {
} from 'src/dtos/person.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity';
import {
AssetType,
CacheControl,
ImageFormat,
Permission,
PersonPathType,
SourceType,
SystemMetadataKey,
} from 'src/enum';
import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@@ -57,7 +51,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { checkAccess, requireAccess } from 'src/utils/access';
import { getAssetFiles } from 'src/utils/asset.util';
import { ImmichFileResponse } from 'src/utils/file';
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types';
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';

View File

@@ -1,7 +1,7 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
import { serverVersion } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit } from 'src/decorators';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
@@ -15,7 +15,7 @@ import {
ServerStorageResponseDto,
UsageByUserDto,
} from 'src/dtos/server.dto';
import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { SystemMetadataKey } from 'src/enum';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.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 { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType } from 'src/enum';
import { AssetPathType } from 'src/entities/move.entity';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';

View File

@@ -13,11 +13,12 @@ import {
supportedWeekTokens,
supportedYearTokens,
} from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
import { AssetPathType } from 'src/entities/move.entity';
import { AssetType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';

View File

@@ -1,8 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { join } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { OnEmit } from 'src/decorators';
import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { SystemMetadataKey } from 'src/enum';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';

View File

@@ -1,18 +1,19 @@
import { BadRequestException } from '@nestjs/common';
import { defaults, SystemConfig } from 'src/config';
import {
AudioCodec,
Colorspace,
CQMode,
Colorspace,
ImageFormat,
LogLevel,
SystemMetadataKey,
SystemConfig,
ToneMapping,
TranscodeHWAccel,
TranscodePolicy,
VideoCodec,
VideoContainer,
} from 'src/enum';
defaults,
} from 'src/config';
import { SystemMetadataKey } from 'src/enum';
import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface';
import { QueueName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';

View File

@@ -1,7 +1,7 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { instanceToPlain } from 'class-transformer';
import _ from 'lodash';
import { SystemConfig, defaults } from 'src/config';
import { LogLevel, SystemConfig, defaults } from 'src/config';
import {
supportedDayTokens,
supportedHourTokens,
@@ -15,7 +15,6 @@ import {
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit, OnServerEvent } from 'src/decorators';
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 { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';

View File

@@ -1,6 +1,6 @@
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, UserMetadataKey } from 'src/enum';
import { UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.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 { IUserRepository } from 'src/interfaces/user.interface';
import { UserService } from 'src/services/user.service';
import { ImmichFileResponse } from 'src/utils/file';
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.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 { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
import { SALT_ROUNDS } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AuthDto } from 'src/dtos/auth.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 { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
import { UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
import { ImmichFileResponse } from 'src/utils/file';
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
@Injectable()

View File

@@ -1,8 +1,8 @@
import { ModuleRef, Reflector } from '@nestjs/core';
import _ from 'lodash';
import { EmitConfig } from 'src/decorators';
import { MetadataKey } from 'src/enum';
import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface';
import { Metadata } from 'src/middleware/auth.guard';
import { services } from 'src/services';
type Item<T extends EmitEvent> = {
@@ -35,7 +35,7 @@ export const setupEventHandlers = (moduleRef: ModuleRef) => {
continue;
}
const options = reflector.get<EmitConfig>(MetadataKey.ON_EMIT_CONFIG, handler);
const options = reflector.get<EmitConfig>(Metadata.ON_EMIT_CONFIG, handler);
if (!options) {
continue;
}

View File

@@ -3,7 +3,6 @@ import { NextFunction, Response } from 'express';
import { access, constants } from 'node:fs/promises';
import { basename, extname, isAbsolute } from 'node:path';
import { promisify } from 'node:util';
import { CacheControl } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ImmichReadStream } from 'src/interfaces/storage.interface';
import { isConnectionAborted } from 'src/utils/misc';
@@ -20,6 +19,12 @@ export function getLivePhotoMotionFilename(stillName: string, motionName: string
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 {
public readonly path!: 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 { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/enum';
import {
AudioStreamInfo,
BitrateDistribution,

View File

@@ -13,8 +13,8 @@ import path from 'node:path';
import { SystemConfig } from 'src/config';
import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants';
import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
import { MetadataKey } from 'src/enum';
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
@@ -210,7 +210,7 @@ export const useSwagger = (app: INestApplication, force = false) => {
in: 'header',
name: ImmichHeader.API_KEY,
},
MetadataKey.API_KEY_SECURITY,
Metadata.API_KEY_SECURITY,
)
.addServer('/api')
.build();

View File

@@ -1,5 +1,4 @@
import _ from 'lodash';
import { PaginationMode } from 'src/enum';
import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
export interface PaginationOptions {
@@ -7,6 +6,11 @@ export interface PaginationOptions {
skip?: number;
}
export enum PaginationMode {
LIMIT_OFFSET = 'limit-offset',
SKIP_TAKE = 'skip-take',
}
export interface PaginatedBuilderOptions {
take: number;
skip?: number;

View File

@@ -1,14 +0,0 @@
import { IConfigRepository } from 'src/interfaces/config.interface';
import { DatabaseExtension } from 'src/interfaces/database.interface';
import { Mocked, vitest } from 'vitest';
export const newConfigRepositoryMock = (): Mocked<IConfigRepository> => {
return {
getEnv: vitest.fn().mockReturnValue({
database: {
skipMigration: false,
vectorExtension: DatabaseExtension.VECTORS,
},
}),
};
};

6
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.116.2",
"version": "1.116.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.116.2",
"version": "1.116.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.7.8",
@@ -74,7 +74,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.116.2",
"version": "1.116.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.116.2",
"version": "1.116.0",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",

View File

@@ -46,7 +46,11 @@ export const contextMenuNavigation: Action<HTMLElement, Options> = (node, option
};
const moveSelection = async (direction: 'up' | 'down', event: KeyboardEvent) => {
const { selectionChanged, container, openDropdown } = options;
const { selectionChanged, container, openDropdown, isOpen } = options;
if (!isOpen) {
// reset the scroll position before opening the menu
container?.scrollTo({ top: 0 });
}
if (openDropdown) {
openDropdown(event);
await tick();

View File

@@ -109,7 +109,13 @@
{/if}
</div>
{#if isOwned}
<ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}>
<ButtonContextMenu
icon={mdiDotsVertical}
size="20"
title={$t('options')}
direction="right"
align="top-left"
>
{#if role === AlbumUserRole.Viewer}
<MenuOption onClick={() => handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} />
{:else}

View File

@@ -186,13 +186,7 @@
{/if}
{#if reaction.user.id === user.id || albumOwnerId === user.id}
<div class="mr-4">
<ButtonContextMenu
icon={mdiDotsVertical}
title={$t('comment_options')}
align="top-right"
direction="left"
size="16"
>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('comment_options')} size="16">
<MenuOption
activeColor="bg-red-200"
icon={mdiDeleteOutline}
@@ -239,13 +233,7 @@
{/if}
{#if reaction.user.id === user.id || albumOwnerId === user.id}
<div class="mr-4">
<ButtonContextMenu
icon={mdiDotsVertical}
title={$t('reaction_options')}
align="top-right"
direction="left"
size="16"
>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('reaction_options')} size="16">
<MenuOption
activeColor="bg-red-200"
icon={mdiDeleteOutline}

View File

@@ -128,7 +128,7 @@
{#if isOwner}
<DeleteAction {asset} {onAction} />
<ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}>
<ButtonContextMenu color="opaque" title={$t('more')} icon={mdiDotsVertical}>
{#if showSlideshow}
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
{/if}

View File

@@ -67,7 +67,6 @@
stopProgress: stopSlideshowProgress,
slideshowNavigation,
slideshowState,
slideshowTransition,
} = slideshowStore;
let appearsInAlbums: AlbumResponseDto[] = [];
@@ -83,14 +82,13 @@
let numberOfComments: number;
let fullscreenElement: Element;
let unsubscribes: (() => void)[] = [];
let selectedEditType: string = '';
let stack: StackResponseDto | null = null;
let zoomToggle = () => void 0;
let copyImage: () => Promise<void>;
$: isFullScreen = fullscreenElement !== null;
let stack: StackResponseDto | null = null;
const refreshStack = async () => {
if (isSharedLink()) {
return;
@@ -392,9 +390,11 @@
onAction?.(action);
};
const handleUpdateSelectedEditType = (type: string) => {
let selectedEditType: string = '';
function handleUpdateSelectedEditType(type: string) {
selectedEditType = type;
};
}
</script>
<svelte:document bind:fullscreenElement />
@@ -508,7 +508,6 @@
onNextAsset={() => navigateAsset('next')}
on:close={closeViewer}
{sharedLink}
haveFadeTransition={$slideshowState === SlideshowState.None || $slideshowTransition}
/>
{/if}
{:else}

View File

@@ -67,6 +67,8 @@
size="20"
icon={mdiDotsVertical}
title={$t('show_person_options')}
direction="right"
align="top-left"
>
<MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
<MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />

View File

@@ -237,7 +237,7 @@
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={handleUpdate} />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem />
<ChangeDate menuItem />
<ChangeLocation menuItem />

View File

@@ -20,11 +20,11 @@
/**
* The alignment of the context menu relative to the button.
*/
export let align: Align = 'top-left';
export let align: Align = 'top-right';
/**
* The direction in which the context menu should open.
*/
export let direction: 'left' | 'right' = 'right';
export let direction: 'left' | 'right' = 'left';
export let color: Color = 'transparent';
export let size: string | undefined = undefined;
export let padding: Padding | undefined = undefined;

View File

@@ -18,25 +18,26 @@
let left: number;
let top: number;
// We need to bind clientHeight since the bounding box may return a height
// of zero when starting the 'slide' animation.
let height: number;
$: {
if (menuElement) {
const rect = menuElement.getBoundingClientRect();
const directionWidth = direction === 'left' ? rect.width : 0;
const menuHeight = Math.min(menuElement.clientHeight, height) || 0;
const menuHeight = menuElement.clientHeight || 0;
left = Math.min(window.innerWidth - rect.width, x - directionWidth);
const calcLeft = Math.min(window.innerWidth - rect.width, x - directionWidth);
left = Math.max(0, calcLeft);
top = Math.min(window.innerHeight - menuHeight, y);
}
}
</script>
<div
bind:clientHeight={height}
class="fixed z-10 min-w-[200px] w-max max-w-[300px] overflow-hidden rounded-lg shadow-lg"
class="fixed z-10 overflow-hidden rounded-lg duration-[250ms] ease-in {isVisible
? 'shadow-lg transition-shadow'
: 'shadow-none transition-none'}"
class:shadow-none={!isVisible}
class:shadow-lg={isVisible}
class:transition-none={!isVisible}
style:left="{left}px"
style:top="{top}px"
transition:slide={{ duration: 250, easing: quintOut }}
@@ -48,9 +49,9 @@
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
bind:this={menuElement}
class:max-h-[100vh]={isVisible}
class:max-h-0={!isVisible}
class="flex flex-col transition-all duration-[250ms] ease-in-out outline-none"
class="flex flex-col transition-all duration-[250ms] ease-in-out outline-none immich-scrollbar bg-slate-100 relative min-w-[200px] max-w-[200px] sm:max-w-[256px] rounded-lg {isVisible
? 'translate-x-0 max-h-dvh overflow-y-auto'
: `${direction === 'left' ? 'translate-x-28' : '-translate-x-28'} max-h-0 overflow-y-hidden`}"
role="menu"
tabindex="-1"
>

View File

@@ -33,7 +33,9 @@
role="menuitem"
>
{#if icon}
<Icon path={icon} ariaHidden={true} size="18" />
<div class="flex-none">
<Icon path={icon} ariaHidden={true} size="18" />
</div>
{/if}
<div>
{text}

View File

@@ -107,6 +107,8 @@
size="24"
padding="3"
hideContent
direction="right"
align="top-left"
>
<SharedLinkEdit menuItem {onEdit} />
<SharedLinkCopy menuItem {link} />

View File

@@ -18,7 +18,7 @@
import SettingDropdown from './shared-components/settings/setting-dropdown.svelte';
import { t } from 'svelte-i18n';
const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook, slideshowTransition } = slideshowStore;
const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook } = slideshowStore;
export let onClose = () => {};
@@ -65,7 +65,6 @@
}}
/>
<SettingSwitch title={$t('show_progress_bar')} bind:checked={$showProgressBar} />
<SettingSwitch title={$t('show_slideshow_transition')} bind:checked={$slideshowTransition} />
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('duration')}

View File

@@ -1144,7 +1144,6 @@
"show_person_options": "Show person options",
"show_progress_bar": "Show Progress Bar",
"show_search_options": "Show search options",
"show_slideshow_transition": "Show slideshow transition",
"show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge",
"shuffle": "Shuffle",

View File

@@ -38,7 +38,6 @@ function createSlideshowStore() {
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
const slideshowDelay = persisted<number>('slideshow-delay', 5, {});
const slideshowTransition = persisted<boolean>('slideshow-transition', true);
return {
restartProgress: {
@@ -68,7 +67,6 @@ function createSlideshowStore() {
slideshowState,
slideshowDelay,
showProgressBar,
slideshowTransition,
};
}

View File

@@ -42,7 +42,7 @@
<AddToAlbum shared />
</ButtonContextMenu>
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem />
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
</ButtonContextMenu>

View File

@@ -31,7 +31,7 @@
{#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}>
<CreateSharedLink />
<ButtonContextMenu icon={mdiPlus} title={$t('add')}>
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
<AddToAlbum />
<AddToAlbum shared />
</ButtonContextMenu>

View File

@@ -385,7 +385,7 @@
<AddToAlbum shared />
</ButtonContextMenu>
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
<MenuOption
icon={mdiAccountMultipleCheckOutline}

View File

@@ -235,7 +235,7 @@
</ButtonContextMenu>
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={triggerAssetUpdate} />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
<DownloadAction menuItem />
<ChangeDate menuItem />
<ChangeLocation menuItem />

View File

@@ -285,14 +285,7 @@
</td>
<td class=" text-ellipsis px-4 text-sm">
<ButtonContextMenu
align="top-right"
direction="left"
color="primary"
size="16"
icon={mdiDotsVertical}
title={$t('library_options')}
>
<ButtonContextMenu color="primary" size="16" icon={mdiDotsVertical} title={$t('library_options')}>
<MenuOption onClick={() => onScanClicked(library)} text={$t('scan_library')} />
<hr />
<MenuOption onClick={() => onRenameClicked(index)} text={$t('rename')} />