Merge branch 'main' into fix/user-removal-from-option-menu-on-the-top-in-shared-album

This commit is contained in:
Pranav tiwari 2024-10-01 23:54:31 +05:30 committed by GitHub
commit c5bfda27df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 1999 additions and 1178 deletions

View File

@ -88,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image
uses: docker/build-push-action@v6.7.0
uses: docker/build-push-action@v6.9.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64

View File

@ -22,7 +22,7 @@ concurrency:
jobs:
cleanup-images:
name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
@ -48,7 +48,7 @@ jobs:
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- cleanup-images
strategy:

View File

@ -173,7 +173,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v6.7.0
uses: docker/build-push-action@v6.9.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
@ -264,7 +264,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v6.7.0
uses: docker/build-push-action@v6.9.0
with:
context: ${{ env.context }}
file: ${{ env.file }}

167
cli/package-lock.json generated
View File

@ -24,7 +24,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@ -59,7 +59,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"typescript": "^5.3.3"
}
},
@ -765,6 +765,16 @@
"node": "*"
}
},
"node_modules/@eslint/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
@ -825,9 +835,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
"dev": true,
"license": "MIT",
"engines": {
@ -845,9 +855,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -1312,6 +1322,13 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
@ -1337,9 +1354,9 @@
}
},
"node_modules/@types/node": {
"version": "20.16.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1353,17 +1370,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -1387,16 +1404,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4"
},
"engines": {
@ -1416,14 +1433,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1434,14 +1451,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -1459,9 +1476,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"dev": true,
"license": "MIT",
"engines": {
@ -1473,14 +1490,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -1502,16 +1519,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1525,13 +1542,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -2170,21 +2187,24 @@
}
},
"node_modules/eslint": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.6.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.10.0",
"@eslint/plugin-kit": "^0.1.0",
"@eslint/js": "9.11.1",
"@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -2335,6 +2355,13 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -2347,9 +2374,9 @@
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
"integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@ -3449,21 +3476,17 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0",
"typescript": ">=2.9",
"vue-tsc": "^2.0.24"
"vue-tsc": "^2.1.0"
},
"peerDependenciesMeta": {
"@vue/language-plugin-pug": {
"optional": true
},
"vue-tsc": {
"optional": true
}
@ -4155,9 +4178,9 @@
}
},
"node_modules/vite": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -20,7 +20,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",

View File

@ -36,6 +36,10 @@ services:
IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849
IMMICH_BUILD_IMAGE: development
IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server
IMMICH_THIRD_PARTY_SOURCE_URL: https://github.com/immich-app/immich/
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://immich.app/docs
IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/third-party
ulimits:
nofile:
soft: 1048576

View File

@ -91,7 +91,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9
image: grafana/grafana:11.2.1-ubuntu@sha256:b90c0fdc482913de7a55fe96539bf9e3c4fbcee835d0c2dffc59152bc3964ff7
volumes:
- grafana-data:/var/lib/grafana

View File

@ -0,0 +1,47 @@
# System Integrity
## Folder checks
:::info
The folders considered for these checks include: `upload/`, `library/`, `thumbs/`, `encoded-video/`, `profile/`
:::
When Immich starts, it performs a series of checks in order to validate that it can read and write files to the volume mounts used by the storage system. If it cannot perform all the required operations, it will fail to start. The checks include:
- Creating an initial hidden file (`.immich`) in each folder
- Reading a hidden file (`.immich`) in each folder
- Overwriting a hidden file (`.immich`) in each folder
The checks are designed to catch the following situations:
- Incorrect permissions (cannot read/write files)
- Missing volume mount (`.immich` files should exist, but are missing)
### Common issues
:::note
`.immich` files serve as markers and help keep track of volume mounts being used by Immich. Except for the situations listed below, they should never be manually created or deleted.
:::
#### Missing `.immich` files
```
Verifying system mount folder checks (enabled=true)
...
ENOENT: no such file or directory, open 'upload/encoded-video/.immich'
```
The above error messages show that the server has previously (successfully) written `.immich` files to each folder, but now does not detect them. This could be because any of the following:
- Permission error - unable to read the file, but it exists
- File does not exist - volume mount has changed and should be corrected
- File does not exist - user manually deleted it and should be manually re-created (`touch .immich`)
- File does not exist - user restored from a backup, but did not restore each folder (user should restore all folders or manually create `.immich` in any missing folders)
### Ignoring the checks
The checks are designed to catch common problems that we have seen users have in the past, but if you want to disable them you can set the following environment variable:
```
IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
```

View File

@ -42,6 +42,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.

29
docs/package-lock.json generated
View File

@ -12715,9 +12715,9 @@
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"license": "ISC"
},
"node_modules/picomatch": {
@ -12829,9 +12829,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [
{
"type": "opencollective",
@ -12849,8 +12849,8 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -15670,9 +15670,10 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@ -16091,9 +16092,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",

241
e2e/package-lock.json generated
View File

@ -15,7 +15,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@ -64,7 +64,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@ -99,7 +99,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"typescript": "^5.3.3"
}
},
@ -784,6 +784,16 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
@ -822,9 +832,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
"dev": true,
"license": "MIT",
"engines": {
@ -842,9 +852,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -1121,10 +1131,11 @@
}
},
"node_modules/@photostructure/tz-lookup": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
"integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA==",
"dev": true
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.0.0.tgz",
"integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw==",
"dev": true,
"license": "CC0-1.0"
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
@ -1149,13 +1160,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz",
"integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==",
"version": "1.47.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
"integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.47.1"
"playwright": "1.47.2"
},
"bin": {
"playwright": "cli.js"
@ -1519,6 +1530,13 @@
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/keygrip": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
@ -1569,9 +1587,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.16.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1733,17 +1751,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -1767,16 +1785,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4"
},
"engines": {
@ -1796,14 +1814,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1814,14 +1832,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -1839,9 +1857,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"dev": true,
"license": "MIT",
"engines": {
@ -1853,14 +1871,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -1908,16 +1926,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1931,13 +1949,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -2854,16 +2872,17 @@
}
},
"node_modules/engine.io-client": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0"
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
@ -2972,21 +2991,24 @@
}
},
"node_modules/eslint": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.6.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.10.0",
"@eslint/plugin-kit": "^0.1.0",
"@eslint/js": "9.11.1",
"@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -3138,9 +3160,9 @@
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
"integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@ -3246,27 +3268,27 @@
}
},
"node_modules/exiftool-vendored": {
"version": "28.2.1",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz",
"integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==",
"version": "28.3.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.3.0.tgz",
"integrity": "sha512-2DOSOvj5c1gkbKtubAnlGglxdYp9h55n0GxjK2nypVivoaCdgP/le3MOZRKgEUNObfJHmYHj4u/NnYVneu/gUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@photostructure/tz-lookup": "^10.0.0",
"@photostructure/tz-lookup": "^11.0.0",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"he": "^1.2.0",
"luxon": "^3.5.0"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.91.0",
"exiftool-vendored.pl": "12.91.0"
"exiftool-vendored.exe": "12.96.0",
"exiftool-vendored.pl": "12.96.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.91.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz",
"integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==",
"version": "12.96.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.96.0.tgz",
"integrity": "sha512-pKPN9F/Evw2yyO5/+ml3spbXIqejzOxyF7jEnj8tLU2JPSmIlziPUZ75XIhcPbilX86jVKmuiso7FUDicOg8pQ==",
"dev": true,
"license": "MIT",
"optional": true,
@ -3275,9 +3297,9 @@
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.91.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz",
"integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==",
"version": "12.96.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.96.0.tgz",
"integrity": "sha512-v4nGnovAMBsTfOWhwAcOiRiq/8kuJOo3GUMHNpug7Mr4jLz3tmWEo7DdNyOYmpcvWbA6smOTG0SmwsrY8fsW+A==",
"dev": true,
"license": "MIT",
"optional": true,
@ -4142,9 +4164,9 @@
}
},
"node_modules/jose": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.2.tgz",
"integrity": "sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.3.tgz",
"integrity": "sha512-egLIoYSpcd+QUF+UHgobt5YzI2Pkw/H39ou9suW687MY6PmCwPmkNV/4TNjn1p2tX5xO3j0d0sq5hiYE24bSlg==",
"dev": true,
"license": "MIT",
"funding": {
@ -5135,13 +5157,13 @@
}
},
"node_modules/playwright": {
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.1.tgz",
"integrity": "sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw==",
"version": "1.47.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
"integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.47.1"
"playwright-core": "1.47.2"
},
"bin": {
"playwright": "cli.js"
@ -5154,9 +5176,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.1.tgz",
"integrity": "sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ==",
"version": "1.47.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
"integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -5296,21 +5318,17 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0",
"typescript": ">=2.9",
"vue-tsc": "^2.0.24"
"vue-tsc": "^2.1.0"
},
"peerDependenciesMeta": {
"@vue/language-plugin-pug": {
"optional": true
},
"vue-tsc": {
"optional": true
}
@ -5796,14 +5814,15 @@
}
},
"node_modules/socket.io-client": {
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz",
"integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
@ -6732,9 +6751,9 @@
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
"integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
"dev": true,
"engines": {
"node": ">=0.4.0"

View File

@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",

View File

@ -1,6 +1,6 @@
ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:e456ff58048f52f121025159e68bf16248c4122c8b96fadffd89331df50c9994 AS builder-cpu
FROM python:3.11-bookworm@sha256:3cdce69fd5663ca47c420ec4d4df8e3545519a4030372f7d2064fb1be2279844 AS builder-cpu
FROM builder-cpu AS builder-openvino
@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
FROM python:3.11-slim-bookworm@sha256:585cf0799407efc267fe1cce318322ec26e015ac1b3d77f2517d50bc3acfc232 AS prod-cpu
FROM python:3.11-slim-bookworm@sha256:5501a4fe605abe24de87c2f3d6cf9fd760354416a0cad0296cf284fddcdca9e2 AS prod-cpu
FROM prod-cpu AS prod-openvino

View File

@ -2,13 +2,13 @@
[[package]]
name = "aiocache"
version = "0.12.2"
version = "0.12.3"
description = "multi backend asyncio cache"
optional = false
python-versions = "*"
files = [
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
{file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
{file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
]
[package.extras]
@ -1237,13 +1237,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "huggingface-hub"
version = "0.25.0"
version = "0.25.1"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.25.0-py3-none-any.whl", hash = "sha256:e2f357b35d72d5012cfd127108c4e14abcd61ba4ebc90a5a374dc2456cb34e12"},
{file = "huggingface_hub-0.25.0.tar.gz", hash = "sha256:fb5fbe6c12fcd99d187ec7db95db9110fb1a20505f23040a5449a717c1a0db4d"},
{file = "huggingface_hub-0.25.1-py3-none-any.whl", hash = "sha256:a5158ded931b3188f54ea9028097312cb0acd50bffaaa2612014c3c526b44972"},
{file = "huggingface_hub-0.25.1.tar.gz", hash = "sha256:9ff7cb327343211fbd06e2b149b8f362fd1e389454f3f14c6db75a4999ee20ff"},
]
[package.dependencies]
@ -1531,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
version = "2.31.6"
version = "2.31.8"
description = "Developer-friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
{file = "locust-2.31.6-py3-none-any.whl", hash = "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b"},
{file = "locust-2.31.6.tar.gz", hash = "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c"},
{file = "locust-2.31.8-py3-none-any.whl", hash = "sha256:4194e3d4a0472f1206c51532ed527017f3da1a7d1037ca4b2f0735d5dcd2f78f"},
{file = "locust-2.31.8.tar.gz", hash = "sha256:b240c0d3e1724317d9211e81e99fbe42a3469071ef4d34d2ae6a727776d56377"},
]
[package.dependencies]

View File

@ -186,10 +186,10 @@ packages:
dependency: "direct dev"
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
logging:
dependency: transitive
description:
@ -367,4 +367,4 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.4.0 <4.0.0"
dart: ">=3.5.0 <4.0.0"

View File

@ -11,4 +11,4 @@ dependencies:
glob: ^2.1.2
dev_dependencies:
lints: ^4.0.0
lints: ^5.0.0

View File

@ -0,0 +1 @@
{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":["<all>"]},"commands":{"<all>":{"tool":"phony","inputs":["<WorkspaceHeaderMapVFSFilesWritten>"],"outputs":["<all>"]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":["<WorkspaceHeaderMapVFSFilesWritten>"]}}}

View File

@ -16,8 +16,12 @@ class FileMediaRepository implements IFileMediaRepository {
required String title,
String? relativePath,
}) async {
final entity = await PhotoManager.editor
.saveImage(data, title: title, relativePath: relativePath);
final entity = await PhotoManager.editor.saveImage(
data,
filename: title,
title: title,
relativePath: relativePath,
);
return AssetMediaRepository.toAsset(entity);
}

View File

@ -1,5 +1,3 @@
library photo_view;
import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';

View File

@ -1,5 +1,3 @@
library photo_view_gallery;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart'

View File

