diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 5292075cce902..a86408eea891c 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -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 diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml index bd0ec91d14d86..6f2c573a9f435 100644 --- a/.github/workflows/docker-cleanup.yml +++ b/.github/workflows/docker-cleanup.yml @@ -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: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bf393bbcf6439..8c7aeb020e7ff 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -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 }} diff --git a/cli/package-lock.json b/cli/package-lock.json index e508fe843f60d..b00ebf09c8d3d 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -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": { diff --git a/cli/package.json b/cli/package.json index 522a8e593e9e7..cee258bff5e21 100644 --- a/cli/package.json +++ b/cli/package.json @@ -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", diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 60685d84d6d63..066dc9c701b45 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -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 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 05e35ac8c1fe5..a6256b33b1e64 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -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 diff --git a/docs/docs/administration/system-integrity.md b/docs/docs/administration/system-integrity.md new file mode 100644 index 0000000000000..5440e42489d83 --- /dev/null +++ b/docs/docs/administration/system-integrity.md @@ -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 +``` diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 3944f6755b65a..29549586d359e 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -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. diff --git a/docs/package-lock.json b/docs/package-lock.json index 3b4e6c4f9546a..909263da859a4 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -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", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index e7b463b0b2696..52a1cbd4afe8a 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -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" diff --git a/e2e/package.json b/e2e/package.json index 7c0025902dd3a..c107732ab3538 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -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", diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index d982962fbcdc6..3bfdf7d2e2cc6 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -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 diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 5bb1726378050..1f6a378edaed0 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -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] diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock index 6b7a4c99c5ae3..e81bad7da2a58 100644 --- a/mobile/immich_lint/pubspec.lock +++ b/mobile/immich_lint/pubspec.lock @@ -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" diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml index 78298f451e7ca..9d1a3c26b3ca8 100644 --- a/mobile/immich_lint/pubspec.yaml +++ b/mobile/immich_lint/pubspec.yaml @@ -11,4 +11,4 @@ dependencies: glob: ^2.1.2 dev_dependencies: - lints: ^4.0.0 + lints: ^5.0.0 diff --git a/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json b/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json new file mode 100644 index 0000000000000..7391713b6fb06 --- /dev/null +++ b/mobile/ios/build/XCBuildData/a34f3d77f077776687d3b444cba8f1c4.xcbuilddata/manifest.json @@ -0,0 +1 @@ +{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":[""]},"commands":{"":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":[""]}}} \ No newline at end of file diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index e115868ba0d60..5612b378c3c9e 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -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); } diff --git a/mobile/lib/widgets/photo_view/photo_view.dart b/mobile/lib/widgets/photo_view/photo_view.dart index 55be81a5b3d9b..7f72750afe37f 100644 --- a/mobile/lib/widgets/photo_view/photo_view.dart +++ b/mobile/lib/widgets/photo_view/photo_view.dart @@ -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'; diff --git a/mobile/lib/widgets/photo_view/photo_view_gallery.dart b/mobile/lib/widgets/photo_view/photo_view_gallery.dart index 9594912078cd6..b8918309bcedf 100644 --- a/mobile/lib/widgets/photo_view/photo_view_gallery.dart +++ b/mobile/lib/widgets/photo_view/photo_view_gallery.dart @@ -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' diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 81827a9079e5a..36f442fd88b51 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -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) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 8be44029805d5..6fb7478d04bf2 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -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'; diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index bde8d595b6fb0..7a832ad61a158 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -418,6 +418,50 @@ class ServerApi { return null; } + /// Performs an HTTP 'GET /server/version-history' operation and returns the [Response]. + Future getVersionHistoryWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/server/version-history'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future?> 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') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Performs an HTTP 'GET /server/ping' operation and returns the [Response]. Future pingServerWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 9e38eaf30a8a9..c1025b0bd4820 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -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': diff --git a/mobile/openapi/lib/model/server_about_response_dto.dart b/mobile/openapi/lib/model/server_about_response_dto.dart index 1ab51a80f1362..5d53d5fdee665 100644 --- a/mobile/openapi/lib/model/server_about_response_dto.dart +++ b/mobile/openapi/lib/model/server_about_response_dto.dart @@ -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 toJson() { final json = {}; @@ -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(json, r'sourceCommit'), sourceRef: mapValueOfType(json, r'sourceRef'), sourceUrl: mapValueOfType(json, r'sourceUrl'), + thirdPartyBugFeatureUrl: mapValueOfType(json, r'thirdPartyBugFeatureUrl'), + thirdPartyDocumentationUrl: mapValueOfType(json, r'thirdPartyDocumentationUrl'), + thirdPartySourceUrl: mapValueOfType(json, r'thirdPartySourceUrl'), + thirdPartySupportUrl: mapValueOfType(json, r'thirdPartySupportUrl'), version: mapValueOfType(json, r'version')!, versionUrl: mapValueOfType(json, r'versionUrl')!, ); diff --git a/mobile/openapi/lib/model/server_version_history_response_dto.dart b/mobile/openapi/lib/model/server_version_history_response_dto.dart new file mode 100644 index 0000000000000..c81cb0e8b9f8b --- /dev/null +++ b/mobile/openapi/lib/model/server_version_history_response_dto.dart @@ -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 toJson() { + final json = {}; + 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(); + + return ServerVersionHistoryResponseDto( + createdAt: mapDateTime(json, r'createdAt', r'')!, + id: mapValueOfType(json, r'id')!, + version: mapValueOfType(json, r'version')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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 mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + 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 = { + 'createdAt', + 'id', + 'version', + }; +} + diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9dadbd1028a64..1088011667dc1 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -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" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 092b0bb75cf1d..67db6cbd6cc8b 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -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 diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 970230f4e3d3e..d28effd6c5676 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -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": { diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 72d7a3ec546d8..e977f56834fb3 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -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": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 41bc3a3b16017..17472327f75dd 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -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": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index aa3501079bf86..4f5eed0d13e21 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -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, diff --git a/server/Dockerfile b/server/Dockerfile index 66965c0edb73e..4ebae191e914d 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -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 \ diff --git a/server/package-lock.json b/server/package-lock.json index 646a26b1ee9d0..affe12e69271d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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": {} }, diff --git a/server/package.json b/server/package.json index d4816109069e9..2e6238ad54d6c 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/config.ts b/server/src/config.ts index 53374d581f6ba..2e11f740d372c 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -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 = diff --git a/server/src/constants.ts b/server/src/constants.ts index e0a4fe8cef306..8115101ca05a7 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -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; diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts index 245bbbd3475a4..36490b71198ba 100644 --- a/server/src/controllers/server-info.controller.ts +++ b/server/src/controllers/server-info.controller.ts @@ -66,7 +66,7 @@ export class ServerInfoController { @Get('config') @EndpointLifecycle({ deprecatedAt: 'v1.107.0' }) getServerConfig(): Promise { - return this.service.getConfig(); + return this.service.getSystemConfig(); } @Get('statistics') diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 75becfe341615..8327ff6d1d46d 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -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 { + return this.versionService.getVersionHistory(); + } + @Get('features') getServerFeatures(): Promise { return this.service.getFeatures(); @@ -58,7 +64,7 @@ export class ServerController { @Get('config') getServerConfig(): Promise { - return this.service.getConfig(); + return this.service.getSystemConfig(); } @Get('statistics') diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index 804c19500facd..f59c8ad66cbeb 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -13,7 +13,7 @@ export class SystemConfigController { @Get() @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) getConfig(): Promise { - 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 { - return this.service.updateConfig(dto); + return this.service.updateSystemConfig(dto); } @Get('storage-template-options') diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 8ce8f6b67a228..d33d81410ce47 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -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); diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts deleted file mode 100644 index 816ab004460a9..0000000000000 --- a/server/src/cores/system-config.core.ts +++ /dev/null @@ -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; - -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 { - 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 { - // get the difference between the new config and the default config - const partialConfig: DeepPartial = {}; - 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; - } - } -} diff --git a/server/src/cores/user.core.ts b/server/src/cores/user.core.ts deleted file mode 100644 index 153463a9cc692..0000000000000 --- a/server/src/cores/user.core.ts +++ /dev/null @@ -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 & { email: string }): Promise { - 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 = { ...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); - } -} diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index aafadff47888f..e54048335129f 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -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; diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts index 0b7ca8c3bd013..7425ee67d8a6e 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -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, ]; diff --git a/server/src/entities/version-history.entity.ts b/server/src/entities/version-history.entity.ts new file mode 100644 index 0000000000000..edccd9aed6118 --- /dev/null +++ b/server/src/entities/version-history.entity.ts @@ -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; +} diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts index 11bccbe348b1a..df999db18b570 100644 --- a/server/src/interfaces/config.interface.ts +++ b/server/src/interfaces/config.interface.ts @@ -7,6 +7,9 @@ export interface EnvData { skipMigrations: boolean; vectorExtension: VectorExtension; }; + storage: { + ignoreMountCheckErrors: boolean; + }; } export interface IConfigRepository { diff --git a/server/src/interfaces/database.interface.ts b/server/src/interfaces/database.interface.ts index 51b39b95a8c08..e388f354f2ac1 100644 --- a/server/src/interfaces/database.interface.ts +++ b/server/src/interfaces/database.interface.ts @@ -17,6 +17,7 @@ export enum DatabaseLock { Migrations = 200, SystemFileMounts = 300, StorageTemplateMigration = 420, + VersionHistory = 500, CLIPDimSize = 512, LibraryWatch = 1337, GetSystemConfig = 69, diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 02027d87e626a..a125e47ada3b4 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -62,36 +62,20 @@ export type EmitHandler = (...args: ArgsOf) => Promise = EventMap[T][0]; export type ArgsOf = 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; - [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 = { @@ -107,11 +91,11 @@ export interface IEventRepository { /** * Send to connected clients for a specific user */ - clientSend(event: E, room: string, data: ClientEventMap[E]): void; + clientSend(event: E, room: string, ...data: ClientEventMap[E]): void; /** * Send to all connected clients */ - clientBroadcast(event: E, data: ClientEventMap[E]): void; + clientBroadcast(event: E, ...data: ClientEventMap[E]): void; /** * Send to all connected servers */ diff --git a/server/src/interfaces/version-history.interface.ts b/server/src/interfaces/version-history.interface.ts new file mode 100644 index 0000000000000..67337062200f0 --- /dev/null +++ b/server/src/interfaces/version-history.interface.ts @@ -0,0 +1,9 @@ +import { VersionHistoryEntity } from 'src/entities/version-history.entity'; + +export const IVersionHistoryRepository = 'IVersionHistoryRepository'; + +export interface IVersionHistoryRepository { + create(version: Omit): Promise; + getAll(): Promise; + getLatest(): Promise; +} diff --git a/server/src/migrations/1727781844613-IsOfflineSetDeletedAt.ts b/server/src/migrations/1727781844613-IsOfflineSetDeletedAt.ts new file mode 100644 index 0000000000000..050e9a93cfb87 --- /dev/null +++ b/server/src/migrations/1727781844613-IsOfflineSetDeletedAt.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class IsOfflineSetDeletedAt1727781844613 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE assets SET "deletedAt" = now() WHERE "isOffline" = true AND "deletedAt" IS NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE assets SET "deletedAt" = null WHERE "isOffline" = true`, + ); + } +} diff --git a/server/src/migrations/1727797340951-AddVersionHistory.ts b/server/src/migrations/1727797340951-AddVersionHistory.ts new file mode 100644 index 0000000000000..7eb731d1a3e18 --- /dev/null +++ b/server/src/migrations/1727797340951-AddVersionHistory.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddVersionHistory1727797340951 implements MigrationInterface { + name = 'AddVersionHistory1727797340951' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE "version_history"`); + } + +} diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index f16fa3bbd4ef9..04d790b8123e8 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -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', + }, }; } } diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index a8b2fa67c3395..90d8e7bf5d7a8 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -106,12 +106,12 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect } } - clientSend(event: E, room: string, data: ClientEventMap[E]) { - this.server?.to(room).emit(event, data); + clientSend(event: T, room: string, ...data: ClientEventMap[T]) { + this.server?.to(room).emit(event, ...data); } - clientBroadcast(event: E, data: ClientEventMap[E]) { - this.server?.emit(event, data); + clientBroadcast(event: T, ...data: ClientEventMap[T]) { + this.server?.emit(event, ...data); } serverSend(event: T, ...args: ArgsOf): void { diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index fac250d6670d7..5da4f678d3237 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -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 }, ]; diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index b02e071b1ac8d..ec79814520394 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -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 */ diff --git a/server/src/repositories/version-history.repository.ts b/server/src/repositories/version-history.repository.ts new file mode 100644 index 0000000000000..26c638bd769a6 --- /dev/null +++ b/server/src/repositories/version-history.repository.ts @@ -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) {} + + async getAll(): Promise { + return this.repository.find({ order: { createdAt: 'DESC' } }); + } + + async getLatest(): Promise { + const results = await this.repository.find({ order: { createdAt: 'DESC' }, take: 1 }); + return results[0] || null; + } + + create(version: Omit): Promise { + return this.repository.save(version); + } +} diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index aa88eaf9576b4..171005ab74a56 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -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 { @@ -214,7 +212,7 @@ export class AssetService { } async handleAssetDeletionCheck(): Promise { - 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 })) diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 6b1e4c512f816..72d251ce78f38 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -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 { - 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 { - 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; } diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts new file mode 100644 index 0000000000000..776858aa1a544 --- /dev/null +++ b/server/src/services/base.service.ts @@ -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, + ); + } +} diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 1c25c306b6843..c7ed510f5d550 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -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 { @@ -42,26 +40,26 @@ export class CliService { } async disablePasswordLogin(): Promise { - 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 { - 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 { - 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 { - 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); } } diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index fc8130cadc273..5bce6d819ccb8 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -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, diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 35a1a7325bb2f..00f738a613e6c 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -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 { @@ -44,7 +42,7 @@ export class DuplicateService { } async handleQueueSearchDuplicates({ force }: IBaseJob): Promise { - 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 { - const { machineLearning } = await this.configCore.getConfig({ withCache: true }); + const { machineLearning } = await this.getConfig({ withCache: true }); if (!isDuplicateDetectionEnabled(machineLearning)) { return JobStatus.SKIPPED; } diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 68da13a8e4d4f..8bcf5e5622430 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -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) { - 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; } } diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index b8b478531ff96..07313e4ec8aa8 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -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 Promise> = {}; @@ -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; diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index f8b73260aff3d..e0127b73efbb9 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -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; - let loggerMock: Mocked; let partnerMock: Mocked; let mapMock: Mocked; - let systemMetadataMock: Mocked; 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', () => { diff --git a/server/src/services/map.service.ts b/server/src/services/map.service.ts index 5836505e54893..3b1ee58cf124d 100644 --- a/server/src/services/map.service.ts +++ b/server/src/services/map.service.ts @@ -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 { const userIds = [auth.user.id]; diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 71f432e040c43..1f72c373f4023 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -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 { - 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) { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 9499a4bdd99b0..e39e22b92fd61 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -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 { - 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; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 106f0be08248d..5fba38d1eb6c3 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -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'); }); }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 626e536c409e2..cf6b89384d5e8 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -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 { - const { notifications } = await this.configCore.getConfig({ withCache: false }); + const { notifications } = await this.getConfig({ withCache: false }); if (!notifications.smtp.enabled) { return JobStatus.SKIPPED; } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index b009696b637fe..7cb6f42f15a44 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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; } diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index c3cc5399c8d73..4227f35ec3a06 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -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 { @@ -101,7 +99,7 @@ export class SearchService { } async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { - 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'); } diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 4e6a8972b008c..e0cd41a27e2d8 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -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, diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 708fe32db52cb..ed1533f6672b9 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -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 { 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 { - const config = await this.configCore.getConfig({ withCache: false }); + async getSystemConfig(): Promise { + const config = await this.getConfig({ withCache: false }); const isInitialized = await this.userRepository.hasAdmin(); const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 54c7fdf25bed7..883270f808b7e 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -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 { @@ -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; diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index ef7865d25c4d9..5db4236d58706 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -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 { - 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 { - const { machineLearning } = await this.configCore.getConfig({ withCache: true }); + const { machineLearning } = await this.getConfig({ withCache: true }); if (!isSmartSearchEnabled(machineLearning)) { return JobStatus.SKIPPED; } diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 33b08efc9bc53..6ce79be33f053 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -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; @@ -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 { - 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 { 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'); diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index 930fb3c726e2f..e0717df66860e 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -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; let databaseMock: Mocked; let storageMock: Mocked; let loggerMock: Mocked; let systemMock: Mocked; 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(); }); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index b32e48ea498ad..e2c1ef28e20ed 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -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 "/${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 "/${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 "/${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 = `/${folder}/.immich`; - return { folderPath, filePath }; + return { folderPath, internalPath, externalPath }; } } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index ac517bb3ff587..0e45e0b6947f2 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -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(); }); }); diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 100ab6f47c9e9..acf9f542ca46a 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -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 { - const config = await this.core.getConfig({ withCache: false }); + async getSystemConfig(): Promise { + 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 { - if (this.core.isUsingConfigFile()) { + async updateSystemConfig(dto: SystemConfigDto): Promise { + 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 { - const { theme } = await this.core.getConfig({ withCache: false }); + const { theme } = await this.getConfig({ withCache: false }); return theme.customCss; } diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 6a5b6ea06e520..75dff32f160a9 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -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 { 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, diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index dca893aa826b4..f770d1d3b65b9 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -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 { @@ -189,7 +187,7 @@ export class UserService { async handleUserDeleteCheck(): Promise { 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 { - 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; diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 02dfe7588fa50..a611ae5ecc828 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -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; let eventMock: Mocked; let jobMock: Mocked; let serverMock: Mocked; let systemMock: Mocked; + let versionMock: Mocked; let loggerMock: Mocked; 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(); diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 0c7ae52cac7a8..92bbb3c06d2fb 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -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 { 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)); } } } diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts new file mode 100644 index 0000000000000..307db173caf80 --- /dev/null +++ b/server/src/utils/config.ts @@ -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; + +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 => { + 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 => { + const { metadataRepo } = repos; + // get the difference between the new config and the default config + const partialConfig: DeepPartial = {}; + 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; +}; diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts new file mode 100644 index 0000000000000..c7029a1ecae4b --- /dev/null +++ b/server/src/utils/user.ts @@ -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 & { email: string }, +): Promise => { + 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 = { ...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); +}; diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts index 40110186f43f3..d18aa1b981e10 100644 --- a/server/test/repositories/config.repository.mock.ts +++ b/server/test/repositories/config.repository.mock.ts @@ -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 => { return { - getEnv: vitest.fn().mockReturnValue({ - database: { - skipMigration: false, - vectorExtension: DatabaseExtension.VECTORS, - }, - }), + getEnv: vitest.fn().mockReturnValue(envData), }; }; + +export const mockEnvData = (config: Partial) => ({ ...envData, ...config }); diff --git a/server/test/repositories/event.repository.mock.ts b/server/test/repositories/event.repository.mock.ts index 78c62e95f2c95..6893b29f49a6f 100644 --- a/server/test/repositories/event.repository.mock.ts +++ b/server/test/repositories/event.repository.mock.ts @@ -5,8 +5,8 @@ export const newEventRepositoryMock = (): Mocked => { 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(), }; }; diff --git a/server/test/repositories/system-metadata.repository.mock.ts b/server/test/repositories/system-metadata.repository.mock.ts index e44301fb2137c..793dd4c1c0bab 100644 --- a/server/test/repositories/system-metadata.repository.mock.ts +++ b/server/test/repositories/system-metadata.repository.mock.ts @@ -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 => { - if (reset) { - SystemConfigCore.reset(); - } - +export const newSystemMetadataRepositoryMock = (): Mocked => { + clearConfigCache(); return { get: vitest.fn() as any, set: vitest.fn(), diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts index 6071ae47fa83f..6362ab6a999ff 100644 --- a/server/test/repositories/user.repository.mock.ts +++ b/server/test/repositories/user.repository.mock.ts @@ -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 => { - if (reset) { - UserCore.reset(); - } - +export const newUserRepositoryMock = (): Mocked => { return { get: vitest.fn(), getAdmin: vitest.fn(), diff --git a/server/test/repositories/version-history.repository.mock.ts b/server/test/repositories/version-history.repository.mock.ts new file mode 100644 index 0000000000000..7c35e316d3315 --- /dev/null +++ b/server/test/repositories/version-history.repository.mock.ts @@ -0,0 +1,10 @@ +import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; +import { Mocked, vitest } from 'vitest'; + +export const newVersionHistoryRepositoryMock = (): Mocked => { + return { + getAll: vitest.fn().mockResolvedValue([]), + getLatest: vitest.fn(), + create: vitest.fn(), + }; +}; diff --git a/web/package-lock.json b/web/package-lock.json index a32e96e67f78b..2170b47dce0e1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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" } diff --git a/web/src/app.html b/web/src/app.html index ff6a8bf580a89..d76e52c8593f4 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -13,6 +13,8 @@ + + %sveltekit.head%