diff --git a/cli/.eslintrc.cjs b/cli/.eslintrc.cjs index 33ee3bd1e..18a48ac7e 100644 --- a/cli/.eslintrc.cjs +++ b/cli/.eslintrc.cjs @@ -19,8 +19,9 @@ module.exports = { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'error', 'unicorn/prefer-module': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/no-process-exit': 'off', curly: 2, 'prettier/prettier': 0, - 'unicorn/prevent-abbreviations': 'error', }, }; diff --git a/cli/package-lock.json b/cli/package-lock.json index 82e920bd9..69be80132 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,7 +1,7 @@ { "name": "@immich/cli", "version": "2.1.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -37,6 +37,7 @@ "prettier-plugin-organize-imports": "^3.2.4", "typescript": "^5.3.3", "vite": "^5.0.12", + "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.2.2", "yaml": "^2.3.1" }, @@ -57,104 +58,6 @@ "typescript": "^5.3.3" } }, - "../server": { - "name": "immich", - "version": "1.94.1", - "extraneous": true, - "license": "GNU Affero General Public License version 3", - "dependencies": { - "@babel/runtime": "^7.22.11", - "@immich/cli": "^2.0.7", - "@nestjs/bullmq": "^10.0.1", - "@nestjs/common": "^10.2.2", - "@nestjs/config": "^3.0.0", - "@nestjs/core": "^10.2.2", - "@nestjs/platform-express": "^10.2.2", - "@nestjs/platform-socket.io": "^10.2.2", - "@nestjs/schedule": "^4.0.0", - "@nestjs/swagger": "^7.1.8", - "@nestjs/typeorm": "^10.0.0", - "@nestjs/websockets": "^10.2.2", - "@socket.io/postgres-adapter": "^0.3.1", - "@types/picomatch": "^2.3.3", - "archiver": "^6.0.0", - "async-lock": "^1.4.0", - "bcrypt": "^5.1.1", - "bullmq": "^4.8.0", - "chokidar": "^3.5.3", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.0", - "cookie-parser": "^1.4.6", - "exiftool-vendored": "~24.4.0", - "exiftool-vendored.pl": "12.73", - "fluent-ffmpeg": "^2.1.2", - "geo-tz": "^8.0.0", - "glob": "^10.3.3", - "handlebars": "^4.7.8", - "i18n-iso-countries": "^7.6.0", - "ioredis": "^5.3.2", - "joi": "^17.10.0", - "lodash": "^4.17.21", - "luxon": "^3.4.2", - "nest-commander": "^3.11.1", - "node-addon-api": "^7.0.0", - "openid-client": "^5.4.3", - "pg": "^8.11.3", - "picomatch": "^4.0.0", - "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1", - "sanitize-filename": "^1.6.3", - "sharp": "^0.33.0", - "thumbhash": "^0.1.1", - "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35" - }, - "devDependencies": { - "@nestjs/cli": "^10.1.16", - "@nestjs/schematics": "^10.0.2", - "@nestjs/testing": "^10.2.2", - "@testcontainers/postgresql": "^10.2.1", - "@types/archiver": "^6.0.0", - "@types/async-lock": "^1.4.2", - "@types/bcrypt": "^5.0.0", - "@types/cookie-parser": "^1.4.3", - "@types/express": "^4.17.17", - "@types/fluent-ffmpeg": "^2.1.21", - "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.12", - "@types/jest-when": "^3.5.2", - "@types/lodash": "^4.14.197", - "@types/mock-fs": "^4.13.1", - "@types/multer": "^1.4.7", - "@types/node": "^20.5.7", - "@types/sharp": "^0.31.1", - "@types/supertest": "^6.0.0", - "@types/ua-parser-js": "^0.7.36", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "dotenv": "^16.3.1", - "eslint": "^8.56.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^51.0.0", - "jest": "^29.6.4", - "jest-when": "^3.6.0", - "mock-fs": "^5.2.0", - "prettier": "^3.0.2", - "prettier-plugin-organize-imports": "^3.2.3", - "rimraf": "^5.0.1", - "source-map-support": "^0.5.21", - "sql-formatter": "^15.0.0", - "supertest": "^6.3.3", - "testcontainers": "^10.2.1", - "ts-jest": "^29.1.1", - "ts-loader": "^9.4.4", - "ts-node": "^10.9.1", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.3.3", - "utimes": "^5.2.1" - } - }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -165,13 +68,13 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -365,9 +268,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -377,9 +280,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -780,9 +683,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -811,6 +714,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", @@ -834,6 +759,28 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -946,32 +893,32 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -984,9 +931,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1039,9 +986,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", - "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" @@ -1051,9 +998,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", "cpu": [ "arm" ], @@ -1064,9 +1011,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", "cpu": [ "arm64" ], @@ -1077,9 +1024,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", "cpu": [ "arm64" ], @@ -1090,9 +1037,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", "cpu": [ "x64" ], @@ -1103,9 +1050,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", "cpu": [ "arm" ], @@ -1116,9 +1063,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", "cpu": [ "arm64" ], @@ -1129,9 +1076,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", "cpu": [ "arm64" ], @@ -1142,9 +1089,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", "cpu": [ "riscv64" ], @@ -1155,9 +1102,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", "cpu": [ "x64" ], @@ -1168,9 +1115,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", "cpu": [ "x64" ], @@ -1181,9 +1128,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", "cpu": [ "arm64" ], @@ -1194,9 +1141,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", "cpu": [ "ia32" ], @@ -1207,9 +1154,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", "cpu": [ "x64" ], @@ -1247,9 +1194,9 @@ "dev": true }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "node_modules/@types/json-schema": { @@ -1259,9 +1206,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", "dev": true }, "node_modules/@types/lodash-es": { @@ -1283,18 +1230,18 @@ } }, "node_modules/@types/node": { - "version": "20.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", - "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", + "version": "20.11.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz", + "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/normalize-package-data": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz", - "integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, "node_modules/@types/semver": { @@ -1304,16 +1251,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", - "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/type-utils": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1339,15 +1286,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", - "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4" }, "engines": { @@ -1367,13 +1314,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1384,13 +1331,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", - "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/utils": "7.1.1", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1411,9 +1358,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1424,13 +1371,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1451,42 +1398,18 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", "semver": "^7.5.4" }, "engines": { @@ -1501,12 +1424,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/types": "7.2.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1747,13 +1670,12 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -1769,9 +1691,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -1788,8 +1710,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -1840,9 +1762,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001572", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz", - "integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==", + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", "dev": true, "funding": [ { @@ -1993,12 +1915,12 @@ "dev": true }, "node_modules/core-js-compat": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", - "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", + "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", "dev": true, "dependencies": { - "browserslist": "^4.22.2" + "browserslist": "^4.22.3" }, "funding": { "type": "opencollective", @@ -2094,9 +2016,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.616", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", - "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==", + "version": "1.4.705", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.705.tgz", + "integrity": "sha512-LKqhpwJCLhYId2VVwEzFXWrqQI5n5zBppz1W9ehhTlfYU8CUUW6kClbN8LHF/v7flMgRdETS772nqywJ+ckVAw==", "dev": true }, "node_modules/emoji-regex": { @@ -2153,9 +2075,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -2331,6 +2253,28 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -2475,9 +2419,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -2524,23 +2468,23 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { - "flatted": "^3.2.7", + "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/foreground-child": { @@ -2580,10 +2524,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-func-name": { "version": "2.0.2", @@ -2640,30 +2587,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2699,24 +2622,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2726,6 +2643,18 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -2748,9 +2677,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -2828,12 +2757,12 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2943,9 +2872,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -3034,9 +2963,9 @@ "dev": true }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -3122,9 +3051,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -3209,15 +3138,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -3230,9 +3162,9 @@ } }, "node_modules/mlly": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", - "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", + "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", "dev": true, "dependencies": { "acorn": "^8.11.3", @@ -3833,9 +3765,9 @@ } }, "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { "is-core-module": "^2.13.0", @@ -3883,6 +3815,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3903,10 +3845,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", - "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -3919,19 +3873,19 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", "fsevents": "~2.3.2" } }, @@ -3959,9 +3913,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -3985,12 +3939,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4068,9 +4016,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -4084,9 +4032,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", - "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, "node_modules/stackback": { @@ -4263,6 +4211,16 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4283,6 +4241,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4335,17 +4305,37 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" } }, + "node_modules/tsconfck": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.3.tgz", + "integrity": "sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -4399,9 +4389,9 @@ } }, "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", + "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", "dev": true }, "node_modules/undici-types": { @@ -4474,9 +4464,9 @@ } }, "node_modules/vite": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz", - "integrity": "sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", "dev": true, "dependencies": { "esbuild": "^0.19.3", @@ -4550,6 +4540,25 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vitest": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", @@ -4749,6 +4758,12 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/yaml": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", @@ -4773,3125 +4788,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true - }, - "@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true - }, - "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", - "dev": true - }, - "@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "dev": true, - "optional": true - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true - }, - "@immich/sdk": { - "version": "file:../open-api/typescript-sdk", - "requires": { - "@oazapfts/runtime": "^1.0.2", - "@types/node": "^20.11.0", - "typescript": "^5.3.3" - } - }, - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.27.8" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true - }, - "@pkgr/core": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", - "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", - "dev": true - }, - "@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", - "dev": true, - "optional": true - }, - "@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", - "dev": true, - "optional": true - }, - "@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", - "dev": true, - "optional": true - }, - "@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", - "dev": true, - "optional": true - }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "@types/byte-size": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", - "integrity": "sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==", - "dev": true - }, - "@types/cli-progress": { - "version": "3.11.5", - "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz", - "integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "@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 - }, - "@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", - "dev": true - }, - "@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "dev": true, - "requires": { - "@types/lodash": "*" - } - }, - "@types/mock-fs": { - "version": "4.13.4", - "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", - "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "20.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", - "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", - "dev": true, - "requires": { - "undici-types": "~5.26.4" - } - }, - "@types/normalize-package-data": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz", - "integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==", - "dev": true - }, - "@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", - "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/type-utils": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", - "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" - } - }, - "@typescript-eslint/type-utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", - "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "semver": "^7.5.4" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "eslint-visitor-keys": "^3.4.1" - } - }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "@vitest/coverage-v8": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", - "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.2.1", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^4.0.1", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.2.0" - } - }, - "@vitest/expect": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", - "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", - "dev": true, - "requires": { - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", - "chai": "^4.3.10" - } - }, - "@vitest/runner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", - "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", - "dev": true, - "requires": { - "@vitest/utils": "1.3.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "dependencies": { - "p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "requires": { - "yocto-queue": "^1.0.0" - } - }, - "yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true - } - } - }, - "@vitest/snapshot": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", - "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", - "dev": true, - "requires": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - } - }, - "@vitest/spy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", - "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", - "dev": true, - "requires": { - "tinyspy": "^2.2.0" - } - }, - "@vitest/utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", - "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", - "dev": true, - "requires": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - } - }, - "acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - } - }, - "builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true - }, - "byte-size": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", - "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==", - "dev": true - }, - "cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001572", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz", - "integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==", - "dev": true - }, - "chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } - }, - "ci-info": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", - "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", - "dev": true - }, - "clean-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", - "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - } - } - }, - "cli-progress": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", - "dev": true, - "requires": { - "string-width": "^4.2.3" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "commander": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", - "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "core-js-compat": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", - "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", - "dev": true, - "requires": { - "browserslist": "^4.22.2" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.616", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", - "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - } - }, - "eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "requires": {} - }, - "eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" - } - }, - "eslint-plugin-unicorn": { - "version": "51.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz", - "integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^2.1.4", - "ci-info": "^4.0.0", - "clean-regexp": "^1.0.0", - "core-js-compat": "^3.34.0", - "esquery": "^1.5.0", - "indent-string": "^4.0.0", - "is-builtin-module": "^3.2.1", - "jsesc": "^3.0.2", - "pluralize": "^8.0.0", - "read-pkg-up": "^7.0.1", - "regexp-tree": "^0.1.27", - "regjsparser": "^0.10.0", - "semver": "^7.5.4", - "strip-indent": "^3.0.0" - } - }, - "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - }, - "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "requires": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0" - } - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, - "fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", - "dev": true, - "requires": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true - }, - "get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true - }, - "glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "requires": { - "builtin-modules": "^3.3.0" - } - }, - "is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true - }, - "istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, - "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "requires": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "requires": { - "get-func-name": "^2.0.1" - } - }, - "lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true - }, - "magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "magicast": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", - "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", - "dev": true, - "requires": { - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "source-map-js": "^1.0.2" - } - }, - "make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "requires": { - "semver": "^7.5.3" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true - }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true - }, - "mlly": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", - "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", - "dev": true, - "requires": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" - } - }, - "mock-fs": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", - "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - } - } - }, - "npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "requires": { - "path-key": "^4.0.0" - }, - "dependencies": { - "path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true - } - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "requires": { - "mimic-fn": "^4.0.0" - } - }, - "optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "requires": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, - "requires": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "requires": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, - "pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true - }, - "postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", - "dev": true, - "requires": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, - "prettier-plugin-organize-imports": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", - "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", - "dev": true, - "requires": {} - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "regexp-tree": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", - "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", - "dev": true - }, - "regjsparser": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", - "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true - } - } - }, - "resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", - "dev": true, - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "rollup": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", - "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", - "@types/estree": "1.0.5", - "fsevents": "~2.3.2" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz", - "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", - "dev": true - }, - "stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", - "dev": true - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "requires": { - "js-tokens": "^8.0.2" - }, - "dependencies": { - "js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - } - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", - "dev": true, - "requires": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "tinybench": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", - "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", - "dev": true - }, - "tinypool": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", - "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", - "dev": true - }, - "tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", - "dev": true, - "requires": {} - }, - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", - "dev": true - }, - "ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, - "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "v8-to-istanbul": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", - "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "vite": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.5.tgz", - "integrity": "sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==", - "dev": true, - "requires": { - "esbuild": "^0.19.3", - "fsevents": "~2.3.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" - } - }, - "vite-node": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", - "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", - "dev": true, - "requires": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - } - }, - "vitest": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", - "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", - "dev": true, - "requires": { - "@vitest/expect": "1.3.1", - "@vitest/runner": "1.3.1", - "@vitest/snapshot": "1.3.1", - "@vitest/spy": "1.3.1", - "@vitest/utils": "1.3.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.2", - "vite": "^5.0.0", - "vite-node": "1.3.1", - "why-is-node-running": "^2.2.2" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", - "dev": true, - "requires": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - } - }, - "wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } diff --git a/cli/package.json b/cli/package.json index b76c24f94..45c60569e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -35,6 +35,7 @@ "prettier-plugin-organize-imports": "^3.2.4", "typescript": "^5.3.3", "vite": "^5.0.12", + "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.2.2", "yaml": "^2.3.1" }, diff --git a/cli/src/commands/upload.command.ts b/cli/src/commands/asset.ts similarity index 90% rename from cli/src/commands/upload.command.ts rename to cli/src/commands/asset.ts index 250fd79c6..b6c159c9b 100644 --- a/cli/src/commands/upload.command.ts +++ b/cli/src/commands/asset.ts @@ -1,4 +1,12 @@ -import { AssetBulkUploadCheckResult } from '@immich/sdk'; +import { + AssetBulkUploadCheckResult, + addAssetsToAlbum, + checkBulkUpload, + createAlbum, + defaults, + getAllAlbums, + getSupportedMediaTypes, +} from '@immich/sdk'; import byteSize from 'byte-size'; import cliProgress from 'cli-progress'; import { chunk, zip } from 'lodash-es'; @@ -7,9 +15,8 @@ import fs, { createReadStream } from 'node:fs'; import { access, constants, stat, unlink } from 'node:fs/promises'; import os from 'node:os'; import { basename } from 'node:path'; -import { ImmichApi } from 'src/services/api.service'; -import { CrawlService } from '../services/crawl.service'; -import { BaseCommand } from './base-command'; +import { CrawlService } from 'src/services/crawl.service'; +import { BaseOptions, authenticate } from 'src/utils'; const zipDefined = zip as (a: T[], b: U[]) => [T, U][]; @@ -106,7 +113,7 @@ class Asset { } } -export class UploadOptionsDto { +class UploadOptionsDto { recursive? = false; exclusionPatterns?: string[] = []; dryRun? = false; @@ -118,11 +125,13 @@ export class UploadOptionsDto { concurrency? = 4; } -export class UploadCommand extends BaseCommand { - api!: ImmichApi; +export const upload = (paths: string[], baseOptions: BaseOptions, uploadOptions: UploadOptionsDto) => + new UploadCommand().run(paths, baseOptions, uploadOptions); - public async run(paths: string[], options: UploadOptionsDto): Promise { - this.api = await this.connect(); +// TODO refactor this +class UploadCommand { + public async run(paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto): Promise { + await authenticate(baseOptions); console.log('Crawling for assets...'); const files = await this.getFiles(paths, options); @@ -264,7 +273,7 @@ export class UploadCommand extends BaseCommand { } public async getAlbums(): Promise> { - const existingAlbums = await this.api.getAllAlbums(); + const existingAlbums = await getAllAlbums({}); const albumMapping = new Map(); for (const album of existingAlbums) { @@ -313,7 +322,7 @@ export class UploadCommand extends BaseCommand { try { for (const albumNames of chunk(newAlbums, options.concurrency)) { const newAlbumIds = await Promise.all( - albumNames.map((albumName: string) => this.api.createAlbum({ albumName }).then((r) => r.id)), + albumNames.map((albumName: string) => createAlbum({ createAlbumDto: { albumName } }).then((r) => r.id)), ); for (const [albumName, albumId] of zipDefined(albumNames, newAlbumIds)) { @@ -348,7 +357,7 @@ export class UploadCommand extends BaseCommand { try { for (const [albumId, assets] of albumToAssets.entries()) { for (const assetBatch of chunk(assets, Math.min(1000 * (options.concurrency ?? 4), 65_000))) { - await this.api.addAssetsToAlbum(albumId, { ids: assetBatch }); + await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } }); albumUpdateProgress.increment(assetBatch.length); } } @@ -404,17 +413,18 @@ export class UploadCommand extends BaseCommand { const assetBulkUploadCheckDto = { assets: zipDefined(assetsToCheck, checksums).map(([asset, checksum]) => ({ id: asset.path, checksum })), }; - const checkResponse = await this.api.checkBulkUpload(assetBulkUploadCheckDto); + const checkResponse = await checkBulkUpload({ assetBulkUploadCheckDto }); return checkResponse.results; } private async uploadAssets(assets: Asset[]): Promise { const fileRequests = await Promise.all(assets.map((asset) => asset.getUploadFormData())); - return Promise.all(fileRequests.map((request) => this.uploadAsset(request).then((response) => response.id))); + const results = await Promise.all(fileRequests.map((request) => this.uploadAsset(request))); + return results.map((response) => response.id); } private async crawl(paths: string[], options: UploadOptionsDto): Promise { - const formatResponse = await this.api.getSupportedMediaTypes(); + const formatResponse = await getSupportedMediaTypes(); const crawlService = new CrawlService(formatResponse.image, formatResponse.video); return crawlService.crawl({ @@ -426,14 +436,12 @@ export class UploadCommand extends BaseCommand { } private async uploadAsset(data: FormData): Promise<{ id: string }> { - const url = this.api.instanceUrl + '/asset/upload'; + const { baseUrl, headers } = defaults; - const response = await fetch(url, { + const response = await fetch(`${baseUrl}/asset/upload`, { method: 'post', redirect: 'error', - headers: { - 'x-api-key': this.api.apiKey, - }, + headers: headers as Record, body: data, }); if (response.status !== 200 && response.status !== 201) { diff --git a/cli/src/commands/auth.ts b/cli/src/commands/auth.ts new file mode 100644 index 000000000..05f3d7953 --- /dev/null +++ b/cli/src/commands/auth.ts @@ -0,0 +1,48 @@ +import { getMyUserInfo } from '@immich/sdk'; +import { existsSync } from 'node:fs'; +import { mkdir, unlink } from 'node:fs/promises'; +import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils'; + +export const login = async (instanceUrl: string, apiKey: string, options: BaseOptions) => { + console.log(`Logging in to ${instanceUrl}`); + + const { configDirectory: configDir } = options; + + await connect(instanceUrl, apiKey); + + const [error, userInfo] = await withError(getMyUserInfo()); + if (error) { + logError(error, 'Failed to load user info'); + process.exit(1); + } + + console.log(`Logged in as ${userInfo.email}`); + + if (!existsSync(configDir)) { + // Create config folder if it doesn't exist + const created = await mkdir(configDir, { recursive: true }); + if (!created) { + console.log(`Failed to create config folder: ${configDir}`); + return; + } + } + + await writeAuthFile(configDir, { instanceUrl, apiKey }); + + console.log(`Wrote auth info to ${getAuthFilePath(configDir)}`); +}; + +export const logout = async (options: BaseOptions) => { + console.log('Logging out...'); + + const { configDirectory: configDir } = options; + + const authFile = getAuthFilePath(configDir); + + if (existsSync(authFile)) { + await unlink(authFile); + console.log(`Removed auth file: ${authFile}`); + } + + console.log('Successfully logged out'); +}; diff --git a/cli/src/commands/base-command.ts b/cli/src/commands/base-command.ts deleted file mode 100644 index 2ecb3fef2..000000000 --- a/cli/src/commands/base-command.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ServerVersionResponseDto, UserResponseDto } from '@immich/sdk'; -import { ImmichApi } from 'src/services/api.service'; -import { SessionService } from '../services/session.service'; - -export abstract class BaseCommand { - protected sessionService!: SessionService; - protected user!: UserResponseDto; - protected serverVersion!: ServerVersionResponseDto; - - constructor(options: { configDirectory?: string }) { - if (!options.configDirectory) { - throw new Error('Config directory is required'); - } - this.sessionService = new SessionService(options.configDirectory); - } - - public async connect(): Promise { - return await this.sessionService.connect(); - } -} diff --git a/cli/src/commands/login.command.ts b/cli/src/commands/login.command.ts deleted file mode 100644 index 863c28701..000000000 --- a/cli/src/commands/login.command.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BaseCommand } from './base-command'; - -export class LoginCommand extends BaseCommand { - public async run(instanceUrl: string, apiKey: string): Promise { - await this.sessionService.login(instanceUrl, apiKey); - } -} diff --git a/cli/src/commands/logout.command.ts b/cli/src/commands/logout.command.ts deleted file mode 100644 index 736f77424..000000000 --- a/cli/src/commands/logout.command.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BaseCommand } from './base-command'; - -export class LogoutCommand extends BaseCommand { - public static readonly description = 'Logout and remove persisted credentials'; - public async run(): Promise { - await this.sessionService.logout(); - } -} diff --git a/cli/src/commands/server-info.command.ts b/cli/src/commands/server-info.command.ts deleted file mode 100644 index c6029b130..000000000 --- a/cli/src/commands/server-info.command.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BaseCommand } from './base-command'; - -export class ServerInfoCommand extends BaseCommand { - public async run() { - const api = await this.connect(); - const versionInfo = await api.getServerVersion(); - const mediaTypes = await api.getSupportedMediaTypes(); - const statistics = await api.getAssetStatistics(); - - console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`); - console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`); - console.log(`Video Types: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`); - console.log( - `Statistics:\n Images: ${statistics.images}\n Videos: ${statistics.videos}\n Total: ${statistics.total}`, - ); - } -} diff --git a/cli/src/commands/server-info.ts b/cli/src/commands/server-info.ts new file mode 100644 index 000000000..a7de804df --- /dev/null +++ b/cli/src/commands/server-info.ts @@ -0,0 +1,15 @@ +import { getAssetStatistics, getServerVersion, getSupportedMediaTypes } from '@immich/sdk'; +import { BaseOptions, authenticate } from 'src/utils'; + +export const serverInfo = async (options: BaseOptions) => { + await authenticate(options); + + const versionInfo = await getServerVersion(); + const mediaTypes = await getSupportedMediaTypes(); + const stats = await getAssetStatistics({}); + + console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`); + console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`); + console.log(`Video Types: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`); + console.log(`Statistics:\n Images: ${stats.images}\n Videos: ${stats.videos}\n Total: ${stats.total}`); +}; diff --git a/cli/src/index.ts b/cli/src/index.ts index e9485190a..bf7e13f44 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -2,11 +2,10 @@ import { Command, Option } from 'commander'; import os from 'node:os'; import path from 'node:path'; +import { upload } from 'src/commands/asset'; +import { login, logout } from 'src/commands/auth'; +import { serverInfo } from 'src/commands/server-info'; import { version } from '../package.json'; -import { LoginCommand } from './commands/login.command'; -import { LogoutCommand } from './commands/logout.command'; -import { ServerInfoCommand } from './commands/server-info.command'; -import { UploadCommand } from './commands/upload.command'; const defaultConfigDirectory = path.join(os.homedir(), '.config/immich/'); @@ -18,14 +17,34 @@ const program = new Command() new Option('-d, --config-directory ', 'Configuration directory where auth.yml will be stored') .env('IMMICH_CONFIG_DIR') .default(defaultConfigDirectory), - ); + ) + .addOption(new Option('-u, --url [url]', 'Immich server URL').env('IMMICH_INSTANCE_URL')) + .addOption(new Option('-k, --key [apiKey]', 'Immich API key').env('IMMICH_API_KEY')); + +program + .command('login') + .alias('login-key') + .description('Login using an API key') + .argument('url', 'Immich server URL') + .argument('key', 'Immich API key') + .action((url, key) => login(url, key, program.opts())); + +program + .command('logout') + .description('Remove stored credentials') + .action(() => logout(program.opts())); + +program + .command('server-info') + .description('Display server information') + .action(() => serverInfo(program.opts())); program .command('upload') .description('Upload assets') - .usage('[options] [paths...]') + .usage('[paths...] [options]') .addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false)) - .addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS')) + .addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default([])) .addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false)) .addOption(new Option('-H, --include-hidden', 'Include hidden folders').env('IMMICH_INCLUDE_HIDDEN').default(false)) .addOption( @@ -50,32 +69,6 @@ program ) .addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS')) .argument('[paths...]', 'One or more paths to assets to be uploaded') - .action(async (paths, options) => { - options.exclusionPatterns = options.ignore; - await new UploadCommand(program.opts()).run(paths, options); - }); - -program - .command('server-info') - .description('Display server information') - .action(async () => { - await new ServerInfoCommand(program.opts()).run(); - }); - -program - .command('login-key') - .description('Login using an API key') - .argument('url') - .argument('key') - .action(async (url, key) => { - await new LoginCommand(program.opts()).run(url, key); - }); - -program - .command('logout') - .description('Remove stored credentials') - .action(async () => { - await new LogoutCommand(program.opts()).run(); - }); + .action((paths, options) => upload(paths, program.opts(), options)); program.parse(process.argv); diff --git a/cli/src/services/api.service.ts b/cli/src/services/api.service.ts deleted file mode 100644 index 089eda120..000000000 --- a/cli/src/services/api.service.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - ApiKeyCreateDto, - AssetBulkUploadCheckDto, - BulkIdsDto, - CreateAlbumDto, - CreateAssetDto, - LoginCredentialDto, - SignUpDto, - addAssetsToAlbum, - checkBulkUpload, - createAlbum, - createApiKey, - getAllAlbums, - getAllAssets, - getAssetStatistics, - getMyUserInfo, - getServerVersion, - getSupportedMediaTypes, - login, - pingServer, - signUpAdmin, - uploadFile, -} from '@immich/sdk'; - -/** - * Wraps the underlying API to abstract away the options and make API calls mockable for testing. - */ -export class ImmichApi { - private readonly options; - - constructor( - public instanceUrl: string, - public apiKey: string, - ) { - this.options = { - baseUrl: instanceUrl, - headers: { - 'x-api-key': apiKey, - }, - }; - } - - setApiKey(apiKey: string) { - this.apiKey = apiKey; - if (!this.options.headers) { - throw new Error('missing headers'); - } - this.options.headers['x-api-key'] = apiKey; - } - - addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto) { - return addAssetsToAlbum({ id, bulkIdsDto }, this.options); - } - - checkBulkUpload(assetBulkUploadCheckDto: AssetBulkUploadCheckDto) { - return checkBulkUpload({ assetBulkUploadCheckDto }, this.options); - } - - createAlbum(createAlbumDto: CreateAlbumDto) { - return createAlbum({ createAlbumDto }, this.options); - } - - createApiKey(apiKeyCreateDto: ApiKeyCreateDto, options: { headers: { Authorization: string } }) { - return createApiKey({ apiKeyCreateDto }, { ...this.options, ...options }); - } - - getAllAlbums() { - return getAllAlbums({}, this.options); - } - - getAllAssets() { - return getAllAssets({}, this.options); - } - - getAssetStatistics() { - return getAssetStatistics({}, this.options); - } - - getMyUserInfo() { - return getMyUserInfo(this.options); - } - - getServerVersion() { - return getServerVersion(this.options); - } - - getSupportedMediaTypes() { - return getSupportedMediaTypes(this.options); - } - - login(loginCredentialDto: LoginCredentialDto) { - return login({ loginCredentialDto }, this.options); - } - - pingServer() { - return pingServer(this.options); - } - - signUpAdmin(signUpDto: SignUpDto) { - return signUpAdmin({ signUpDto }, this.options); - } - - uploadFile(createAssetDto: CreateAssetDto) { - return uploadFile({ createAssetDto }, this.options); - } -} diff --git a/cli/src/services/session.service.spec.ts b/cli/src/services/session.service.spec.ts deleted file mode 100644 index c217ab4e6..000000000 --- a/cli/src/services/session.service.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import yaml from 'yaml'; -import { SessionService } from './session.service'; - -const TEST_CONFIG_DIR = '/tmp/immich/'; -const TEST_AUTH_FILE = path.join(TEST_CONFIG_DIR, 'auth.yml'); -const TEST_IMMICH_INSTANCE_URL = 'https://test/api'; -const TEST_IMMICH_API_KEY = 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg'; - -const spyOnConsole = () => vi.spyOn(console, 'log').mockImplementation(() => {}); - -const createTestAuthFile = async (contents: string) => { - if (!fs.existsSync(TEST_CONFIG_DIR)) { - // Create config folder if it doesn't exist - const created = await fs.promises.mkdir(TEST_CONFIG_DIR, { recursive: true }); - if (!created) { - throw new Error(`Failed to create config folder ${TEST_CONFIG_DIR}`); - } - } - - fs.writeFileSync(TEST_AUTH_FILE, contents); -}; - -const readTestAuthFile = async (): Promise => { - return await fs.promises.readFile(TEST_AUTH_FILE, 'utf8'); -}; - -const deleteAuthFile = () => { - try { - fs.unlinkSync(TEST_AUTH_FILE); - } catch (error: any) { - if (error.code !== 'ENOENT') { - throw error; - } - } -}; - -const mocks = vi.hoisted(() => { - return { - getMyUserInfo: vi.fn(() => Promise.resolve({ email: 'admin@example.com' })), - pingServer: vi.fn(() => Promise.resolve({ res: 'pong' })), - }; -}); - -vi.mock('./api.service', async (importOriginal) => { - const module = await importOriginal(); - // @ts-expect-error this is only a partial implementation of the return value - module.ImmichApi.prototype.getMyUserInfo = mocks.getMyUserInfo; - module.ImmichApi.prototype.pingServer = mocks.pingServer; - return module; -}); - -describe('SessionService', () => { - let sessionService: SessionService; - - beforeEach(() => { - deleteAuthFile(); - sessionService = new SessionService(TEST_CONFIG_DIR); - }); - - afterEach(() => { - deleteAuthFile(); - }); - - it('should connect to immich', async () => { - await createTestAuthFile( - JSON.stringify({ - apiKey: TEST_IMMICH_API_KEY, - instanceUrl: TEST_IMMICH_INSTANCE_URL, - }), - ); - - await sessionService.connect(); - expect(mocks.pingServer).toHaveBeenCalledTimes(1); - }); - - it('should error if no auth file exists', async () => { - await sessionService.connect().catch((error) => { - expect(error.message).toEqual('No auth file exist. Please login first'); - }); - }); - - it('should error if auth file is missing instance URl', async () => { - await createTestAuthFile( - JSON.stringify({ - apiKey: TEST_IMMICH_API_KEY, - }), - ); - await sessionService.connect().catch((error) => { - expect(error.message).toEqual(`Instance URL missing in auth config file ${TEST_AUTH_FILE}`); - }); - }); - - it('should error if auth file is missing api key', async () => { - await createTestAuthFile( - JSON.stringify({ - instanceUrl: TEST_IMMICH_INSTANCE_URL, - }), - ); - - await expect(sessionService.connect()).rejects.toThrow(`API key missing in auth config file ${TEST_AUTH_FILE}`); - }); - - it('should create auth file when logged in', async () => { - await sessionService.login(TEST_IMMICH_INSTANCE_URL, TEST_IMMICH_API_KEY); - - const data: string = await readTestAuthFile(); - const authConfig = yaml.parse(data); - expect(authConfig.instanceUrl).toBe(TEST_IMMICH_INSTANCE_URL); - expect(authConfig.apiKey).toBe(TEST_IMMICH_API_KEY); - }); - - it('should delete auth file when logging out', async () => { - const consoleSpy = spyOnConsole(); - - await createTestAuthFile( - JSON.stringify({ - apiKey: TEST_IMMICH_API_KEY, - instanceUrl: TEST_IMMICH_INSTANCE_URL, - }), - ); - await sessionService.logout(); - - await fs.promises.access(TEST_AUTH_FILE, fs.constants.F_OK).catch((error) => { - expect(error.message).toContain('ENOENT'); - }); - - expect(consoleSpy.mock.calls).toEqual([ - ['Logging out...'], - [`Removed auth file ${TEST_AUTH_FILE}`], - ['Successfully logged out'], - ]); - }); -}); diff --git a/cli/src/services/session.service.ts b/cli/src/services/session.service.ts deleted file mode 100644 index 0235b30a4..000000000 --- a/cli/src/services/session.service.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { existsSync } from 'node:fs'; -import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import yaml from 'yaml'; -import { ImmichApi } from './api.service'; - -class LoginError extends Error { - constructor(message: string) { - super(message); - - this.name = this.constructor.name; - - Error.captureStackTrace(this, this.constructor); - } -} - -export class SessionService { - private get authPath() { - return path.join(this.configDirectory, '/auth.yml'); - } - - constructor(private configDirectory: string) {} - - async connect(): Promise { - let instanceUrl = process.env.IMMICH_INSTANCE_URL; - let apiKey = process.env.IMMICH_API_KEY; - - if (!instanceUrl || !apiKey) { - await access(this.authPath, constants.F_OK).catch((error) => { - if (error.code === 'ENOENT') { - throw new LoginError('No auth file exist. Please login first'); - } - }); - - const data: string = await readFile(this.authPath, 'utf8'); - const parsedConfig = yaml.parse(data); - - instanceUrl = parsedConfig.instanceUrl; - apiKey = parsedConfig.apiKey; - - if (!instanceUrl) { - throw new LoginError(`Instance URL missing in auth config file ${this.authPath}`); - } - - if (!apiKey) { - throw new LoginError(`API key missing in auth config file ${this.authPath}`); - } - } - - instanceUrl = await this.resolveApiEndpoint(instanceUrl); - - const api = new ImmichApi(instanceUrl, apiKey); - - const pingResponse = await api.pingServer().catch((error) => { - throw new Error(`Failed to connect to server ${instanceUrl}: ${error.message}`, error); - }); - - if (pingResponse.res !== 'pong') { - throw new Error(`Could not parse response. Is Immich listening on ${instanceUrl}?`); - } - - return api; - } - - async login(instanceUrl: string, apiKey: string): Promise { - console.log(`Logging in to ${instanceUrl}`); - - instanceUrl = await this.resolveApiEndpoint(instanceUrl); - - const api = new ImmichApi(instanceUrl, apiKey); - - // Check if server and api key are valid - const userInfo = await api.getMyUserInfo().catch((error) => { - throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`); - }); - - console.log(`Logged in as ${userInfo.email}`); - - if (!existsSync(this.configDirectory)) { - // Create config folder if it doesn't exist - const created = await mkdir(this.configDirectory, { recursive: true }); - if (!created) { - throw new Error(`Failed to create config folder ${this.configDirectory}`); - } - } - - await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey }), { mode: 0o600 }); - - console.log(`Wrote auth info to ${this.authPath}`); - - return api; - } - - async logout(): Promise { - console.log('Logging out...'); - - if (existsSync(this.authPath)) { - await unlink(this.authPath); - console.log('Removed auth file ' + this.authPath); - } - - console.log('Successfully logged out'); - } - - private async resolveApiEndpoint(instanceUrl: string): Promise { - const wellKnownUrl = new URL('.well-known/immich', instanceUrl); - try { - const wellKnown = await fetch(wellKnownUrl).then((response) => response.json()); - const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString(); - if (endpoint !== instanceUrl) { - console.debug(`Discovered API at ${endpoint}`); - } - return endpoint; - } catch { - return instanceUrl; - } - } -} diff --git a/cli/src/utils.ts b/cli/src/utils.ts new file mode 100644 index 000000000..f99a0e66a --- /dev/null +++ b/cli/src/utils.ts @@ -0,0 +1,89 @@ +import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk'; +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import yaml from 'yaml'; + +export interface BaseOptions { + configDirectory: string; + apiKey?: string; + instanceUrl?: string; +} + +export interface AuthDto { + instanceUrl: string; + apiKey: string; +} + +export const authenticate = async (options: BaseOptions): Promise => { + const { configDirectory: configDir, instanceUrl, apiKey } = options; + + // provided in command + if (instanceUrl && apiKey) { + await connect(instanceUrl, apiKey); + return; + } + + // fallback to file + const config = await readAuthFile(configDir); + await connect(config.instanceUrl, config.apiKey); +}; + +export const connect = async (instanceUrl: string, apiKey: string): Promise => { + const wellKnownUrl = new URL('.well-known/immich', instanceUrl); + try { + const wellKnown = await fetch(wellKnownUrl).then((response) => response.json()); + const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString(); + if (endpoint !== instanceUrl) { + console.debug(`Discovered API at ${endpoint}`); + } + instanceUrl = endpoint; + } catch { + // noop + } + + defaults.baseUrl = instanceUrl; + defaults.headers = { 'x-api-key': apiKey }; + + const [error] = await withError(getMyUserInfo()); + if (isHttpError(error)) { + logError(error, 'Failed to connect to server'); + process.exit(1); + } +}; + +export const logError = (error: unknown, message: string) => { + if (isHttpError(error)) { + console.error(`${message}: ${error.status}`); + console.error(JSON.stringify(error.data, undefined, 2)); + } else { + console.error(`${message} - ${error}`); + } +}; + +export const getAuthFilePath = (dir: string) => join(dir, 'auth.yml'); + +export const readAuthFile = async (dir: string) => { + try { + const data = await readFile(getAuthFilePath(dir)); + // TODO add class-transform/validation + return yaml.parse(data.toString()) as AuthDto; + } catch (error: Error | any) { + if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { + console.log('No auth file exists. Please login first.'); + process.exit(1); + } + throw error; + } +}; + +export const writeAuthFile = async (dir: string, auth: AuthDto) => + writeFile(getAuthFilePath(dir), yaml.stringify(auth), { mode: 0o600 }); + +export const withError = async (promise: Promise): Promise<[Error, undefined] | [undefined, T]> => { + try { + const result = await promise; + return [undefined, result]; + } catch (error: Error | any) { + return [error, undefined]; + } +}; diff --git a/cli/vite.config.ts b/cli/vite.config.ts index 89ee3a3d3..f5dd7c8e1 100644 --- a/cli/vite.config.ts +++ b/cli/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ build: { @@ -14,4 +15,5 @@ export default defineConfig({ // bundle everything except for Node built-ins noExternal: /^(?!node:).*$/, }, + plugins: [tsconfigPaths()], }); diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 4d418a0c2..77effc15f 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -78,7 +78,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus + image: prom/prometheus@sha256:bc1794e85c9e00293351b967efa267ce6af1c824ac875a9d0c7ac84700a8b53e volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus diff --git a/docs/docs/overview/img/feature-panel.png b/docs/docs/overview/img/feature-panel.png index cab58441c..8c39fe0d4 100644 Binary files a/docs/docs/overview/img/feature-panel.png and b/docs/docs/overview/img/feature-panel.png differ diff --git a/docs/docs/overview/logo.md b/docs/docs/overview/logo.md deleted file mode 100644 index e30140f4f..000000000 --- a/docs/docs/overview/logo.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Logo - -Why the colorful flower, you ask? - -I really like the Japanese culture, especially the books, history, and food. The current logo is a spin-off of [the Oda clan's symbol](https://en.wikipedia.org/wiki/Oda_clan). - -![Oda_emblem](https://user-images.githubusercontent.com/27055614/182044504-a5ed33a8-5640-42de-b359-18fdbee9fb90.svg) - -One of my favorite books is [Taikō](https://www.goodreads.com/book/show/336228.Taiko), it is a story about a prominent figure in the history of Japan, [Toyotomi Hideyoshi](https://www.britannica.com/biography/Toyotomi-Hideyoshi). He came from nothing, and through his resilience and wonderful mind, he has become one of the most powerful rulers in Japan's history. I enjoy his personality and the way he moved through life. - -The color is an adaptation of **_App-Which-Must-Not-Be-Named_**'s color scheme, with an extra color (pink) to complete the flower's fifth petal. The petal layers are the same color scheme as the main layer rotating back and forth to "bring the flower to life." - -![image](https://user-images.githubusercontent.com/27055614/182044984-2ee6d1ed-c4a7-4331-8a4b-64fcde77fe1f.png) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 053e89fa4..afe9f0eec 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -88,11 +88,10 @@ const config = { }, }, navbar: { - title: 'IMMICH', logo: { - alt: 'Immich University Logo', - src: 'img/color-logo.png', - srcDark: 'img/logo.png', + alt: 'Immich Logo', + src: 'img/immich-logo-inline-light.png', + srcDark: 'img/immich-logo-inline-dark.png', }, items: [ { diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 3fc2e6b87..388095ebb 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -1,23 +1,24 @@ import React from 'react'; import Link from '@docusaurus/Link'; import Layout from '@theme/Layout'; - +import { useColorMode } from '@docusaurus/theme-common'; function HomepageHeader() { + const { isDarkTheme } = useColorMode(); + return (
-
- Immich logo -

- Immich -

-
-

- Self-hosted backup solution - for photos and videos - on mobile device +

+ Immich logo +
+

+ Self-hosted photo and + video management solution

-
- - screenshots - + screenshots
diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico index aff1d30ff..6082fc3bd 100644 Binary files a/docs/static/img/favicon.ico and b/docs/static/img/favicon.ico differ diff --git a/docs/static/img/favicon.png b/docs/static/img/favicon.png index ba1b8409f..4e642631a 100644 Binary files a/docs/static/img/favicon.png and b/docs/static/img/favicon.png differ diff --git a/docs/static/img/immich-logo-inline-dark.png b/docs/static/img/immich-logo-inline-dark.png new file mode 100644 index 000000000..cc6cb23b6 Binary files /dev/null and b/docs/static/img/immich-logo-inline-dark.png differ diff --git a/docs/static/img/immich-logo-inline-light.png b/docs/static/img/immich-logo-inline-light.png new file mode 100644 index 000000000..b910b3790 Binary files /dev/null and b/docs/static/img/immich-logo-inline-light.png differ diff --git a/docs/static/img/immich-logo-stacked-dark.svg b/docs/static/img/immich-logo-stacked-dark.svg new file mode 100644 index 000000000..7f8381869 --- /dev/null +++ b/docs/static/img/immich-logo-stacked-dark.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/immich-logo-stacked-light.svg b/docs/static/img/immich-logo-stacked-light.svg new file mode 100644 index 000000000..8c4505d97 --- /dev/null +++ b/docs/static/img/immich-logo-stacked-light.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/immich-logo.svg b/docs/static/img/immich-logo.svg index e7edba069..376fa6f3e 100644 --- a/docs/static/img/immich-logo.svg +++ b/docs/static/img/immich-logo.svg @@ -1,98 +1,29 @@ - - + + - - - - - - - - - - - - - - - - + + + + + + + diff --git a/docs/static/img/immich-screenshots.png b/docs/static/img/immich-screenshots.png index c8569528d..6123279f2 100644 Binary files a/docs/static/img/immich-screenshots.png and b/docs/static/img/immich-screenshots.png differ diff --git a/docs/static/img/logo.png b/docs/static/img/logo.png deleted file mode 100644 index ba1b8409f..000000000 Binary files a/docs/static/img/logo.png and /dev/null differ diff --git a/e2e/package-lock.json b/e2e/package-lock.json index f94958113..c4d81bd48 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -2493,26 +2493,26 @@ } }, "node_modules/exiftool-vendored": { - "version": "24.5.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz", - "integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==", + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.6.0.tgz", + "integrity": "sha512-jGjsoeYmR9VUrlZn0j1wcxMVi5y8C7A4FAa4vm3/l7ThT8d0f+jRcBqtdjaf+P5Ds/F4OgUq+ee/fRVhLy2DrA==", "dev": true, "dependencies": { - "@photostructure/tz-lookup": "^9.0.1", + "@photostructure/tz-lookup": "^9.0.2", "@types/luxon": "^3.4.2", "batch-cluster": "^13.0.0", "he": "^1.2.0", "luxon": "^3.4.4" }, "optionalDependencies": { - "exiftool-vendored.exe": "12.76.0", - "exiftool-vendored.pl": "12.76.0" + "exiftool-vendored.exe": "12.78.0", + "exiftool-vendored.pl": "12.78.0" } }, "node_modules/exiftool-vendored.exe": { - "version": "12.76.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz", - "integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==", + "version": "12.78.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.78.0.tgz", + "integrity": "sha512-eMN7L67sb89xi8sN7INPg19uwa1KibG2oOyGcfOvB47h+1hzmGgivVu/SZIMeOToVIbLRwUl+AFwLYSTNXsJEg==", "dev": true, "optional": true, "os": [ @@ -2520,9 +2520,9 @@ ] }, "node_modules/exiftool-vendored.pl": { - "version": "12.76.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz", - "integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==", + "version": "12.78.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.78.0.tgz", + "integrity": "sha512-K8j9NgxRpTFskFuXEl0AGsc692yYyThe4i3SXgx7xc0fu/vwD2c7tRGljkEtvaweYnMmfrF4DhCpuTu0aux6sg==", "dev": true, "optional": true, "os": [ diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index e8f9a46bb..d9ac1eddb 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -1,20 +1,48 @@ -import { LibraryResponseDto, LibraryType, LoginResponseDto, getAllLibraries } from '@immich/sdk'; +import { + LibraryResponseDto, + LibraryType, + LoginResponseDto, + ScanLibraryDto, + getAllLibraries, + scanLibrary, +} from '@immich/sdk'; +import { existsSync, rmdirSync } from 'node:fs'; +import { Socket } from 'socket.io-client'; import { userDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, testAssetDirInternal, utils } from 'src/utils'; +import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils'; import request from 'supertest'; -import { beforeAll, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) => + scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) }); describe('/library', () => { let admin: LoginResponseDto; let user: LoginResponseDto; let library: LibraryResponseDto; + let websocket: Socket; beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup(); user = await utils.userSetup(admin.accessToken, userDto.user1); library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External }); + websocket = await utils.connectWebsocket(admin.accessToken); + }); + + afterAll(() => { + utils.disconnectWebsocket(websocket); + }); + + beforeEach(() => { + utils.resetEvents(); + const tempDir = `${testAssetDir}/temp`; + if (existsSync(tempDir)) { + rmdirSync(tempDir, { recursive: true }); + } + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`); + utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`); }); describe('GET /library', () => { @@ -376,6 +404,36 @@ describe('/library', () => { ]), ); }); + + it('should delete an external library with assets', async () => { + const library = await utils.createLibrary(admin.accessToken, { + type: LibraryType.External, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); + + const { status, body } = await request(app) + .delete(`/library/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(204); + expect(body).toEqual({}); + + const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) }); + expect(libraries).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: library.id, + }), + ]), + ); + + // ensure no files get deleted + expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true); + expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true); + }); }); describe('GET /library/:id/statistics', () => { @@ -394,6 +452,89 @@ describe('/library', () => { expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); }); + + it('should not scan an upload library', async () => { + const library = await utils.createLibrary(admin.accessToken, { + type: LibraryType.Upload, + }); + + const { status, body } = await request(app) + .post(`/library/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Can only refresh external libraries')); + }); + + it('should scan external library', async () => { + const library = await utils.createLibrary(admin.accessToken, { + type: LibraryType.External, + importPaths: [`${testAssetDirInternal}/temp/directoryA`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 }); + + const { assets } = await utils.metadataSearch(admin.accessToken, { + originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`, + }); + expect(assets.count).toBe(1); + }); + + it('should scan external library with exclusion pattern', async () => { + const library = await utils.createLibrary(admin.accessToken, { + type: LibraryType.External, + importPaths: [`${testAssetDirInternal}/temp`], + exclusionPatterns: ['**/directoryA'], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 }); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(1); + expect(assets.items[0].originalPath.includes('directoryB')); + }); + + it('should scan multiple import paths', async () => { + const library = await utils.createLibrary(admin.accessToken, { + type: LibraryType.External, + importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(2); + expect(assets.items.find((asset) => asset.originalPath.includes('directoryA'))).toBeDefined(); + expect(assets.items.find((asset) => asset.originalPath.includes('directoryB'))).toBeDefined(); + }); + + it('should pick up new files', async () => { + const library = await utils.createLibrary(admin.accessToken, { + type: LibraryType.External, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(2); + + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 }); + + const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.count).toBe(3); + }); }); describe('POST /library/:id/removeOffline', () => { diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts index 61702769c..42877221f 100644 --- a/e2e/src/cli/specs/login.e2e-spec.ts +++ b/e2e/src/cli/specs/login.e2e-spec.ts @@ -21,7 +21,9 @@ describe(`immich login-key`, () => { it('should require a valid key', async () => { const { stderr, exitCode } = await immichCli(['login-key', app, 'immich-is-so-cool']); - expect(stderr).toContain('Failed to connect to server http://127.0.0.1:2283/api: Error: 401'); + expect(stderr).toContain('Failed to connect to server'); + expect(stderr).toContain('Invalid API key'); + expect(stderr).toContain('401'); expect(exitCode).toBe(1); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index dde9ed22c..8ca7fba60 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -5,6 +5,7 @@ import { CreateAssetDto, CreateLibraryDto, CreateUserDto, + MetadataSearchDto, PersonCreateDto, SharedLinkCreateDto, ValidateLibraryDto, @@ -16,8 +17,10 @@ import { createUser, defaults, deleteAssets, + getAllAssets, getAssetInfo, login, + searchMetadata, setAdminOnboarding, signUpAdmin, validate, @@ -25,9 +28,9 @@ import { import { BrowserContext } from '@playwright/test'; import { exec, spawn } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { existsSync } from 'node:fs'; +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import path from 'node:path'; +import path, { dirname } from 'node:path'; import { promisify } from 'node:util'; import pg from 'pg'; import { io, type Socket } from 'socket.io-client'; @@ -37,7 +40,7 @@ import request from 'supertest'; type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; type EventType = 'assetUpload' | 'assetDelete' | 'userDelete'; -type WaitOptions = { event: EventType; id: string; timeout?: number }; +type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number }; type AdminSetupOptions = { onboarding?: boolean }; type AssetData = { bytes?: Buffer; filename: string }; @@ -83,16 +86,30 @@ const events: Record> = { userDelete: new Set(), }; -const callbacks: Record void> = {}; +const idCallbacks: Record void> = {}; +const countCallbacks: Record void }> = {}; const execPromise = promisify(exec); const onEvent = ({ event, id }: { event: EventType; id: string }) => { - events[event].add(id); - const callback = callbacks[id]; - if (callback) { - callback(); - delete callbacks[id]; + // console.log(`Received event: ${event} [id=${id}]`); + const set = events[event]; + set.add(id); + + const idCallback = idCallbacks[id]; + if (idCallback) { + idCallback(); + delete idCallbacks[id]; + } + + const item = countCallbacks[event]; + if (item) { + const { count, callback: countCallback } = item; + + if (set.size >= count) { + countCallback(); + delete countCallbacks[event]; + } } }; @@ -184,20 +201,43 @@ export const utils = { } }, - waitForWebsocketEvent: async ({ event, id, timeout: ms }: WaitOptions): Promise => { - console.log(`Waiting for ${event} [${id}]`); + resetEvents: () => { + for (const set of Object.values(events)) { + set.clear(); + } + }, + + waitForWebsocketEvent: async ({ event, id, total: count, timeout: ms }: WaitOptions): Promise => { + if (!id && !count) { + throw new Error('id or count must be provided for waitForWebsocketEvent'); + } + + const type = id ? `id=${id}` : `count=${count}`; + console.log(`Waiting for ${event} [${type}]`); const set = events[event]; - if (set.has(id)) { + if ((id && set.has(id)) || (count && set.size >= count)) { return; } return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000); - callbacks[id] = () => { - clearTimeout(timeout); - resolve(); - }; + if (id) { + idCallbacks[id] = () => { + clearTimeout(timeout); + resolve(); + }; + } + + if (count) { + countCallbacks[event] = { + count, + callback: () => { + clearTimeout(timeout); + resolve(); + }, + }; + } }); }, @@ -263,8 +303,31 @@ export const utils = { return body as AssetFileUploadResponseDto; }, + createImageFile: (path: string) => { + if (!existsSync(dirname(path))) { + mkdirSync(dirname(path), { recursive: true }); + } + if (!existsSync(path)) { + writeFileSync(path, makeRandomImage()); + } + }, + + removeImageFile: (path: string) => { + if (!existsSync(path)) { + return; + } + + rmSync(path); + }, + getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), + getAllAssets: (accessToken: string) => getAllAssets({}, { headers: asBearerAuth(accessToken) }), + + metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => { + return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); + }, + deleteAssets: (accessToken: string, ids: string[]) => deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }), diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 561d2af41..1ed90c46c 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:8e697181d24bd77cc4251fdd37e4cdd6d725c5de2ed63b9bc8db77357400c5e2 as builder-cpu +FROM python:3.11-bookworm@sha256:991e20a11120277e977cadbc104e7a9b196a68a346597879821b19034285a403 as builder-cpu FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as builder-openvino USER root @@ -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:ce81dc539f0aedc9114cae640f8352fad83d37461c24a3615b01f081d0c0583a as prod-cpu +FROM python:3.11-slim-bookworm@sha256:a2eb07f336e4f194358382611b4fea136c632b40baa6314cb27a366deeaf0144 as prod-cpu FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino USER root diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 955a64f8e..a41aed55e 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1828,38 +1828,38 @@ files = [ [[package]] name = "mypy" -version = "1.8.0" +version = "1.9.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] @@ -2289,13 +2289,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -2472,13 +2472,13 @@ files = [ [[package]] name = "pytest" -version = "8.0.2" +version = "8.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, - {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, ] [package.dependencies] @@ -2486,21 +2486,21 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.3.0,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.23.5" +version = "0.23.5.post1" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, - {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, + {file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"}, + {file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"}, ] [package.dependencies] @@ -3288,13 +3288,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.27.1" +version = "0.28.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, - {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, + {file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"}, + {file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"}, ] [package.dependencies] diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index a57d56c21..4a50ef5a3 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -58,6 +58,7 @@ + @@ -75,4 +76,4 @@ - \ No newline at end of file + diff --git a/mobile/lib/modules/activities/widgets/activity_tile.dart b/mobile/lib/modules/activities/widgets/activity_tile.dart index cb434d22d..77dcb9892 100644 --- a/mobile/lib/modules/activities/widgets/activity_tile.dart +++ b/mobile/lib/modules/activities/widgets/activity_tile.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/modules/activities/models/activity.model.dart'; -import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; @@ -106,9 +106,8 @@ class _ActivityAssetThumbnail extends StatelessWidget { decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4)), image: DecorationImage( - image: ImmichRemoteImageProvider( + image: ImmichRemoteThumbnailProvider( assetId: assetId, - isThumbnail: true, ), fit: BoxFit.cover, ), diff --git a/mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart b/mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart new file mode 100644 index 000000000..e5204acde --- /dev/null +++ b/mobile/lib/modules/asset_viewer/image_providers/cache/image_loader.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/exceptions/image_loading_exception.dart'; +import 'package:immich_mobile/shared/models/store.dart'; + +/// Loads the codec from the URI and sends the events to the [chunkEvents] stream +/// +/// Credit to [flutter_cached_network_image](https://github.com/Baseflow/flutter_cached_network_image/blob/develop/cached_network_image/lib/src/image_provider/_image_loader.dart) +/// for this wonderful implementation of their image loader +class ImageLoader { + static Future loadImageFromCache( + String uri, { + required ImageCacheManager cache, + required ImageDecoderCallback decode, + StreamController? chunkEvents, + int? height, + int? width, + }) async { + final headers = { + 'x-immich-user-token': Store.get(StoreKey.accessToken), + }; + + final stream = cache.getImageFile( + uri, + withProgress: true, + headers: headers, + maxHeight: height, + maxWidth: width, + ); + + await for (final result in stream) { + if (result is DownloadProgress) { + // We are downloading the file, so update the [chunkEvents] + chunkEvents?.add( + ImageChunkEvent( + cumulativeBytesLoaded: result.downloaded, + expectedTotalBytes: result.totalSize, + ), + ); + } + + if (result is FileInfo) { + // We have the file + final file = result.file; + final bytes = await file.readAsBytes(); + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + final decoded = await decode(buffer); + return decoded; + } + } + + // If we get here, the image failed to load from the cache stream + throw ImageLoadingException('Could not load image from stream'); + } +} diff --git a/mobile/lib/modules/asset_viewer/image_providers/cache/remote_image_cache_manager.dart b/mobile/lib/modules/asset_viewer/image_providers/cache/remote_image_cache_manager.dart new file mode 100644 index 000000000..b0f242a99 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/image_providers/cache/remote_image_cache_manager.dart @@ -0,0 +1,20 @@ +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +/// The cache manager for full size images [ImmichRemoteImageProvider] +class RemoteImageCacheManager extends CacheManager with ImageCacheManager { + static const key = 'remoteImageCacheKey'; + static final RemoteImageCacheManager _instance = RemoteImageCacheManager._(); + + factory RemoteImageCacheManager() { + return _instance; + } + + RemoteImageCacheManager._() + : super( + Config( + key, + maxNrOfCacheObjects: 500, + stalePeriod: const Duration(days: 30), + ), + ); +} diff --git a/mobile/lib/modules/asset_viewer/image_providers/cache/thumbnail_image_cache_manager.dart b/mobile/lib/modules/asset_viewer/image_providers/cache/thumbnail_image_cache_manager.dart new file mode 100644 index 000000000..8bd320b72 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/image_providers/cache/thumbnail_image_cache_manager.dart @@ -0,0 +1,21 @@ +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +/// The cache manager for thumbnail images [ImmichRemoteThumbnailProvider] +class ThumbnailImageCacheManager extends CacheManager with ImageCacheManager { + static const key = 'thumbnailImageCacheKey'; + static final ThumbnailImageCacheManager _instance = + ThumbnailImageCacheManager._(); + + factory ThumbnailImageCacheManager() { + return _instance; + } + + ThumbnailImageCacheManager._() + : super( + Config( + key, + maxNrOfCacheObjects: 5000, + stalePeriod: const Duration(days: 30), + ), + ); +} diff --git a/mobile/lib/modules/asset_viewer/image_providers/exceptions/image_loading_exception.dart b/mobile/lib/modules/asset_viewer/image_providers/exceptions/image_loading_exception.dart new file mode 100644 index 000000000..5e5ff7235 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/image_providers/exceptions/image_loading_exception.dart @@ -0,0 +1,5 @@ +/// An exception for the [ImageLoader] and the Immich image providers +class ImageLoadingException implements Exception { + final String message; + ImageLoadingException(this.message); +} diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart index d9fbd8048..095f15b82 100644 --- a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart @@ -1,8 +1,10 @@ import 'dart:async'; -import 'dart:io'; import 'dart:ui' as ui; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/image_loader.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/remote_image_cache_manager.dart'; import 'package:openapi/api.dart' as api; import 'package:flutter/foundation.dart'; @@ -12,24 +14,18 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; -/// Our Image Provider HTTP client to make the request -final _httpClient = HttpClient() - ..autoUncompress = false - ..maxConnectionsPerHost = 10; - -/// The remote image provider +/// The remote image provider for full size remote images class ImmichRemoteImageProvider extends ImageProvider { /// The [Asset.remoteId] of the asset to fetch final String assetId; - // If this is a thumbnail, we stop at loading the - // smallest version of the remote image - final bool isThumbnail; + /// The image cache manager + final ImageCacheManager? cacheManager; ImmichRemoteImageProvider({ required this.assetId, - this.isThumbnail = false, + this.cacheManager, }); /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key @@ -46,9 +42,10 @@ class ImmichRemoteImageProvider ImmichRemoteImageProvider key, ImageDecoderCallback decode, ) { + final cache = cacheManager ?? RemoteImageCacheManager(); final chunkEvents = StreamController(); return MultiImageStreamCompleter( - codec: _codec(key, decode, chunkEvents), + codec: _codec(key, cache, decode, chunkEvents), scale: 1.0, chunkEvents: chunkEvents.stream, ); @@ -69,82 +66,61 @@ class ImmichRemoteImageProvider // Streams in each stage of the image as we ask for it Stream _codec( ImmichRemoteImageProvider key, + ImageCacheManager cache, ImageDecoderCallback decode, StreamController chunkEvents, ) async* { // Load a preview to the chunk events - if (_loadPreview || key.isThumbnail) { + if (_loadPreview) { final preview = getThumbnailUrlForRemoteId( key.assetId, type: api.ThumbnailFormat.WEBP, ); - yield await _loadFromUri( - Uri.parse(preview), - decode, - chunkEvents, + yield await ImageLoader.loadImageFromCache( + preview, + cache: cache, + decode: decode, + chunkEvents: chunkEvents, ); } - // Guard thumnbail rendering - if (key.isThumbnail) { - await chunkEvents.close(); - return; - } - // Load the higher resolution version of the image final url = getThumbnailUrlForRemoteId( key.assetId, type: api.ThumbnailFormat.JPEG, ); - final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); + final codec = await ImageLoader.loadImageFromCache( + url, + cache: cache, + decode: decode, + chunkEvents: chunkEvents, + ); yield codec; // Load the final remote image if (_useOriginal) { // Load the original image final url = getImageUrlFromId(key.assetId); - final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); + final codec = await ImageLoader.loadImageFromCache( + url, + cache: cache, + decode: decode, + chunkEvents: chunkEvents, + ); yield codec; } await chunkEvents.close(); } - // Loads the codec from the URI and sends the events to the [chunkEvents] stream - Future _loadFromUri( - Uri uri, - ImageDecoderCallback decode, - StreamController chunkEvents, - ) async { - final request = await _httpClient.getUrl(uri); - request.headers.add( - 'x-immich-user-token', - Store.get(StoreKey.accessToken), - ); - final response = await request.close(); - // Chunks of the completed image can be shown - final data = await consolidateHttpClientResponseBytes( - response, - onBytesReceived: (cumulative, total) { - chunkEvents.add( - ImageChunkEvent( - cumulativeBytesLoaded: cumulative, - expectedTotalBytes: total, - ), - ); - }, - ); - - // Decode the response - final buffer = await ui.ImmutableBuffer.fromUint8List(data); - return decode(buffer); - } - @override bool operator ==(Object other) { - if (other is! ImmichRemoteImageProvider) return false; if (identical(this, other)) return true; - return assetId == other.assetId && isThumbnail == other.isThumbnail; + if (other is ImmichRemoteImageProvider) { + return assetId == other.assetId; + } + + return false; } @override diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart index 92b85b347..84fdbd740 100644 --- a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart @@ -1,30 +1,34 @@ import 'dart:async'; -import 'dart:io'; import 'dart:ui' as ui; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/image_loader.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/cache/thumbnail_image_cache_manager.dart'; import 'package:openapi/api.dart' as api; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; -/// Our HTTP client to make the request -final _httpClient = HttpClient() - ..autoUncompress = false - ..maxConnectionsPerHost = 100; - /// The remote image provider class ImmichRemoteThumbnailProvider extends ImageProvider { /// The [Asset.remoteId] of the asset to fetch final String assetId; + final int? height; + final int? width; + + /// The image cache manager + final ImageCacheManager? cacheManager; + ImmichRemoteThumbnailProvider({ required this.assetId, + this.height, + this.width, + this.cacheManager, }); /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key @@ -41,19 +45,18 @@ class ImmichRemoteThumbnailProvider ImmichRemoteThumbnailProvider key, ImageDecoderCallback decode, ) { - final chunkEvents = StreamController(); + final cache = cacheManager ?? ThumbnailImageCacheManager(); return MultiImageStreamCompleter( - codec: _codec(key, decode, chunkEvents), + codec: _codec(key, cache, decode), scale: 1.0, - chunkEvents: chunkEvents.stream, ); } // Streams in each stage of the image as we ask for it Stream _codec( ImmichRemoteThumbnailProvider key, + ImageCacheManager cache, ImageDecoderCallback decode, - StreamController chunkEvents, ) async* { // Load a preview to the chunk events final preview = getThumbnailUrlForRemoteId( @@ -61,50 +64,21 @@ class ImmichRemoteThumbnailProvider type: api.ThumbnailFormat.WEBP, ); - yield await _loadFromUri( - Uri.parse(preview), - decode, - chunkEvents, + yield await ImageLoader.loadImageFromCache( + preview, + cache: cache, + decode: decode, ); - - await chunkEvents.close(); - } - - // Loads the codec from the URI and sends the events to the [chunkEvents] stream - Future _loadFromUri( - Uri uri, - ImageDecoderCallback decode, - StreamController chunkEvents, - ) async { - final request = await _httpClient.getUrl(uri); - request.headers.add( - 'x-immich-user-token', - Store.get(StoreKey.accessToken), - ); - final response = await request.close(); - // Chunks of the completed image can be shown - final data = await consolidateHttpClientResponseBytes( - response, - onBytesReceived: (cumulative, total) { - chunkEvents.add( - ImageChunkEvent( - cumulativeBytesLoaded: cumulative, - expectedTotalBytes: total, - ), - ); - }, - ); - - // Decode the response - final buffer = await ui.ImmutableBuffer.fromUint8List(data); - return decode(buffer); } @override bool operator ==(Object other) { - if (other is! ImmichRemoteImageProvider) return false; if (identical(this, other)) return true; - return assetId == other.assetId; + if (other is ImmichRemoteThumbnailProvider) { + return assetId == other.assetId; + } + + return false; } @override diff --git a/mobile/lib/modules/backup/models/backup_state.model.dart b/mobile/lib/modules/backup/models/backup_state.model.dart index dd90251b8..3a9003731 100644 --- a/mobile/lib/modules/backup/models/backup_state.model.dart +++ b/mobile/lib/modules/backup/models/backup_state.model.dart @@ -21,6 +21,11 @@ class BackUpState { final BackUpProgressEnum backupProgress; final List allAssetsInDatabase; final double progressInPercentage; + final String progressInFileSize; + final double progressInFileSpeed; + final List progressInFileSpeeds; + final DateTime progressInFileSpeedUpdateTime; + final int progressInFileSpeedUpdateSentBytes; final double iCloudDownloadProgress; final CancellationToken cancelToken; final ServerDiskInfo serverInfo; @@ -48,6 +53,11 @@ class BackUpState { required this.backupProgress, required this.allAssetsInDatabase, required this.progressInPercentage, + required this.progressInFileSize, + required this.progressInFileSpeed, + required this.progressInFileSpeeds, + required this.progressInFileSpeedUpdateTime, + required this.progressInFileSpeedUpdateSentBytes, required this.iCloudDownloadProgress, required this.cancelToken, required this.serverInfo, @@ -68,6 +78,11 @@ class BackUpState { BackUpProgressEnum? backupProgress, List? allAssetsInDatabase, double? progressInPercentage, + String? progressInFileSize, + double? progressInFileSpeed, + List? progressInFileSpeeds, + DateTime? progressInFileSpeedUpdateTime, + int? progressInFileSpeedUpdateSentBytes, double? iCloudDownloadProgress, CancellationToken? cancelToken, ServerDiskInfo? serverInfo, @@ -87,6 +102,13 @@ class BackUpState { backupProgress: backupProgress ?? this.backupProgress, allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase, progressInPercentage: progressInPercentage ?? this.progressInPercentage, + progressInFileSize: progressInFileSize ?? this.progressInFileSize, + progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, + progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, + progressInFileSpeedUpdateTime: + progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, + progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? + this.progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, cancelToken: cancelToken ?? this.cancelToken, @@ -109,7 +131,7 @@ class BackUpState { @override String toString() { - return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, iCloudDownloadProgress: $iCloudDownloadProgress, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; + return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, iCloudDownloadProgress: $iCloudDownloadProgress, cancelToken: $cancelToken, serverInfo: $serverInfo, autoBackup: $autoBackup, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)'; } @override @@ -120,6 +142,12 @@ class BackUpState { return other.backupProgress == backupProgress && collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) && other.progressInPercentage == progressInPercentage && + other.progressInFileSize == progressInFileSize && + other.progressInFileSpeed == progressInFileSpeed && + collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && + other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && + other.progressInFileSpeedUpdateSentBytes == + progressInFileSpeedUpdateSentBytes && other.iCloudDownloadProgress == iCloudDownloadProgress && other.cancelToken == cancelToken && other.serverInfo == serverInfo && @@ -144,6 +172,11 @@ class BackUpState { return backupProgress.hashCode ^ allAssetsInDatabase.hashCode ^ progressInPercentage.hashCode ^ + progressInFileSize.hashCode ^ + progressInFileSpeed.hashCode ^ + progressInFileSpeeds.hashCode ^ + progressInFileSpeedUpdateTime.hashCode ^ + progressInFileSpeedUpdateSentBytes.hashCode ^ iCloudDownloadProgress.hashCode ^ cancelToken.hashCode ^ serverInfo.hashCode ^ diff --git a/mobile/lib/modules/backup/models/current_upload_asset.model.dart b/mobile/lib/modules/backup/models/current_upload_asset.model.dart index ae75f68f8..9a761c9e4 100644 --- a/mobile/lib/modules/backup/models/current_upload_asset.model.dart +++ b/mobile/lib/modules/backup/models/current_upload_asset.model.dart @@ -6,6 +6,7 @@ class CurrentUploadAsset { final DateTime fileCreatedAt; final String fileName; final String fileType; + final int? fileSize; final bool? iCloudAsset; CurrentUploadAsset({ @@ -13,6 +14,7 @@ class CurrentUploadAsset { required this.fileCreatedAt, required this.fileName, required this.fileType, + this.fileSize, this.iCloudAsset, }); @@ -21,6 +23,7 @@ class CurrentUploadAsset { DateTime? fileCreatedAt, String? fileName, String? fileType, + int? fileSize, bool? iCloudAsset, }) { return CurrentUploadAsset( @@ -28,6 +31,7 @@ class CurrentUploadAsset { fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt, fileName: fileName ?? this.fileName, fileType: fileType ?? this.fileType, + fileSize: fileSize ?? this.fileSize, iCloudAsset: iCloudAsset ?? this.iCloudAsset, ); } @@ -38,6 +42,7 @@ class CurrentUploadAsset { 'fileCreatedAt': fileCreatedAt.millisecondsSinceEpoch, 'fileName': fileName, 'fileType': fileType, + 'fileSize': fileSize, 'iCloudAsset': iCloudAsset, }; } @@ -49,6 +54,7 @@ class CurrentUploadAsset { DateTime.fromMillisecondsSinceEpoch(map['fileCreatedAt'] as int), fileName: map['fileName'] as String, fileType: map['fileType'] as String, + fileSize: map['fileSize'] as int, iCloudAsset: map['iCloudAsset'] != null ? map['iCloudAsset'] as bool : null, ); @@ -61,7 +67,7 @@ class CurrentUploadAsset { @override String toString() { - return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, iCloudAsset: $iCloudAsset)'; + return 'CurrentUploadAsset(id: $id, fileCreatedAt: $fileCreatedAt, fileName: $fileName, fileType: $fileType, fileSize: $fileSize, iCloudAsset: $iCloudAsset)'; } @override @@ -72,6 +78,7 @@ class CurrentUploadAsset { other.fileCreatedAt == fileCreatedAt && other.fileName == fileName && other.fileType == fileType && + other.fileSize == fileSize && other.iCloudAsset == iCloudAsset; } @@ -81,6 +88,7 @@ class CurrentUploadAsset { fileCreatedAt.hashCode ^ fileName.hashCode ^ fileType.hashCode ^ + fileSize.hashCode ^ iCloudAsset.hashCode; } } diff --git a/mobile/lib/modules/backup/models/manual_upload_state.model.dart b/mobile/lib/modules/backup/models/manual_upload_state.model.dart index 1a83609c2..3b56672cf 100644 --- a/mobile/lib/modules/backup/models/manual_upload_state.model.dart +++ b/mobile/lib/modules/backup/models/manual_upload_state.model.dart @@ -1,4 +1,6 @@ import 'package:cancellation_token_http/http.dart'; +import 'package:collection/collection.dart'; + import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; class ManualUploadState { @@ -14,9 +16,19 @@ class ManualUploadState { final int totalAssetsToUpload; final int successfulUploads; final double progressInPercentage; + final String progressInFileSize; + final double progressInFileSpeed; + final List progressInFileSpeeds; + final DateTime progressInFileSpeedUpdateTime; + final int progressInFileSpeedUpdateSentBytes; const ManualUploadState({ required this.progressInPercentage, + required this.progressInFileSize, + required this.progressInFileSpeed, + required this.progressInFileSpeeds, + required this.progressInFileSpeedUpdateTime, + required this.progressInFileSpeedUpdateSentBytes, required this.cancelToken, required this.currentUploadAsset, required this.totalAssetsToUpload, @@ -27,6 +39,11 @@ class ManualUploadState { ManualUploadState copyWith({ double? progressInPercentage, + String? progressInFileSize, + double? progressInFileSpeed, + List? progressInFileSpeeds, + DateTime? progressInFileSpeedUpdateTime, + int? progressInFileSpeedUpdateSentBytes, CancellationToken? cancelToken, CurrentUploadAsset? currentUploadAsset, int? totalAssetsToUpload, @@ -36,6 +53,13 @@ class ManualUploadState { }) { return ManualUploadState( progressInPercentage: progressInPercentage ?? this.progressInPercentage, + progressInFileSize: progressInFileSize ?? this.progressInFileSize, + progressInFileSpeed: progressInFileSpeed ?? this.progressInFileSpeed, + progressInFileSpeeds: progressInFileSpeeds ?? this.progressInFileSpeeds, + progressInFileSpeedUpdateTime: + progressInFileSpeedUpdateTime ?? this.progressInFileSpeedUpdateTime, + progressInFileSpeedUpdateSentBytes: progressInFileSpeedUpdateSentBytes ?? + this.progressInFileSpeedUpdateSentBytes, cancelToken: cancelToken ?? this.cancelToken, currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset, totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload, @@ -48,15 +72,22 @@ class ManualUploadState { @override String toString() { - return 'ManualUploadState(progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)'; + return 'ManualUploadState(progressInPercentage: $progressInPercentage, progressInFileSize: $progressInFileSize, progressInFileSpeed: $progressInFileSpeed, progressInFileSpeeds: $progressInFileSpeeds, progressInFileSpeedUpdateTime: $progressInFileSpeedUpdateTime, progressInFileSpeedUpdateSentBytes: $progressInFileSpeedUpdateSentBytes, cancelToken: $cancelToken, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)'; } @override bool operator ==(Object other) { if (identical(this, other)) return true; + final collectionEquals = const DeepCollectionEquality().equals; return other is ManualUploadState && other.progressInPercentage == progressInPercentage && + other.progressInFileSize == progressInFileSize && + other.progressInFileSpeed == progressInFileSpeed && + collectionEquals(other.progressInFileSpeeds, progressInFileSpeeds) && + other.progressInFileSpeedUpdateTime == progressInFileSpeedUpdateTime && + other.progressInFileSpeedUpdateSentBytes == + progressInFileSpeedUpdateSentBytes && other.cancelToken == cancelToken && other.currentUploadAsset == currentUploadAsset && other.totalAssetsToUpload == totalAssetsToUpload && @@ -68,6 +99,11 @@ class ManualUploadState { @override int get hashCode { return progressInPercentage.hashCode ^ + progressInFileSize.hashCode ^ + progressInFileSpeed.hashCode ^ + progressInFileSpeeds.hashCode ^ + progressInFileSpeedUpdateTime.hashCode ^ + progressInFileSpeedUpdateSentBytes.hashCode ^ cancelToken.hashCode ^ currentUploadAsset.hashCode ^ totalAssetsToUpload.hashCode ^ diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index 68c6bf9e6..a02ddf4e3 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -20,6 +20,7 @@ import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/server_info.service.dart'; +import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; @@ -40,6 +41,11 @@ class BackupNotifier extends StateNotifier { backupProgress: BackUpProgressEnum.idle, allAssetsInDatabase: const [], progressInPercentage: 0, + progressInFileSize: "0 B / 0 B", + progressInFileSpeed: 0, + progressInFileSpeeds: const [], + progressInFileSpeedUpdateTime: DateTime.now(), + progressInFileSpeedUpdateSentBytes: 0, cancelToken: CancellationToken(), autoBackup: Store.get(StoreKey.autoBackup, false), backgroundBackup: Store.get(StoreKey.backgroundBackup, false), @@ -63,6 +69,7 @@ class BackupNotifier extends StateNotifier { fileCreatedAt: DateTime.parse('2020-10-04'), fileName: '...', fileType: '...', + fileSize: 0, iCloudAsset: false, ), iCloudDownloadProgress: 0.0, @@ -495,6 +502,10 @@ class BackupNotifier extends StateNotifier { state = state.copyWith( backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0, + progressInFileSize: "0 B / 0 B", + progressInFileSpeed: 0, + progressInFileSpeedUpdateTime: DateTime.now(), + progressInFileSpeedUpdateSentBytes: 0, ); } @@ -535,6 +546,10 @@ class BackupNotifier extends StateNotifier { .toSet(), backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0, + progressInFileSize: "0 B / 0 B", + progressInFileSpeed: 0, + progressInFileSpeedUpdateTime: DateTime.now(), + progressInFileSpeedUpdateSentBytes: 0, ); _updatePersistentAlbumsSelection(); } @@ -543,8 +558,36 @@ class BackupNotifier extends StateNotifier { } void _onUploadProgress(int sent, int total) { + double lastUploadSpeed = state.progressInFileSpeed; + List lastUploadSpeeds = state.progressInFileSpeeds.toList(); + DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; + int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; + + final now = DateTime.now(); + final duration = now.difference(lastUpdateTime); + + // Keep the upload speed average span limited, to keep it somewhat relevant + if (lastUploadSpeeds.length > 10) { + lastUploadSpeeds.removeAt(0); + } + + if (duration.inSeconds > 0) { + lastUploadSpeeds.add( + ((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble(), + ); + + lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); + lastUpdateTime = now; + lastSentBytes = sent; + } + state = state.copyWith( progressInPercentage: (sent.toDouble() / total.toDouble() * 100), + progressInFileSize: humanReadableFileBytesProgress(sent, total), + progressInFileSpeed: lastUploadSpeed, + progressInFileSpeeds: lastUploadSpeeds, + progressInFileSpeedUpdateTime: lastUpdateTime, + progressInFileSpeedUpdateSentBytes: lastSentBytes, ); } diff --git a/mobile/lib/modules/backup/providers/manual_upload.provider.dart b/mobile/lib/modules/backup/providers/manual_upload.provider.dart index bd7756adf..6d9ecbd20 100644 --- a/mobile/lib/modules/backup/providers/manual_upload.provider.dart +++ b/mobile/lib/modules/backup/providers/manual_upload.provider.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -47,6 +48,11 @@ class ManualUploadNotifier extends StateNotifier { ) : super( ManualUploadState( progressInPercentage: 0, + progressInFileSize: "0 B / 0 B", + progressInFileSpeed: 0, + progressInFileSpeeds: const [], + progressInFileSpeedUpdateTime: DateTime.now(), + progressInFileSpeedUpdateSentBytes: 0, cancelToken: CancellationToken(), currentUploadAsset: CurrentUploadAsset( id: '...', @@ -123,9 +129,38 @@ class ManualUploadNotifier extends StateNotifier { } void _onProgress(int sent, int total) { + double lastUploadSpeed = state.progressInFileSpeed; + List lastUploadSpeeds = state.progressInFileSpeeds.toList(); + DateTime lastUpdateTime = state.progressInFileSpeedUpdateTime; + int lastSentBytes = state.progressInFileSpeedUpdateSentBytes; + + final now = DateTime.now(); + final duration = now.difference(lastUpdateTime); + + // Keep the upload speed average span limited, to keep it somewhat relevant + if (lastUploadSpeeds.length > 10) { + lastUploadSpeeds.removeAt(0); + } + + if (duration.inSeconds > 0) { + lastUploadSpeeds.add( + ((sent - lastSentBytes) / duration.inSeconds).abs().roundToDouble(), + ); + + lastUploadSpeed = lastUploadSpeeds.average.abs().roundToDouble(); + lastUpdateTime = now; + lastSentBytes = sent; + } + state = state.copyWith( progressInPercentage: (sent.toDouble() / total.toDouble() * 100), + progressInFileSize: humanReadableFileBytesProgress(sent, total), + progressInFileSpeed: lastUploadSpeed, + progressInFileSpeeds: lastUploadSpeeds, + progressInFileSpeedUpdateTime: lastUpdateTime, + progressInFileSpeedUpdateSentBytes: lastSentBytes, ); + if (state.showDetailedNotification) { final title = "backup_background_service_current_upload_notification" .tr(args: [state.currentUploadAsset.fileName]); @@ -184,6 +219,8 @@ class ManualUploadNotifier extends StateNotifier { state = state.copyWith( progressInPercentage: 0, + progressInFileSize: "0 B / 0 B", + progressInFileSpeed: 0, totalAssetsToUpload: allUploadAssets.length, successfulUploads: 0, currentAssetIndex: 0, @@ -291,7 +328,13 @@ class ManualUploadNotifier extends StateNotifier { if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) { _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); } - state = state.copyWith(progressInPercentage: 0); + state = state.copyWith( + progressInPercentage: 0, + progressInFileSize: "0 B / 0 B", + progressInFileSpeed: 0, + progressInFileSpeedUpdateTime: DateTime.now(), + progressInFileSpeedUpdateSentBytes: 0, + ); } Future uploadAssets( diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index 48d6f71cf..d4277a822 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -316,6 +316,8 @@ class BackupService { req.files.add(assetRawUploadData); + var fileSize = file.lengthSync(); + if (entity.isLivePhoto) { if (livePhotoFile != null) { final livePhotoTitle = p.setExtension( @@ -330,6 +332,7 @@ class BackupService { filename: livePhotoTitle, ); req.files.add(livePhotoRawUploadData); + fileSize += livePhotoFile.lengthSync(); } else { _log.warning( "Failed to obtain motion part of the livePhoto - $originalFileName", @@ -345,6 +348,7 @@ class BackupService { : entity.createDateTime, fileName: originalFileName, fileType: _getAssetType(entity.type), + fileSize: fileSize, iCloudAsset: false, ), ); diff --git a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart index 417fd3be5..35bee2f8d 100644 --- a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart +++ b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart @@ -26,10 +26,28 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { var uploadProgress = !isManualUpload ? ref.watch(backupProvider).progressInPercentage : ref.watch(manualUploadProvider).progressInPercentage; + var uploadFileProgress = !isManualUpload + ? ref.watch(backupProvider).progressInFileSize + : ref.watch(manualUploadProvider).progressInFileSize; + var uploadFileSpeed = !isManualUpload + ? ref.watch(backupProvider).progressInFileSpeed + : ref.watch(manualUploadProvider).progressInFileSpeed; var iCloudDownloadProgress = ref.watch(backupProvider).iCloudDownloadProgress; final isShowThumbnail = useState(false); + String formatUploadFileSpeed(double uploadFileSpeed) { + if (uploadFileSpeed < 1024) { + return '${uploadFileSpeed.toStringAsFixed(2)} B/s'; + } else if (uploadFileSpeed < 1024 * 1024) { + return '${(uploadFileSpeed / 1024).toStringAsFixed(2)} KB/s'; + } else if (uploadFileSpeed < 1024 * 1024 * 1024) { + return '${(uploadFileSpeed / (1024 * 1024)).toStringAsFixed(2)} MB/s'; + } else { + return '${(uploadFileSpeed / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB/s'; + } + } + String getAssetCreationDate() { return DateFormat.yMMMMd().format( DateTime.parse( @@ -202,7 +220,26 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ), Text( " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12), + style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"), + ), + ], + ), + ); + } + + buildUploadStats() { + return Padding( + padding: const EdgeInsets.only(top: 2.0, bottom: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + uploadFileProgress, + style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"), + ), + Text( + formatUploadFileSpeed(uploadFileSpeed), + style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"), ), ], ), @@ -265,6 +302,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { children: [ if (Platform.isIOS) buildiCloudDownloadProgerssBar(), buildUploadProgressBar(), + buildUploadStats(), Padding( padding: const EdgeInsets.only(top: 8.0), child: buildAssetInfoTable(), diff --git a/mobile/lib/shared/ui/immich_image.dart b/mobile/lib/shared/ui/immich_image.dart index f06f1726a..2e25a67b0 100644 --- a/mobile/lib/shared/ui/immich_image.dart +++ b/mobile/lib/shared/ui/immich_image.dart @@ -42,7 +42,6 @@ class ImmichImage extends StatelessWidget { if (asset == null) { return ImmichRemoteImageProvider( assetId: assetId!, - isThumbnail: false, ); } @@ -53,7 +52,6 @@ class ImmichImage extends StatelessWidget { } else { return ImmichRemoteImageProvider( assetId: asset.remoteId!, - isThumbnail: false, ); } } diff --git a/mobile/lib/shared/ui/immich_thumbnail.dart b/mobile/lib/shared/ui/immich_thumbnail.dart index 77827348d..3bfd16425 100644 --- a/mobile/lib/shared/ui/immich_thumbnail.dart +++ b/mobile/lib/shared/ui/immich_thumbnail.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart'; -import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; @@ -38,9 +38,8 @@ class ImmichThumbnail extends HookWidget { } if (asset == null) { - return ImmichRemoteImageProvider( + return ImmichRemoteThumbnailProvider( assetId: assetId!, - isThumbnail: true, ); } @@ -51,9 +50,10 @@ class ImmichThumbnail extends HookWidget { width: thumbnailSize, ); } else { - return ImmichRemoteImageProvider( + return ImmichRemoteThumbnailProvider( assetId: asset.remoteId!, - isThumbnail: true, + height: thumbnailSize, + width: thumbnailSize, ); } } diff --git a/mobile/lib/utils/backup_progress.dart b/mobile/lib/utils/backup_progress.dart index f24e8c6cf..38cdeacdb 100644 --- a/mobile/lib/utils/backup_progress.dart +++ b/mobile/lib/utils/backup_progress.dart @@ -10,6 +10,25 @@ String formatAssetBackupProgress(int uploadedAssets, int assetsToUpload) { return "$percent% ($uploadedAssets/$assetsToUpload)"; } +/// prints progress in useful (kilo/mega/giga)bytes +String humanReadableFileBytesProgress(int bytes, int bytesTotal) { + String unit = "KB"; + + if (bytesTotal >= 0x40000000) { + unit = "GB"; + bytes >>= 20; + bytesTotal >>= 20; + } else if (bytesTotal >= 0x100000) { + unit = "MB"; + bytes >>= 10; + bytesTotal >>= 10; + } else if (bytesTotal < 0x400) { + return "${(bytes).toStringAsFixed(2)} B / ${(bytesTotal).toStringAsFixed(2)} B"; + } + + return "${(bytes / 1024.0).toStringAsFixed(2)} $unit / ${(bytesTotal / 1024.0).toStringAsFixed(2)} $unit"; +} + /// prints percentage and absolute progress in useful (kilo/mega/giga)bytes String humanReadableBytesProgress(int bytes, int bytesTotal) { String unit = "KB"; // Kilobyte diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts index cbbf55551..bd7b808e9 100644 --- a/server/e2e/jobs/specs/library.e2e-spec.ts +++ b/server/e2e/jobs/specs/library.e2e-spec.ts @@ -34,117 +34,7 @@ describe(`${LibraryController.name} (e2e)`, () => { await restoreTempFolder(); }); - describe('DELETE /library/:id', () => { - it('should delete an external library with assets', async () => { - const library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], - }); - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(assets.length).toBeGreaterThan(2); - - const { status } = await request(server) - .delete(`/library/${library.id}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(204); - - const libraries = await api.libraryApi.getAll(server, admin.accessToken); - expect(libraries).toHaveLength(1); - expect(libraries).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: library.id, - }), - ]), - ); - }); - }); - describe('POST /library/:id/scan', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).post(`/library/${uuidStub.notFound}/scan`).send({}); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should scan external library with import paths', async () => { - const library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], - }); - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: AssetType.IMAGE, - originalFileName: 'el_torcal_rocks', - libraryId: library.id, - resized: true, - thumbhash: expect.any(String), - exifInfo: expect.objectContaining({ - exifImageWidth: 512, - exifImageHeight: 341, - latitude: null, - longitude: null, - }), - }), - expect.objectContaining({ - type: AssetType.IMAGE, - originalFileName: 'silver_fir', - libraryId: library.id, - resized: true, - thumbhash: expect.any(String), - exifInfo: expect.objectContaining({ - exifImageWidth: 511, - exifImageHeight: 323, - latitude: null, - longitude: null, - }), - }), - ]), - ); - }); - - it('should scan external library with exclusion pattern', async () => { - const library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], - exclusionPatterns: ['**/el_corcal*'], - }); - - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toEqual( - expect.arrayContaining([ - expect.not.objectContaining({ - // Excluded by exclusion pattern - originalFileName: 'el_torcal_rocks', - }), - expect.objectContaining({ - type: AssetType.IMAGE, - originalFileName: 'silver_fir', - libraryId: library.id, - resized: true, - exifInfo: expect.objectContaining({ - exifImageWidth: 511, - exifImageHeight: 323, - latitude: null, - longitude: null, - }), - }), - ]), - ); - }); - it('should offline a missing file when called with checkForOffline', async () => { await fs.promises.cp(`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_TEMP_PATH}/albums/nature`, { recursive: true, @@ -421,19 +311,6 @@ describe(`${LibraryController.name} (e2e)`, () => { ); }); }); - - it('should not scan an upload library', async () => { - const library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.UPLOAD, - }); - - const { status, body } = await request(server) - .post(`/library/${library.id}/scan`) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Can only refresh external libraries')); - }); }); describe('POST /library/:id/removeOffline', () => { diff --git a/server/package-lock.json b/server/package-lock.json index 1808d316a..d4ecf0e66 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -34,8 +34,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", - "exiftool-vendored": "~24.5.0", - "exiftool-vendored.pl": "12.76", + "exiftool-vendored": "~24.6.0", + "exiftool-vendored.pl": "12.78", "fast-glob": "^3.3.2", "fluent-ffmpeg": "^2.1.2", "geo-tz": "^8.0.0", @@ -50,7 +50,6 @@ "mnemonist": "^0.39.8", "nest-commander": "^3.11.1", "nestjs-otel": "^5.1.5", - "node-addon-api": "^7.0.0", "openid-client": "^5.4.3", "pg": "^8.11.3", "picomatch": "^4.0.0", @@ -4017,9 +4016,9 @@ } }, "node_modules/@photostructure/tz-lookup": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.1.tgz", - "integrity": "sha512-inMfhc1QVKheq/PHF0v2vRPnPzTljxscuOKK95o3VlZA4T4w4DeYIpu7dC4W1EyjUhfZJlCBUudpmnFGNCqTog==" + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.2.tgz", + "integrity": "sha512-H8+tTt7ilJNkFyb+QgPnLEGUjQzGwiMb9n7lwRZNBgSKL3VZs9AkjI1E//FcwPjNafwAH932U92+xTqJiF3Bbw==" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -7676,34 +7675,34 @@ "dev": true }, "node_modules/exiftool-vendored": { - "version": "24.5.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz", - "integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==", + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.6.0.tgz", + "integrity": "sha512-jGjsoeYmR9VUrlZn0j1wcxMVi5y8C7A4FAa4vm3/l7ThT8d0f+jRcBqtdjaf+P5Ds/F4OgUq+ee/fRVhLy2DrA==", "dependencies": { - "@photostructure/tz-lookup": "^9.0.1", + "@photostructure/tz-lookup": "^9.0.2", "@types/luxon": "^3.4.2", "batch-cluster": "^13.0.0", "he": "^1.2.0", "luxon": "^3.4.4" }, "optionalDependencies": { - "exiftool-vendored.exe": "12.76.0", - "exiftool-vendored.pl": "12.76.0" + "exiftool-vendored.exe": "12.78.0", + "exiftool-vendored.pl": "12.78.0" } }, "node_modules/exiftool-vendored.exe": { - "version": "12.76.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz", - "integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==", + "version": "12.78.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.78.0.tgz", + "integrity": "sha512-eMN7L67sb89xi8sN7INPg19uwa1KibG2oOyGcfOvB47h+1hzmGgivVu/SZIMeOToVIbLRwUl+AFwLYSTNXsJEg==", "optional": true, "os": [ "win32" ] }, "node_modules/exiftool-vendored.pl": { - "version": "12.76.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz", - "integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==", + "version": "12.78.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.78.0.tgz", + "integrity": "sha512-K8j9NgxRpTFskFuXEl0AGsc692yYyThe4i3SXgx7xc0fu/vwD2c7tRGljkEtvaweYnMmfrF4DhCpuTu0aux6sg==", "os": [ "!win32" ] @@ -10679,14 +10678,6 @@ "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, - "node_modules/node-addon-api": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", - "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", - "engines": { - "node": "^16 || ^18 || >= 20" - } - }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -17074,9 +17065,9 @@ } }, "@photostructure/tz-lookup": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.1.tgz", - "integrity": "sha512-inMfhc1QVKheq/PHF0v2vRPnPzTljxscuOKK95o3VlZA4T4w4DeYIpu7dC4W1EyjUhfZJlCBUudpmnFGNCqTog==" + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.2.tgz", + "integrity": "sha512-H8+tTt7ilJNkFyb+QgPnLEGUjQzGwiMb9n7lwRZNBgSKL3VZs9AkjI1E//FcwPjNafwAH932U92+xTqJiF3Bbw==" }, "@pkgjs/parseargs": { "version": "0.11.0", @@ -19950,15 +19941,15 @@ } }, "exiftool-vendored": { - "version": "24.5.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz", - "integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==", + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.6.0.tgz", + "integrity": "sha512-jGjsoeYmR9VUrlZn0j1wcxMVi5y8C7A4FAa4vm3/l7ThT8d0f+jRcBqtdjaf+P5Ds/F4OgUq+ee/fRVhLy2DrA==", "requires": { - "@photostructure/tz-lookup": "^9.0.1", + "@photostructure/tz-lookup": "^9.0.2", "@types/luxon": "^3.4.2", "batch-cluster": "^13.0.0", - "exiftool-vendored.exe": "12.76.0", - "exiftool-vendored.pl": "12.76.0", + "exiftool-vendored.exe": "12.78.0", + "exiftool-vendored.pl": "12.78.0", "he": "^1.2.0", "luxon": "^3.4.4" }, @@ -19971,15 +19962,15 @@ } }, "exiftool-vendored.exe": { - "version": "12.76.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz", - "integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==", + "version": "12.78.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.78.0.tgz", + "integrity": "sha512-eMN7L67sb89xi8sN7INPg19uwa1KibG2oOyGcfOvB47h+1hzmGgivVu/SZIMeOToVIbLRwUl+AFwLYSTNXsJEg==", "optional": true }, "exiftool-vendored.pl": { - "version": "12.76.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz", - "integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==" + "version": "12.78.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.78.0.tgz", + "integrity": "sha512-K8j9NgxRpTFskFuXEl0AGsc692yYyThe4i3SXgx7xc0fu/vwD2c7tRGljkEtvaweYnMmfrF4DhCpuTu0aux6sg==" }, "exit": { "version": "0.1.2", @@ -22263,11 +22254,6 @@ "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, - "node-addon-api": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", - "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==" - }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", diff --git a/server/package.json b/server/package.json index 9faa96608..94bc3f090 100644 --- a/server/package.json +++ b/server/package.json @@ -58,8 +58,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", - "exiftool-vendored": "~24.5.0", - "exiftool-vendored.pl": "12.76", + "exiftool-vendored": "~24.6.0", + "exiftool-vendored.pl": "12.78", "fast-glob": "^3.3.2", "fluent-ffmpeg": "^2.1.2", "geo-tz": "^8.0.0", @@ -74,7 +74,6 @@ "mnemonist": "^0.39.8", "nest-commander": "^3.11.1", "nestjs-otel": "^5.1.5", - "node-addon-api": "^7.0.0", "openid-client": "^5.4.3", "pg": "^8.11.3", "picomatch": "^4.0.0", diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 0b8dea717..8ba5d93ba 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -113,6 +113,7 @@ const validImages = [ '.sr2', '.srf', '.srw', + '.svg', '.tiff', '.webp', '.x3f', diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index dd5408506..857f1648f 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -22,6 +22,7 @@ import { ISystemConfigRepository, IUserRepository, JobItem, + JobStatus, TimeBucketOptions, } from '../repositories'; import { StorageCore, StorageFolder } from '../storage'; @@ -384,7 +385,7 @@ export class AssetService { this.communicationRepository.send(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, ids); } - async handleAssetDeletionCheck() { + async handleAssetDeletionCheck(): Promise { const config = await this.configCore.getConfig(); const trashedDays = config.trash.enabled ? config.trash.days : 0; const trashedBefore = DateTime.now() @@ -400,10 +401,10 @@ export class AssetService { ); } - return true; + return JobStatus.SUCCESS; } - async handleAssetDeletion(job: IAssetDeletionJob) { + async handleAssetDeletion(job: IAssetDeletionJob): Promise { const { id, fromExternal } = job; const asset = await this.assetRepository.getById(id, { @@ -416,12 +417,12 @@ export class AssetService { }); if (!asset) { - return false; + return JobStatus.FAILED; } // Ignore requests that are not from external library job but is for an external asset if (!fromExternal && (!asset.library || asset.library.type === LibraryType.EXTERNAL)) { - return false; + return JobStatus.SKIPPED; } // Replace the parent of the stack children with a new asset @@ -456,7 +457,7 @@ export class AssetService { await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } }); } - return true; + return JobStatus.SUCCESS; } async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise { diff --git a/server/src/domain/audit/audit.service.spec.ts b/server/src/domain/audit/audit.service.spec.ts index 861e0edc1..82c6cc699 100644 --- a/server/src/domain/audit/audit.service.spec.ts +++ b/server/src/domain/audit/audit.service.spec.ts @@ -18,6 +18,7 @@ import { IPersonRepository, IStorageRepository, IUserRepository, + JobStatus, } from '../repositories'; import { AuditService } from './audit.service'; @@ -48,8 +49,8 @@ describe(AuditService.name, () => { describe('handleCleanup', () => { it('should delete old audit entries', async () => { - await expect(sut.handleCleanup()).resolves.toBe(true); - expect(auditMock.removeBefore).toBeCalledWith(expect.any(Date)); + await expect(sut.handleCleanup()).resolves.toBe(JobStatus.SUCCESS); + expect(auditMock.removeBefore).toHaveBeenCalledWith(expect.any(Date)); }); }); diff --git a/server/src/domain/audit/audit.service.ts b/server/src/domain/audit/audit.service.ts index a7c003fad..91ebd78ee 100644 --- a/server/src/domain/audit/audit.service.ts +++ b/server/src/domain/audit/audit.service.ts @@ -16,6 +16,7 @@ import { IPersonRepository, IStorageRepository, IUserRepository, + JobStatus, } from '../repositories'; import { StorageCore, StorageFolder } from '../storage'; import { @@ -44,9 +45,9 @@ export class AuditService { this.access = AccessCore.create(accessRepository); } - async handleCleanup(): Promise { + async handleCleanup(): Promise { await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); - return true; + return JobStatus.SUCCESS; } async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise { diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/domain/domain.constant.spec.ts index 154128a1c..70944328e 100644 --- a/server/src/domain/domain.constant.spec.ts +++ b/server/src/domain/domain.constant.spec.ts @@ -41,6 +41,7 @@ describe('mimeTypes', () => { { mimetype: 'image/sr2', extension: '.sr2' }, { mimetype: 'image/srf', extension: '.srf' }, { mimetype: 'image/srw', extension: '.srw' }, + { mimetype: 'image/svg', extension: '.svg' }, { mimetype: 'image/tiff', extension: '.tif' }, { mimetype: 'image/tiff', extension: '.tiff' }, { mimetype: 'image/webp', extension: '.webp' }, diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index b723474dd..56b455855 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -133,13 +133,14 @@ const image: Record = { '.sr2': ['image/sr2', 'image/x-sony-sr2'], '.srf': ['image/srf', 'image/x-sony-srf'], '.srw': ['image/srw', 'image/x-samsung-srw'], + '.svg': ['image/svg'], '.tif': ['image/tiff'], '.tiff': ['image/tiff'], '.webp': ['image/webp'], '.x3f': ['image/x3f', 'image/x-sigma-x3f'], }; -const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp']); +const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']); const profile: Record = Object.fromEntries( Object.entries(image).filter(([key]) => profileExtensions.has(key)), ); diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts index 9ed738926..c2133a623 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/domain/job/job.service.spec.ts @@ -16,13 +16,14 @@ import { ISystemConfigRepository, JobHandler, JobItem, + JobStatus, } from '../repositories'; import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core'; import { JobCommand, JobName, QueueName } from './job.constants'; import { JobService } from './job.service'; -const makeMockHandlers = (success: boolean) => { - const mock = jest.fn().mockResolvedValue(success); +const makeMockHandlers = (status: JobStatus) => { + const mock = jest.fn().mockResolvedValue(status); return Object.fromEntries(Object.values(JobName).map((jobName) => [jobName, mock])) as unknown as Record< JobName, JobHandler @@ -221,13 +222,13 @@ describe(JobService.name, () => { describe('init', () => { it('should register a handler for each queue', async () => { - await sut.init(makeMockHandlers(true)); + await sut.init(makeMockHandlers(JobStatus.SUCCESS)); expect(configMock.load).toHaveBeenCalled(); expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length); }); it('should subscribe to config changes', async () => { - await sut.init(makeMockHandlers(false)); + await sut.init(makeMockHandlers(JobStatus.FAILED)); SystemConfigCore.create(newSystemConfigRepositoryMock(false)).config$.next({ job: { @@ -332,7 +333,7 @@ describe(JobService.name, () => { } } - await sut.init(makeMockHandlers(true)); + await sut.init(makeMockHandlers(JobStatus.SUCCESS)); await jobMock.addHandler.mock.calls[0][2](item); if (jobs.length > 1) { @@ -348,7 +349,7 @@ describe(JobService.name, () => { }); it(`should not queue any jobs when ${item.name} finishes with 'false'`, async () => { - await sut.init(makeMockHandlers(false)); + await sut.init(makeMockHandlers(JobStatus.FAILED)); await jobMock.addHandler.mock.calls[0][2](item); expect(jobMock.queueAll).not.toHaveBeenCalled(); diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index 129e482bd..e00636ad6 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -11,6 +11,7 @@ import { ISystemConfigRepository, JobHandler, JobItem, + JobStatus, QueueCleanType, } from '../repositories'; import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core'; @@ -155,8 +156,8 @@ export class JobService { try { const handler = jobHandlers[name]; - const success = await handler(data); - if (success) { + const status = await handler(data); + if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) { await this.onDone(item); } } catch (error: Error | any) { diff --git a/server/src/domain/library/library.dto.ts b/server/src/domain/library/library.dto.ts index 0d7ac7c4d..22c70ac99 100644 --- a/server/src/domain/library/library.dto.ts +++ b/server/src/domain/library/library.dto.ts @@ -89,7 +89,7 @@ export class ValidateLibraryResponseDto { export class ValidateLibraryImportPathResponseDto { importPath!: string; - isValid?: boolean = false; + isValid: boolean = false; message?: string; } diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index 70113915a..d9ad2aefe 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -18,6 +18,7 @@ import { userStub, } from '@test'; import { when } from 'jest-when'; +import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job'; import { @@ -28,6 +29,7 @@ import { ILibraryRepository, IStorageRepository, ISystemConfigRepository, + JobStatus, StorageEventType, } from '../repositories'; import { SystemConfigCore } from '../system-config/system-config.core'; @@ -214,7 +216,7 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1); - await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(false); + await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED); }); it('should ignore import paths that do not exist', async () => { @@ -320,7 +322,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.image); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.create.mock.calls).toEqual([ [ @@ -368,7 +370,7 @@ describe(LibraryService.name, () => { assetMock.create.mockResolvedValue(assetStub.image); storageMock.checkFileExists.mockResolvedValue(true); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.create.mock.calls).toEqual([ [ @@ -415,7 +417,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.video); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.create.mock.calls).toEqual([ [ @@ -471,7 +473,7 @@ describe(LibraryService.name, () => { assetMock.create.mockResolvedValue(assetStub.image); libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() }); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(false); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED); expect(assetMock.create.mock.calls).toEqual([]); }); @@ -492,7 +494,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); @@ -509,7 +511,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); assetMock.create.mockResolvedValue(assetStub.image); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.METADATA_EXTRACTION, @@ -540,7 +542,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); assetMock.create.mockResolvedValue(assetStub.image); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true }); expect(jobMock.queue).not.toHaveBeenCalled(); @@ -558,7 +560,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.offline); assetMock.create.mockResolvedValue(assetStub.offline); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false }); @@ -591,7 +593,7 @@ describe(LibraryService.name, () => { expect(assetMock.save).not.toHaveBeenCalled(); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); }); it('should refresh an existing asset if forced', async () => { @@ -605,7 +607,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); assetMock.create.mockResolvedValue(assetStub.image); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], { fileCreatedAt: new Date('2023-01-01'), @@ -633,7 +635,7 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.image); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true); + await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.create).toHaveBeenCalled(); const createdAsset = assetMock.create.mock.calls[0][0]; @@ -1056,7 +1058,7 @@ describe(LibraryService.name, () => { describe('handleQueueCleanup', () => { it('should queue cleanup jobs', async () => { libraryMock.getAllDeleted.mockResolvedValue([libraryStub.uploadLibrary1, libraryStub.externalLibrary1]); - await expect(sut.handleQueueCleanup()).resolves.toBe(true); + await expect(sut.handleQueueCleanup()).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queueAll).toHaveBeenCalledWith([ { name: JobName.LIBRARY_DELETE, data: { id: libraryStub.uploadLibrary1.id } }, @@ -1343,7 +1345,7 @@ describe(LibraryService.name, () => { libraryMock.getAssetIds.mockResolvedValue([]); libraryMock.delete.mockImplementation(async () => {}); - await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(false); + await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.FAILED); }); it('should delete an empty library', async () => { @@ -1351,7 +1353,7 @@ describe(LibraryService.name, () => { libraryMock.getAssetIds.mockResolvedValue([]); libraryMock.delete.mockImplementation(async () => {}); - await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(true); + await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); }); it('should delete a library with assets', async () => { @@ -1361,7 +1363,7 @@ describe(LibraryService.name, () => { assetMock.getById.mockResolvedValue(assetStub.image1); - await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(true); + await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); }); }); @@ -1494,7 +1496,7 @@ describe(LibraryService.name, () => { it('should queue the refresh job', async () => { libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]); - await expect(sut.handleQueueAllScan({})).resolves.toBe(true); + await expect(sut.handleQueueAllScan({})).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queue.mock.calls).toEqual([ [ @@ -1519,7 +1521,7 @@ describe(LibraryService.name, () => { it('should queue the force refresh job', async () => { libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]); - await expect(sut.handleQueueAllScan({ force: true })).resolves.toBe(true); + await expect(sut.handleQueueAllScan({ force: true })).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.LIBRARY_QUEUE_CLEANUP, @@ -1544,7 +1546,7 @@ describe(LibraryService.name, () => { assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false }); assetMock.getById.mockResolvedValue(assetStub.image1); - await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(true); + await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queueAll).toHaveBeenCalledWith([ { @@ -1650,5 +1652,32 @@ describe(LibraryService.name, () => { }, ]); }); + + it('should detect when import path is in immich media folder', async () => { + storageMock.stat.mockResolvedValue({ isDirectory: () => true } as Stats); + const validImport = libraryStub.hasImmichPaths.importPaths[1]; + when(storageMock.checkFileExists).calledWith(validImport, R_OK).mockResolvedValue(true); + + const result = await sut.validate(authStub.external1, libraryStub.hasImmichPaths.id, { + importPaths: libraryStub.hasImmichPaths.importPaths, + }); + + expect(result.importPaths).toEqual([ + { + importPath: libraryStub.hasImmichPaths.importPaths[0], + isValid: false, + message: 'Cannot use media upload folder for external libraries', + }, + { + importPath: validImport, + isValid: true, + }, + { + importPath: libraryStub.hasImmichPaths.importPaths[2], + isValid: false, + message: 'Cannot use media upload folder for external libraries', + }, + ]); + }); }); }); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index 45ae7fa3d..c01a5cf36 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -21,9 +21,11 @@ import { ILibraryRepository, IStorageRepository, ISystemConfigRepository, + JobStatus, StorageEventType, WithProperty, } from '../repositories'; +import { StorageCore } from '../storage'; import { SystemConfigCore } from '../system-config'; import { CreateLibraryDto, @@ -240,13 +242,13 @@ export class LibraryService extends EventEmitter { return libraries.map((library) => mapLibrary(library)); } - async handleQueueCleanup(): Promise { + async handleQueueCleanup(): Promise { this.logger.debug('Cleaning up any pending library deletions'); const pendingDeletion = await this.repository.getAllDeleted(); await this.jobRepository.queueAll( pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })), ); - return true; + return JobStatus.SUCCESS; } async create(auth: AuthDto, dto: CreateLibraryDto): Promise { @@ -325,9 +327,13 @@ export class LibraryService extends EventEmitter { const validation = new ValidateLibraryImportPathResponseDto(); validation.importPath = importPath; + if (StorageCore.isImmichPath(importPath)) { + validation.message = 'Cannot use media upload folder for external libraries'; + return validation; + } + try { const stat = await this.storageRepository.stat(importPath); - if (!stat.isDirectory()) { validation.message = 'Not a directory'; return validation; @@ -409,10 +415,10 @@ export class LibraryService extends EventEmitter { await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } }); } - async handleDeleteLibrary(job: IEntityJob): Promise { + async handleDeleteLibrary(job: IEntityJob): Promise { const library = await this.repository.get(job.id, true); if (!library) { - return false; + return JobStatus.FAILED; } // TODO use pagination @@ -426,10 +432,10 @@ export class LibraryService extends EventEmitter { this.logger.log(`Deleting library ${job.id}`); await this.repository.delete(job.id); } - return true; + return JobStatus.SUCCESS; } - async handleAssetRefresh(job: ILibraryFileJob) { + async handleAssetRefresh(job: ILibraryFileJob): Promise { const assetPath = path.normalize(job.assetPath); const existingAssetEntity = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath); @@ -444,7 +450,7 @@ export class LibraryService extends EventEmitter { this.logger.debug(`Marking asset as offline: ${assetPath}`); await this.assetRepository.save({ id: existingAssetEntity.id, isOffline: true }); - return true; + return JobStatus.SUCCESS; } else { // File can't be accessed and does not already exist in db throw new BadRequestException('Cannot access file', { cause: error }); @@ -482,7 +488,7 @@ export class LibraryService extends EventEmitter { if (!doImport && !doRefresh) { // If we don't import, exit here - return true; + return JobStatus.SKIPPED; } let assetType: AssetType; @@ -508,7 +514,7 @@ export class LibraryService extends EventEmitter { const library = await this.repository.get(job.id, true); if (library?.deletedAt) { this.logger.error('Cannot import asset into deleted library'); - return false; + return JobStatus.FAILED; } const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`); @@ -539,7 +545,7 @@ export class LibraryService extends EventEmitter { }); } else { // Not importing and not refreshing, do nothing - return true; + return JobStatus.SKIPPED; } this.logger.debug(`Queuing metadata extraction for: ${assetPath}`); @@ -550,7 +556,7 @@ export class LibraryService extends EventEmitter { await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: assetId } }); } - return true; + return JobStatus.SUCCESS; } async queueScan(auth: AuthDto, id: string, dto: ScanLibraryDto) { @@ -590,7 +596,7 @@ export class LibraryService extends EventEmitter { }); } - async handleQueueAllScan(job: IBaseJob): Promise { + async handleQueueAllScan(job: IBaseJob): Promise { this.logger.debug(`Refreshing all external libraries: force=${job.force}`); // Queue cleanup @@ -608,7 +614,7 @@ export class LibraryService extends EventEmitter { }, })), ); - return true; + return JobStatus.SUCCESS; } async handleQueueOnlineStatusCheck(job: IEntityJob): Promise { @@ -651,7 +657,7 @@ export class LibraryService extends EventEmitter { return true; } - async handleOfflineRemoval(job: IEntityJob): Promise { + async handleOfflineRemoval(job: IEntityJob): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id), ); @@ -663,14 +669,14 @@ export class LibraryService extends EventEmitter { ); } - return true; + return JobStatus.SUCCESS; } - async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise { + async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise { const library = await this.repository.get(job.id); if (!library || library.type !== LibraryType.EXTERNAL) { this.logger.warn('Can only refresh external libraries'); - return false; + return JobStatus.FAILED; } this.logger.log(`Refreshing library: ${job.id}`); @@ -728,7 +734,7 @@ export class LibraryService extends EventEmitter { await this.repository.update({ id: job.id, refreshedAt: new Date() }); - return true; + return JobStatus.SUCCESS; } private async findOrFail(id: string) { diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts index 8a6eae4cc..beea126bf 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/domain/media/media.service.spec.ts @@ -34,6 +34,7 @@ import { IPersonRepository, IStorageRepository, ISystemConfigRepository, + JobStatus, WithoutProperty, } from '../repositories'; import { MediaService } from './media.service'; @@ -1214,22 +1215,22 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).not.toHaveBeenCalled(); }); - it('should return false if hwaccel is enabled for an unsupported codec', async () => { + it('should fail if hwaccel is enabled for an unsupported codec', async () => { mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); configMock.load.mockResolvedValue([ { key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC }, { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 }, ]); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toEqual(false); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); - it('should return false if hwaccel option is invalid', async () => { + it('should fail if hwaccel option is invalid', async () => { mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: 'invalid' }]); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toEqual(false); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); @@ -1548,12 +1549,12 @@ describe(MediaService.name, () => { ); }); - it('should return false for qsv if no hw devices', async () => { + it('should fail for qsv if no hw devices', async () => { storageMock.readdir.mockResolvedValue([]); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV }]); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toEqual(false); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); @@ -1777,12 +1778,12 @@ describe(MediaService.name, () => { ); }); - it('should return false for vaapi if no hw devices', async () => { + it('should fail for vaapi if no hw devices', async () => { storageMock.readdir.mockResolvedValue([]); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toEqual(false); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); diff --git a/server/src/domain/media/media.service.ts b/server/src/domain/media/media.service.ts index c7e9798c8..9d522d104 100644 --- a/server/src/domain/media/media.service.ts +++ b/server/src/domain/media/media.service.ts @@ -24,6 +24,7 @@ import { IStorageRepository, ISystemConfigRepository, JobItem, + JobStatus, VideoCodecHWConfig, VideoStreamInfo, WithoutProperty, @@ -70,7 +71,7 @@ export class MediaService { ); } - async handleQueueGenerateThumbnails({ force }: IBaseJob) { + async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force ? this.assetRepository.getAll(pagination) @@ -118,10 +119,10 @@ export class MediaService { await this.jobRepository.queueAll(jobs); - return true; + return JobStatus.SUCCESS; } - async handleQueueMigration() { + async handleQueueMigration(): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => this.assetRepository.getAll(pagination), ); @@ -148,31 +149,31 @@ export class MediaService { ); } - return true; + return JobStatus.SUCCESS; } - async handleAssetMigration({ id }: IEntityJob) { + async handleAssetMigration({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { - return false; + return JobStatus.FAILED; } await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL); await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL); await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO); - return true; + return JobStatus.SUCCESS; } - async handleGenerateJpegThumbnail({ id }: IEntityJob) { + async handleGenerateJpegThumbnail({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { - return false; + return JobStatus.FAILED; } const resizePath = await this.generateThumbnail(asset, 'jpeg'); await this.assetRepository.save({ id: asset.id, resizePath }); - return true; + return JobStatus.SUCCESS; } private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') { @@ -214,30 +215,30 @@ export class MediaService { return path; } - async handleGenerateWebpThumbnail({ id }: IEntityJob) { + async handleGenerateWebpThumbnail({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { - return false; + return JobStatus.FAILED; } const webpPath = await this.generateThumbnail(asset, 'webp'); await this.assetRepository.save({ id: asset.id, webpPath }); - return true; + return JobStatus.SUCCESS; } - async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise { + async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); if (!asset?.resizePath) { - return false; + return JobStatus.FAILED; } const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath); await this.assetRepository.save({ id: asset.id, thumbhash }); - return true; + return JobStatus.SUCCESS; } - async handleQueueVideoConversion(job: IBaseJob) { + async handleQueueVideoConversion(job: IBaseJob): Promise { const { force } = job; const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { @@ -252,13 +253,13 @@ export class MediaService { ); } - return true; + return JobStatus.SUCCESS; } - async handleVideoConversion({ id }: IEntityJob) { + async handleVideoConversion({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); if (!asset || asset.type !== AssetType.VIDEO) { - return false; + return JobStatus.FAILED; } const input = asset.originalPath; @@ -270,12 +271,12 @@ export class MediaService { const mainAudioStream = this.getMainStream(audioStreams); const containerExtension = format.formatName; if (!mainVideoStream || !containerExtension) { - return false; + return JobStatus.FAILED; } if (!mainVideoStream.height || !mainVideoStream.width) { this.logger.warn(`Skipped transcoding for asset ${asset.id}: no video streams found`); - return false; + return JobStatus.FAILED; } const { ffmpeg: config } = await this.configCore.getConfig(); @@ -288,7 +289,7 @@ export class MediaService { await this.assetRepository.save({ id: asset.id, encodedVideoPath: null }); } - return true; + return JobStatus.SKIPPED; } let transcodeOptions; @@ -298,7 +299,7 @@ export class MediaService { ); } catch (error) { this.logger.error(`An error occurred while configuring transcoding options: ${error}`); - return false; + return JobStatus.FAILED; } this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`); @@ -322,7 +323,7 @@ export class MediaService { await this.assetRepository.save({ id: asset.id, encodedVideoPath: output }); - return true; + return JobStatus.SUCCESS; } private getMainStream(streams: T[]): T { diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index 3c8175f40..c28c61f22 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -37,6 +37,7 @@ import { IStorageRepository, ISystemConfigRepository, ImmichTags, + JobStatus, WithoutProperty, } from '../repositories'; import { MetadataService, Orientation } from './metadata.service'; @@ -113,7 +114,7 @@ describe(MetadataService.name, () => { describe('handleLivePhotoLinking', () => { it('should handle an asset that could not be found', async () => { - await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(false); + await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled(); expect(assetMock.save).not.toHaveBeenCalled(); @@ -123,7 +124,7 @@ describe(MetadataService.name, () => { it('should handle an asset without exif info', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, exifInfo: undefined }]); - await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(false); + await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled(); expect(assetMock.save).not.toHaveBeenCalled(); @@ -133,7 +134,7 @@ describe(MetadataService.name, () => { it('should handle livePhotoCID not set', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]); - await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(true); + await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled(); expect(assetMock.save).not.toHaveBeenCalled(); @@ -148,7 +149,9 @@ describe(MetadataService.name, () => { }, ]); - await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(true); + await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe( + JobStatus.SKIPPED, + ); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true }); expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({ livePhotoCID: assetStub.livePhotoStillAsset.id, @@ -169,7 +172,9 @@ describe(MetadataService.name, () => { ]); assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); - await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(true); + await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( + JobStatus.SUCCESS, + ); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true }); expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({ livePhotoCID: assetStub.livePhotoMotionAsset.id, @@ -194,7 +199,9 @@ describe(MetadataService.name, () => { ]); assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); - await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(true); + await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( + JobStatus.SUCCESS, + ); expect(communicationMock.send).toHaveBeenCalledWith( ClientEvent.ASSET_HIDDEN, assetStub.livePhotoMotionAsset.ownerId, @@ -207,7 +214,7 @@ describe(MetadataService.name, () => { it('should queue metadata extraction for all assets without exif values', async () => { assetMock.getWithout.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); - await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(true); + await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.getWithout).toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { @@ -220,7 +227,7 @@ describe(MetadataService.name, () => { it('should queue metadata extraction for all assets', async () => { assetMock.getAll.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); - await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(true); + await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.getAll).toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { @@ -237,7 +244,7 @@ describe(MetadataService.name, () => { }); it('should handle an asset that could not be found', async () => { - await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(false); + await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.upsertExif).not.toHaveBeenCalled(); @@ -630,19 +637,13 @@ describe(MetadataService.name, () => { describe('handleSidecarSync', () => { it('should do nothing if asset could not be found', async () => { assetMock.getByIds.mockResolvedValue([]); - await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false); + await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); expect(assetMock.save).not.toHaveBeenCalled(); }); it('should do nothing if asset has no sidecar path', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false); - expect(assetMock.save).not.toHaveBeenCalled(); - }); - - it('should do nothing if asset has no sidecar path', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false); + await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); expect(assetMock.save).not.toHaveBeenCalled(); }); @@ -650,7 +651,7 @@ describe(MetadataService.name, () => { assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); storageMock.checkFileExists.mockResolvedValue(true); - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true); + await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS); expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK); expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.sidecar.id, @@ -663,7 +664,7 @@ describe(MetadataService.name, () => { storageMock.checkFileExists.mockResolvedValueOnce(false); storageMock.checkFileExists.mockResolvedValueOnce(true); - await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(true); + await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(JobStatus.SUCCESS); expect(storageMock.checkFileExists).toHaveBeenNthCalledWith( 2, assetStub.sidecarWithoutExt.sidecarPath, @@ -680,7 +681,7 @@ describe(MetadataService.name, () => { storageMock.checkFileExists.mockResolvedValueOnce(true); storageMock.checkFileExists.mockResolvedValueOnce(true); - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true); + await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS); expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(1, assetStub.sidecar.sidecarPath, constants.R_OK); expect(storageMock.checkFileExists).toHaveBeenNthCalledWith( 2, @@ -697,7 +698,7 @@ describe(MetadataService.name, () => { assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); storageMock.checkFileExists.mockResolvedValue(false); - await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true); + await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS); expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK); expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.sidecar.id, @@ -754,13 +755,13 @@ describe(MetadataService.name, () => { describe('handleSidecarWrite', () => { it('should skip assets that do not exist anymore', async () => { assetMock.getByIds.mockResolvedValue([]); - await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(false); + await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.FAILED); expect(metadataMock.writeTags).not.toHaveBeenCalled(); }); it('should skip jobs with not metadata', async () => { assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); - await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(true); + await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SKIPPED); expect(metadataMock.writeTags).not.toHaveBeenCalled(); }); @@ -778,7 +779,7 @@ describe(MetadataService.name, () => { longitude: gps, dateTimeOriginal: date, }), - ).resolves.toBe(true); + ).resolves.toBe(JobStatus.SUCCESS); expect(metadataMock.writeTags).toHaveBeenCalledWith(assetStub.sidecar.sidecarPath, { ImageDescription: description, CreationDate: date, diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 73b36f445..5f0b28fc4 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -26,6 +26,7 @@ import { IStorageRepository, ISystemConfigRepository, ImmichTags, + JobStatus, WithoutProperty, } from '../repositories'; import { StorageCore } from '../storage'; @@ -151,15 +152,15 @@ export class MetadataService { await this.repository.teardown(); } - async handleLivePhotoLinking(job: IEntityJob) { + async handleLivePhotoLinking(job: IEntityJob): Promise { const { id } = job; const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset?.exifInfo) { - return false; + return JobStatus.FAILED; } if (!asset.exifInfo.livePhotoCID) { - return true; + return JobStatus.SKIPPED; } const otherType = asset.type === AssetType.VIDEO ? AssetType.IMAGE : AssetType.VIDEO; @@ -171,7 +172,7 @@ export class MetadataService { }); if (!match) { - return true; + return JobStatus.SKIPPED; } const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset]; @@ -183,10 +184,10 @@ export class MetadataService { // Notify clients to hide the linked live photo asset this.communicationRepository.send(ClientEvent.ASSET_HIDDEN, motionAsset.ownerId, motionAsset.id); - return true; + return JobStatus.SUCCESS; } - async handleQueueMetadataExtraction(job: IBaseJob) { + async handleQueueMetadataExtraction(job: IBaseJob): Promise { const { force } = job; const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force @@ -200,13 +201,13 @@ export class MetadataService { ); } - return true; + return JobStatus.SUCCESS; } - async handleMetadataExtraction({ id }: IEntityJob) { + async handleMetadataExtraction({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { - return false; + return JobStatus.FAILED; } const { exifData, tags } = await this.exifData(asset); @@ -260,10 +261,10 @@ export class MetadataService { metadataExtractedAt: new Date(), }); - return true; + return JobStatus.SUCCESS; } - async handleQueueSidecar(job: IBaseJob) { + async handleQueueSidecar(job: IBaseJob): Promise { const { force } = job; const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force @@ -280,22 +281,22 @@ export class MetadataService { ); } - return true; + return JobStatus.SUCCESS; } - handleSidecarSync({ id }: IEntityJob) { + handleSidecarSync({ id }: IEntityJob): Promise { return this.processSidecar(id, true); } - handleSidecarDiscovery({ id }: IEntityJob) { + handleSidecarDiscovery({ id }: IEntityJob): Promise { return this.processSidecar(id, false); } - async handleSidecarWrite(job: ISidecarWriteJob) { + async handleSidecarWrite(job: ISidecarWriteJob): Promise { const { id, description, dateTimeOriginal, latitude, longitude } = job; const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { - return false; + return JobStatus.FAILED; } const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`; @@ -310,7 +311,7 @@ export class MetadataService { ); if (Object.keys(exif).length === 0) { - return true; + return JobStatus.SKIPPED; } await this.repository.writeTags(sidecarPath, exif); @@ -319,7 +320,7 @@ export class MetadataService { await this.assetRepository.save({ id, sidecarPath }); } - return true; + return JobStatus.SUCCESS; } private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) { @@ -552,19 +553,19 @@ export class MetadataService { return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS'); } - private async processSidecar(id: string, isSync: boolean) { + private async processSidecar(id: string, isSync: boolean): Promise { const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { - return false; + return JobStatus.FAILED; } if (isSync && !asset.sidecarPath) { - return false; + return JobStatus.FAILED; } if (!isSync && (!asset.isVisible || asset.sidecarPath)) { - return false; + return JobStatus.FAILED; } // XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp @@ -587,11 +588,11 @@ export class MetadataService { if (sidecarPath) { await this.assetRepository.save({ id: asset.id, sidecarPath }); - return true; + return JobStatus.SUCCESS; } if (!isSync) { - return false; + return JobStatus.FAILED; } this.logger.debug( @@ -599,6 +600,6 @@ export class MetadataService { ); await this.assetRepository.save({ id: asset.id, sidecarPath: null }); - return true; + return JobStatus.SUCCESS; } } diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 191356d2c..08b5875a5 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -34,6 +34,7 @@ import { ISearchRepository, IStorageRepository, ISystemConfigRepository, + JobStatus, WithoutProperty, } from '../repositories'; import { PersonResponseDto, mapFaces, mapPerson } from './person.dto'; @@ -357,7 +358,7 @@ describe(PersonService.name, () => { describe('handlePersonMigration', () => { it('should not move person files', async () => { personMock.getById.mockResolvedValue(null); - await expect(sut.handlePersonMigration(personStub.noName)).resolves.toStrictEqual(false); + await expect(sut.handlePersonMigration(personStub.noName)).resolves.toBe(JobStatus.FAILED); }); }); @@ -454,10 +455,10 @@ describe(PersonService.name, () => { }); describe('handleQueueDetectFaces', () => { - it('should return if machine learning is disabled', async () => { + it('should skip if machine learning is disabled', async () => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]); - await expect(sut.handleQueueDetectFaces({})).resolves.toBe(true); + await expect(sut.handleQueueDetectFaces({})).resolves.toBe(JobStatus.SKIPPED); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); expect(configMock.load).toHaveBeenCalled(); @@ -530,19 +531,19 @@ describe(PersonService.name, () => { }); describe('handleQueueRecognizeFaces', () => { - it('should return if machine learning is disabled', async () => { + it('should skip if machine learning is disabled', async () => { jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 }); configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]); - await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(true); + await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED); expect(jobMock.queueAll).not.toHaveBeenCalled(); expect(configMock.load).toHaveBeenCalled(); }); - it('should return if recognition jobs are already queued', async () => { + it('should skip if recognition jobs are already queued', async () => { jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 1, paused: 0, completed: 0, failed: 0, delayed: 0 }); - await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(true); + await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED); expect(jobMock.queueAll).not.toHaveBeenCalled(); }); @@ -612,10 +613,10 @@ describe(PersonService.name, () => { }); describe('handleDetectFaces', () => { - it('should return if machine learning is disabled', async () => { + it('should skip if machine learning is disabled', async () => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]); - await expect(sut.handleDetectFaces({ id: 'foo' })).resolves.toBe(true); + await expect(sut.handleDetectFaces({ id: 'foo' })).resolves.toBe(JobStatus.SKIPPED); expect(assetMock.getByIds).not.toHaveBeenCalled(); expect(configMock.load).toHaveBeenCalled(); }); @@ -701,31 +702,31 @@ describe(PersonService.name, () => { }); describe('handleRecognizeFaces', () => { - it('should return false if face does not exist', async () => { + it('should fail if face does not exist', async () => { personMock.getFaceByIdWithAssets.mockResolvedValue(null); - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(false); + expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.FAILED); expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.createFaces).not.toHaveBeenCalled(); }); - it('should return false if face does not have asset', async () => { + it('should fail if face does not have asset', async () => { const face = { ...faceStub.face1, asset: null } as AssetFaceEntity & { asset: null }; personMock.getFaceByIdWithAssets.mockResolvedValue(face); - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(false); + expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.FAILED); expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.create).not.toHaveBeenCalled(); expect(personMock.createFaces).not.toHaveBeenCalled(); }); - it('should return true if face already has an assigned person', async () => { + it('should skip if face already has an assigned person', async () => { personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1); - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(true); + expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.SKIPPED); expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.create).not.toHaveBeenCalled(); @@ -852,10 +853,10 @@ describe(PersonService.name, () => { }); describe('handleGeneratePersonThumbnail', () => { - it('should return if machine learning is disabled', async () => { + it('should skip if machine learning is disabled', async () => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]); - await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(true); + await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.SKIPPED); expect(assetMock.getByIds).not.toHaveBeenCalled(); expect(configMock.load).toHaveBeenCalled(); }); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 235867314..1a2233f3c 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -24,6 +24,7 @@ import { IStorageRepository, ISystemConfigRepository, JobItem, + JobStatus, UpdateFacesData, WithoutProperty, } from '../repositories'; @@ -265,16 +266,16 @@ export class PersonService { } } - async handlePersonCleanup() { + async handlePersonCleanup(): Promise { const people = await this.repository.getAllWithoutFaces(); await this.delete(people); - return true; + return JobStatus.SUCCESS; } - async handleQueueDetectFaces({ force }: IBaseJob) { + async handleQueueDetectFaces({ force }: IBaseJob): Promise { const { machineLearning } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) { - return true; + return JobStatus.SKIPPED; } if (force) { @@ -294,13 +295,13 @@ export class PersonService { ); } - return true; + return JobStatus.SUCCESS; } - async handleDetectFaces({ id }: IEntityJob) { + async handleDetectFaces({ id }: IEntityJob): Promise { const { machineLearning } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) { - return true; + return JobStatus.SKIPPED; } const relations = { @@ -311,7 +312,7 @@ export class PersonService { }; const [asset] = await this.assetRepository.getByIds([id], relations); if (!asset || !asset.resizePath || asset.faces?.length > 0) { - return false; + return JobStatus.FAILED; } const faces = await this.machineLearningRepository.detectFaces( @@ -346,13 +347,13 @@ export class PersonService { facesRecognizedAt: new Date(), }); - return true; + return JobStatus.SUCCESS; } - async handleQueueRecognizeFaces({ force }: IBaseJob) { + async handleQueueRecognizeFaces({ force }: IBaseJob): Promise { const { machineLearning } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) { - return true; + return JobStatus.SKIPPED; } await this.jobRepository.waitForQueueCompletion(QueueName.THUMBNAIL_GENERATION, QueueName.FACE_DETECTION); @@ -364,7 +365,7 @@ export class PersonService { this.logger.debug( `Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`, ); - return true; + return JobStatus.SKIPPED; } const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => @@ -377,13 +378,13 @@ export class PersonService { ); } - return true; + return JobStatus.SUCCESS; } - async handleRecognizeFaces({ id, deferred }: IDeferrableJob) { + async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise { const { machineLearning } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) { - return true; + return JobStatus.SKIPPED; } const face = await this.repository.getFaceByIdWithAssets( @@ -393,12 +394,12 @@ export class PersonService { ); if (!face || !face.asset) { this.logger.warn(`Face ${id} not found`); - return false; + return JobStatus.FAILED; } if (face.personId) { this.logger.debug(`Face ${id} already has a person assigned`); - return true; + return JobStatus.SKIPPED; } const matches = await this.smartInfoRepository.searchFaces({ @@ -411,7 +412,7 @@ export class PersonService { // `matches` also includes the face itself if (machineLearning.facialRecognition.minFaces > 1 && matches.length <= 1) { this.logger.debug(`Face ${id} only matched the face itself, skipping`); - return true; + return JobStatus.SKIPPED; } this.logger.debug(`Face ${id} has ${matches.length} matches`); @@ -420,7 +421,7 @@ export class PersonService { if (!isCore && !deferred) { this.logger.debug(`Deferring non-core face ${id} for later processing`); await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } }); - return true; + return JobStatus.SKIPPED; } let personId = matches.find((match) => match.face.personId)?.face.personId; @@ -450,34 +451,34 @@ export class PersonService { await this.repository.reassignFaces({ faceIds: [id], newPersonId: personId }); } - return true; + return JobStatus.SUCCESS; } - async handlePersonMigration({ id }: IEntityJob) { + async handlePersonMigration({ id }: IEntityJob): Promise { const person = await this.repository.getById(id); if (!person) { - return false; + return JobStatus.FAILED; } await this.storageCore.movePersonFile(person, PersonPathType.FACE); - return true; + return JobStatus.SUCCESS; } - async handleGeneratePersonThumbnail(data: IEntityJob) { + async handleGeneratePersonThumbnail(data: IEntityJob): Promise { const { machineLearning, thumbnail } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) { - return true; + return JobStatus.SKIPPED; } const person = await this.repository.getById(data.id); if (!person?.faceAssetId) { - return false; + return JobStatus.FAILED; } const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId); if (face === null) { - return false; + return JobStatus.FAILED; } const { @@ -492,7 +493,7 @@ export class PersonService { const [asset] = await this.assetRepository.getByIds([assetId]); if (!asset?.resizePath) { - return false; + return JobStatus.FAILED; } this.logger.verbose(`Cropping face for person: ${person.id}`); const thumbnailPath = StorageCore.getPersonThumbnailPath(person); @@ -533,7 +534,7 @@ export class PersonService { await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions); await this.repository.update({ id: person.id, thumbnailPath }); - return true; + return JobStatus.SUCCESS; } async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise { diff --git a/server/src/domain/repositories/job.repository.ts b/server/src/domain/repositories/job.repository.ts index ada5e84b4..b3a280e61 100644 --- a/server/src/domain/repositories/job.repository.ts +++ b/server/src/domain/repositories/job.repository.ts @@ -96,7 +96,13 @@ export type JobItem = | { name: JobName.LIBRARY_CHECK_IF_ASSET_ONLINE; data: IEntityJob } | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }; -export type JobHandler = (data: T) => boolean | Promise; +export enum JobStatus { + SUCCESS = 'success', + FAILED = 'failed', + SKIPPED = 'skipped', +} + +export type JobHandler = (data: T) => Promise; export type JobItemHandler = (item: JobItem) => Promise; export const IJobRepository = 'IJobRepository'; diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/domain/smart-info/smart-info.service.ts index 974646f5e..b7dd1a91f 100644 --- a/server/src/domain/smart-info/smart-info.service.ts +++ b/server/src/domain/smart-info/smart-info.service.ts @@ -10,6 +10,7 @@ import { IMachineLearningRepository, ISearchRepository, ISystemConfigRepository, + JobStatus, WithoutProperty, } from '../repositories'; import { SystemConfigCore } from '../system-config'; @@ -44,10 +45,10 @@ export class SmartInfoService { await this.jobRepository.resume(QueueName.SMART_SEARCH); } - async handleQueueEncodeClip({ force }: IBaseJob) { + async handleQueueEncodeClip({ force }: IBaseJob): Promise { const { machineLearning } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.clip.enabled) { - return true; + return JobStatus.SKIPPED; } if (force) { @@ -66,22 +67,22 @@ export class SmartInfoService { ); } - return true; + return JobStatus.SUCCESS; } - async handleEncodeClip({ id }: IEntityJob) { + async handleEncodeClip({ id }: IEntityJob): Promise { const { machineLearning } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.clip.enabled) { - return true; + return JobStatus.SKIPPED; } const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { - return false; + return JobStatus.FAILED; } if (!asset.resizePath) { - return false; + return JobStatus.FAILED; } const clipEmbedding = await this.machineLearning.encodeImage( @@ -97,6 +98,6 @@ export class SmartInfoService { await this.repository.upsert({ assetId: asset.id }, clipEmbedding); - return true; + return JobStatus.SUCCESS; } } diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/domain/storage-template/storage-template.service.spec.ts index a01bcdc12..388b9c4d6 100644 --- a/server/src/domain/storage-template/storage-template.service.spec.ts +++ b/server/src/domain/storage-template/storage-template.service.spec.ts @@ -8,6 +8,7 @@ import { IStorageRepository, ISystemConfigRepository, IUserRepository, + JobStatus, StorageTemplateService, defaults, } from '@app/domain'; @@ -76,7 +77,7 @@ describe(StorageTemplateService.name, () => { describe('handleMigrationSingle', () => { it('should skip when storage template is disabled', async () => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.STORAGE_TEMPLATE_ENABLED, value: false }]); - await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true); + await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED); expect(assetMock.getByIds).not.toHaveBeenCalled(); expect(storageMock.checkFileExists).not.toHaveBeenCalled(); expect(storageMock.rename).not.toHaveBeenCalled(); @@ -138,7 +139,9 @@ describe(StorageTemplateService.name, () => { newPath: newMotionPicturePath, }); - await expect(sut.handleMigrationSingle({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(true); + await expect(sut.handleMigrationSingle({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( + JobStatus.SUCCESS, + ); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true }); @@ -190,7 +193,7 @@ describe(StorageTemplateService.name, () => { newPath, }); - await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true); + await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3); @@ -247,7 +250,7 @@ describe(StorageTemplateService.name, () => { newPath, }); - await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true); + await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3); @@ -298,7 +301,7 @@ describe(StorageTemplateService.name, () => { newPath, }); - await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true); + await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(storageMock.checkFileExists).toHaveBeenCalledTimes(1); @@ -364,7 +367,7 @@ describe(StorageTemplateService.name, () => { newPath, }); - await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true); + await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3); diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/domain/storage-template/storage-template.service.ts index c4e9e2e70..e2b74c0da 100644 --- a/server/src/domain/storage-template/storage-template.service.ts +++ b/server/src/domain/storage-template/storage-template.service.ts @@ -18,6 +18,7 @@ import { IStorageRepository, ISystemConfigRepository, IUserRepository, + JobStatus, } from '../repositories'; import { StorageCore, StorageFolder } from '../storage'; import { @@ -85,16 +86,16 @@ export class StorageTemplateService { ); } - async handleMigrationSingle({ id }: IEntityJob) { + async handleMigrationSingle({ id }: IEntityJob): Promise { const config = await this.configCore.getConfig(); const storageTemplateEnabled = config.storageTemplate.enabled; if (!storageTemplateEnabled) { - return true; + return JobStatus.SKIPPED; } const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { - return false; + return JobStatus.FAILED; } const user = await this.userRepository.get(asset.ownerId, {}); @@ -106,21 +107,21 @@ export class StorageTemplateService { if (asset.livePhotoVideoId) { const [livePhotoVideo] = await this.assetRepository.getByIds([asset.livePhotoVideoId], { exifInfo: true }); if (!livePhotoVideo) { - return false; + return JobStatus.FAILED; } const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath); await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }); } - return true; + return JobStatus.SUCCESS; } - async handleMigration() { + async handleMigration(): Promise { this.logger.log('Starting storage template migration'); const { storageTemplate } = await this.configCore.getConfig(); const { enabled } = storageTemplate; if (!enabled) { this.logger.log('Storage template migration disabled, skipping'); - return true; + return JobStatus.SKIPPED; } const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => this.assetRepository.getAll(pagination, { withExif: true }), @@ -142,7 +143,7 @@ export class StorageTemplateService { this.logger.log('Finished storage template migration'); - return true; + return JobStatus.SUCCESS; } async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) { diff --git a/server/src/domain/storage/storage.core.ts b/server/src/domain/storage/storage.core.ts index 30a6002be..36e600b24 100644 --- a/server/src/domain/storage/storage.core.ts +++ b/server/src/domain/storage/storage.core.ts @@ -20,6 +20,9 @@ export enum StorageFolder { THUMBNAILS = 'thumbs', } +export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS)); +export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO)); + export interface MoveRequest { entityId: string; pathType: PathType; @@ -115,6 +118,10 @@ export class StorageCore { return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION)); } + static isGeneratedAsset(path: string) { + return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR); + } + async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) { const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset; switch (pathType) { diff --git a/server/src/domain/storage/storage.service.ts b/server/src/domain/storage/storage.service.ts index 994a2b6fd..95c311881 100644 --- a/server/src/domain/storage/storage.service.ts +++ b/server/src/domain/storage/storage.service.ts @@ -1,7 +1,7 @@ import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; import { IDeleteFilesJob } from '../job'; -import { IStorageRepository } from '../repositories'; +import { IStorageRepository, JobStatus } from '../repositories'; import { StorageCore, StorageFolder } from './storage.core'; @Injectable() @@ -31,6 +31,6 @@ export class StorageService { } } - return true; + return JobStatus.SUCCESS; } } diff --git a/server/src/domain/system-config/system-config.constants.ts b/server/src/domain/system-config/system-config.constants.ts index 47bddb97b..0290472aa 100644 --- a/server/src/domain/system-config/system-config.constants.ts +++ b/server/src/domain/system-config/system-config.constants.ts @@ -4,7 +4,7 @@ export const supportedWeekTokens = ['W', 'WW']; export const supportedDayTokens = ['d', 'dd']; export const supportedHourTokens = ['h', 'hh', 'H', 'HH']; export const supportedMinuteTokens = ['m', 'mm']; -export const supportedSecondTokens = ['s', 'ss']; +export const supportedSecondTokens = ['s', 'ss', 'SSS']; export const supportedPresetTokens = [ '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', '{{y}}/{{MM}}-{{dd}}/{{filename}}', diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 8fa203ae2..5602c378b 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -323,7 +323,7 @@ describe(SystemConfigService.name, () => { '{{y}}/{{y}}-{{WW}}/{{assetId}}', '{{album}}/{{filename}}', ], - secondOptions: ['s', 'ss'], + secondOptions: ['s', 'ss', 'SSS'], weekOptions: ['W', 'WW'], yearOptions: ['y', 'yy'], }); diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index 564163d77..a1db1fb04 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -14,6 +14,7 @@ import { IStorageRepository, ISystemConfigRepository, IUserRepository, + JobStatus, UserFindOptions, } from '../repositories'; import { StorageCore, StorageFolder } from '../storage'; @@ -143,12 +144,12 @@ export class UserService { return { admin, password, provided: !!providedPassword }; } - async handleUserSyncUsage() { + async handleUserSyncUsage(): Promise { await this.userRepository.syncUsage(); - return true; + return JobStatus.SUCCESS; } - async handleUserDeleteCheck() { + async handleUserDeleteCheck(): Promise { const users = await this.userRepository.getDeletedUsers(); const config = await this.configCore.getConfig(); await this.jobRepository.queueAll( @@ -158,20 +159,20 @@ export class UserService { : [], ), ); - return true; + return JobStatus.SUCCESS; } - async handleUserDelete({ id, force }: IEntityJob) { + async handleUserDelete({ id, force }: IEntityJob): Promise { const config = await this.configCore.getConfig(); const user = await this.userRepository.get(id, { withDeleted: true }); if (!user) { - return false; + return JobStatus.FAILED; } // just for extra protection here if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) { this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`); - return false; + return JobStatus.SKIPPED; } this.logger.log(`Deleting user: ${user.id}`); @@ -193,7 +194,7 @@ export class UserService { await this.albumRepository.deleteAll(user.id); await this.userRepository.delete(user, true); - return true; + return JobStatus.SUCCESS; } private isReadyForDeletion(user: UserEntity, deleteDelay: number): boolean { diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index bc59e2dfe..37b561490 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -13,7 +13,6 @@ import { Res, UploadedFiles, UseInterceptors, - ValidationPipe, } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; @@ -58,7 +57,7 @@ export class AssetController { async uploadFile( @Auth() auth: AuthDto, @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] })) files: UploadFiles, - @Body(new ValidationPipe({ transform: true })) dto: CreateAssetDto, + @Body() dto: CreateAssetDto, @Res({ passthrough: true }) res: Response, ): Promise { const file = mapToUploadFile(files.assetData[0]); @@ -90,7 +89,7 @@ export class AssetController { @Next() next: NextFunction, @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Query(new ValidationPipe({ transform: true })) dto: ServeFileDto, + @Query() dto: ServeFileDto, ) { await sendFile(res, next, () => this.serviceV1.serveFile(auth, id, dto)); } @@ -103,7 +102,7 @@ export class AssetController { @Next() next: NextFunction, @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Query(new ValidationPipe({ transform: true })) dto: GetAssetThumbnailDto, + @Query() dto: GetAssetThumbnailDto, ) { await sendFile(res, next, () => this.serviceV1.serveThumbnail(auth, id, dto)); } @@ -133,10 +132,7 @@ export class AssetController { required: false, schema: { type: 'string' }, }) - getAllAssets( - @Auth() auth: AuthDto, - @Query(new ValidationPipe({ transform: true })) dto: AssetSearchDto, - ): Promise { + getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise { return this.serviceV1.getAllAssets(auth, dto); } @@ -147,7 +143,7 @@ export class AssetController { @HttpCode(HttpStatus.OK) checkExistingAssets( @Auth() auth: AuthDto, - @Body(ValidationPipe) dto: CheckExistingAssetsDto, + @Body() dto: CheckExistingAssetsDto, ): Promise { return this.serviceV1.checkExistingAssets(auth, dto); } @@ -159,7 +155,7 @@ export class AssetController { @HttpCode(HttpStatus.OK) checkBulkUpload( @Auth() auth: AuthDto, - @Body(ValidationPipe) dto: AssetBulkUploadCheckDto, + @Body() dto: AssetBulkUploadCheckDto, ): Promise { return this.serviceV1.bulkUploadCheck(auth, dto); } diff --git a/server/src/immich/app.module.ts b/server/src/immich/app.module.ts index 8d02a44a9..662f45f7c 100644 --- a/server/src/immich/app.module.ts +++ b/server/src/immich/app.module.ts @@ -1,8 +1,8 @@ import { DomainModule } from '@app/domain'; import { InfraModule } from '@app/infra'; import { AssetEntity, ExifEntity } from '@app/infra/entities'; -import { Module, OnModuleInit } from '@nestjs/common'; -import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; +import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common'; +import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AssetRepositoryV1, IAssetRepositoryV1 } from './api-v1/asset/asset-repository'; @@ -70,6 +70,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors'; PersonController, ], providers: [ + { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, { provide: APP_GUARD, useClass: AppGuard }, { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 }, diff --git a/server/src/immich/app.utils.ts b/server/src/immich/app.utils.ts index 938f6f470..26aca2a9b 100644 --- a/server/src/immich/app.utils.ts +++ b/server/src/immich/app.utils.ts @@ -17,28 +17,15 @@ import { SwaggerDocumentOptions, SwaggerModule, } from '@nestjs/swagger'; +import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; import { NextFunction, Response } from 'express'; import _ from 'lodash'; import { writeFileSync } from 'node:fs'; import { access, constants } from 'node:fs/promises'; import path, { isAbsolute } from 'node:path'; import { promisify } from 'node:util'; - -import { applyDecorators, UsePipes, ValidationPipe } from '@nestjs/common'; -import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; import { Metadata } from './app.guard'; -export function UseValidation() { - return applyDecorators( - UsePipes( - new ValidationPipe({ - transform: true, - whitelist: true, - }), - ), - ); -} - type SendFile = Parameters; type SendFileOptions = SendFile[1]; diff --git a/server/src/immich/controllers/activity.controller.ts b/server/src/immich/controllers/activity.controller.ts index d01d4e903..0808c7d4d 100644 --- a/server/src/immich/controllers/activity.controller.ts +++ b/server/src/immich/controllers/activity.controller.ts @@ -11,13 +11,11 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; import { Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Activity') @Controller('activity') @Authenticated() -@UseValidation() export class ActivityController { constructor(private service: ActivityService) {} diff --git a/server/src/immich/controllers/album.controller.ts b/server/src/immich/controllers/album.controller.ts index d388dba77..ea1c5a428 100644 --- a/server/src/immich/controllers/album.controller.ts +++ b/server/src/immich/controllers/album.controller.ts @@ -15,13 +15,11 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@ import { ApiTags } from '@nestjs/swagger'; import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe'; import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Album') @Controller('album') @Authenticated() -@UseValidation() export class AlbumController { constructor(private service: AlbumService) {} diff --git a/server/src/immich/controllers/api-key.controller.ts b/server/src/immich/controllers/api-key.controller.ts index ba0aaf661..5b5072533 100644 --- a/server/src/immich/controllers/api-key.controller.ts +++ b/server/src/immich/controllers/api-key.controller.ts @@ -9,13 +9,11 @@ import { import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('API Key') @Controller('api-key') @Authenticated() -@UseValidation() export class APIKeyController { constructor(private service: APIKeyService) {} diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index 57a67d33f..39a36b175 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -8,7 +8,6 @@ import { AssetStatsResponseDto, AuthDto, DeviceIdDto, - DownloadService, MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, @@ -19,21 +18,18 @@ import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto, - TrashService, UpdateAssetDto as UpdateDto, UpdateStackParentDto, } from '@app/domain'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { Route } from '../interceptors'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Asset') @Controller('assets') @Authenticated() -@UseValidation() export class AssetsController { constructor(private searchService: SearchService) {} @@ -50,13 +46,8 @@ export class AssetsController { @ApiTags('Asset') @Controller(Route.ASSET) @Authenticated() -@UseValidation() export class AssetController { - constructor( - private service: AssetService, - private downloadService: DownloadService, - private trashService: TrashService, - ) {} + constructor(private service: AssetService) {} @Get('map-marker') getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise { diff --git a/server/src/immich/controllers/audit.controller.ts b/server/src/immich/controllers/audit.controller.ts index 785d48bd2..09b707b8a 100644 --- a/server/src/immich/controllers/audit.controller.ts +++ b/server/src/immich/controllers/audit.controller.ts @@ -11,12 +11,10 @@ import { import { Body, Controller, Get, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AdminRoute, Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; @ApiTags('Audit') @Controller('audit') @Authenticated() -@UseValidation() export class AuditController { constructor(private service: AuditService) {} diff --git a/server/src/immich/controllers/auth.controller.ts b/server/src/immich/controllers/auth.controller.ts index c8ffd52fd..ac1fea2bc 100644 --- a/server/src/immich/controllers/auth.controller.ts +++ b/server/src/immich/controllers/auth.controller.ts @@ -19,13 +19,11 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Authentication') @Controller('auth') @Authenticated() -@UseValidation() export class AuthController { constructor(private service: AuthService) {} diff --git a/server/src/immich/controllers/download.controller.ts b/server/src/immich/controllers/download.controller.ts index fcddac53d..743797f74 100644 --- a/server/src/immich/controllers/download.controller.ts +++ b/server/src/immich/controllers/download.controller.ts @@ -3,13 +3,12 @@ import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, Streama import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { Auth, Authenticated, FileResponse, SharedLinkRoute } from '../app.guard'; -import { UseValidation, asStreamableFile, sendFile } from '../app.utils'; +import { asStreamableFile, sendFile } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Download') @Controller('download') @Authenticated() -@UseValidation() export class DownloadController { constructor(private service: DownloadService) {} diff --git a/server/src/immich/controllers/face.controller.ts b/server/src/immich/controllers/face.controller.ts index 59473950c..f4014713b 100644 --- a/server/src/immich/controllers/face.controller.ts +++ b/server/src/immich/controllers/face.controller.ts @@ -2,13 +2,11 @@ import { AssetFaceResponseDto, AuthDto, FaceDto, PersonResponseDto, PersonServic import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Face') @Controller('face') @Authenticated() -@UseValidation() export class FaceController { constructor(private service: PersonService) {} diff --git a/server/src/immich/controllers/job.controller.ts b/server/src/immich/controllers/job.controller.ts index 243b7537d..413af44de 100644 --- a/server/src/immich/controllers/job.controller.ts +++ b/server/src/immich/controllers/job.controller.ts @@ -2,12 +2,10 @@ import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobService, JobS import { Body, Controller, Get, Param, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; @ApiTags('Job') @Controller('jobs') @Authenticated({ admin: true }) -@UseValidation() export class JobController { constructor(private service: JobService) {} diff --git a/server/src/immich/controllers/library.controller.ts b/server/src/immich/controllers/library.controller.ts index 9ad711979..801dc173d 100644 --- a/server/src/immich/controllers/library.controller.ts +++ b/server/src/immich/controllers/library.controller.ts @@ -13,13 +13,11 @@ import { import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AdminRoute, Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Library') @Controller('library') @Authenticated() -@UseValidation() @AdminRoute() export class LibraryController { constructor(private service: LibraryService) {} diff --git a/server/src/immich/controllers/oauth.controller.ts b/server/src/immich/controllers/oauth.controller.ts index a62458715..c7a5717af 100644 --- a/server/src/immich/controllers/oauth.controller.ts +++ b/server/src/immich/controllers/oauth.controller.ts @@ -12,12 +12,10 @@ import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@ne import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; -import { UseValidation } from '../app.utils'; @ApiTags('OAuth') @Controller('oauth') @Authenticated() -@UseValidation() export class OAuthController { constructor(private service: AuthService) {} diff --git a/server/src/immich/controllers/partner.controller.ts b/server/src/immich/controllers/partner.controller.ts index 6370d8e71..65d95438d 100644 --- a/server/src/immich/controllers/partner.controller.ts +++ b/server/src/immich/controllers/partner.controller.ts @@ -3,13 +3,11 @@ import { PartnerResponseDto, UpdatePartnerDto } from '@app/domain/partner/partne import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Partner') @Controller('partner') @Authenticated() -@UseValidation() export class PartnerController { constructor(private service: PartnerService) {} diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/immich/controllers/person.controller.ts index 2447f982b..3408aa6ec 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/immich/controllers/person.controller.ts @@ -17,13 +17,12 @@ import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nest import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { Auth, Authenticated, FileResponse } from '../app.guard'; -import { UseValidation, sendFile } from '../app.utils'; +import { sendFile } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Person') @Controller('person') @Authenticated() -@UseValidation() export class PersonController { constructor(private service: PersonService) {} diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index df1bec7c6..d508531dd 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -15,12 +15,10 @@ import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-sugges import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; @ApiTags('Search') @Controller('search') @Authenticated() -@UseValidation() export class SearchController { constructor(private service: SearchService) {} diff --git a/server/src/immich/controllers/server-info.controller.ts b/server/src/immich/controllers/server-info.controller.ts index 66835501c..4987a8984 100644 --- a/server/src/immich/controllers/server-info.controller.ts +++ b/server/src/immich/controllers/server-info.controller.ts @@ -12,12 +12,10 @@ import { import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AdminRoute, Authenticated, PublicRoute } from '../app.guard'; -import { UseValidation } from '../app.utils'; @ApiTags('Server Info') @Controller('server-info') @Authenticated() -@UseValidation() export class ServerInfoController { constructor(private service: ServerInfoService) {} diff --git a/server/src/immich/controllers/shared-link.controller.ts b/server/src/immich/controllers/shared-link.controller.ts index 86045433d..d265d018d 100644 --- a/server/src/immich/controllers/shared-link.controller.ts +++ b/server/src/immich/controllers/shared-link.controller.ts @@ -13,13 +13,11 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; import { Auth, Authenticated, SharedLinkRoute } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Shared Link') @Controller('shared-link') @Authenticated() -@UseValidation() export class SharedLinkController { constructor(private readonly service: SharedLinkService) {} diff --git a/server/src/immich/controllers/system-config.controller.ts b/server/src/immich/controllers/system-config.controller.ts index da2402efe..73cf2c3c0 100644 --- a/server/src/immich/controllers/system-config.controller.ts +++ b/server/src/immich/controllers/system-config.controller.ts @@ -3,12 +3,10 @@ import { MapThemeDto } from '@app/domain/system-config/system-config-map-theme.d import { Body, Controller, Get, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AdminRoute, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; @ApiTags('System Config') @Controller('system-config') @Authenticated({ admin: true }) -@UseValidation() export class SystemConfigController { constructor(private readonly service: SystemConfigService) {} diff --git a/server/src/immich/controllers/tag.controller.ts b/server/src/immich/controllers/tag.controller.ts index b9d363600..0d0c563d4 100644 --- a/server/src/immich/controllers/tag.controller.ts +++ b/server/src/immich/controllers/tag.controller.ts @@ -11,13 +11,11 @@ import { import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Tag') @Controller('tag') @Authenticated() -@UseValidation() export class TagController { constructor(private service: TagService) {} diff --git a/server/src/immich/controllers/trash.controller.ts b/server/src/immich/controllers/trash.controller.ts index 9f7abe311..b61893817 100644 --- a/server/src/immich/controllers/trash.controller.ts +++ b/server/src/immich/controllers/trash.controller.ts @@ -2,12 +2,10 @@ import { AuthDto, BulkIdsDto, TrashService } from '@app/domain'; import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; -import { UseValidation } from '../app.utils'; @ApiTags('Trash') @Controller('trash') @Authenticated() -@UseValidation() export class TrashController { constructor(private service: TrashService) {} diff --git a/server/src/immich/controllers/user.controller.ts b/server/src/immich/controllers/user.controller.ts index 7fa7ccd0f..0b3828f5c 100644 --- a/server/src/immich/controllers/user.controller.ts +++ b/server/src/immich/controllers/user.controller.ts @@ -27,14 +27,13 @@ import { import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { AdminRoute, Auth, Authenticated, FileResponse } from '../app.guard'; -import { UseValidation, sendFile } from '../app.utils'; +import { sendFile } from '../app.utils'; import { FileUploadInterceptor, Route } from '../interceptors'; import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('User') @Controller(Route.USER) @Authenticated() -@UseValidation() export class UserController { constructor(private service: UserService) {} diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts index ec8e19683..db7687f28 100644 --- a/server/test/fixtures/library.stub.ts +++ b/server/test/fixtures/library.stub.ts @@ -1,4 +1,6 @@ +import { APP_MEDIA_LOCATION, THUMBNAIL_DIR } from '@app/domain'; import { LibraryEntity, LibraryType } from '@app/infra/entities'; +import { join } from 'node:path'; import { userStub } from './user.stub'; export const libraryStub = { @@ -100,4 +102,18 @@ export const libraryStub = { isVisible: true, exclusionPatterns: ['**/dir1/**'], }), + hasImmichPaths: Object.freeze({ + id: 'library-id1337', + name: 'importpath-exclusion-library1', + assets: [], + owner: userStub.admin, + ownerId: 'user-id', + type: LibraryType.EXTERNAL, + importPaths: [join(THUMBNAIL_DIR, 'library'), '/xyz', join(APP_MEDIA_LOCATION, 'library')], + createdAt: new Date('2023-01-01'), + updatedAt: new Date('2023-01-01'), + refreshedAt: null, + isVisible: true, + exclusionPatterns: ['**/dir1/**'], + }), }; diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index 8cca2f8c0..b273271ce 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -13,6 +13,7 @@ vi.mock('@immich/sdk', async (originalImport) => { }); const sdkMock: MockedObject = sdk as MockedObject; +const onShowContextMenu = vi.fn(); describe('AlbumCard component', () => { let sut: RenderResult; @@ -90,34 +91,30 @@ describe('AlbumCard component', () => { expect(albumDetailsElement).toHaveTextContent('0 items'); }); + it('hides context menu when "onShowContextMenu" is undefined', () => { + const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null })); + sut = render(AlbumCard, { album }); + + const contextButtonParent = sut.queryByTestId('context-button-parent'); + expect(contextButtonParent).not.toBeInTheDocument(); + }); + describe('with rendered component - no thumbnail', () => { const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null })); beforeEach(async () => { - sut = render(AlbumCard, { album }); + sut = render(AlbumCard, { album, onShowContextMenu }); const albumImgElement = sut.getByTestId('album-image'); await waitFor(() => expect(albumImgElement).toHaveAttribute('src')); }); - it('dispatches custom "click" event with the album in context', async () => { - const onClickHandler = vi.fn(); - sut.component.$on('click', onClickHandler); - const albumCardElement = sut.getByTestId('album-card'); - - await fireEvent.click(albumCardElement); - expect(onClickHandler).toHaveBeenCalledTimes(1); - expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: album })); - }); - - it('dispatches custom "click" event on context menu click with mouse coordinates', async () => { - const onClickHandler = vi.fn(); - sut.component.$on('showalbumcontextmenu', onClickHandler); - - const contextMenuButtonParent = sut.getByTestId('context-button-parent'); + it('dispatches "onShowContextMenu" event on context menu click with mouse coordinates', async () => { + const contextMenuButton = sut.getByTestId('context-button-parent').children[0]; + expect(contextMenuButton).toBeDefined(); // Mock getBoundingClientRect to return a bounding rectangle that will result in the expected position - contextMenuButtonParent.getBoundingClientRect = () => ({ + contextMenuButton.getBoundingClientRect = () => ({ x: 123, y: 456, width: 0, @@ -130,14 +127,14 @@ describe('AlbumCard component', () => { }); await fireEvent( - contextMenuButtonParent, + contextMenuButton, new MouseEvent('click', { clientX: 123, clientY: 456, }), ); - expect(onClickHandler).toHaveBeenCalledTimes(1); - expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: { x: 123, y: 456 } })); + expect(onShowContextMenu).toHaveBeenCalledTimes(1); + expect(onShowContextMenu).toHaveBeenCalledWith(expect.objectContaining({ x: 123, y: 456 })); }); }); }); diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index 8e54af18c..295866077 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -5,25 +5,20 @@ import { getAssetThumbnailUrl } from '$lib/utils'; import { ThumbnailFormat, getAssetThumbnail, getUserById, type AlbumResponseDto } from '@immich/sdk'; import { mdiDotsVertical } from '@mdi/js'; - import { createEventDispatcher, onMount } from 'svelte'; - import { getContextMenuPosition } from '../../utils/context-menu'; + import { onMount } from 'svelte'; + import { getContextMenuPosition, type ContextMenuPosition } from '../../utils/context-menu'; import IconButton from '../elements/buttons/icon-button.svelte'; - import type { OnClick, OnShowContextMenu } from './album-card'; export let album: AlbumResponseDto; export let isSharingView = false; export let showItemCount = true; - export let showContextMenu = true; export let preload = false; - let showVerticalDots = false; + export let onShowContextMenu: ((position: ContextMenuPosition) => void) | undefined = undefined; $: imageData = album.albumThumbnailAssetId ? getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Webp) : null; - const dispatchClick = createEventDispatcher(); - const dispatchShowContextMenu = createEventDispatcher(); - const loadHighQualityThumbnail = async (assetId: string | null) => { if (!assetId) { return; @@ -33,8 +28,11 @@ return URL.createObjectURL(data); }; - const showAlbumContextMenu = (e: MouseEvent) => - dispatchShowContextMenu('showalbumcontextmenu', getContextMenuPosition(e)); + const showAlbumContextMenu = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onShowContextMenu?.(getContextMenuPosition(e)); + }; onMount(async () => { imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || null; @@ -43,25 +41,17 @@ const getAlbumOwnerInfo = () => getUserById({ id: album.ownerId }); -
dispatchClick('click', album)} - on:keydown={() => dispatchClick('click', album)} - on:mouseenter={() => (showVerticalDots = true)} - on:mouseleave={() => (showVerticalDots = false)} + class="group relative rounded-2xl border border-transparent p-5 hover:bg-gray-100 hover:border-gray-200 dark:hover:border-gray-800 dark:hover:bg-gray-900" data-testid="album-card" > - - {#if showContextMenu} + {#if onShowContextMenu}
- +
diff --git a/web/src/lib/components/album-page/album-card.ts b/web/src/lib/components/album-page/album-card.ts deleted file mode 100644 index deb98e976..000000000 --- a/web/src/lib/components/album-page/album-card.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { AlbumResponseDto } from '@immich/sdk'; - -export type OnShowContextMenu = { - showalbumcontextmenu: OnShowContextMenuDetail; -}; - -export type OnClick = { - click: OnClickDetail; -}; - -export type OnShowContextMenuDetail = { x: number; y: number }; -export type OnClickDetail = AlbumResponseDto; diff --git a/web/src/lib/components/album-page/album-description.svelte b/web/src/lib/components/album-page/album-description.svelte index 9e988ba75..e860fbcd2 100644 --- a/web/src/lib/components/album-page/album-description.svelte +++ b/web/src/lib/components/album-page/album-description.svelte @@ -2,6 +2,7 @@ import { autoGrowHeight } from '$lib/utils/autogrow'; import { updateAlbumInfo } from '@immich/sdk'; import { handleError } from '$lib/utils/handle-error'; + import { shortcut } from '$lib/utils/shortcut'; export let id: string; export let description: string; @@ -37,6 +38,10 @@ on:focusout={handleUpdateDescription} use:autoGrowHeight placeholder="Add description" + use:shortcut={{ + shortcut: { key: 'Enter', ctrl: true }, + onShortcut: (e) => e.currentTarget.blur(), + }} /> {:else if description}

diff --git a/web/src/lib/components/album-page/album-title.svelte b/web/src/lib/components/album-page/album-title.svelte index 8a4af18ea..2725bb892 100644 --- a/web/src/lib/components/album-page/album-title.svelte +++ b/web/src/lib/components/album-page/album-title.svelte @@ -1,6 +1,7 @@ e.key === 'Enter' && e.currentTarget.blur()} + use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: (e) => e.currentTarget.blur() }} on:blur={handleUpdateName} class="w-[99%] mb-2 border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned ? 'hover:border-gray-400' diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index ab5eb91d5..08d610ceb 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -1,11 +1,9 @@ + { + if (!$showAssetViewer && $isMultiSelectState) { + assetInteractionStore.clearMultiselect(); + } + }, + }} +/> +