@ -183,6 +183,7 @@ Class | Method | HTTP request | Description
*ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |
*ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |
*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme |
*ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history |
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
@ -400,6 +401,7 @@ Class | Method | HTTP request | Description
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
- [ServerStorageResponseDto](doc//ServerStorageResponseDto.md)
- [ServerThemeDto](doc//ServerThemeDto.md)
- [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md)
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
- [SessionResponseDto](doc//SessionResponseDto.md)
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)

View File

@ -213,6 +213,7 @@ part 'model/server_ping_response.dart';
part 'model/server_stats_response_dto.dart';
part 'model/server_storage_response_dto.dart';
part 'model/server_theme_dto.dart';
part 'model/server_version_history_response_dto.dart';
part 'model/server_version_response_dto.dart';
part 'model/session_response_dto.dart';
part 'model/shared_link_create_dto.dart';

View File

@ -418,6 +418,50 @@ class ServerApi {
return null;
}
/// Performs an HTTP 'GET /server/version-history' operation and returns the [Response].
Future<Response> getVersionHistoryWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/version-history';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<List<ServerVersionHistoryResponseDto>?> getVersionHistory() async {
final response = await getVersionHistoryWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<ServerVersionHistoryResponseDto>') as List)
.cast<ServerVersionHistoryResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'GET /server/ping' operation and returns the [Response].
Future<Response> pingServerWithHttpInfo() async {
// ignore: prefer_const_declarations

View File

@ -480,6 +480,8 @@ class ApiClient {
return ServerStorageResponseDto.fromJson(value);
case 'ServerThemeDto':
return ServerThemeDto.fromJson(value);
case 'ServerVersionHistoryResponseDto':
return ServerVersionHistoryResponseDto.fromJson(value);
case 'ServerVersionResponseDto':
return ServerVersionResponseDto.fromJson(value);
case 'SessionResponseDto':

View File

@ -28,6 +28,10 @@ class ServerAboutResponseDto {
this.sourceCommit,
this.sourceRef,
this.sourceUrl,
this.thirdPartyBugFeatureUrl,
this.thirdPartyDocumentationUrl,
this.thirdPartySourceUrl,
this.thirdPartySupportUrl,
required this.version,
required this.versionUrl,
});
@ -146,6 +150,38 @@ class ServerAboutResponseDto {
///
String? sourceUrl;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? thirdPartyBugFeatureUrl;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? thirdPartyDocumentationUrl;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? thirdPartySourceUrl;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? thirdPartySupportUrl;
String version;
String versionUrl;
@ -167,6 +203,10 @@ class ServerAboutResponseDto {
other.sourceCommit == sourceCommit &&
other.sourceRef == sourceRef &&
other.sourceUrl == sourceUrl &&
other.thirdPartyBugFeatureUrl == thirdPartyBugFeatureUrl &&
other.thirdPartyDocumentationUrl == thirdPartyDocumentationUrl &&
other.thirdPartySourceUrl == thirdPartySourceUrl &&
other.thirdPartySupportUrl == thirdPartySupportUrl &&
other.version == version &&
other.versionUrl == versionUrl;
@ -188,11 +228,15 @@ class ServerAboutResponseDto {
(sourceCommit == null ? 0 : sourceCommit!.hashCode) +
(sourceRef == null ? 0 : sourceRef!.hashCode) +
(sourceUrl == null ? 0 : sourceUrl!.hashCode) +
(thirdPartyBugFeatureUrl == null ? 0 : thirdPartyBugFeatureUrl!.hashCode) +
(thirdPartyDocumentationUrl == null ? 0 : thirdPartyDocumentationUrl!.hashCode) +
(thirdPartySourceUrl == null ? 0 : thirdPartySourceUrl!.hashCode) +
(thirdPartySupportUrl == null ? 0 : thirdPartySupportUrl!.hashCode) +
(version.hashCode) +
(versionUrl.hashCode);
@override
String toString() => 'ServerAboutResponseDto[build=$build, buildImage=$buildImage, buildImageUrl=$buildImageUrl, buildUrl=$buildUrl, exiftool=$exiftool, ffmpeg=$ffmpeg, imagemagick=$imagemagick, libvips=$libvips, licensed=$licensed, nodejs=$nodejs, repository=$repository, repositoryUrl=$repositoryUrl, sourceCommit=$sourceCommit, sourceRef=$sourceRef, sourceUrl=$sourceUrl, version=$version, versionUrl=$versionUrl]';
String toString() => 'ServerAboutResponseDto[build=$build, buildImage=$buildImage, buildImageUrl=$buildImageUrl, buildUrl=$buildUrl, exiftool=$exiftool, ffmpeg=$ffmpeg, imagemagick=$imagemagick, libvips=$libvips, licensed=$licensed, nodejs=$nodejs, repository=$repository, repositoryUrl=$repositoryUrl, sourceCommit=$sourceCommit, sourceRef=$sourceRef, sourceUrl=$sourceUrl, thirdPartyBugFeatureUrl=$thirdPartyBugFeatureUrl, thirdPartyDocumentationUrl=$thirdPartyDocumentationUrl, thirdPartySourceUrl=$thirdPartySourceUrl, thirdPartySupportUrl=$thirdPartySupportUrl, version=$version, versionUrl=$versionUrl]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -266,6 +310,26 @@ class ServerAboutResponseDto {
json[r'sourceUrl'] = this.sourceUrl;
} else {
// json[r'sourceUrl'] = null;
}
if (this.thirdPartyBugFeatureUrl != null) {
json[r'thirdPartyBugFeatureUrl'] = this.thirdPartyBugFeatureUrl;
} else {
// json[r'thirdPartyBugFeatureUrl'] = null;
}
if (this.thirdPartyDocumentationUrl != null) {
json[r'thirdPartyDocumentationUrl'] = this.thirdPartyDocumentationUrl;
} else {
// json[r'thirdPartyDocumentationUrl'] = null;
}
if (this.thirdPartySourceUrl != null) {
json[r'thirdPartySourceUrl'] = this.thirdPartySourceUrl;
} else {
// json[r'thirdPartySourceUrl'] = null;
}
if (this.thirdPartySupportUrl != null) {
json[r'thirdPartySupportUrl'] = this.thirdPartySupportUrl;
} else {
// json[r'thirdPartySupportUrl'] = null;
}
json[r'version'] = this.version;
json[r'versionUrl'] = this.versionUrl;
@ -296,6 +360,10 @@ class ServerAboutResponseDto {
sourceCommit: mapValueOfType<String>(json, r'sourceCommit'),
sourceRef: mapValueOfType<String>(json, r'sourceRef'),
sourceUrl: mapValueOfType<String>(json, r'sourceUrl'),
thirdPartyBugFeatureUrl: mapValueOfType<String>(json, r'thirdPartyBugFeatureUrl'),
thirdPartyDocumentationUrl: mapValueOfType<String>(json, r'thirdPartyDocumentationUrl'),
thirdPartySourceUrl: mapValueOfType<String>(json, r'thirdPartySourceUrl'),
thirdPartySupportUrl: mapValueOfType<String>(json, r'thirdPartySupportUrl'),
version: mapValueOfType<String>(json, r'version')!,
versionUrl: mapValueOfType<String>(json, r'versionUrl')!,
);

View File

@ -0,0 +1,115 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class ServerVersionHistoryResponseDto {
/// Returns a new [ServerVersionHistoryResponseDto] instance.
ServerVersionHistoryResponseDto({
required this.createdAt,
required this.id,
required this.version,
});
DateTime createdAt;
String id;
String version;
@override
bool operator ==(Object other) => identical(this, other) || other is ServerVersionHistoryResponseDto &&
other.createdAt == createdAt &&
other.id == id &&
other.version == version;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(createdAt.hashCode) +
(id.hashCode) +
(version.hashCode);
@override
String toString() => 'ServerVersionHistoryResponseDto[createdAt=$createdAt, id=$id, version=$version]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
json[r'id'] = this.id;
json[r'version'] = this.version;
return json;
}
/// Returns a new [ServerVersionHistoryResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ServerVersionHistoryResponseDto? fromJson(dynamic value) {
upgradeDto(value, "ServerVersionHistoryResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return ServerVersionHistoryResponseDto(
createdAt: mapDateTime(json, r'createdAt', r'')!,
id: mapValueOfType<String>(json, r'id')!,
version: mapValueOfType<String>(json, r'version')!,
);
}
return null;
}
static List<ServerVersionHistoryResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ServerVersionHistoryResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ServerVersionHistoryResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ServerVersionHistoryResponseDto> mapFromJson(dynamic json) {
final map = <String, ServerVersionHistoryResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ServerVersionHistoryResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ServerVersionHistoryResponseDto-objects as value to a dart map
static Map<String, List<ServerVersionHistoryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ServerVersionHistoryResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ServerVersionHistoryResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'createdAt',
'id',
'version',
};
}

View File

@ -540,10 +540,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
@ -940,10 +940,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
logging:
dependency: "direct main"
description:
@ -1211,10 +1211,10 @@ packages:
dependency: "direct main"
description:
name: photo_manager
sha256: "1e8bbe46a6858870e34c976aafd85378bed221ce31c1201961eba9ad3d94df9f"
sha256: "32a1ce1095aeaaa792a29f28c1f74613aa75109f21c2d4ab85be3ad9964230a4"
url: "https://pub.dev"
source: hosted
version: "3.2.3"
version: "3.5.0"
photo_manager_image_provider:
dependency: "direct main"
description:
@ -1861,5 +1861,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.4.0 <4.0.0"
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.3"

View File

@ -13,7 +13,7 @@ dependencies:
sdk: flutter
path_provider_ios:
photo_manager: ^3.2.3
photo_manager: ^3.5.0
photo_manager_image_provider: ^2.1.1
flutter_hooks: ^0.20.4
hooks_riverpod: ^2.4.9
@ -87,7 +87,7 @@ dependency_overrides:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter_lints: ^5.0.0
build_runner: ^2.4.8
auto_route_generator: ^9.0.0
flutter_launcher_icons: ^0.13.1

View File

@ -5088,6 +5088,30 @@
]
}
},
"/server/version-history": {
"get": {
"operationId": "getVersionHistory",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/ServerVersionHistoryResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"tags": [
"Server"
]
}
},
"/sessions": {
"delete": {
"operationId": "deleteAllSessions",
@ -10780,6 +10804,18 @@
"sourceUrl": {
"type": "string"
},
"thirdPartyBugFeatureUrl": {
"type": "string"
},
"thirdPartyDocumentationUrl": {
"type": "string"
},
"thirdPartySourceUrl": {
"type": "string"
},
"thirdPartySupportUrl": {
"type": "string"
},
"version": {
"type": "string"
},
@ -11030,6 +11066,26 @@
],
"type": "object"
},
"ServerVersionHistoryResponseDto": {
"properties": {
"createdAt": {
"format": "date-time",
"type": "string"
},
"id": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"createdAt",
"id",
"version"
],
"type": "object"
},
"ServerVersionResponseDto": {
"properties": {
"major": {

View File

@ -12,7 +12,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"typescript": "^5.3.3"
}
},
@ -22,9 +22,9 @@
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
},
"node_modules/@types/node": {
"version": "20.16.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -19,7 +19,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"typescript": "^5.3.3"
},
"repository": {

View File

@ -917,6 +917,10 @@ export type ServerAboutResponseDto = {
sourceCommit?: string;
sourceRef?: string;
sourceUrl?: string;
thirdPartyBugFeatureUrl?: string;
thirdPartyDocumentationUrl?: string;
thirdPartySourceUrl?: string;
thirdPartySupportUrl?: string;
version: string;
versionUrl: string;
};
@ -996,6 +1000,11 @@ export type ServerVersionResponseDto = {
minor: number;
patch: number;
};
export type ServerVersionHistoryResponseDto = {
createdAt: string;
id: string;
version: string;
};
export type SessionResponseDto = {
createdAt: string;
current: boolean;
@ -2663,6 +2672,14 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) {
...opts
}));
}
export function getVersionHistory(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: ServerVersionHistoryResponseDto[];
}>("/server/version-history", {
...opts
}));
}
export function deleteAllSessions(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/sessions", {
...opts,

View File

@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20240924@sha256:fff4358d435065a626c64a4c015cbfce6ee714b05fabe39aa0d83d8cff3951f2 AS dev
FROM ghcr.io/immich-app/base-server-dev:20241001@sha256:bb10832c2567f5625df68bb790523e85a358031ddcb3d7ac98b669f62ed8de27 AS dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@ -41,7 +41,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20240924@sha256:af3089fe48d7ff162594bd7edfffa56ba4e7014ad10ad69c4ebfd428e39b06ff
FROM ghcr.io/immich-app/base-server-prod:20241001@sha256:a9a0745a486e9cbd73fa06b49168e985f8f2c1be0fca9fb0a8e06916246c7087
WORKDIR /usr/src/app
ENV NODE_ENV=production \

421
server/package-lock.json generated
View File

@ -83,7 +83,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/react": "^18.3.4",
@ -1138,6 +1138,15 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
@ -1198,9 +1207,9 @@
"dev": true
},
"node_modules/@eslint/js": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1216,9 +1225,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
"dependencies": {
"levn": "^0.4.1"
@ -2043,9 +2052,9 @@
}
},
"node_modules/@nestjs/common": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.3.tgz",
"integrity": "sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==",
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.4.tgz",
"integrity": "sha512-0j2/zqRw9nvHV1GKTktER8B/hIC/Z8CYFjN/ZqUuvwayCH+jZZBhCR2oRyuvLTXdnlSmtCAg2xvQ0ULqQvzqhA==",
"dependencies": {
"iterare": "1.2.1",
"tslib": "2.7.0",
@ -2188,9 +2197,9 @@
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
},
"node_modules/@nestjs/platform-socket.io": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.3.tgz",
"integrity": "sha512-jTatT8q15LB5CFWsaIez3IigMixt7tNGJ4QLlRJ5NggPOPKRZssJnloODyEadFNHJjZiyufp5/NoPKBtNMf+lg==",
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.4.tgz",
"integrity": "sha512-5GEYUA3sNbX2jOBP6FmrIK/zv9VCdvpdr4Sef1OKvt1U0qsV1YgmWPWDPumZM77n5DI0VHSJPyo7yjZaEKWOiQ==",
"dependencies": {
"socket.io": "4.7.5",
"tslib": "2.7.0"
@ -2290,9 +2299,9 @@
}
},
"node_modules/@nestjs/testing": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.3.tgz",
"integrity": "sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==",
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.4.tgz",
"integrity": "sha512-qRGFj51A5RM7JqA8pcyEwSLA3Y0dle/PAZ8oxP0suimoCusRY3Tk7wYqutZdCNj1ATb678SDaUZDHk2pwSv9/g==",
"dev": true,
"dependencies": {
"tslib": "2.7.0"
@ -2338,9 +2347,9 @@
}
},
"node_modules/@nestjs/websockets": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.3.tgz",
"integrity": "sha512-EW5/GR0jImJwrb8+YpHPoFN2tlhYQzVE2yAN5Se5sygUr/ZFMNAG84sd79NmWGd4RxoxR0aFH9nRycQ/0Ebe5w==",
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.4.tgz",
"integrity": "sha512-ZHnak04i/iKBS0csjJa7K6D6xdsB0Yz6duJuCR7xGLItchFK+Ne21m9rEF8ffvW74U7UAYkQHBgD5242LBBYiQ==",
"dependencies": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
@ -4198,9 +4207,9 @@
}
},
"node_modules/@photostructure/tz-lookup": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
"integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA=="
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.0.0.tgz",
"integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw=="
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
@ -5332,9 +5341,9 @@
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
"integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz",
"integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==",
"dev": true
},
"node_modules/@types/luxon": {
@ -5389,9 +5398,9 @@
}
},
"node_modules/@types/node": {
"version": "20.16.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"dependencies": {
"undici-types": "~6.19.2"
}
@ -5506,9 +5515,9 @@
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
"version": "18.3.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz",
"integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
@ -5625,16 +5634,16 @@
"integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ=="
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -5658,15 +5667,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4"
},
"engines": {
@ -5686,13 +5695,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -5703,13 +5712,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -5727,9 +5736,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -5740,13 +5749,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -5792,15 +5801,15 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -5814,12 +5823,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -8167,20 +8176,23 @@
}
},
"node_modules/eslint": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.6.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.10.0",
"@eslint/plugin-kit": "^0.1.0",
"@eslint/js": "9.11.1",
"@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -8328,6 +8340,12 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -8345,9 +8363,9 @@
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
"integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -8497,34 +8515,34 @@
}
},
"node_modules/exiftool-vendored": {
"version": "28.2.1",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz",
"integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==",
"version": "28.3.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.3.0.tgz",
"integrity": "sha512-2DOSOvj5c1gkbKtubAnlGglxdYp9h55n0GxjK2nypVivoaCdgP/le3MOZRKgEUNObfJHmYHj4u/NnYVneu/gUw==",
"dependencies": {
"@photostructure/tz-lookup": "^10.0.0",
"@photostructure/tz-lookup": "^11.0.0",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"he": "^1.2.0",
"luxon": "^3.5.0"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.91.0",
"exiftool-vendored.pl": "12.91.0"
"exiftool-vendored.exe": "12.96.0",
"exiftool-vendored.pl": "12.96.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.91.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz",
"integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==",
"version": "12.96.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.96.0.tgz",
"integrity": "sha512-pKPN9F/Evw2yyO5/+ml3spbXIqejzOxyF7jEnj8tLU2JPSmIlziPUZ75XIhcPbilX86jVKmuiso7FUDicOg8pQ==",
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.91.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz",
"integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==",
"version": "12.96.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.96.0.tgz",
"integrity": "sha512-v4nGnovAMBsTfOWhwAcOiRiq/8kuJOo3GUMHNpug7Mr4jLz3tmWEo7DdNyOYmpcvWbA6smOTG0SmwsrY8fsW+A==",
"optional": true,
"os": [
"!win32"
@ -11669,20 +11687,16 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true,
"peerDependencies": {
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0",
"typescript": ">=2.9",
"vue-tsc": "^2.0.24"
"vue-tsc": "^2.1.0"
},
"peerDependenciesMeta": {
"@vue/language-plugin-pug": {
"optional": true
},
"vue-tsc": {
"optional": true
}
@ -15977,6 +15991,12 @@
"minimatch": "^3.1.2"
}
},
"@eslint/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
"dev": true
},
"@eslint/eslintrc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
@ -16021,9 +16041,9 @@
}
},
"@eslint/js": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
"dev": true
},
"@eslint/object-schema": {
@ -16033,9 +16053,9 @@
"dev": true
},
"@eslint/plugin-kit": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
"requires": {
"levn": "^0.4.1"
@ -16512,9 +16532,9 @@
}
},
"@nestjs/common": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.3.tgz",
"integrity": "sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==",
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.4.tgz",
"integrity": "sha512-0j2/zqRw9nvHV1GKTktER8B/hIC/Z8CYFjN/ZqUuvwayCH+jZZBhCR2oRyuvLTXdnlSmtCAg2xvQ0ULqQvzqhA==",
"requires": {
"iterare": "1.2.1",
"tslib": "2.7.0",
@ -16592,9 +16612,9 @@
}
},
"@nestjs/platform-socket.io": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.3.tgz",
"integrity": "sha512-jTatT8q15LB5CFWsaIez3IigMixt7tNGJ4QLlRJ5NggPOPKRZssJnloODyEadFNHJjZiyufp5/NoPKBtNMf+lg==",
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.4.tgz",
"integrity": "sha512-5GEYUA3sNbX2jOBP6FmrIK/zv9VCdvpdr4Sef1OKvt1U0qsV1YgmWPWDPumZM77n5DI0VHSJPyo7yjZaEKWOiQ==",
"requires": {
"socket.io": "4.7.5",
"tslib": "2.7.0"
@ -16658,9 +16678,9 @@
}
},
"@nestjs/testing": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.3.tgz",
"integrity": "sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==",
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.4.tgz",
"integrity": "sha512-qRGFj51A5RM7JqA8pcyEwSLA3Y0dle/PAZ8oxP0suimoCusRY3Tk7wYqutZdCNj1ATb678SDaUZDHk2pwSv9/g==",
"dev": true,
"requires": {
"tslib": "2.7.0"
@ -16683,9 +16703,9 @@
}
},
"@nestjs/websockets": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.3.tgz",
"integrity": "sha512-EW5/GR0jImJwrb8+YpHPoFN2tlhYQzVE2yAN5Se5sygUr/ZFMNAG84sd79NmWGd4RxoxR0aFH9nRycQ/0Ebe5w==",
"version": "10.4.4",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.4.tgz",
"integrity": "sha512-ZHnak04i/iKBS0csjJa7K6D6xdsB0Yz6duJuCR7xGLItchFK+Ne21m9rEF8ffvW74U7UAYkQHBgD5242LBBYiQ==",
"requires": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
@ -17860,9 +17880,9 @@
}
},
"@photostructure/tz-lookup": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
"integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA=="
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.0.0.tgz",
"integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw=="
},
"@pkgjs/parseargs": {
"version": "0.11.0",
@ -18644,9 +18664,9 @@
"dev": true
},
"@types/lodash": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
"integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz",
"integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==",
"dev": true
},
"@types/luxon": {
@ -18701,9 +18721,9 @@
}
},
"@types/node": {
"version": "20.16.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
"version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"requires": {
"undici-types": "~6.19.2"
}
@ -18805,9 +18825,9 @@
"dev": true
},
"@types/react": {
"version": "18.3.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
"version": "18.3.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.9.tgz",
"integrity": "sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==",
"dev": true,
"requires": {
"@types/prop-types": "*",
@ -18924,16 +18944,16 @@
"integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ=="
},
"@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -18941,54 +18961,54 @@
}
},
"@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0"
}
},
"@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"dev": true,
"requires": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
}
},
"@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -19018,24 +19038,24 @@
}
},
"@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"eslint-visitor-keys": "^3.4.3"
}
},
@ -20786,20 +20806,23 @@
"dev": true
},
"eslint": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.6.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.10.0",
"@eslint/plugin-kit": "^0.1.0",
"@eslint/js": "9.11.1",
"@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -20827,6 +20850,12 @@
"text-table": "^0.2.0"
},
"dependencies": {
"@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -20840,9 +20869,9 @@
}
},
"eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
"integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
"dev": true
},
"glob-parent": {
@ -21004,29 +21033,29 @@
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"exiftool-vendored": {
"version": "28.2.1",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz",
"integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==",
"version": "28.3.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.3.0.tgz",
"integrity": "sha512-2DOSOvj5c1gkbKtubAnlGglxdYp9h55n0GxjK2nypVivoaCdgP/le3MOZRKgEUNObfJHmYHj4u/NnYVneu/gUw==",
"requires": {
"@photostructure/tz-lookup": "^10.0.0",
"@photostructure/tz-lookup": "^11.0.0",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"exiftool-vendored.exe": "12.91.0",
"exiftool-vendored.pl": "12.91.0",
"exiftool-vendored.exe": "12.96.0",
"exiftool-vendored.pl": "12.96.0",
"he": "^1.2.0",
"luxon": "^3.5.0"
}
},
"exiftool-vendored.exe": {
"version": "12.91.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz",
"integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==",
"version": "12.96.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.96.0.tgz",
"integrity": "sha512-pKPN9F/Evw2yyO5/+ml3spbXIqejzOxyF7jEnj8tLU2JPSmIlziPUZ75XIhcPbilX86jVKmuiso7FUDicOg8pQ==",
"optional": true
},
"exiftool-vendored.pl": {
"version": "12.91.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz",
"integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==",
"version": "12.96.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.96.0.tgz",
"integrity": "sha512-v4nGnovAMBsTfOWhwAcOiRiq/8kuJOo3GUMHNpug7Mr4jLz3tmWEo7DdNyOYmpcvWbA6smOTG0SmwsrY8fsW+A==",
"optional": true
},
"express": {
@ -23312,9 +23341,9 @@
}
},
"prettier-plugin-organize-imports": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true,
"requires": {}
},

View File

@ -109,7 +109,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/react": "^18.3.4",

View File

@ -415,6 +415,10 @@ export const getBuildMetadata = () => ({
sourceRef: process.env.IMMICH_SOURCE_REF,
sourceCommit: process.env.IMMICH_SOURCE_COMMIT,
sourceUrl: process.env.IMMICH_SOURCE_URL,
thirdPartySourceUrl: process.env.IMMICH_THIRD_PARTY_SOURCE_URL,
thirdPartyBugFeatureUrl: process.env.IMMICH_THIRD_PARTY_BUG_FEATURE_URL,
thirdPartyDocumentationUrl: process.env.IMMICH_THIRD_PARTY_DOCUMENTATION_URL,
thirdPartySupportUrl: process.env.IMMICH_THIRD_PARTY_SUPPORT_URL,
});
const clientLicensePublicKeyProd =

View File

@ -23,7 +23,6 @@ export const ONE_HOUR = Duration.fromObject({ hours: 1 });
export const envName = (process.env.IMMICH_ENV || 'production').toUpperCase();
export const isDev = () => process.env.IMMICH_ENV === 'development';
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283';
export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT;

View File

@ -66,7 +66,7 @@ export class ServerInfoController {
@Get('config')
@EndpointLifecycle({ deprecatedAt: 'v1.107.0' })
getServerConfig(): Promise<ServerConfigDto> {
return this.service.getConfig();
return this.service.getSystemConfig();
}
@Get('statistics')

View File

@ -10,6 +10,7 @@ import {
ServerStatsResponseDto,
ServerStorageResponseDto,
ServerThemeDto,
ServerVersionHistoryResponseDto,
ServerVersionResponseDto,
} from 'src/dtos/server.dto';
import { Authenticated } from 'src/middleware/auth.guard';
@ -46,6 +47,11 @@ export class ServerController {
return this.versionService.getVersion();
}
@Get('version-history')
getVersionHistory(): Promise<ServerVersionHistoryResponseDto[]> {
return this.versionService.getVersionHistory();
}
@Get('features')
getServerFeatures(): Promise<ServerFeaturesDto> {
return this.service.getFeatures();
@ -58,7 +64,7 @@ export class ServerController {
@Get('config')
getServerConfig(): Promise<ServerConfigDto> {
return this.service.getConfig();
return this.service.getSystemConfig();
}
@Get('statistics')

View File

@ -13,7 +13,7 @@ export class SystemConfigController {
@Get()
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
getConfig(): Promise<SystemConfigDto> {
return this.service.getConfig();
return this.service.getSystemConfig();
}
@Get('defaults')
@ -25,7 +25,7 @@ export class SystemConfigController {
@Put()
@Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true })
updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
return this.service.updateConfig(dto);
return this.service.updateSystemConfig(dto);
}
@Get('storage-template-options')

View File

@ -1,7 +1,6 @@
import { randomUUID } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
import { APP_MEDIA_LOCATION } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
@ -13,6 +12,7 @@ import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getAssetFiles } from 'src/utils/asset.util';
import { getConfig } from 'src/utils/config';
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
@ -34,18 +34,15 @@ export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDE
let instance: StorageCore | null;
export class StorageCore {
private configCore;
private constructor(
private assetRepository: IAssetRepository,
private cryptoRepository: ICryptoRepository,
private moveRepository: IMoveRepository,
private personRepository: IPersonRepository,
private storageRepository: IStorageRepository,
systemMetadataRepository: ISystemMetadataRepository,
private systemMetadataRepository: ISystemMetadataRepository,
private logger: ILoggerRepository,
) {
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
) {}
static create(
assetRepository: IAssetRepository,
@ -248,7 +245,8 @@ export class StorageCore {
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
return false;
}
const config = await this.configCore.getConfig({ withCache: true });
const repos = { metadataRepo: this.systemMetadataRepository, logger: this.logger };
const config = await getConfig(repos, { withCache: true });
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
const { checksum } = assetInfo;
const newChecksum = await this.cryptoRepository.hashFile(newPath);

View File

@ -1,143 +0,0 @@
import { Injectable } from '@nestjs/common';
import AsyncLock from 'async-lock';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { load as loadYaml } from 'js-yaml';
import * as _ from 'lodash';
import { SystemConfig, defaults } from 'src/config';
import { SystemConfigDto } from 'src/dtos/system-config.dto';
import { SystemMetadataKey } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
import { DeepPartial } from 'typeorm';
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
let instance: SystemConfigCore | null;
@Injectable()
export class SystemConfigCore {
private readonly asyncLock = new AsyncLock();
private config: SystemConfig | null = null;
private lastUpdated: number | null = null;
private constructor(
private repository: ISystemMetadataRepository,
private logger: ILoggerRepository,
) {}
static create(repository: ISystemMetadataRepository, logger: ILoggerRepository) {
if (!instance) {
instance = new SystemConfigCore(repository, logger);
}
return instance;
}
static reset() {
instance = null;
}
invalidateCache() {
this.config = null;
this.lastUpdated = null;
}
async getConfig({ withCache }: { withCache: boolean }): Promise<SystemConfig> {
if (!withCache || !this.config) {
const lastUpdated = this.lastUpdated;
await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
if (lastUpdated === this.lastUpdated) {
this.config = await this.buildConfig();
this.lastUpdated = Date.now();
}
});
}
return this.config!;
}
async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
// get the difference between the new config and the default config
const partialConfig: DeepPartial<SystemConfig> = {};
for (const property of getKeysDeep(defaults)) {
const newValue = _.get(newConfig, property);
const isEmpty = newValue === undefined || newValue === null || newValue === '';
const defaultValue = _.get(defaults, property);
const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue);
if (isEmpty || isEqual) {
continue;
}
_.set(partialConfig, property, newValue);
}
await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
return this.getConfig({ withCache: false });
}
isUsingConfigFile() {
return !!process.env.IMMICH_CONFIG_FILE;
}
private async buildConfig() {
// load partial
const partial = this.isUsingConfigFile()
? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE as string)
: await this.repository.get(SystemMetadataKey.SYSTEM_CONFIG);
// merge with defaults
const config = _.cloneDeep(defaults);
for (const property of getKeysDeep(partial)) {
_.set(config, property, _.get(partial, property));
}
// check for extra properties
const unknownKeys = _.cloneDeep(config);
for (const property of getKeysDeep(defaults)) {
unsetDeep(unknownKeys, property);
}
if (!_.isEmpty(unknownKeys)) {
this.logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`);
}
// validate full config
const errors = await validate(plainToInstance(SystemConfigDto, config));
if (errors.length > 0) {
if (this.isUsingConfigFile()) {
throw new Error(`Invalid value(s) in file: ${errors}`);
} else {
this.logger.error('Validation error', errors);
}
}
if (config.server.externalDomain.length > 0) {
config.server.externalDomain = new URL(config.server.externalDomain).origin;
}
if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
}
if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec);
}
return config;
}
private async loadFromFile(filepath: string) {
try {
const file = await this.repository.readFile(filepath);
return loadYaml(file.toString()) as unknown;
} catch (error: Error | any) {
this.logger.error(`Unable to load configuration file: ${filepath}`);
this.logger.error(error);
throw error;
}
}
}

View File

@ -1,51 +0,0 @@
import { BadRequestException } from '@nestjs/common';
import sanitize from 'sanitize-filename';
import { SALT_ROUNDS } from 'src/constants';
import { UserEntity } from 'src/entities/user.entity';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
let instance: UserCore | null;
export class UserCore {
private constructor(
private cryptoRepository: ICryptoRepository,
private userRepository: IUserRepository,
) {}
static create(cryptoRepository: ICryptoRepository, userRepository: IUserRepository) {
if (!instance) {
instance = new UserCore(cryptoRepository, userRepository);
}
return instance;
}
static reset() {
instance = null;
}
async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
const user = await this.userRepository.getByEmail(dto.email);
if (user) {
throw new BadRequestException('User exists');
}
if (!dto.isAdmin) {
const localAdmin = await this.userRepository.getAdmin();
if (!localAdmin) {
throw new BadRequestException('The first registered account must the administrator.');
}
}
const payload: Partial<UserEntity> = { ...dto };
if (payload.password) {
payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
}
if (payload.storageLabel) {
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
}
return this.userRepository.create(payload);
}
}

View File

@ -30,6 +30,11 @@ export class ServerAboutResponseDto {
exiftool?: string;
licensed!: boolean;
thirdPartySourceUrl?: string;
thirdPartyBugFeatureUrl?: string;
thirdPartyDocumentationUrl?: string;
thirdPartySupportUrl?: string;
}
export class ServerStorageResponseDto {
@ -63,6 +68,12 @@ export class ServerVersionResponseDto {
}
}
export class ServerVersionHistoryResponseDto {
id!: string;
createdAt!: Date;
version!: string;
}
export class UsageByUserDto {
@ApiProperty({ type: 'string' })
userId!: string;

View File

@ -25,6 +25,7 @@ import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
import { TagEntity } from 'src/entities/tag.entity';
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
export const entities = [
ActivityEntity,
@ -54,4 +55,5 @@ export const entities = [
UserMetadataEntity,
SessionEntity,
LibraryEntity,
VersionHistoryEntity,
];

View File

@ -0,0 +1,13 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('version_history')
export class VersionHistoryEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date;
@Column()
version!: string;
}

View File

@ -7,6 +7,9 @@ export interface EnvData {
skipMigrations: boolean;
vectorExtension: VectorExtension;
};
storage: {
ignoreMountCheckErrors: boolean;
};
}
export interface IConfigRepository {

View File

@ -17,6 +17,7 @@ export enum DatabaseLock {
Migrations = 200,
SystemFileMounts = 300,
StorageTemplateMigration = 420,
VersionHistory = 500,
CLIPDimSize = 512,
LibraryWatch = 1337,
GetSystemConfig = 69,

View File

@ -62,36 +62,20 @@ export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<v
export type ArgOf<T extends EmitEvent> = EventMap[T][0];
export type ArgsOf<T extends EmitEvent> = EventMap[T];
export enum ClientEvent {
UPLOAD_SUCCESS = 'on_upload_success',
USER_DELETE = 'on_user_delete',
ASSET_DELETE = 'on_asset_delete',
ASSET_TRASH = 'on_asset_trash',
ASSET_UPDATE = 'on_asset_update',
ASSET_HIDDEN = 'on_asset_hidden',
ASSET_RESTORE = 'on_asset_restore',
ASSET_STACK_UPDATE = 'on_asset_stack_update',
PERSON_THUMBNAIL = 'on_person_thumbnail',
SERVER_VERSION = 'on_server_version',
CONFIG_UPDATE = 'on_config_update',
NEW_RELEASE = 'on_new_release',
SESSION_DELETE = 'on_session_delete',
}
export interface ClientEventMap {
[ClientEvent.UPLOAD_SUCCESS]: AssetResponseDto;
[ClientEvent.USER_DELETE]: string;
[ClientEvent.ASSET_DELETE]: string;
[ClientEvent.ASSET_TRASH]: string[];
[ClientEvent.ASSET_UPDATE]: AssetResponseDto;
[ClientEvent.ASSET_HIDDEN]: string;
[ClientEvent.ASSET_RESTORE]: string[];
[ClientEvent.ASSET_STACK_UPDATE]: string[];
[ClientEvent.PERSON_THUMBNAIL]: string;
[ClientEvent.SERVER_VERSION]: ServerVersionResponseDto;
[ClientEvent.CONFIG_UPDATE]: Record<string, never>;
[ClientEvent.NEW_RELEASE]: ReleaseNotification;
[ClientEvent.SESSION_DELETE]: string;
on_upload_success: [AssetResponseDto];
on_user_delete: [string];
on_asset_delete: [string];
on_asset_trash: [string[]];
on_asset_update: [AssetResponseDto];
on_asset_hidden: [string];
on_asset_restore: [string[]];
on_asset_stack_update: string[];
on_person_thumbnail: [string];
on_server_version: [ServerVersionResponseDto];
on_config_update: [];
on_new_release: [ReleaseNotification];
on_session_delete: [string];
}
export type EventItem<T extends EmitEvent> = {
@ -107,11 +91,11 @@ export interface IEventRepository {
/**
* Send to connected clients for a specific user
*/
clientSend<E extends keyof ClientEventMap>(event: E, room: string, data: ClientEventMap[E]): void;
clientSend<E extends keyof ClientEventMap>(event: E, room: string, ...data: ClientEventMap[E]): void;
/**
* Send to all connected clients
*/
clientBroadcast<E extends keyof ClientEventMap>(event: E, data: ClientEventMap[E]): void;
clientBroadcast<E extends keyof ClientEventMap>(event: E, ...data: ClientEventMap[E]): void;
/**
* Send to all connected servers
*/

View File

@ -0,0 +1,9 @@
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
export const IVersionHistoryRepository = 'IVersionHistoryRepository';
export interface IVersionHistoryRepository {
create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity>;
getAll(): Promise<VersionHistoryEntity[]>;
getLatest(): Promise<VersionHistoryEntity | null>;
}

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class IsOfflineSetDeletedAt1727781844613 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`UPDATE assets SET "deletedAt" = now() WHERE "isOffline" = true AND "deletedAt" IS NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`UPDATE assets SET "deletedAt" = null WHERE "isOffline" = true`,
);
}
}