{#if $isMultiSelectState} - - + + diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index 1a681a884..607457e1c 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -1,7 +1,6 @@ - + navigateAsset('previous') }, + { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, + { shortcut: { key: 'd', shift: true }, onShortcut: () => downloadFile(asset) }, + { shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete(false) }, + { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, + { shortcut: { key: 'Escape' }, onShortcut: closeViewer }, + { shortcut: { key: 'f' }, onShortcut: toggleFavorite }, + { shortcut: { key: 'i' }, onShortcut: toggleDetailPanel }, + ]} +/>
+ {:else} {:else} - + {/if} {:else}
- {#each $stackAssetsStore as stackedAsset (stackedAsset.id)} + {#each $stackAssetsStore as stackedAsset, index (stackedAsset.id)}
(asset = stackedAsset)} + onClick={() => { + asset = stackedAsset; + preloadAssets = index + 1 >= $stackAssetsStore.length ? [] : [$stackAssetsStore[index + 1]]; + }} on:mouse-event={(e) => handleStackedAssetMouseEvent(e, stackedAsset)} readonly thumbnailSize={stackedAsset.id == asset.id ? 65 : 60} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 334b406bb..318007280 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -41,6 +41,7 @@ import UserAvatar from '../shared-components/user-avatar.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; + import { shortcut } from '$lib/utils/shortcut'; export let asset: AssetResponseDto; export let albums: AlbumResponseDto[] = []; @@ -105,20 +106,6 @@ closeViewer: void; }>(); - const handleKeypress = async (event: KeyboardEvent) => { - if (event.target !== textArea) { - return; - } - const ctrl = event.ctrlKey; - switch (event.key) { - case 'Enter': { - if (ctrl && event.target === textArea) { - await handleFocusOut(); - } - } - } - }; - const getMegapixel = (width: number, height: number): number | undefined => { const megapixel = Math.round((height * width) / 1_000_000); @@ -180,8 +167,6 @@ } - -
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 00ef670c9..14bbb6b1f 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -6,8 +6,8 @@ import { downloadRequest, getAssetFileUrl, handlePromiseError } from '$lib/utils'; import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { getBoundingBox } from '$lib/utils/people-utils'; - import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; - import { type AssetResponseDto } from '@immich/sdk'; + import { shortcuts } from '$lib/utils/shortcut'; + import { type AssetResponseDto, AssetTypeEnum } from '@immich/sdk'; import { useZoomImageWheel } from '@zoom-image/svelte'; import { onDestroy, onMount } from 'svelte'; import { fade } from 'svelte/transition'; @@ -16,6 +16,7 @@ import { getAltText } from '$lib/utils/thumbnail-util'; export let asset: AssetResponseDto; + export let preloadAssets: AssetResponseDto[] | null = null; export let element: HTMLDivElement | undefined = undefined; export let haveFadeTransition = true; @@ -25,6 +26,7 @@ let hasZoomed = false; let copyImageToClipboard: (source: string) => Promise; let canCopyImagesToClipboard: () => boolean; + let imageLoaded: boolean = false; const loadOriginalByDefault = $alwaysLoadOriginalFile && isWebCompatibleImage(asset); @@ -41,6 +43,9 @@ const module = await import('copy-image-clipboard'); copyImageToClipboard = module.copyImageToClipboard; canCopyImagesToClipboard = module.canCopyImagesToClipboard; + + imageLoaded = false; + await loadAssetData({ loadOriginal: loadOriginalByDefault }); }); onDestroy(() => { @@ -60,20 +65,22 @@ }); assetData = URL.createObjectURL(data); - } catch { - // Do nothing - } - }; + imageLoaded = true; - const handleKeypress = async (event: KeyboardEvent) => { - if (shouldIgnoreShortcut(event)) { - return; - } - if (window.getSelection()?.type === 'Range') { - return; - } - if ((event.metaKey || event.ctrlKey) && event.key === 'c') { - await doCopy(); + if (!preloadAssets) { + return; + } + + for (const preloadAsset of preloadAssets) { + if (preloadAsset.type === AssetTypeEnum.Image) { + await downloadRequest({ + url: getAssetFileUrl(preloadAsset.id, !loadOriginal, false), + signal: abortController.signal, + }); + } + } + } catch { + imageLoaded = false; } }; @@ -119,18 +126,32 @@ handlePromiseError(loadAssetData({ loadOriginal: true })); } }); + + const onCopyShortcut = () => { + if (window.getSelection()?.type === 'Range') { + return; + } + handlePromiseError(doCopy()); + }; - +
- {#await loadAssetData({ loadOriginal: loadOriginalByDefault })} + {#if !imageLoaded} - {:then} + {:else}
{/each}
- {/await} + {/if}
diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 796be13be..efbe38424 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -21,9 +21,9 @@ import { fade } from 'svelte/transition'; import ImageThumbnail from './image-thumbnail.svelte'; import VideoThumbnail from './video-thumbnail.svelte'; + import { shortcut } from '$lib/utils/shortcut'; const dispatch = createEventDispatcher<{ - click: { asset: AssetResponseDto }; select: { asset: AssetResponseDto }; 'mouse-event': { isMouseOver: boolean; selectedGroupIndex: number }; }>(); @@ -40,12 +40,13 @@ export let readonly = false; export let showArchiveIcon = false; export let showStackedIcon = true; - export let intersecting = false; + export let onClick: ((asset: AssetResponseDto) => void) | undefined = undefined; let className = ''; export { className as class }; let mouseOver = false; + $: clickable = !disabled && onClick; $: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex }); @@ -62,14 +63,8 @@ })(); const thumbnailClickedHandler = () => { - if (!disabled) { - dispatch('click', { asset }); - } - }; - - const thumbnailKeyDownHandler = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - thumbnailClickedHandler(); + if (clickable) { + onClick?.(asset); } }; @@ -89,20 +84,22 @@ }; - - + +
{#if intersecting}
@@ -140,6 +137,11 @@ class:rounded-xl={selected} /> + +
+ {#if !isSharedLink() && asset.isFavorite}
diff --git a/web/src/lib/components/elements/buttons/link-button.svelte b/web/src/lib/components/elements/buttons/link-button.svelte index 2cb22d41d..6911fd2fc 100644 --- a/web/src/lib/components/elements/buttons/link-button.svelte +++ b/web/src/lib/components/elements/buttons/link-button.svelte @@ -8,8 +8,9 @@ export let color: Color = 'transparent-gray'; export let disabled = false; export let fullwidth = false; + export let title: string | undefined = undefined; - diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index 13a797a9c..cdd25c8d6 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -13,6 +13,7 @@ export let user: UserResponseDto; export let canResetPassword = true; + export let newPassword: string; let error: string; let success: string; @@ -53,12 +54,12 @@ const resetPassword = async () => { try { - const defaultPassword = 'password'; + newPassword = generatePassword(); await updateUser({ updateUserDto: { id: user.id, - password: defaultPassword, + password: newPassword, shouldChangePassword: true, }, }); @@ -70,6 +71,23 @@ isShowResetPasswordConfirmation = false; } }; + + // TODO move password reset server-side + function generatePassword(length: number = 16) { + let generatedPassword = ''; + + const characterSet = '0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + ',.-{}+!#$%/()=?'; + + for (let i = 0; i < length; i++) { + let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0]; + randomNumber = randomNumber / 2 ** 32; + randomNumber = Math.floor(randomNumber * characterSet.length); + + generatedPassword += characterSet[randomNumber]; + } + + return generatedPassword; + }
{ - if (e.key === 'ArrowRight' && canGoForward) { - e.preventDefault(); - await toNext(); - } else if (e.key === 'ArrowLeft' && canGoBack) { - e.preventDefault(); - await toPrevious(); - } else if (e.key === 'Escape') { - e.preventDefault(); - await goto(AppRoute.PHOTOS); - } - }; - onMount(async () => { if (!$memoryStore) { const localTime = new Date(); @@ -101,7 +89,13 @@ let galleryInView = false; - + canGoForward && toNext() }, + { shortcut: { key: 'ArrowLeft' }, onShortcut: () => canGoBack && toPrevious() }, + { shortcut: { key: 'Escape' }, onShortcut: () => goto(AppRoute.PHOTOS) }, + ]} +/>
{#if currentMemory} diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index 6f7af44ef..e3af25984 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -178,7 +178,7 @@ {showArchiveIcon} {asset} {groupIndex} - on:click={() => assetClickHandler(asset, groupAssets, groupTitle)} + onClick={() => assetClickHandler(asset, groupAssets, groupTitle)} on:select={() => assetSelectHandler(asset, groupAssets, groupTitle)} on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)} selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index b66bfc96f..c21439d24 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -1,5 +1,4 @@ - + {#if isShowDeleteConfirmation} goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}> - - + + diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index 786e8721b..bf5c4f5cf 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -1,7 +1,7 @@ - -
-
(hideSuggestion = true)}> +
(hideSuggestion = true) }} + use:listNavigation={suggestionContainer} + > -
+
{#if !hideSuggestion} {#each suggestedPlaces as place, index} - {/each} +
+ {#each suggestedPeople as person, index (person.id)} + + {/each} +
{/if}
{/if} diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index c9893166f..77ebe7ad2 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -20,7 +20,7 @@ import { AppRoute, QueryParameter } from '$lib/constants'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { preventRaceConditionSearchBar } from '$lib/stores/search.store'; - import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; + import { shortcut } from '$lib/utils/shortcut'; import { type AssetResponseDto, searchSmart, @@ -52,27 +52,19 @@ let searchResultAssets: AssetResponseDto[] = []; let isLoading = true; - const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); - - const handleKeyboardPress = async (event: KeyboardEvent) => { - if (shouldIgnoreShortcut(event)) { + const onEscape = () => { + if ($showAssetViewer) { return; } - if (!$showAssetViewer) { - switch (event.key) { - case 'Escape': { - if (isMultiSelectionMode) { - selectedAssets = new Set(); - return; - } - if (!$preventRaceConditionSearchBar) { - await goto(previousRoute); - } - $preventRaceConditionSearchBar = false; - return; - } - } + + if (isMultiSelectionMode) { + selectedAssets = new Set(); + return; } + if (!$preventRaceConditionSearchBar) { + handlePromiseError(goto(previousRoute)); + } + $preventRaceConditionSearchBar = false; }; afterNavigate(({ from }) => { @@ -201,7 +193,7 @@ } - +
{#if isMultiSelectionMode} @@ -275,16 +267,10 @@ {#if searchResultAlbums.length > 0}
ALBUMS
-
+
{#each searchResultAlbums as album, index (album.id)} - + {/each}
diff --git a/web/src/routes/(user)/share/[key]/+page.svelte b/web/src/routes/(user)/share/[key]/+page.svelte index f64575924..1b87063da 100644 --- a/web/src/routes/(user)/share/[key]/+page.svelte +++ b/web/src/routes/(user)/share/[key]/+page.svelte @@ -38,7 +38,7 @@ - + diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte index 2d6e5b532..8ff990644 100644 --- a/web/src/routes/(user)/sharing/+page.svelte +++ b/web/src/routes/(user)/sharing/+page.svelte @@ -80,10 +80,10 @@
-
+
{#each data.sharedAlbums as album, index (album.id)} - + {/each}
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index dbcee1b91..a0fb30b8b 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -1,6 +1,8 @@