View File

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddVersionHistory1727797340951 implements MigrationInterface {
name = 'AddVersionHistory1727797340951'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "version" character varying NOT NULL, CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "version_history"`);
}
}

View File

@ -10,6 +10,9 @@ export class ConfigRepository implements IConfigRepository {
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
vectorExtension: getVectorExtension(),
},
storage: {
ignoreMountCheckErrors: process.env.IMMICH_IGNORE_MOUNT_CHECK_ERRORS === 'true',
},
};
}
}

View File

@ -106,12 +106,12 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
}
}
clientSend<E extends keyof ClientEventMap>(event: E, room: string, data: ClientEventMap[E]) {
this.server?.to(room).emit(event, data);
clientSend<T extends keyof ClientEventMap>(event: T, room: string, ...data: ClientEventMap[T]) {
this.server?.to(room).emit(event, ...data);
}
clientBroadcast<E extends keyof ClientEventMap>(event: E, data: ClientEventMap[E]) {
this.server?.emit(event, data);
clientBroadcast<T extends keyof ClientEventMap>(event: T, ...data: ClientEventMap[T]) {
this.server?.emit(event, ...data);
}
serverSend<T extends ServerEvents>(event: T, ...args: ArgsOf<T>): void {

View File

@ -32,6 +32,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
import { ITagRepository } from 'src/interfaces/tag.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { IViewRepository } from 'src/interfaces/view.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
@ -67,6 +68,7 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos
import { TagRepository } from 'src/repositories/tag.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-repository';
export const repositories = [
@ -104,5 +106,6 @@ export const repositories = [
{ provide: ITagRepository, useClass: TagRepository },
{ provide: ITrashRepository, useClass: TrashRepository },
{ provide: IUserRepository, useClass: UserRepository },
{ provide: IVersionHistoryRepository, useClass: VersionHistoryRepository },
{ provide: IViewRepository, useClass: ViewRepository },
];

View File

@ -15,6 +15,7 @@ export class MetadataRepository implements IMetadataRepository {
defaultVideosToUTC: true,
backfillTimezones: true,
inferTimezoneFromDatestamps: true,
inferTimezoneFromTimeStamp: true,
useMWG: true,
numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength'],
/* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */

View File

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
@Instrumentation()
@Injectable()
export class VersionHistoryRepository implements IVersionHistoryRepository {
constructor(@InjectRepository(VersionHistoryEntity) private repository: Repository<VersionHistoryEntity>) {}
async getAll(): Promise<VersionHistoryEntity[]> {
return this.repository.find({ order: { createdAt: 'DESC' } });
}
async getLatest(): Promise<VersionHistoryEntity | null> {
const results = await this.repository.find({ order: { createdAt: 'DESC' }, take: 1 });
return results[0] || null;
}
create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity> {
return this.repository.save(version);
}
}

View File

@ -1,7 +1,6 @@
import { BadRequestException, Inject } from '@nestjs/common';
import _ from 'lodash';
import { DateTime, Duration } from 'luxon';
import { SystemConfigCore } from 'src/cores/system-config.core';
import {
AssetResponseDto,
MemoryLaneResponseDto,
@ -38,13 +37,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { requireAccess } from 'src/utils/access';
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
import { usePagination } from 'src/utils/pagination';
export class AssetService {
private configCore: SystemConfigCore;
export class AssetService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@ -54,10 +52,10 @@ export class AssetService {
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IStackRepository) private stackRepository: IStackRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(AssetService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
@ -214,7 +212,7 @@ export class AssetService {
}
async handleAssetDeletionCheck(): Promise<JobStatus> {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
const trashedDays = config.trash.enabled ? config.trash.days : 0;
const trashedBefore = DateTime.now()
.minus(Duration.fromObject({ days: trashedDays }))

View File

@ -13,8 +13,6 @@ 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 { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.core';
import {
AuthDto,
ChangePasswordDto,
@ -40,8 +38,10 @@ import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { isGranted } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes';
import { createUser } from 'src/utils/user';
export interface LoginDetails {
isSecure: boolean;
@ -70,29 +70,25 @@ export type ValidateRequest = {
};
@Injectable()
export class AuthService {
private configCore: SystemConfigCore;
private userCore: UserCore;
export class AuthService extends BaseService {
constructor(
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(AuthService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
this.userCore = UserCore.create(cryptoRepository, userRepository);
custom.setHttpOptionsDefaults({ timeout: 30_000 });
}
async login(dto: LoginCredentialDto, details: LoginDetails) {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
if (!config.passwordLogin.enabled) {
throw new UnauthorizedException('Password login has been disabled');
}
@ -150,13 +146,16 @@ export class AuthService {
throw new BadRequestException('The server already has an admin');
}
const admin = await this.userCore.createUser({
isAdmin: true,
email: dto.email,
name: dto.name,
password: dto.password,
storageLabel: 'admin',
});
const admin = await createUser(
{ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository },
{
isAdmin: true,
email: dto.email,
name: dto.name,
password: dto.password,
storageLabel: 'admin',
},
);
return mapUserAdmin(admin);
}
@ -211,7 +210,7 @@ export class AuthService {
}
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
if (!config.oauth.enabled) {
throw new BadRequestException('OAuth is not enabled');
}
@ -227,7 +226,7 @@ export class AuthService {
}
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
const profile = await this.getOAuthProfile(config, dto.url);
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = config.oauth;
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
@ -271,20 +270,23 @@ export class AuthService {
});
const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
user = await this.userCore.createUser({
name: userName,
email: profile.email,
oauthId: profile.sub,
quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null,
storageLabel: storageLabel || null,
});
user = await createUser(
{ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository },
{
name: userName,
email: profile.email,
oauthId: profile.sub,
quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null,
storageLabel: storageLabel || null,
},
);
}
return this.createLoginResponse(user, loginDetails);
}
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
const duplicate = await this.userRepository.getByOAuthId(oauthId);
if (duplicate && duplicate.id !== auth.user.id) {
@ -306,7 +308,7 @@ export class AuthService {
return LOGIN_URL;
}
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
if (!config.oauth.enabled) {
return LOGIN_URL;
}

View File

@ -0,0 +1,32 @@
import { Inject } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getConfig, updateConfig } from 'src/utils/config';
export class BaseService {
constructor(
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
) {}
getConfig(options: { withCache: boolean }) {
return getConfig(
{
metadataRepo: this.systemMetadataRepository,
logger: this.logger,
},
options,
);
}
updateConfig(newConfig: SystemConfig) {
return updateConfig(
{
metadataRepo: this.systemMetadataRepository,
logger: this.logger,
},
newConfig,
);
}
}

View File

@ -1,24 +1,22 @@
import { Inject, Injectable } from '@nestjs/common';
import { SALT_ROUNDS } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class CliService {
private configCore: SystemConfigCore;
export class CliService extends BaseService {
constructor(
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(CliService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
async listUsers(): Promise<UserAdminResponseDto[]> {
@ -42,26 +40,26 @@ export class CliService {
}
async disablePasswordLogin(): Promise<void> {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
config.passwordLogin.enabled = false;
await this.configCore.updateConfig(config);
await this.updateConfig(config);
}
async enablePasswordLogin(): Promise<void> {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
config.passwordLogin.enabled = true;
await this.configCore.updateConfig(config);
await this.updateConfig(config);
}
async disableOAuthLogin(): Promise<void> {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
config.oauth.enabled = false;
await this.configCore.updateConfig(config);
await this.updateConfig(config);
}
async enableOAuthLogin(): Promise<void> {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
config.oauth.enabled = true;
await this.configCore.updateConfig(config);
await this.updateConfig(config);
}
}

View File

@ -7,7 +7,7 @@ import {
} 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 { mockEnvData, 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';
@ -60,7 +60,9 @@ describe(DatabaseService.name, () => {
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
])('should work with $extensionName', ({ extension, extensionName }) => {
beforeEach(() => {
configMock.getEnv.mockReturnValue({ database: { skipMigrations: false, vectorExtension: extension } });
configMock.getEnv.mockReturnValue(
mockEnvData({ database: { skipMigrations: false, vectorExtension: extension } }),
);
});
it(`should start up successfully with ${extension}`, async () => {
@ -244,12 +246,14 @@ describe(DatabaseService.name, () => {
});
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
configMock.getEnv.mockReturnValue({
database: {
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTORS,
},
});
configMock.getEnv.mockReturnValue(
mockEnvData({
database: {
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTORS,
},
}),
);
await expect(sut.onBootstrap()).resolves.toBeUndefined();
@ -257,12 +261,14 @@ describe(DatabaseService.name, () => {
});
it(`should throw error if pgvector extension could not be created`, async () => {
configMock.getEnv.mockReturnValue({
database: {
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTOR,
},
});
configMock.getEnv.mockReturnValue(
mockEnvData({
database: {
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTOR,
},
}),
);
databaseMock.getExtensionVersion.mockResolvedValue({
installedVersion: null,
availableVersion: minVersionInRange,

View File

@ -1,5 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
@ -17,24 +16,23 @@ import {
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { isDuplicateDetectionEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
@Injectable()
export class DuplicateService {
private configCore: SystemConfigCore;
export class DuplicateService extends BaseService {
constructor(
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(DuplicateService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
}
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
@ -44,7 +42,7 @@ export class DuplicateService {
}
async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isDuplicateDetectionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@ -65,7 +63,7 @@ export class DuplicateService {
}
async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
const { machineLearning } = await this.getConfig({ withCache: true });
if (!isDuplicateDetectionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}

View File

@ -1,12 +1,11 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { snakeCase } from 'lodash';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import { AssetType, ManualJobName } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import {
ConcurrentQueueName,
IJobRepository,
@ -22,6 +21,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMetricRepository } from 'src/interfaces/metric.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
const asJobItem = (dto: JobCreateDto): JobItem => {
switch (dto.name) {
@ -44,8 +44,7 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
};
@Injectable()
export class JobService {
private configCore: SystemConfigCore;
export class JobService extends BaseService {
private isMicroservices = false;
constructor(
@ -55,10 +54,10 @@ export class JobService {
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(JobService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
}
@OnEvent({ name: 'app.bootstrap' })
@ -198,7 +197,7 @@ export class JobService {
}
async init(jobHandlers: Record<JobName, JobHandler>) {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
for (const queueName of Object.values(QueueName)) {
let concurrency = 1;
@ -279,7 +278,7 @@ export class JobService {
if (item.data.source === 'sidecar-write') {
const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]);
if (asset) {
this.eventRepository.clientSend(ClientEvent.ASSET_UPDATE, asset.ownerId, mapAsset(asset));
this.eventRepository.clientSend('on_asset_update', asset.ownerId, mapAsset(asset));
}
}
await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data });
@ -302,7 +301,7 @@ export class JobService {
const { id } = item.data;
const person = await this.personRepository.getById(id);
if (person) {
this.eventRepository.clientSend(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id);
this.eventRepository.clientSend('on_person_thumbnail', person.ownerId, person.id);
}
break;
}
@ -331,7 +330,7 @@ export class JobService {
await this.jobRepository.queueAll(jobs);
if (asset.isVisible) {
this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset));
this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
}
break;
@ -345,7 +344,7 @@ export class JobService {
}
case JobName.USER_DELETION: {
this.eventRepository.clientBroadcast(ClientEvent.USER_DELETE, item.data.id);
this.eventRepository.clientBroadcast('on_user_delete', item.data.id);
break;
}
}

View File

@ -3,7 +3,6 @@ import { R_OK } from 'node:constants';
import path, { basename, parse } from 'node:path';
import picomatch from 'picomatch';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import {
CreateLibraryDto,
@ -35,14 +34,14 @@ import { ILibraryRepository } from 'src/interfaces/library.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { mimeTypes } from 'src/utils/mime-types';
import { handlePromiseError } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
import { validateCronExpression } from 'src/validation';
@Injectable()
export class LibraryService {
private configCore: SystemConfigCore;
export class LibraryService extends BaseService {
private watchLibraries = false;
private watchLock = false;
private watchers: Record<string, () => Promise<void>> = {};
@ -55,15 +54,15 @@ export class LibraryService {
@Inject(ILibraryRepository) private repository: ILibraryRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(LibraryService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
@OnEvent({ name: 'app.bootstrap' })
async onBootstrap() {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
const { watch, scan } = config.library;
@ -142,7 +141,13 @@ export class LibraryService {
const handler = async () => {
this.logger.debug(`File add event received for ${path} in library ${library.id}}`);
if (matcher(path)) {
await this.syncFiles(library, [path]);
const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path);
if (asset) {
await this.syncAssets(library, [asset.id]);
}
if (matcher(path)) {
await this.syncFiles(library, [path]);
}
}
};
return handlePromiseError(handler(), this.logger);
@ -605,7 +610,7 @@ export class LibraryService {
this.logger.log(`Scanning library ${library.id} for removed assets`);
const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getAll(pagination, { libraryId: job.id }),
this.assetRepository.getAll(pagination, { libraryId: job.id, withDeleted: true }),
);
let assetCount = 0;

View File

@ -1,34 +1,26 @@
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { MapService } from 'src/services/map.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { Mocked } from 'vitest';
describe(MapService.name, () => {
let sut: MapService;
let albumMock: Mocked<IAlbumRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let partnerMock: Mocked<IPartnerRepository>;
let mapMock: Mocked<IMapRepository>;
let systemMetadataMock: Mocked<ISystemMetadataRepository>;
beforeEach(() => {
albumMock = newAlbumRepositoryMock();
loggerMock = newLoggerRepositoryMock();
partnerMock = newPartnerRepositoryMock();
mapMock = newMapRepositoryMock();
systemMetadataMock = newSystemMetadataRepositoryMock();
sut = new MapService(albumMock, loggerMock, partnerMock, mapMock, systemMetadataMock);
sut = new MapService(albumMock, partnerMock, mapMock);
});
describe('getMapMarkers', () => {

View File

@ -1,27 +1,17 @@
import { Inject } from '@nestjs/common';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getMyPartnerIds } from 'src/utils/asset.util';
export class MapService {
private configCore: SystemConfigCore;
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IMapRepository) private mapRepository: IMapRepository,
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
) {
this.logger.setContext(MapService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
}
) {}
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
const userIds = [auth.user.id];

View File

@ -1,8 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { dirname } from 'node:path';
import { 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 {
@ -43,14 +41,14 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
import { mimeTypes } from 'src/utils/mime-types';
import { usePagination } from 'src/utils/pagination';
@Injectable()
export class MediaService {
private configCore: SystemConfigCore;
export class MediaService extends BaseService {
private storageCore: StorageCore;
private maliOpenCL?: boolean;
private devices?: string[];
@ -64,10 +62,10 @@ export class MediaService {
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(MediaService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
this.storageCore = StorageCore.create(
assetRepository,
cryptoRepository,
@ -161,7 +159,7 @@ export class MediaService {
}
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
const { image } = await this.configCore.getConfig({ withCache: true });
const { image } = await this.getConfig({ withCache: true });
const [asset] = await this.assetRepository.getByIds([id], { files: true });
if (!asset) {
return JobStatus.FAILED;
@ -235,7 +233,7 @@ export class MediaService {
}
private async generateImageThumbnails(asset: AssetEntity) {
const { image } = await this.configCore.getConfig({ withCache: true });
const { image } = await this.getConfig({ withCache: true });
const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format);
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
this.storageCore.ensureFolders(previewPath);
@ -269,7 +267,7 @@ export class MediaService {
}
private async generateVideoThumbnails(asset: AssetEntity) {
const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true });
const { image, ffmpeg } = await this.getConfig({ withCache: true });
const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format);
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
this.storageCore.ensureFolders(previewPath);
@ -339,7 +337,7 @@ export class MediaService {
return JobStatus.FAILED;
}
const { ffmpeg } = await this.configCore.getConfig({ withCache: true });
const { ffmpeg } = await this.getConfig({ withCache: true });
const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream);
if (target === TranscodeTarget.NONE && !this.isRemuxRequired(ffmpeg, format)) {
if (asset.encodedVideoPath) {

View File

@ -7,7 +7,6 @@ import { constants } from 'node:fs/promises';
import path from 'node:path';
import { SystemConfig } from 'src/config';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
@ -39,6 +38,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { isFaceImportEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
import { upsertTags } from 'src/utils/tag';
@ -97,9 +97,8 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
};
@Injectable()
export class MetadataService {
export class MetadataService extends BaseService {
private storageCore: StorageCore;
private configCore: SystemConfigCore;
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@ -117,10 +116,10 @@ export class MetadataService {
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ITagRepository) private tagRepository: ITagRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(MetadataService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
this.storageCore = StorageCore.create(
assetRepository,
cryptoRepository,
@ -137,7 +136,7 @@ export class MetadataService {
if (app !== 'microservices') {
return;
}
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
await this.init(config);
}
@ -222,7 +221,7 @@ export class MetadataService {
}
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
const { metadata, reverseGeocoding } = await this.configCore.getConfig({ withCache: true });
const { metadata, reverseGeocoding } = await this.getConfig({ withCache: true });
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) {
return JobStatus.FAILED;

View File

@ -6,7 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetFileType, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
@ -104,7 +104,7 @@ describe(NotificationService.name, () => {
it('should emit client and server events', () => {
const update = { newConfig: defaults };
expect(sut.onConfigUpdate(update)).toBeUndefined();
expect(eventMock.clientBroadcast).toHaveBeenCalledWith(ClientEvent.CONFIG_UPDATE, {});
expect(eventMock.clientBroadcast).toHaveBeenCalledWith('on_config_update');
expect(eventMock.serverSend).toHaveBeenCalledWith('config.update', update);
});
});
@ -236,28 +236,28 @@ describe(NotificationService.name, () => {
describe('onStackCreate', () => {
it('should send connected clients an event', () => {
sut.onStackCreate({ stackId: 'stack-id', userId: 'user-id' });
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []);
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
});
});
describe('onStackUpdate', () => {
it('should send connected clients an event', () => {
sut.onStackUpdate({ stackId: 'stack-id', userId: 'user-id' });
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []);
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
});
});
describe('onStackDelete', () => {
it('should send connected clients an event', () => {
sut.onStackDelete({ stackId: 'stack-id', userId: 'user-id' });
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []);
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
});
});
describe('onStacksDelete', () => {
it('should send connected clients an event', () => {
sut.onStacksDelete({ stackIds: ['stack-id'], userId: 'user-id' });
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []);
expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id');
});
});

View File

@ -1,12 +1,11 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { AlbumEntity } from 'src/entities/album.entity';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import {
IEmailJob,
IJobRepository,
@ -20,32 +19,31 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { getFilenameExtension } from 'src/utils/file';
import { isEqualObject } from 'src/utils/object';
import { getPreferences } from 'src/utils/preferences';
@Injectable()
export class NotificationService {
private configCore: SystemConfigCore;
export class NotificationService extends BaseService {
constructor(
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(NotificationService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
}
@OnEvent({ name: 'config.update' })
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {});
this.eventRepository.clientBroadcast('on_config_update');
this.eventRepository.serverSend('config.update', { oldConfig, newConfig });
}
@ -66,7 +64,7 @@ export class NotificationService {
@OnEvent({ name: 'asset.hide' })
onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId);
this.eventRepository.clientSend('on_asset_hidden', userId, assetId);
}
@OnEvent({ name: 'asset.show' })
@ -76,42 +74,42 @@ export class NotificationService {
@OnEvent({ name: 'asset.trash' })
onAssetTrash({ assetId, userId }: ArgOf<'asset.trash'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, [assetId]);
this.eventRepository.clientSend('on_asset_trash', userId, [assetId]);
}
@OnEvent({ name: 'asset.delete' })
onAssetDelete({ assetId, userId }: ArgOf<'asset.delete'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, userId, assetId);
this.eventRepository.clientSend('on_asset_delete', userId, assetId);
}
@OnEvent({ name: 'assets.trash' })
onAssetsTrash({ assetIds, userId }: ArgOf<'assets.trash'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, assetIds);
this.eventRepository.clientSend('on_asset_trash', userId, assetIds);
}
@OnEvent({ name: 'assets.restore' })
onAssetsRestore({ assetIds, userId }: ArgOf<'assets.restore'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, userId, assetIds);
this.eventRepository.clientSend('on_asset_restore', userId, assetIds);
}
@OnEvent({ name: 'stack.create' })
onStackCreate({ userId }: ArgOf<'stack.create'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []);
this.eventRepository.clientSend('on_asset_stack_update', userId);
}
@OnEvent({ name: 'stack.update' })
onStackUpdate({ userId }: ArgOf<'stack.update'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []);
this.eventRepository.clientSend('on_asset_stack_update', userId);
}
@OnEvent({ name: 'stack.delete' })
onStackDelete({ userId }: ArgOf<'stack.delete'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []);
this.eventRepository.clientSend('on_asset_stack_update', userId);
}
@OnEvent({ name: 'stacks.delete' })
onStacksDelete({ userId }: ArgOf<'stacks.delete'>) {
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []);
this.eventRepository.clientSend('on_asset_stack_update', userId);
}
@OnEvent({ name: 'user.signup' })
@ -134,7 +132,7 @@ export class NotificationService {
@OnEvent({ name: 'session.delete' })
onSessionDelete({ sessionId }: ArgOf<'session.delete'>) {
// after the response is sent
setTimeout(() => this.eventRepository.clientSend(ClientEvent.SESSION_DELETE, sessionId, sessionId), 500);
setTimeout(() => this.eventRepository.clientSend('on_session_delete', sessionId, sessionId), 500);
}
async sendTestEmail(id: string, dto: SystemConfigSmtpDto) {
@ -149,7 +147,7 @@ export class NotificationService {
throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });
}
const { server } = await this.configCore.getConfig({ withCache: false });
const { server } = await this.getConfig({ withCache: false });
const { html, text } = await this.notificationRepository.renderEmail({
template: EmailTemplate.TEST_EMAIL,
data: {
@ -177,7 +175,7 @@ export class NotificationService {
return JobStatus.SKIPPED;
}
const { server } = await this.configCore.getConfig({ withCache: true });
const { server } = await this.getConfig({ withCache: true });
const { html, text } = await this.notificationRepository.renderEmail({
template: EmailTemplate.WELCOME,
data: {
@ -220,7 +218,7 @@ export class NotificationService {
const attachment = await this.getAlbumThumbnailAttachment(album);
const { server } = await this.configCore.getConfig({ withCache: false });
const { server } = await this.getConfig({ withCache: false });
const { html, text } = await this.notificationRepository.renderEmail({
template: EmailTemplate.ALBUM_INVITE,
data: {
@ -262,7 +260,7 @@ export class NotificationService {
const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) => user.id !== senderId);
const attachment = await this.getAlbumThumbnailAttachment(album);
const { server } = await this.configCore.getConfig({ withCache: false });
const { server } = await this.getConfig({ withCache: false });
for (const recipient of recipients) {
const user = await this.userRepository.get(recipient.id, { withDeleted: false });
@ -303,7 +301,7 @@ export class NotificationService {
}
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
const { notifications } = await this.configCore.getConfig({ withCache: false });
const { notifications } = await this.getConfig({ withCache: false });
if (!notifications.smtp.enabled) {
return JobStatus.SKIPPED;
}

View File

@ -1,7 +1,6 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@ -55,6 +54,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf
import { ISearchRepository } from 'src/interfaces/search.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { checkAccess, requireAccess } from 'src/utils/access';
import { getAssetFiles } from 'src/utils/asset.util';
import { ImmichFileResponse } from 'src/utils/file';
@ -64,8 +64,7 @@ import { usePagination } from 'src/utils/pagination';
import { IsNull } from 'typeorm';
@Injectable()
export class PersonService {
private configCore: SystemConfigCore;
export class PersonService extends BaseService {
private storageCore: StorageCore;
constructor(
@ -75,15 +74,15 @@ export class PersonService {
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
@Inject(IPersonRepository) private repository: IPersonRepository,
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(PersonService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
this.storageCore = StorageCore.create(
assetRepository,
cryptoRepository,
@ -102,7 +101,7 @@ export class PersonService {
skip: (page - 1) * size,
};
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
const { machineLearning } = await this.getConfig({ withCache: false });
const { items, hasNextPage } = await this.repository.getAllForUser(pagination, auth.user.id, {
minimumFaceCount: machineLearning.facialRecognition.minFaces,
withHidden,
@ -283,7 +282,7 @@ export class PersonService {
}
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@ -314,7 +313,7 @@ export class PersonService {
}
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
const { machineLearning } = await this.getConfig({ withCache: true });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@ -375,7 +374,7 @@ export class PersonService {
}
async handleQueueRecognizeFaces({ force, nightly }: INightlyJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@ -425,7 +424,7 @@ export class PersonService {
}
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
const { machineLearning } = await this.getConfig({ withCache: true });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@ -519,7 +518,7 @@ export class PersonService {
}
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
const { machineLearning, metadata, image } = await this.configCore.getConfig({ withCache: true });
const { machineLearning, metadata, image } = await this.getConfig({ withCache: true });
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
return JobStatus.SKIPPED;
}

View File

@ -1,5 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto';
@ -24,13 +23,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { getMyPartnerIds } from 'src/utils/asset.util';
import { isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class SearchService {
private configCore: SystemConfigCore;
export class SearchService extends BaseService {
constructor(
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@ -38,10 +36,10 @@ export class SearchService {
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(SearchService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
}
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
@ -101,7 +99,7 @@ export class SearchService {
}
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isSmartSearchEnabled(machineLearning)) {
throw new BadRequestException('Smart search is not enabled');
}

View File

@ -176,9 +176,9 @@ describe(ServerService.name, () => {
});
});
describe('getConfig', () => {
describe('getSystemConfig', () => {
it('should respond the server configuration', async () => {
await expect(sut.getConfig()).resolves.toEqual({
await expect(sut.getSystemConfig()).resolves.toEqual({
loginPageMessage: '',
oauthButtonText: 'Login with OAuth',
trashDays: 30,

View File

@ -2,7 +2,6 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nes
import { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
import { serverVersion } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
import {
@ -22,24 +21,24 @@ import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { asHumanReadable } from 'src/utils/bytes';
import { isUsingConfigFile } from 'src/utils/config';
import { mimeTypes } from 'src/utils/mime-types';
import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class ServerService {
private configCore: SystemConfigCore;
export class ServerService extends BaseService {
constructor(
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(ServerService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
@OnEvent({ name: 'app.bootstrap' })
@ -91,7 +90,7 @@ export class ServerService {
async getFeatures(): Promise<ServerFeaturesDto> {
const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
await this.configCore.getConfig({ withCache: false });
await this.getConfig({ withCache: false });
return {
smartSearch: isSmartSearchEnabled(machineLearning),
@ -106,18 +105,18 @@ export class ServerService {
oauth: oauth.enabled,
oauthAutoLaunch: oauth.autoLaunch,
passwordLogin: passwordLogin.enabled,
configFile: this.configCore.isUsingConfigFile(),
configFile: isUsingConfigFile(),
email: notifications.smtp.enabled,
};
}
async getTheme() {
const { theme } = await this.configCore.getConfig({ withCache: false });
const { theme } = await this.getConfig({ withCache: false });
return theme;
}
async getConfig(): Promise<ServerConfigDto> {
const config = await this.configCore.getConfig({ withCache: false });
async getSystemConfig(): Promise<ServerConfigDto> {
const config = await this.getConfig({ withCache: false });
const isInitialized = await this.userRepository.hasAdmin();
const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);

View File

@ -1,6 +1,5 @@
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto';
@ -20,22 +19,21 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { checkAccess, requireAccess } from 'src/utils/access';
import { OpenGraphTags } from 'src/utils/misc';
@Injectable()
export class SharedLinkService {
private configCore: SystemConfigCore;
export class SharedLinkService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(ISharedLinkRepository) private repository: ISharedLinkRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(SharedLinkService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
@ -195,7 +193,7 @@ export class SharedLinkService {
return null;
}
const config = await this.configCore.getConfig({ withCache: true });
const config = await this.getConfig({ withCache: true });
const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
const assetCount = sharedLink.assets.length > 0 ? sharedLink.assets.length : sharedLink.album?.assets.length || 0;

View File

@ -1,6 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
@ -18,14 +17,13 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
@Injectable()
export class SmartInfoService {
private configCore: SystemConfigCore;
export class SmartInfoService extends BaseService {
constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@ -33,10 +31,10 @@ export class SmartInfoService {
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@Inject(ISearchRepository) private repository: ISearchRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(SmartInfoService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
@OnEvent({ name: 'app.bootstrap' })
@ -45,7 +43,7 @@ export class SmartInfoService {
return;
}
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
await this.init(config);
}
@ -106,7 +104,7 @@ export class SmartInfoService {
}
async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isSmartSearchEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@ -131,7 +129,7 @@ export class SmartInfoService {
}
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
const { machineLearning } = await this.getConfig({ withCache: true });
if (!isSmartSearchEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}

View File

@ -13,7 +13,6 @@ import {
supportedYearTokens,
} from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
@ -29,6 +28,7 @@ import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { getLivePhotoMotionFilename } from 'src/utils/file';
import { usePagination } from 'src/utils/pagination';
@ -45,8 +45,7 @@ interface RenderMetadata {
}
@Injectable()
export class StorageTemplateService {
private configCore: SystemConfigCore;
export class StorageTemplateService extends BaseService {
private storageCore: StorageCore;
private _template: {
compiled: HandlebarsTemplateDelegate<any>;
@ -71,10 +70,10 @@ export class StorageTemplateService {
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(StorageTemplateService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
this.storageCore = StorageCore.create(
assetRepository,
cryptoRepository,
@ -117,7 +116,7 @@ export class StorageTemplateService {
}
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
const config = await this.configCore.getConfig({ withCache: true });
const config = await this.getConfig({ withCache: true });
const storageTemplateEnabled = config.storageTemplate.enabled;
if (!storageTemplateEnabled) {
return JobStatus.SKIPPED;
@ -147,7 +146,7 @@ export class StorageTemplateService {
async handleMigration(): Promise<JobStatus> {
this.logger.log('Starting storage template migration');
const { storageTemplate } = await this.configCore.getConfig({ withCache: true });
const { storageTemplate } = await this.getConfig({ withCache: true });
const { enabled } = storageTemplate;
if (!enabled) {
this.logger.log('Storage template migration disabled, skipping');

View File

@ -1,9 +1,11 @@
import { SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { StorageService } from 'src/services/storage.service';
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
@ -12,18 +14,20 @@ import { Mocked } from 'vitest';
describe(StorageService.name, () => {
let sut: StorageService;
let configMock: Mocked<IConfigRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let storageMock: Mocked<IStorageRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
beforeEach(() => {
configMock = newConfigRepositoryMock();
databaseMock = newDatabaseRepositoryMock();
storageMock = newStorageRepositoryMock();
loggerMock = newLoggerRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
sut = new StorageService(databaseMock, storageMock, loggerMock, systemMock);
sut = new StorageService(configMock, databaseMock, storageMock, loggerMock, systemMock);
});
it('should work', () => {
@ -52,7 +56,7 @@ describe(StorageService.name, () => {
systemMock.get.mockResolvedValue({ mountFiles: true });
storageMock.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount');
await expect(sut.onBootstrap()).rejects.toThrow('Failed to read');
expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled();
expect(systemMock.set).not.toHaveBeenCalled();
@ -62,7 +66,21 @@ describe(StorageService.name, () => {
systemMock.get.mockResolvedValue({ mountFiles: true });
storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount');
await expect(sut.onBootstrap()).rejects.toThrow('Failed to write');
expect(systemMock.set).not.toHaveBeenCalled();
});
it('should startup if checks are disabled', async () => {
systemMock.get.mockResolvedValue({ mountFiles: true });
configMock.getEnv.mockReturnValue(
mockEnvData({
storage: { ignoreMountCheckErrors: true },
}),
);
storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'"));
await expect(sut.onBootstrap()).resolves.toBeUndefined();
expect(systemMock.set).not.toHaveBeenCalled();
});

View File

@ -3,6 +3,7 @@ import { join } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent } from 'src/decorators';
import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
@ -10,9 +11,12 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ImmichStartupError } from 'src/utils/events';
const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`;
@Injectable()
export class StorageService {
constructor(
@Inject(IConfigRepository) private configRepository: IConfigRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@ -23,30 +27,41 @@ export class StorageService {
@OnEvent({ name: 'app.bootstrap' })
async onBootstrap() {
const envData = this.configRepository.getEnv();
await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => {
const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false };
const enabled = flags.mountFiles ?? false;
this.logger.log(`Verifying system mount folder checks (enabled=${enabled})`);
// check each folder exists and is writable
for (const folder of Object.values(StorageFolder)) {
if (!enabled) {
this.logger.log(`Writing initial mount file for the ${folder} folder`);
await this.createMountFile(folder);
try {
// check each folder exists and is writable
for (const folder of Object.values(StorageFolder)) {
if (!enabled) {
this.logger.log(`Writing initial mount file for the ${folder} folder`);
await this.createMountFile(folder);
}
await this.verifyReadAccess(folder);
await this.verifyWriteAccess(folder);
}
await this.verifyReadAccess(folder);
await this.verifyWriteAccess(folder);
}
if (!flags.mountFiles) {
flags.mountFiles = true;
await this.systemMetadata.set(SystemMetadataKey.SYSTEM_FLAGS, flags);
this.logger.log('Successfully enabled system mount folders checks');
}
if (!flags.mountFiles) {
flags.mountFiles = true;
await this.systemMetadata.set(SystemMetadataKey.SYSTEM_FLAGS, flags);
this.logger.log('Successfully enabled system mount folders checks');
this.logger.log('Successfully verified system mount folder checks');
} catch (error) {
if (envData.storage.ignoreMountCheckErrors) {
this.logger.error(error);
this.logger.warn('Ignoring mount folder errors');
} else {
throw error;
}
}
this.logger.log('Successfully verified system mount folder checks');
});
}
@ -70,49 +85,45 @@ export class StorageService {
}
private async verifyReadAccess(folder: StorageFolder) {
const { filePath } = this.getMountFilePaths(folder);
const { internalPath, externalPath } = this.getMountFilePaths(folder);
try {
await this.storageRepository.readFile(filePath);
await this.storageRepository.readFile(internalPath);
} catch (error) {
this.logger.error(`Failed to read ${filePath}: ${error}`);
this.logger.error(
`The "${folder}" folder appears to be offline/missing, please make sure the volume is mounted with the correct permissions`,
);
throw new ImmichStartupError(`Failed to validate folder mount (read from "<MEDIA_LOCATION>/${folder}")`);
this.logger.error(`Failed to read ${internalPath}: ${error}`);
throw new ImmichStartupError(`Failed to read "${externalPath} - ${docsMessage}"`);
}
}
private async createMountFile(folder: StorageFolder) {
const { folderPath, filePath } = this.getMountFilePaths(folder);
const { folderPath, internalPath, externalPath } = this.getMountFilePaths(folder);
try {
this.storageRepository.mkdirSync(folderPath);
await this.storageRepository.createFile(filePath, Buffer.from(`${Date.now()}`));
await this.storageRepository.createFile(internalPath, Buffer.from(`${Date.now()}`));
} catch (error) {
this.logger.error(`Failed to create ${filePath}: ${error}`);
this.logger.error(
`The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`,
);
throw new ImmichStartupError(`Failed to validate folder mount (write to "<UPLOAD_LOCATION>/${folder}")`);
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
this.logger.warn('Found existing mount file, skipping creation');
return;
}
this.logger.error(`Failed to create ${internalPath}: ${error}`);
throw new ImmichStartupError(`Failed to create "${externalPath} - ${docsMessage}"`);
}
}
private async verifyWriteAccess(folder: StorageFolder) {
const { filePath } = this.getMountFilePaths(folder);
const { internalPath, externalPath } = this.getMountFilePaths(folder);
try {
await this.storageRepository.overwriteFile(filePath, Buffer.from(`${Date.now()}`));
await this.storageRepository.overwriteFile(internalPath, Buffer.from(`${Date.now()}`));
} catch (error) {
this.logger.error(`Failed to write ${filePath}: ${error}`);
this.logger.error(
`The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`,
);
throw new ImmichStartupError(`Failed to validate folder mount (write to "<UPLOAD_LOCATION>/${folder}")`);
this.logger.error(`Failed to write ${internalPath}: ${error}`);
throw new ImmichStartupError(`Failed to write "${externalPath} - ${docsMessage}"`);
}
}
private getMountFilePaths(folder: StorageFolder) {
const folderPath = StorageCore.getBaseFolder(folder);
const filePath = join(folderPath, '.immich');
const internalPath = join(folderPath, '.immich');
const externalPath = `<UPLOAD_LOCATION>/${folder}/.immich`;
return { folderPath, filePath };
return { folderPath, internalPath, externalPath };
}
}

View File

@ -216,7 +216,7 @@ describe(SystemConfigService.name, () => {
it('should return the default config', async () => {
systemMock.get.mockResolvedValue({});
await expect(sut.getConfig()).resolves.toEqual(defaults);
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
});
it('should merge the overrides', async () => {
@ -227,7 +227,7 @@ describe(SystemConfigService.name, () => {
user: { deleteDelay: 15 },
});
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
});
it('should load the config from a json file', async () => {
@ -235,7 +235,7 @@ describe(SystemConfigService.name, () => {
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
});
@ -245,7 +245,7 @@ describe(SystemConfigService.name, () => {
systemMock.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`);
await expect(sut.getConfig()).rejects.toBeInstanceOf(Error);
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
expect(loggerMock.error).toHaveBeenCalledTimes(2);
@ -269,7 +269,7 @@ describe(SystemConfigService.name, () => {
`;
systemMock.readFile.mockResolvedValue(partialConfig);
await expect(sut.getConfig()).resolves.toEqual(updatedConfig);
await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig);
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.yaml');
});
@ -278,7 +278,7 @@ describe(SystemConfigService.name, () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
await expect(sut.getConfig()).resolves.toEqual(defaults);
await expect(sut.getSystemConfig()).resolves.toEqual(defaults);
expect(systemMock.readFile).toHaveBeenCalledWith('immich-config.json');
});
@ -288,7 +288,7 @@ describe(SystemConfigService.name, () => {
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
const config = await sut.getConfig();
const config = await sut.getSystemConfig();
expect(config.machineLearning.url).toEqual('immich_machine_learning');
});
@ -304,7 +304,7 @@ describe(SystemConfigService.name, () => {
const partialConfig = { server: { externalDomain } };
systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
const config = await sut.getConfig();
const config = await sut.getSystemConfig();
expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app');
});
}
@ -316,7 +316,7 @@ describe(SystemConfigService.name, () => {
`;
systemMock.readFile.mockResolvedValue(partialConfig);
await sut.getConfig();
await sut.getSystemConfig();
expect(loggerMock.warn).toHaveBeenCalled();
});
@ -335,10 +335,10 @@ describe(SystemConfigService.name, () => {
systemMock.readFile.mockResolvedValue(JSON.stringify(test.config));
if (test.warn) {
await sut.getConfig();
await sut.getSystemConfig();
expect(loggerMock.warn).toHaveBeenCalled();
} else {
await expect(sut.getConfig()).rejects.toBeInstanceOf(Error);
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
}
});
}
@ -382,7 +382,7 @@ describe(SystemConfigService.name, () => {
describe('updateConfig', () => {
it('should update the config and emit an event', async () => {
systemMock.get.mockResolvedValue(partialConfig);
await expect(sut.updateConfig(updatedConfig)).resolves.toEqual(updatedConfig);
await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig);
expect(eventMock.emit).toHaveBeenCalledWith(
'config.update',
expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }),
@ -392,7 +392,7 @@ describe(SystemConfigService.name, () => {
it('should throw an error if a config file is in use', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
systemMock.readFile.mockResolvedValue(JSON.stringify({}));
await expect(sut.updateConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException);
expect(systemMock.set).not.toHaveBeenCalled();
});
});

View File

@ -12,36 +12,35 @@ import {
supportedWeekTokens,
supportedYearTokens,
} from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
import { LogLevel } from 'src/enum';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
import { clearConfigCache, isUsingConfigFile } from 'src/utils/config';
import { toPlainObject } from 'src/utils/object';
@Injectable()
export class SystemConfigService {
private core: SystemConfigCore;
export class SystemConfigService extends BaseService {
constructor(
@Inject(ISystemMetadataRepository) repository: ISystemMetadataRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(SystemConfigService.name);
this.core = SystemConfigCore.create(repository, this.logger);
}
@OnEvent({ name: 'app.bootstrap', priority: -100 })
async onBootstrap() {
const config = await this.core.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
await this.eventRepository.emit('config.update', { newConfig: config });
}
async getConfig(): Promise<SystemConfigDto> {
const config = await this.core.getConfig({ withCache: false });
async getSystemConfig(): Promise<SystemConfigDto> {
const config = await this.getConfig({ withCache: false });
return mapConfig(config);
}
@ -57,7 +56,7 @@ export class SystemConfigService {
this.logger.setLogLevel(level);
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
// TODO only do this if the event is a socket.io event
this.core.invalidateCache();
clearConfigCache();
}
@OnEvent({ name: 'config.validate' })
@ -67,12 +66,12 @@ export class SystemConfigService {
}
}
async updateConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
if (this.core.isUsingConfigFile()) {
async updateSystemConfig(dto: SystemConfigDto): Promise<SystemConfigDto> {
if (isUsingConfigFile()) {
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
}
const oldConfig = await this.core.getConfig({ withCache: false });
const oldConfig = await this.getConfig({ withCache: false });
try {
await this.eventRepository.emit('config.validate', { newConfig: toPlainObject(dto), oldConfig });
@ -81,7 +80,7 @@ export class SystemConfigService {
throw new BadRequestException(error instanceof Error ? error.message : error);
}
const newConfig = await this.core.updateConfig(dto);
const newConfig = await this.updateConfig(dto);
await this.eventRepository.emit('config.update', { newConfig, oldConfig });
@ -104,7 +103,7 @@ export class SystemConfigService {
}
async getCustomCss(): Promise<string> {
const { theme } = await this.core.getConfig({ withCache: false });
const { theme } = await this.getConfig({ withCache: false });
return theme.customCss;
}

View File

@ -1,6 +1,5 @@
import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
import { SALT_ROUNDS } from 'src/constants';
import { UserCore } from 'src/cores/user.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
import {
@ -19,11 +18,10 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
import { createUser } from 'src/utils/user';
@Injectable()
export class UserAdminService {
private userCore: UserCore;
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@ -32,7 +30,6 @@ export class UserAdminService {
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
) {
this.userCore = UserCore.create(cryptoRepository, userRepository);
this.logger.setContext(UserAdminService.name);
}
@ -43,7 +40,7 @@ export class UserAdminService {
async create(dto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
const { notify, ...rest } = dto;
const user = await this.userCore.createUser(rest);
const user = await createUser({ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, rest);
await this.eventRepository.emit('user.signup', {
notify: !!notify,

View File

@ -3,7 +3,6 @@ import { DateTime } from 'luxon';
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
import { SALT_ROUNDS } from 'src/constants';
import { StorageCore } 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';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
@ -19,13 +18,12 @@ 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 { BaseService } from 'src/services/base.service';
import { ImmichFileResponse } from 'src/utils/file';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
@Injectable()
export class UserService {
private configCore: SystemConfigCore;
export class UserService extends BaseService {
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@ -33,10 +31,10 @@ export class UserService {
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(UserService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
async search(): Promise<UserResponseDto[]> {
@ -189,7 +187,7 @@ export class UserService {
async handleUserDeleteCheck(): Promise<JobStatus> {
const users = await this.userRepository.getDeletedUsers();
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
await this.jobRepository.queueAll(
users.flatMap((user) =>
this.isReadyForDeletion(user, config.user.deleteDelay)
@ -201,7 +199,7 @@ export class UserService {
}
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
const config = await this.configCore.getConfig({ withCache: false });
const config = await this.getConfig({ withCache: false });
const user = await this.userRepository.get(id, { withDeleted: true });
if (!user) {
return JobStatus.FAILED;

View File

@ -1,17 +1,21 @@
import { DateTime } from 'luxon';
import { serverVersion } from 'src/constants';
import { SystemMetadataKey } from 'src/enum';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { VersionService } from 'src/services/version.service';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock';
import { Mocked } from 'vitest';
const mockRelease = (version: string) => ({
@ -26,26 +30,47 @@ const mockRelease = (version: string) => ({
describe(VersionService.name, () => {
let sut: VersionService;
let databaseMock: Mocked<IDatabaseRepository>;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>;
let serverMock: Mocked<IServerInfoRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let versionMock: Mocked<IVersionHistoryRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => {
databaseMock = newDatabaseRepositoryMock();
eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock();
serverMock = newServerInfoRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
versionMock = newVersionHistoryRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new VersionService(eventMock, jobMock, serverMock, systemMock, loggerMock);
sut = new VersionService(databaseMock, eventMock, jobMock, serverMock, systemMock, versionMock, loggerMock);
});
it('should work', () => {
expect(sut).toBeDefined();
});
describe('onBootstrap', () => {
it('should record a new version', async () => {
await expect(sut.onBootstrap()).resolves.toBeUndefined();
expect(versionMock.create).toHaveBeenCalledWith({ version: expect.any(String) });
});
it('should skip a duplicate version', async () => {
versionMock.getLatest.mockResolvedValue({
id: 'version-1',
createdAt: new Date(),
version: serverVersion.toString(),
});
await expect(sut.onBootstrap()).resolves.toBeUndefined();
expect(versionMock.create).not.toHaveBeenCalled();
});
});
describe('getVersion', () => {
it('should respond the server version', () => {
expect(sut.getVersion()).toEqual({
@ -56,6 +81,14 @@ describe(VersionService.name, () => {
});
});
describe('getVersionHistory', () => {
it('should respond the server version history', async () => {
const upgrade = { id: 'upgrade-1', createdAt: new Date(), version: '1.0.0' };
versionMock.getAll.mockResolvedValue([upgrade]);
await expect(sut.getVersionHistory()).resolves.toEqual([upgrade]);
});
});
describe('handQueueVersionCheck', () => {
it('should queue a version check job', async () => {
await expect(sut.handleQueueVersionCheck()).resolves.toBeUndefined();

View File

@ -2,16 +2,18 @@ import { Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import semver, { SemVer } from 'semver';
import { isDev, serverVersion } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEvent } from 'src/decorators';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
import { SystemMetadataKey } from 'src/enum';
import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { BaseService } from 'src/services/base.service';
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
return {
@ -23,29 +25,42 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
};
@Injectable()
export class VersionService {
private configCore: SystemConfigCore;
export class VersionService extends BaseService {
constructor(
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IVersionHistoryRepository) private versionRepository: IVersionHistoryRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(systemMetadataRepository, logger);
this.logger.setContext(VersionService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
@OnEvent({ name: 'app.bootstrap' })
async onBootstrap(): Promise<void> {
await this.handleVersionCheck();
await this.databaseRepository.withLock(DatabaseLock.VersionHistory, async () => {
const latest = await this.versionRepository.getLatest();
const current = serverVersion.toString();
if (!latest || latest.version !== current) {
this.logger.log(`Version has changed, adding ${current} to history`);
await this.versionRepository.create({ version: current });
}
});
}
getVersion() {
return ServerVersionResponseDto.fromSemVer(serverVersion);
}
getVersionHistory() {
return this.versionRepository.getAll();
}
async handleQueueVersionCheck() {
await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} });
}
@ -58,7 +73,7 @@ export class VersionService {
return JobStatus.SKIPPED;
}
const { newVersionCheck } = await this.configCore.getConfig({ withCache: true });
const { newVersionCheck } = await this.getConfig({ withCache: true });
if (!newVersionCheck.enabled) {
return JobStatus.SKIPPED;
}
@ -80,7 +95,7 @@ export class VersionService {
if (semver.gt(releaseVersion, serverVersion)) {
this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`);
this.eventRepository.clientBroadcast(ClientEvent.NEW_RELEASE, asNotification(metadata));
this.eventRepository.clientBroadcast('on_new_release', asNotification(metadata));
}
} catch (error: Error | any) {
this.logger.warn(`Unable to run version check: ${error}`, error?.stack);
@ -92,10 +107,10 @@ export class VersionService {
@OnEvent({ name: 'websocket.connect' })
async onWebsocketConnection({ userId }: ArgOf<'websocket.connect'>) {
this.eventRepository.clientSend(ClientEvent.SERVER_VERSION, userId, serverVersion);
this.eventRepository.clientSend('on_server_version', userId, serverVersion);
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE);
if (metadata) {
this.eventRepository.clientSend(ClientEvent.NEW_RELEASE, userId, asNotification(metadata));
this.eventRepository.clientSend('on_new_release', userId, asNotification(metadata));
}
}
}

129
server/src/utils/config.ts Normal file
View File

@ -0,0 +1,129 @@
import AsyncLock from 'async-lock';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { load as loadYaml } from 'js-yaml';
import * as _ from 'lodash';
import { SystemConfig, defaults } from 'src/config';
import { SystemConfigDto } from 'src/dtos/system-config.dto';
import { SystemMetadataKey } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
import { DeepPartial } from 'typeorm';
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
type RepoDeps = {
metadataRepo: ISystemMetadataRepository;
logger: ILoggerRepository;
};
const asyncLock = new AsyncLock();
let config: SystemConfig | null = null;
let lastUpdated: number | null = null;
export const clearConfigCache = () => {
config = null;
lastUpdated = null;
};
export const isUsingConfigFile = () => {
return !!process.env.IMMICH_CONFIG_FILE;
};
export const getConfig = async (repos: RepoDeps, { withCache }: { withCache: boolean }): Promise<SystemConfig> => {
if (!withCache || !config) {
const timestamp = lastUpdated;
await asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
if (timestamp === lastUpdated) {
config = await buildConfig(repos);
lastUpdated = Date.now();
}
});
}
return config!;
};
export const updateConfig = async (repos: RepoDeps, newConfig: SystemConfig): Promise<SystemConfig> => {
const { metadataRepo } = repos;
// get the difference between the new config and the default config
const partialConfig: DeepPartial<SystemConfig> = {};
for (const property of getKeysDeep(defaults)) {
const newValue = _.get(newConfig, property);
const isEmpty = newValue === undefined || newValue === null || newValue === '';
const defaultValue = _.get(defaults, property);
const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue);
if (isEmpty || isEqual) {
continue;
}
_.set(partialConfig, property, newValue);
}
await metadataRepo.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
return getConfig(repos, { withCache: false });
};
const loadFromFile = async ({ metadataRepo, logger }: RepoDeps, filepath: string) => {
try {
const file = await metadataRepo.readFile(filepath);
return loadYaml(file.toString()) as unknown;
} catch (error: Error | any) {
logger.error(`Unable to load configuration file: ${filepath}`);
logger.error(error);
throw error;
}
};
const buildConfig = async (repos: RepoDeps) => {
const { metadataRepo, logger } = repos;
// load partial
const partial = isUsingConfigFile()
? await loadFromFile(repos, process.env.IMMICH_CONFIG_FILE as string)
: await metadataRepo.get(SystemMetadataKey.SYSTEM_CONFIG);
// merge with defaults
const config = _.cloneDeep(defaults);
for (const property of getKeysDeep(partial)) {
_.set(config, property, _.get(partial, property));
}
// check for extra properties
const unknownKeys = _.cloneDeep(config);
for (const property of getKeysDeep(defaults)) {
unsetDeep(unknownKeys, property);
}
if (!_.isEmpty(unknownKeys)) {
logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`);
}
// validate full config
const errors = await validate(plainToInstance(SystemConfigDto, config));
if (errors.length > 0) {
if (isUsingConfigFile()) {
throw new Error(`Invalid value(s) in file: ${errors}`);
} else {
logger.error('Validation error', errors);
}
}
if (config.server.externalDomain.length > 0) {
config.server.externalDomain = new URL(config.server.externalDomain).origin;
}
if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
}
if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec);
}
return config;
};

35
server/src/utils/user.ts Normal file
View File

@ -0,0 +1,35 @@
import { BadRequestException } from '@nestjs/common';
import sanitize from 'sanitize-filename';
import { SALT_ROUNDS } from 'src/constants';
import { UserEntity } from 'src/entities/user.entity';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
type RepoDeps = { userRepo: IUserRepository; cryptoRepo: ICryptoRepository };
export const createUser = async (
{ userRepo, cryptoRepo }: RepoDeps,
dto: Partial<UserEntity> & { email: string },
): Promise<UserEntity> => {
const user = await userRepo.getByEmail(dto.email);
if (user) {
throw new BadRequestException('User exists');
}
if (!dto.isAdmin) {
const localAdmin = await userRepo.getAdmin();
if (!localAdmin) {
throw new BadRequestException('The first registered account must the administrator.');
}
}
const payload: Partial<UserEntity> = { ...dto };
if (payload.password) {
payload.password = await cryptoRepo.hashBcrypt(payload.password, SALT_ROUNDS);
}
if (payload.storageLabel) {
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
}
return userRepo.create(payload);
};

View File

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

View File

@ -5,8 +5,8 @@ export const newEventRepositoryMock = (): Mocked<IEventRepository> => {
return {
on: vitest.fn() as any,
emit: vitest.fn() as any,
clientSend: vitest.fn(),
clientBroadcast: vitest.fn(),
clientSend: vitest.fn() as any,
clientBroadcast: vitest.fn() as any,
serverSend: vitest.fn(),
};
};

View File

@ -1,12 +1,9 @@
import { SystemConfigCore } from 'src/cores/system-config.core';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { clearConfigCache } from 'src/utils/config';
import { Mocked, vitest } from 'vitest';
export const newSystemMetadataRepositoryMock = (reset = true): Mocked<ISystemMetadataRepository> => {
if (reset) {
SystemConfigCore.reset();
}
export const newSystemMetadataRepositoryMock = (): Mocked<ISystemMetadataRepository> => {
clearConfigCache();
return {
get: vitest.fn() as any,
set: vitest.fn(),

View File

@ -1,12 +1,7 @@
import { UserCore } from 'src/cores/user.core';
import { IUserRepository } from 'src/interfaces/user.interface';
import { Mocked, vitest } from 'vitest';
export const newUserRepositoryMock = (reset = true): Mocked<IUserRepository> => {
if (reset) {
UserCore.reset();
}
export const newUserRepositoryMock = (): Mocked<IUserRepository> => {
return {
get: vitest.fn(),
getAdmin: vitest.fn(),

View File

@ -0,0 +1,10 @@
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { Mocked, vitest } from 'vitest';
export const newVersionHistoryRepositoryMock = (): Mocked<IVersionHistoryRepository> => {
return {
getAll: vitest.fn().mockResolvedValue([]),
getLatest: vitest.fn(),
create: vitest.fn(),
};
};

321
web/package-lock.json generated
View File

@ -80,7 +80,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.16.5",
"@types/node": "^20.16.9",
"typescript": "^5.3.3"
}
},
@ -657,6 +657,16 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
@ -726,9 +736,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
"dev": true,
"license": "MIT",
"engines": {
@ -746,9 +756,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -759,9 +769,9 @@
}
},
"node_modules/@faker-js/faker": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.1.tgz",
"integrity": "sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.2.tgz",
"integrity": "sha512-nI/FP30ZGXb+UaR7yXawVTH40NVKXPIx0tA3GKjkKLjorqBoMAeq4iSEacl8mJmpVhOCDa0vYHwYDmOOcFMrYw==",
"dev": true,
"funding": [
{
@ -1575,30 +1585,30 @@
}
},
"node_modules/@photo-sphere-viewer/core": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.10.0.tgz",
"integrity": "sha512-VRXPTq6bFxkf5YqmijXLTKV+K05ZZHDoccXBsoxWsS9wKA5gCfKLlebRxQBBazmnwr1KmsLbAIsd1ZGbLdhI4g==",
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.10.1.tgz",
"integrity": "sha512-xdvPbfQqLl8tggqNDMcczbQGNQHbA4N9u3RCuzYzhrN2PBE2ihL5TsH85smkH4GcRrJKypSzwXF7rDrQhcs7aQ==",
"license": "MIT",
"dependencies": {
"three": "^0.167.0"
"three": "^0.168.0"
}
},
"node_modules/@photo-sphere-viewer/equirectangular-video-adapter": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.10.0.tgz",
"integrity": "sha512-qnxPDsT88x+mdiC//+/VnFe9z4ANz/xHjQbtm9zMNwVOryZdo1hhHtp3k80+ZnkBCF3tA8TkcEpiLifhWrHWPw==",
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.10.1.tgz",
"integrity": "sha512-159vPvsqPJ2prxnWpRH8QSaT+QlCOIac8XmhmkfwBoMqTZ8B1P+JWyuKYaDpqz4Bk/K+kncVBMNVvdro6bIccw==",
"license": "MIT",
"peerDependencies": {
"@photo-sphere-viewer/core": "5.10.0"
"@photo-sphere-viewer/core": "5.10.1"
}
},
"node_modules/@photo-sphere-viewer/video-plugin": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.10.0.tgz",
"integrity": "sha512-Zym35YVdNwx6Kng/P9vOJ/UmLkarJNmjYUsP2hllQVl6Xy/iznfzE9NLCr5gJ3fmLT7ICkuz5dLfdDUxOl6Btw==",
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.10.1.tgz",
"integrity": "sha512-rIoXvHuuB+qg8I0wqbe4S3YUXiliN4kTeV5Xx70dyPFtroGVEKpbvBZ8ygy029np0xALMkeKvi6cYiB40Lj1Pw==",
"license": "MIT",
"peerDependencies": {
"@photo-sphere-viewer/core": "5.10.0"
"@photo-sphere-viewer/core": "5.10.1"
}
},
"node_modules/@pkgjs/parseargs": {
@ -2240,6 +2250,13 @@
"@types/geojson": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/justified-layout": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.4.tgz",
@ -2318,17 +2335,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@ -2352,16 +2369,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4"
},
"engines": {
@ -2381,14 +2398,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2399,14 +2416,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.7.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@ -2424,9 +2441,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"dev": true,
"license": "MIT",
"engines": {
@ -2438,14 +2455,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -2493,16 +2510,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2516,13 +2533,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.7.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@ -3581,35 +3598,16 @@
"dev": true
},
"node_modules/engine.io-client": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
"integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
@ -3750,21 +3748,24 @@
}
},
"node_modules/eslint": {
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.6.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.10.0",
"@eslint/plugin-kit": "^0.1.0",
"@eslint/js": "9.11.1",
"@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -3947,6 +3948,13 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -4014,9 +4022,9 @@
}
},
"node_modules/eslint/node_modules/eslint-scope": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
"integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz",
"integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@ -4031,9 +4039,9 @@
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
"integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@ -4044,15 +4052,15 @@
}
},
"node_modules/eslint/node_modules/espree": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
"integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
"integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.12.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
"eslint-visitor-keys": "^4.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -6092,21 +6100,17 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0",
"typescript": ">=2.9",
"vue-tsc": "^2.0.24"
"vue-tsc": "^2.1.0"
},
"peerDependenciesMeta": {
"@vue/language-plugin-pug": {
"optional": true
},
"vue-tsc": {
"optional": true
}
@ -6125,9 +6129,9 @@
}
},
"node_modules/prettier-plugin-svelte": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.6.tgz",
"integrity": "sha512-Y1XWLw7vXUQQZmgv1JAEiLcErqUniAF2wO7QJsw8BVMvpLET2dI5WpEIEJx1r11iHVdSMzQxivyfrH9On9t2IQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.7.tgz",
"integrity": "sha512-/Dswx/ea0lV34If1eDcG3nulQ63YNr5KPDfMsjbdtpSWOxKKJ7nAc2qlVuYwEvCr4raIuredNoR7K4JCkmTGaQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -6740,13 +6744,14 @@
}
},
"node_modules/socket.io-client": {
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz",
"integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
@ -7080,14 +7085,14 @@
}
},
"node_modules/svelte-check": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.2.tgz",
"integrity": "sha512-w2yqcG9ELJe2RJCnAvB7v0OgkHhL3czzz/tVoxGFfO6y4mOrF6QHCDhXijeXzsU7LVKEwWS3Qd9tza4JBuDxqA==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.3.tgz",
"integrity": "sha512-V2eqOEuNrPi1jGf307opR1JZ+ITP6/7R8ALKSw4Uw3NWp6GfA+fe7tYtEvZc7QHCavYKBizCK4JFwYjbuPCeXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"chokidar": "^3.4.1",
"chokidar": "^4.0.1",
"fdir": "^6.2.0",
"picocolors": "^1.0.0",
"sade": "^1.7.4"
@ -7103,10 +7108,26 @@
"typescript": ">=5.0.0"
}
},
"node_modules/svelte-check/node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/svelte-check/node_modules/fdir": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
"integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz",
"integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -7133,6 +7154,20 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/svelte-check/node_modules/readdirp": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz",
"integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.41.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.1.tgz",
@ -7652,9 +7687,9 @@
"peer": true
},
"node_modules/tailwindcss": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -7837,9 +7872,9 @@
}
},
"node_modules/three": {
"version": "0.167.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.167.1.tgz",
"integrity": "sha512-gYTLJA/UQip6J/tJvl91YYqlZF47+D/kxiWrbTon35ZHlXEN0VOo+Qke2walF1/x92v55H6enomymg4Dak52kw==",
"version": "0.168.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.168.0.tgz",
"integrity": "sha512-6m6jXtDwMJEK/GGMbAOTSAmxNdzKvvBzgd7q8bE/7Tr6m7PaBh5kKLrN7faWtlglXbzj7sVba48Idwx+NRsZXw==",
"license": "MIT"
},
"node_modules/thumbhash": {
@ -8147,9 +8182,9 @@
}
},
"node_modules/vite": {
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -8539,12 +8574,10 @@
"dev": true
},
"node_modules/ws": {
"version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"dev": true,
"optional": true,
"peer": true,
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
@ -8581,9 +8614,9 @@
"peer": true
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
"integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
"engines": {
"node": ">=0.4.0"
}

View File

@ -13,6 +13,8 @@
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96.png" />
<link rel="icon" type="image/png" sizes="144x144" href="/favicon-144.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180.png" />
<link rel="preload" as="font" type="font/ttf" href="%app.font%" />
<link rel="preload" as="font" type="font/ttf" href="%app.monofont%" />
%sveltekit.head%
<style>
/* prevent FOUC */

12
web/src/hooks.server.ts Normal file
View File

@ -0,0 +1,12 @@
import overpass from '$lib/assets/fonts/overpass/Overpass.ttf?url';
import overpassMono from '$lib/assets/fonts/overpass/OverpassMono.ttf?url';
import type { Handle } from '@sveltejs/kit';
// only used during the build to replace the variables from app.html
export const handle = (async ({ event, resolve }) => {
return resolve(event, {
transformPageChunk: ({ html }) => {
return html.replace('%app.font%', overpass).replace('%app.monofont%', overpassMono);
},
});
}) satisfies Handle;

View File

@ -4,3 +4,6 @@ export const sunPath =
export const moonViewBox = '0 0 20 20';
export const sunViewBox = '0 0 20 20';
export const discordPath =
'M 9.1367188 3.8691406 C 9.1217187 3.8691406 9.1067969 3.8700938 9.0917969 3.8710938 C 8.9647969 3.8810937 5.9534375 4.1403594 4.0234375 5.6933594 C 3.0154375 6.6253594 1 12.073203 1 16.783203 C 1 16.866203 1.0215 16.946531 1.0625 17.019531 C 2.4535 19.462531 6.2473281 20.102859 7.1113281 20.130859 L 7.1269531 20.130859 C 7.2799531 20.130859 7.4236719 20.057594 7.5136719 19.933594 L 8.3886719 18.732422 C 6.0296719 18.122422 4.8248594 17.086391 4.7558594 17.025391 C 4.5578594 16.850391 4.5378906 16.549563 4.7128906 16.351562 C 4.8068906 16.244563 4.9383125 16.189453 5.0703125 16.189453 C 5.1823125 16.189453 5.2957188 16.228594 5.3867188 16.308594 C 5.4157187 16.334594 7.6340469 18.216797 11.998047 18.216797 C 16.370047 18.216797 18.589328 16.325641 18.611328 16.306641 C 18.702328 16.227641 18.815734 16.189453 18.927734 16.189453 C 19.059734 16.189453 19.190156 16.243562 19.285156 16.351562 C 19.459156 16.549563 19.441141 16.851391 19.244141 17.025391 C 19.174141 17.087391 17.968375 18.120469 15.609375 18.730469 L 16.484375 19.933594 C 16.574375 20.057594 16.718094 20.130859 16.871094 20.130859 L 16.886719 20.130859 C 17.751719 20.103859 21.5465 19.463531 22.9375 17.019531 C 22.9785 16.947531 23 16.866203 23 16.783203 C 23 12.073203 20.984172 6.624875 19.951172 5.671875 C 18.047172 4.140875 15.036203 3.8820937 14.908203 3.8710938 C 14.895203 3.8700938 14.880188 3.8691406 14.867188 3.8691406 C 14.681188 3.8691406 14.510594 3.9793906 14.433594 4.1503906 C 14.427594 4.1623906 14.362062 4.3138281 14.289062 4.5488281 C 15.548063 4.7608281 17.094141 5.1895937 18.494141 6.0585938 C 18.718141 6.1975938 18.787437 6.4917969 18.648438 6.7167969 C 18.558438 6.8627969 18.402188 6.9433594 18.242188 6.9433594 C 18.156188 6.9433594 18.069234 6.9200937 17.990234 6.8710938 C 15.584234 5.3800938 12.578 5.3046875 12 5.3046875 C 11.422 5.3046875 8.4157187 5.3810469 6.0117188 6.8730469 C 5.9327188 6.9210469 5.8457656 6.9433594 5.7597656 6.9433594 C 5.5997656 6.9433594 5.4425625 6.86475 5.3515625 6.71875 C 5.2115625 6.49375 5.2818594 6.1985938 5.5058594 6.0585938 C 6.9058594 5.1905937 8.4528906 4.7627812 9.7128906 4.5507812 C 9.6388906 4.3147813 9.5714062 4.1643437 9.5664062 4.1523438 C 9.4894063 3.9813438 9.3217188 3.8691406 9.1367188 3.8691406 z M 12 7.3046875 C 12.296 7.3046875 14.950594 7.3403125 16.933594 8.5703125 C 17.326594 8.8143125 17.777234 8.9453125 18.240234 8.9453125 C 18.633234 8.9453125 19.010656 8.8555 19.347656 8.6875 C 19.964656 10.2405 20.690828 12.686219 20.923828 15.199219 C 20.883828 15.143219 20.840922 15.089109 20.794922 15.037109 C 20.324922 14.498109 19.644687 14.191406 18.929688 14.191406 C 18.332687 14.191406 17.754078 14.405437 17.330078 14.773438 C 17.257078 14.832437 15.505 16.21875 12 16.21875 C 8.496 16.21875 6.7450313 14.834687 6.7070312 14.804688 C 6.2540312 14.407687 5.6742656 14.189453 5.0722656 14.189453 C 4.3612656 14.189453 3.6838438 14.494391 3.2148438 15.025391 C 3.1658438 15.080391 3.1201719 15.138266 3.0761719 15.197266 C 3.3091719 12.686266 4.0344375 10.235594 4.6484375 8.6835938 C 4.9864375 8.8525938 5.3657656 8.9433594 5.7597656 8.9433594 C 6.2217656 8.9433594 6.6724531 8.8143125 7.0644531 8.5703125 C 9.0494531 7.3393125 11.704 7.3046875 12 7.3046875 z M 8.890625 10.044922 C 7.966625 10.044922 7.2167969 10.901031 7.2167969 11.957031 C 7.2167969 13.013031 7.965625 13.869141 8.890625 13.869141 C 9.815625 13.869141 10.564453 13.013031 10.564453 11.957031 C 10.564453 10.900031 9.815625 10.044922 8.890625 10.044922 z M 15.109375 10.044922 C 14.185375 10.044922 13.435547 10.901031 13.435547 11.957031 C 13.435547 13.013031 14.184375 13.869141 15.109375 13.869141 C 16.034375 13.869141 16.783203 13.013031 16.783203 11.957031 C 16.783203 10.900031 16.033375 10.044922 15.109375 10.044922 z';

View File

@ -0,0 +1,131 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { type ServerAboutResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiBugOutline, mdiFaceAgent, mdiGit, mdiGithub, mdiInformationOutline } from '@mdi/js';
import { discordPath } from '$lib/assets/svg-paths';
export let onClose: () => void;
export let info: ServerAboutResponseDto;
</script>
<Portal>
<FullScreenModal title={$t('support_and_feedback')} {onClose}>
<p>{$t('official_immich_resources')}</p>
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-3">
<div>
<a href="https://{info.version}.archive.immich.app/docs/overview/introduction" target="_blank" rel="noreferrer">
<Icon path={mdiInformationOutline} size="1.5em" class="inline-block" />
<p
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
id="documentation-label"
>
{$t('documentation')}
</p>
</a>
</div>
<div>
<a href="https://github.com/immich-app/immich/" target="_blank" rel="noreferrer">
<Icon path={mdiGithub} size="1.5em" class="inline-block" />
<p
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
id="github-label"
>
{$t('source')}
</p>
</a>
</div>
<div>
<a href="https://discord.immich.app" target="_blank" rel="noreferrer">
<Icon path={discordPath} class="inline-block" size="1.5em" />
<p
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
id="github-label"
>
{$t('discord')}
</p>
</a>
</div>
<div>
<a href="https://github.com/immich-app/immich/issues/new/choose" target="_blank" rel="noreferrer">
<Icon path={mdiBugOutline} size="1.5em" class="inline-block" />
<p
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
id="github-label"
>
{$t('bugs_and_feature_requests')}
</p>
</a>
</div>
</div>
{#if info.thirdPartyBugFeatureUrl || info.thirdPartySourceUrl || info.thirdPartyDocumentationUrl || info.thirdPartySupportUrl}
<p class="mt-3">{$t('third_party_resources')}</p>
<p class="text-xs mt-1">
{$t('support_third_party_description')}
</p>
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-3">
{#if info.thirdPartyDocumentationUrl}
<div>
<a href={info.thirdPartyDocumentationUrl} target="_blank" rel="noreferrer">
<Icon path={mdiInformationOutline} size="1.5em" class="inline-block" />
<p
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
id="documentation-label"
>
{$t('documentation')}
</p>
</a>
</div>
{/if}
{#if info.thirdPartySourceUrl}
<div>
<a href={info.thirdPartySourceUrl} target="_blank" rel="noreferrer">
<Icon path={mdiGit} size="1.5em" class="inline-block" />
<p
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
id="github-label"
>
{$t('source')}
</p>
</a>
</div>
{/if}
{#if info.thirdPartySupportUrl}
<div>
<a href={info.thirdPartySupportUrl} target="_blank" rel="noreferrer">
<Icon path={mdiFaceAgent} class="inline-block" size="1.5em" />
<p
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
id="github-label"
>
{$t('support')}
</p>
</a>
</div>
{/if}
{#if info.thirdPartyBugFeatureUrl}
<div>
<a href={info.thirdPartyBugFeatureUrl} target="_blank" rel="noreferrer">
<Icon path={mdiBugOutline} size="1.5em" class="inline-block" />
<p
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm underline inline-block"
id="github-label"
>
{$t('bugs_and_feature_requests')}
</p>
</a>
</div>
{/if}
</div>
{/if}
</FullScreenModal>
</Portal>

View File

@ -8,32 +8,45 @@
import { featureFlags } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store';
import { handleLogout } from '$lib/utils/auth';
import { logout } from '@immich/sdk';
import { mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import { AppRoute } from '../../../constants';
import ImmichLogo from '../immich-logo.svelte';
import SearchBar from '../search-bar/search-bar.svelte';
import { AppRoute } from '$lib/constants';
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import ThemeButton from '../theme-button.svelte';
import UserAvatar from '../user-avatar.svelte';
import AccountInfoPanel from './account-info-panel.svelte';
import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte';
import { onMount } from 'svelte';
export let showUploadButton = true;
export let onUploadClick: () => void;
let shouldShowAccountInfo = false;
let shouldShowAccountInfoPanel = false;
let shouldShowHelpPanel = false;
let innerWidth: number;
const onLogout = async () => {
const { redirectUri } = await logout();
await handleLogout(redirectUri);
};
let aboutInfo: ServerAboutResponseDto;
onMount(async () => {
aboutInfo = await getAboutInfo();
});
</script>
<svelte:window bind:innerWidth />
{#if shouldShowHelpPanel}
<HelpAndFeedbackModal onClose={() => (shouldShowHelpPanel = false)} info={aboutInfo} />
{/if}
<section id="dashboard-navbar" class="fixed z-[900] h-[var(--navbar-height)] w-screen text-sm">
<SkipLink text={$t('skip_to_content')} />
<div
@ -49,7 +62,7 @@
{/if}
</div>
<section class="flex place-items-center justify-end gap-2 md:gap-4 w-full sm:w-auto">
<section class="flex place-items-center justify-end gap-1 md:gap-2 w-full sm:w-auto">
{#if $featureFlags.search}
<CircleIconButton
href={AppRoute.SEARCH}
@ -63,6 +76,20 @@
<ThemeButton padding="2" />
<div
use:clickOutside={{
onEscape: () => (shouldShowHelpPanel = false),
}}
>
<CircleIconButton
id="support-feedback-button"
title={$t('support_and_feedback')}
icon={mdiHelpCircleOutline}
on:click={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
padding="1"
/>
</div>
{#if !$page.url.pathname.includes('/admin') && showUploadButton}
<LinkButton on:click={onUploadClick} class="hidden lg:block">
<div class="flex gap-2">
@ -87,7 +114,7 @@
>
<button
type="button"
class="flex"
class="flex pl-2"
on:mouseover={() => (shouldShowAccountInfo = true)}
on:focus={() => (shouldShowAccountInfo = true)}
on:blur={() => (shouldShowAccountInfo = false)}

View File

@ -1,19 +1,19 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { type ServerAboutResponseDto } from '@immich/sdk';
import { type ServerAboutResponseDto, type ServerVersionHistoryResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
export let onClose: () => void;
export let info: ServerAboutResponseDto;
export let versions: ServerVersionHistoryResponseDto[];
</script>
<Portal>
<FullScreenModal title={$t('about')} {onClose}>
<div
class="immich-scrollbar max-h-[500px] overflow-y-auto flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary"
>
<div class="flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary">
<div>
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-desc"
>Immich</label
@ -151,6 +151,35 @@
</div>
</div>
{/if}
<div class="col-span-full">
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-history"
>{$t('version_history')}</label
>
<ul id="version-history" class="list-none">
{#each versions.slice(0, 5) as item (item.id)}
{@const createdAt = DateTime.fromISO(item.createdAt)}
<li>
<span
class="immich-form-label pb-2 text-xs"
id="version-history"
title={createdAt.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS)}
>
{$t('version_history_item', {
values: {
version: item.version,
date: createdAt.toLocaleString({
month: 'short',
day: 'numeric',
year: 'numeric',
}),
},
})}
</span>
</li>
{/each}
</ul>
</div>
</div>
</FullScreenModal>
</Portal>

Some files were not shown because too many files have changed in this diff Show More