diff --git a/package-lock.json b/package-lock.json index 4c9576821..31d6f2e84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "p-throttle": "^4.1.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", + "rewire": "^9.0.0", "semver": "^7.6.3", "sequelize": "^6.35.2", "socket.io": "^4.5.4", @@ -38,7 +39,7 @@ "mocha": "^10.2.0", "nodemon": "^2.0.20", "nyc": "^15.1.0", - "sinon": "^17.0.1" + "sinon": "^21.0.0" } }, "node_modules/@ampproject/remapping": { @@ -509,12 +510,280 @@ "node": ">=6.9.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/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==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "optional": true }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -613,49 +882,46 @@ } }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" } }, - "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, - "dependencies": { - "type-detect": "4.0.8" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -691,6 +957,18 @@ "@types/ms": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -724,6 +1002,27 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -809,6 +1108,22 @@ "node": ">=8" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -831,7 +1146,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -919,8 +1233,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -1006,7 +1319,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1167,6 +1479,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -1218,7 +1539,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1234,7 +1554,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1243,7 +1562,6 @@ "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, "dependencies": { "has-flag": "^4.0.0" }, @@ -1336,7 +1654,6 @@ "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, "dependencies": { "color-name": "~1.1.4" }, @@ -1347,8 +1664,7 @@ "node_modules/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 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/color-support": { "version": "1.1.3", @@ -1379,8 +1695,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/console-control-strings": { "version": "1.1.0", @@ -1459,10 +1774,10 @@ } }, "node_modules/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, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1528,6 +1843,12 @@ "node": ">=4.0.0" } }, + "node_modules/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==", + "license": "MIT" + }, "node_modules/default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -1823,6 +2144,204 @@ "node": ">=0.8.0" } }, + "node_modules/eslint": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/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==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/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==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/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==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/eslint/node_modules/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==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -1836,6 +2355,48 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1935,6 +2496,36 @@ "node": ">= 0.6" } }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -2009,6 +2600,25 @@ "flat": "cli.js" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" + }, "node_modules/follow-redirects": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", @@ -2442,17 +3052,50 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true, "engines": { "node": ">=0.8.19" } @@ -2531,7 +3174,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2549,7 +3191,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2620,17 +3261,10 @@ "node": ">=0.10.0" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -2845,6 +3479,24 @@ "node": ">=4" } }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2883,12 +3535,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true - }, "node_modules/jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -2908,6 +3554,28 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -2931,12 +3599,6 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2967,6 +3629,12 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "node_modules/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==", + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -3135,7 +3803,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3559,6 +4226,12 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -3567,37 +4240,6 @@ "node": ">= 0.6" } }, - "node_modules/nise": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", - "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^10.0.2", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - } - }, - "node_modules/nise/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "dependencies": { - "isarray": "0.0.1" - } - }, "node_modules/node-abi": { "version": "3.74.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", @@ -3958,11 +4600,27 @@ "node": ">=10" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/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, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4050,6 +4708,18 @@ "node": ">=8" } }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -4096,7 +4766,6 @@ "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, "engines": { "node": ">=8" } @@ -4114,7 +4783,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -4199,6 +4867,15 @@ "node": ">=10" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -4258,6 +4935,15 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -4410,6 +5096,15 @@ "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==" }, + "node_modules/rewire": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/rewire/-/rewire-9.0.0.tgz", + "integrity": "sha512-c21+Rzev3vAdtCpXD3dT4ZVw9w99E3dppyi94HlCmSyn+OSx94iJZWCdnhqBYXO+E8Sm9p3Ky6gduYOroAypvw==", + "license": "MIT", + "dependencies": { + "eslint": "^9.30" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4621,7 +5316,6 @@ "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, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4633,7 +5327,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -4724,16 +5417,16 @@ } }, "node_modules/sinon": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", + "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.1.0", - "nise": "^5.1.5", + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", "supports-color": "^7.2.0" }, "funding": { @@ -4741,20 +5434,12 @@ "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/@sinonjs/fake-timers": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, "node_modules/sinon/node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -5059,7 +5744,6 @@ "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, "engines": { "node": ">=8" }, @@ -5209,6 +5893,18 @@ "node": "*" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -5321,6 +6017,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5362,7 +6067,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -5396,6 +6100,15 @@ "@types/node": "*" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -5580,7 +6293,6 @@ "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, "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 0d7ede593..2bebe409a 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "mocha": "^10.2.0", "nodemon": "^2.0.20", "nyc": "^15.1.0", - "sinon": "^17.0.1" + "sinon": "^21.0.0", + "rewire": "^9.0.0" } } diff --git a/server/managers/LogManager.js b/server/managers/LogManager.js index 8946e8bf5..d93fcef36 100644 --- a/server/managers/LogManager.js +++ b/server/managers/LogManager.js @@ -169,10 +169,10 @@ class LogManager { /** * Most recent 5000 daily logs * - * @returns {string} + * @returns {LogObject[]} */ getMostRecentCurrentDailyLogs() { - return this.currentDailyLog?.logs.slice(-5000) || '' + return this.currentDailyLog?.logs.slice(-5000) || [] } } module.exports = LogManager diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 16a521615..5a538ebc5 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -11,6 +11,7 @@ const Podcast = require('./Podcast') /** * @typedef LibraryFileObject * @property {string} ino + * @property {string} deviceId * @property {boolean} isSupplementary * @property {number} addedAt * @property {number} updatedAt @@ -33,6 +34,8 @@ class LibraryItem extends Model { /** @type {string} */ this.ino /** @type {string} */ + this.deviceId + /** @type {string} */ this.path /** @type {string} */ this.relPath @@ -237,7 +240,7 @@ class LibraryItem extends Model { * @param {import('sequelize').WhereOptions} where * @param {import('sequelize').BindOrReplacements} [replacements] * @param {import('sequelize').IncludeOptions} [include] - * @returns {Promise} + * @returns {Promise} */ static async findOneExpanded(where, replacements = null, include = null) { const libraryItem = await this.findOne({ @@ -289,7 +292,7 @@ class LibraryItem extends Model { * @param {import('./Library')} library * @param {import('./User')} user * @param {object} options - * @returns {{ libraryItems:Object[], count:number }} + * @returns {Promise<{ libraryItems:Object[], count:number }>} */ static async getByFilterAndSort(library, user, options) { let start = Date.now() @@ -670,6 +673,7 @@ class LibraryItem extends Model { primaryKey: true }, ino: DataTypes.STRING, + deviceId: DataTypes.STRING, path: DataTypes.STRING, relPath: DataTypes.STRING, mediaId: DataTypes.UUID, diff --git a/server/objects/Task.js b/server/objects/Task.js index e6fb39636..b146b7bfd 100644 --- a/server/objects/Task.js +++ b/server/objects/Task.js @@ -113,7 +113,7 @@ class Task { /** * Set task as finished * - * @param {TaskString} [newDescriptionString] update description + * @param {TaskString | null} [newDescriptionString] update description * @param {boolean} [clearDescription] clear description */ setFinished(newDescriptionString = null, clearDescription = false) { diff --git a/server/objects/files/AudioFile.js b/server/objects/files/AudioFile.js index c0c425ba3..17e4ffcb7 100644 --- a/server/objects/files/AudioFile.js +++ b/server/objects/files/AudioFile.js @@ -6,6 +6,7 @@ class AudioFile { constructor(data) { this.index = null this.ino = null + this.deviceId = null /** @type {FileMetadata} */ this.metadata = null this.addedAt = null @@ -44,6 +45,7 @@ class AudioFile { return { index: this.index, ino: this.ino, + deviceId: this.deviceId, metadata: this.metadata.toJSON(), addedAt: this.addedAt, updatedAt: this.updatedAt, @@ -72,6 +74,7 @@ class AudioFile { construct(data) { this.index = data.index this.ino = data.ino + this.deviceId = data.dev this.metadata = new FileMetadata(data.metadata || {}) this.addedAt = data.addedAt this.updatedAt = data.updatedAt @@ -112,6 +115,7 @@ class AudioFile { // New scanner creates AudioFile from AudioFileScanner setDataFromProbe(libraryFile, probeData) { this.ino = libraryFile.ino || null + this.deviceId = libraryFile.deviceId || null if (libraryFile.metadata instanceof FileMetadata) { this.metadata = libraryFile.metadata.clone() @@ -137,7 +141,7 @@ class AudioFile { syncChapters(updatedChapters) { if (this.chapters.length !== updatedChapters.length) { - this.chapters = updatedChapters.map(ch => ({ ...ch })) + this.chapters = updatedChapters.map((ch) => ({ ...ch })) return true } else if (updatedChapters.length === 0) { if (this.chapters.length > 0) { @@ -154,7 +158,7 @@ class AudioFile { } } if (hasUpdates) { - this.chapters = updatedChapters.map(ch => ({ ...ch })) + this.chapters = updatedChapters.map((ch) => ({ ...ch })) } return hasUpdates } @@ -164,8 +168,8 @@ class AudioFile { } /** - * - * @param {AudioFile} scannedAudioFile + * + * @param {AudioFile} scannedAudioFile * @returns {boolean} true if updates were made */ updateFromScan(scannedAudioFile) { @@ -196,4 +200,4 @@ class AudioFile { return hasUpdated } } -module.exports = AudioFile \ No newline at end of file +module.exports = AudioFile diff --git a/server/objects/files/EBookFile.js b/server/objects/files/EBookFile.js index ae40dc884..37138bb5b 100644 --- a/server/objects/files/EBookFile.js +++ b/server/objects/files/EBookFile.js @@ -3,6 +3,7 @@ const FileMetadata = require('../metadata/FileMetadata') class EBookFile { constructor(file) { this.ino = null + this.deviceId = null this.metadata = null this.ebookFormat = null this.addedAt = null @@ -15,6 +16,7 @@ class EBookFile { construct(file) { this.ino = file.ino + this.deviceId = file.dev this.metadata = new FileMetadata(file.metadata) this.ebookFormat = file.ebookFormat || this.metadata.format this.addedAt = file.addedAt @@ -24,6 +26,7 @@ class EBookFile { toJSON() { return { ino: this.ino, + deviceId: this.deviceId, metadata: this.metadata.toJSON(), ebookFormat: this.ebookFormat, addedAt: this.addedAt, @@ -37,6 +40,7 @@ class EBookFile { setData(libraryFile) { this.ino = libraryFile.ino + this.deviceId = libraryFile.deviceId this.metadata = libraryFile.metadata.clone() this.ebookFormat = libraryFile.metadata.format this.addedAt = Date.now() @@ -58,4 +62,4 @@ class EBookFile { return hasUpdated } } -module.exports = EBookFile \ No newline at end of file +module.exports = EBookFile diff --git a/server/objects/files/LibraryFile.js b/server/objects/files/LibraryFile.js index 8669e3876..946f31516 100644 --- a/server/objects/files/LibraryFile.js +++ b/server/objects/files/LibraryFile.js @@ -1,11 +1,12 @@ const Path = require('path') -const { getFileTimestampsWithIno, filePathToPOSIX } = require('../../utils/fileUtils') +const fileUtils = require('../../utils/fileUtils') const globals = require('../../utils/globals') const FileMetadata = require('../metadata/FileMetadata') class LibraryFile { constructor(file) { this.ino = null + this.deviceId = null this.metadata = null this.isSupplementary = null this.addedAt = null @@ -18,6 +19,7 @@ class LibraryFile { construct(file) { this.ino = file.ino + this.deviceId = file.deviceId this.metadata = new FileMetadata(file.metadata) this.isSupplementary = file.isSupplementary === undefined ? null : file.isSupplementary this.addedAt = file.addedAt @@ -27,7 +29,8 @@ class LibraryFile { toJSON() { return { ino: this.ino, - metadata: this.metadata.toJSON(), + deviceId: this.deviceId, + metadata: this.metadata ? this.metadata.toJSON() : null, isSupplementary: this.isSupplementary, addedAt: this.addedAt, updatedAt: this.updatedAt, @@ -40,11 +43,13 @@ class LibraryFile { } get fileType() { - if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image' - if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio' - if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook' - if (globals.TextFileTypes.includes(this.metadata.format)) return 'text' - if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata' + if (this.metadata) { + if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image' + if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio' + if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook' + if (globals.TextFileTypes.includes(this.metadata.format)) return 'text' + if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata' + } return 'unknown' } @@ -61,14 +66,15 @@ class LibraryFile { } async setDataFromPath(path, relPath) { - var fileTsData = await getFileTimestampsWithIno(path) + var fileTsData = await fileUtils.getFileTimestampsWithIno(path) var fileMetadata = new FileMetadata() fileMetadata.setData(fileTsData) fileMetadata.filename = Path.basename(relPath) - fileMetadata.path = filePathToPOSIX(path) - fileMetadata.relPath = filePathToPOSIX(relPath) + fileMetadata.path = fileUtils.filePathToPOSIX(path) + fileMetadata.relPath = fileUtils.filePathToPOSIX(relPath) fileMetadata.ext = Path.extname(relPath) this.ino = fileTsData.ino + this.deviceId = fileTsData.dev this.metadata = fileMetadata this.addedAt = Date.now() this.updatedAt = Date.now() diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index d5a4a7a29..2778495ca 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -19,6 +19,8 @@ class LibraryItemScanData { this.mediaType = data.mediaType /** @type {string} */ this.ino = data.ino + /** @type {string} */ + this.deviceId = data.dev /** @type {number} */ this.mtimeMs = data.mtimeMs /** @type {number} */ @@ -54,9 +56,10 @@ class LibraryItemScanData { */ get libraryItemObject() { let size = 0 - this.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) + this.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) return { ino: this.ino, + deviceId: this.deviceId, path: this.path, relPath: this.relPath, mediaType: this.mediaType, @@ -80,107 +83,107 @@ class LibraryItemScanData { /** @type {boolean} */ get hasAudioFileChanges() { - return (this.audioLibraryFilesRemoved.length + this.audioLibraryFilesAdded.length + this.audioLibraryFilesModified.length) > 0 + return this.audioLibraryFilesRemoved.length + this.audioLibraryFilesAdded.length + this.audioLibraryFilesModified.length > 0 } /** @type {LibraryFileModifiedObject[]} */ get audioLibraryFilesModified() { - return this.libraryFilesModified.filter(lf => globals.SupportedAudioTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesModified.filter((lf) => globals.SupportedAudioTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get audioLibraryFilesRemoved() { - return this.libraryFilesRemoved.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesRemoved.filter((lf) => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get audioLibraryFilesAdded() { - return this.libraryFilesAdded.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesAdded.filter((lf) => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get audioLibraryFiles() { - return this.libraryFiles.filter(lf => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFiles.filter((lf) => globals.SupportedAudioTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryFileModifiedObject[]} */ get imageLibraryFilesModified() { - return this.libraryFilesModified.filter(lf => globals.SupportedImageTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesModified.filter((lf) => globals.SupportedImageTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get imageLibraryFilesRemoved() { - return this.libraryFilesRemoved.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesRemoved.filter((lf) => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get imageLibraryFilesAdded() { - return this.libraryFilesAdded.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesAdded.filter((lf) => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get imageLibraryFiles() { - return this.libraryFiles.filter(lf => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFiles.filter((lf) => globals.SupportedImageTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryFileModifiedObject[]} */ get ebookLibraryFilesModified() { - return this.libraryFilesModified.filter(lf => globals.SupportedEbookTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesModified.filter((lf) => globals.SupportedEbookTypes.includes(lf.old.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get ebookLibraryFilesRemoved() { - return this.libraryFilesRemoved.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesRemoved.filter((lf) => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get ebookLibraryFilesAdded() { - return this.libraryFilesAdded.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFilesAdded.filter((lf) => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject[]} */ get ebookLibraryFiles() { - return this.libraryFiles.filter(lf => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) + return this.libraryFiles.filter((lf) => globals.SupportedEbookTypes.includes(lf.metadata.ext?.slice(1).toLowerCase() || '')) } /** @type {LibraryItem.LibraryFileObject} */ get descTxtLibraryFile() { - return this.libraryFiles.find(lf => lf.metadata.filename === 'desc.txt') + return this.libraryFiles.find((lf) => lf.metadata.filename === 'desc.txt') } /** @type {LibraryItem.LibraryFileObject} */ get readerTxtLibraryFile() { - return this.libraryFiles.find(lf => lf.metadata.filename === 'reader.txt') + return this.libraryFiles.find((lf) => lf.metadata.filename === 'reader.txt') } /** @type {LibraryItem.LibraryFileObject} */ get metadataAbsLibraryFile() { - return this.libraryFiles.find(lf => lf.metadata.filename === 'metadata.abs') + return this.libraryFiles.find((lf) => lf.metadata.filename === 'metadata.abs') } /** @type {LibraryItem.LibraryFileObject} */ get metadataJsonLibraryFile() { - return this.libraryFiles.find(lf => lf.metadata.filename === 'metadata.json') + return this.libraryFiles.find((lf) => lf.metadata.filename === 'metadata.json') } /** @type {LibraryItem.LibraryFileObject} */ get metadataOpfLibraryFile() { - return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.opf') + return this.libraryFiles.find((lf) => lf.metadata.ext.toLowerCase() === '.opf') } /** @type {LibraryItem.LibraryFileObject} */ get metadataNfoLibraryFile() { - return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.nfo') + return this.libraryFiles.find((lf) => lf.metadata.ext.toLowerCase() === '.nfo') } /** - * - * @param {LibraryItem} existingLibraryItem + * + * @param {LibraryItem} existingLibraryItem * @param {import('./LibraryScan')} libraryScan * @returns {boolean} true if changes found */ async checkLibraryItemData(existingLibraryItem, libraryScan) { - const keysToCompare = ['libraryFolderId', 'ino', 'path', 'relPath', 'isFile'] + const keysToCompare = ['libraryFolderId', 'ino', 'deviceId', 'path', 'relPath', 'isFile'] this.hasChanges = false this.hasPathChange = false for (const key of keysToCompare) { @@ -219,28 +222,29 @@ class LibraryItemScanData { this.libraryFilesRemoved = [] this.libraryFilesModified = [] - let libraryFilesAdded = this.libraryFiles.map(lf => lf) + let libraryFilesAdded = this.libraryFiles.map((lf) => lf) for (const existingLibraryFile of existingLibraryItem.libraryFiles) { // Find matching library file using path first and fallback to using inode value - let matchingLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === existingLibraryFile.metadata.path) + let matchingLibraryFile = this.libraryFiles.find((lf) => lf.metadata.path === existingLibraryFile.metadata.path) if (!matchingLibraryFile) { - matchingLibraryFile = this.libraryFiles.find(lf => lf.ino === existingLibraryFile.ino) + matchingLibraryFile = this.libraryFiles.find((lf) => lf.ino === existingLibraryFile.ino) if (matchingLibraryFile) { libraryScan.addLog(LogLevel.INFO, `Library file with path "${existingLibraryFile.metadata.path}" not found, but found file with matching inode value "${existingLibraryFile.ino}" at path "${matchingLibraryFile.metadata.path}"`) } } - if (!matchingLibraryFile) { // Library file removed + if (!matchingLibraryFile) { + // Library file removed libraryScan.addLog(LogLevel.INFO, `Library file "${existingLibraryFile.metadata.path}" was removed from library item "${existingLibraryItem.relPath}"`) this.libraryFilesRemoved.push(existingLibraryFile) - existingLibraryItem.libraryFiles = existingLibraryItem.libraryFiles.filter(lf => lf !== existingLibraryFile) + existingLibraryItem.libraryFiles = existingLibraryItem.libraryFiles.filter((lf) => lf !== existingLibraryFile) this.hasChanges = true } else { - libraryFilesAdded = libraryFilesAdded.filter(lf => lf !== matchingLibraryFile) + libraryFilesAdded = libraryFilesAdded.filter((lf) => lf !== matchingLibraryFile) let existingLibraryFileBefore = structuredClone(existingLibraryFile) if (this.compareUpdateLibraryFile(existingLibraryItem.path, existingLibraryFile, matchingLibraryFile, libraryScan)) { - this.libraryFilesModified.push({old: existingLibraryFileBefore, new: existingLibraryFile}) + this.libraryFilesModified.push({ old: existingLibraryFileBefore, new: existingLibraryFile }) this.hasChanges = true } } @@ -263,7 +267,7 @@ class LibraryItemScanData { if (this.hasChanges) { existingLibraryItem.size = 0 - existingLibraryItem.libraryFiles.forEach((lf) => existingLibraryItem.size += lf.metadata.size) + existingLibraryItem.libraryFiles.forEach((lf) => (existingLibraryItem.size += lf.metadata.size)) existingLibraryItem.lastScan = Date.now() existingLibraryItem.lastScanVersion = packageJson.version @@ -283,16 +287,17 @@ class LibraryItemScanData { /** * Update existing library file with scanned in library file data * @param {string} libraryItemPath - * @param {LibraryItem.LibraryFileObject} existingLibraryFile - * @param {import('../objects/files/LibraryFile')} scannedLibraryFile + * @param {LibraryItem.LibraryFileObject} existingLibraryFile + * @param {import('../objects/files/LibraryFile')} scannedLibraryFile * @param {import('./LibraryScan')} libraryScan * @returns {boolean} false if no changes */ compareUpdateLibraryFile(libraryItemPath, existingLibraryFile, scannedLibraryFile, libraryScan) { let hasChanges = false - if (existingLibraryFile.ino !== scannedLibraryFile.ino) { + if (existingLibraryFile.ino !== scannedLibraryFile.ino && existingLibraryFile.deviceId !== scannedLibraryFile.deviceId) { existingLibraryFile.ino = scannedLibraryFile.ino + existingLibraryFile.deviceId = scannedLibraryFile.deviceId hasChanges = true } @@ -317,38 +322,38 @@ class LibraryItemScanData { /** * Check if existing audio file on Book was removed - * @param {import('../models/Book').AudioFileObject} existingAudioFile + * @param {import('../models/Book').AudioFileObject} existingAudioFile * @returns {boolean} true if audio file was removed */ checkAudioFileRemoved(existingAudioFile) { if (!this.audioLibraryFilesRemoved.length) return false // First check exact path - if (this.audioLibraryFilesRemoved.some(af => af.metadata.path === existingAudioFile.metadata.path)) { + if (this.audioLibraryFilesRemoved.some((af) => af.metadata.path === existingAudioFile.metadata.path)) { return true } // Fallback to check inode value - return this.audioLibraryFilesRemoved.some(af => af.ino === existingAudioFile.ino) + return this.audioLibraryFilesRemoved.some((af) => af.ino === existingAudioFile.ino) } /** * Check if existing ebook file on Book was removed - * @param {import('../models/Book').EBookFileObject} ebookFile + * @param {import('../models/Book').EBookFileObject} ebookFile * @returns {boolean} true if ebook file was removed */ checkEbookFileRemoved(ebookFile) { if (!this.ebookLibraryFiles.length) return true - if (this.ebookLibraryFiles.some(lf => lf.metadata.path === ebookFile.metadata.path)) { + if (this.ebookLibraryFiles.some((lf) => lf.metadata.path === ebookFile.metadata.path)) { return false } - return !this.ebookLibraryFiles.some(lf => lf.ino === ebookFile.ino) + return !this.ebookLibraryFiles.some((lf) => lf.ino === ebookFile.ino) } /** * Set data parsed from filenames - * - * @param {Object} bookMetadata + * + * @param {Object} bookMetadata */ setBookMetadataFromFilenames(bookMetadata) { const keysToMap = ['title', 'subtitle', 'publishedYear', 'asin'] @@ -374,4 +379,4 @@ class LibraryItemScanData { } } } -module.exports = LibraryItemScanData \ No newline at end of file +module.exports = LibraryItemScanData diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js index 501df4274..4d55b4301 100644 --- a/server/scanner/LibraryItemScanner.js +++ b/server/scanner/LibraryItemScanner.js @@ -139,26 +139,15 @@ class LibraryItemScanner { const newLibraryFile = new LibraryFile() // fileItem.path is the relative path await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path) + // TODO: BUGBUG - this is pushing the object, not a JSON string of the object like elsewhere libraryFiles.push(newLibraryFile) } - const libraryItemStats = await fileUtils.getFileTimestampsWithIno(libraryItemData.path) - return new LibraryItemScanData({ - libraryFolderId: folder.id, - libraryId: library.id, - mediaType: library.mediaType, - ino: libraryItemStats.ino, - mtimeMs: libraryItemStats.mtimeMs || 0, - ctimeMs: libraryItemStats.ctimeMs || 0, - birthtimeMs: libraryItemStats.birthtimeMs || 0, - path: libraryItemData.path, - relPath: libraryItemData.relPath, - isFile: isSingleMediaItem, - mediaMetadata: libraryItemData.mediaMetadata || null, - libraryFiles - }) + return await buildLibraryItemScanData(libraryItemData, folder, library, isSingleMediaItem, libraryFiles) } + async setDataFromPath(path) {} + /** * * @param {import('../models/LibraryItem')} existingLibraryItem @@ -219,3 +208,22 @@ class LibraryItemScanner { } } module.exports = new LibraryItemScanner() + +async function buildLibraryItemScanData(libraryItemData, folder, library, isSingleMediaItem, libraryFiles) { + const libraryItemStats = await fileUtils.getFileTimestampsWithIno(libraryItemData.path) + return new LibraryItemScanData({ + libraryFolderId: folder.id, + libraryId: library.id, + mediaType: library.mediaType, + ino: libraryItemStats.ino, + deviceId: libraryItemStats.dev, + mtimeMs: libraryItemStats.mtimeMs || 0, + ctimeMs: libraryItemStats.ctimeMs || 0, + birthtimeMs: libraryItemStats.birthtimeMs || 0, + path: libraryItemData.path, + relPath: libraryItemData.relPath, + isFile: isSingleMediaItem, + mediaMetadata: libraryItemData.mediaMetadata || null, + libraryFiles + }) +} diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 640c82d76..58bce0f8b 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -297,7 +297,7 @@ class LibraryScanner { * Get scan data for library folder * @param {import('../models/Library')} library * @param {import('../models/LibraryFolder')} folder - * @returns {LibraryItemScanData[]} + * @returns {Promise} */ async scanFolder(library, folder) { const folderPath = fileUtils.filePathToPOSIX(folder.path) @@ -350,6 +350,7 @@ class LibraryScanner { libraryId: folder.libraryId, mediaType: library.mediaType, ino: libraryItemFolderStats.ino, + deviceId: libraryItemFolderStats.dev, mtimeMs: libraryItemFolderStats.mtimeMs || 0, ctimeMs: libraryItemFolderStats.ctimeMs || 0, birthtimeMs: libraryItemFolderStats.birthtimeMs || 0, @@ -642,12 +643,25 @@ class LibraryScanner { } module.exports = new LibraryScanner() +/** + * @param {import("../models/LibraryItem") | LibraryItemScanData} libraryItem1 + * @param {import("../models/LibraryItem") | LibraryItemScanData} libraryItem2 + */ function ItemToFileInoMatch(libraryItem1, libraryItem2) { - return libraryItem1.isFile && libraryItem2.libraryFiles.some((lf) => lf.ino === libraryItem1.ino) + return ( + libraryItem1.isFile && + libraryItem2.libraryFiles.some((lf) => { + return lf.ino === libraryItem1.ino && lf.deviceId === libraryItem1.deviceId + }) + ) } +/** + * @param {LibraryItemScanData} libraryItem1 + * @param {import("../models/LibraryItem")} libraryItem2 + */ function ItemToItemInoMatch(libraryItem1, libraryItem2) { - return libraryItem1.ino === libraryItem2.ino + return libraryItem1.ino === libraryItem2.ino && libraryItem1.deviceId === libraryItem2.deviceId } function hasAudioFiles(fileUpdateGroup, itemDir) { @@ -658,54 +672,85 @@ function isSingleMediaFile(fileUpdateGroup, itemDir) { return itemDir === fileUpdateGroup[itemDir] } +/** + * @param {UUIDV4} libraryId + * @param {string} fullPath + * @returns {Promise} library item that matches + */ async function findLibraryItemByItemToItemInoMatch(libraryId, fullPath) { const ino = await fileUtils.getIno(fullPath) + const deviceId = await fileUtils.getDeviceId(fullPath) if (!ino) return null const existingLibraryItem = await Database.libraryItemModel.findOneExpanded({ libraryId: libraryId, - ino: ino + ino: ino, + deviceId: deviceId }) if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with matching inode "${ino}" at path "${existingLibraryItem.path}"`) return existingLibraryItem } +/** + * @param {UUIDV4} libraryId + * @param {string} fullPath + * @param {boolean} isSingleMedia + * @returns {Promise} library item that matches + */ async function findLibraryItemByItemToFileInoMatch(libraryId, fullPath, isSingleMedia) { if (!isSingleMedia) return null // check if it was moved from another folder by comparing the ino to the library files const ino = await fileUtils.getIno(fullPath) + const deviceId = await fileUtils.getDeviceId(fullPath) if (!ino) return null const existingLibraryItem = await Database.libraryItemModel.findOneExpanded( [ { libraryId: libraryId }, - sequelize.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode)'), { + sequelize.where(sequelize.literal('(SELECT count(*) FROM json_each(libraryFiles) WHERE json_valid(json_each.value) AND json_each.value->>"$.ino" = :inode AND json_each.value->>"$.deviceId" = :deviceId)'), { [sequelize.Op.gt]: 0 }) ], { - inode: ino + inode: ino, + deviceId: deviceId } ) if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with a library file matching inode "${ino}" at path "${existingLibraryItem.path}"`) return existingLibraryItem } +/** + * @param {UUIDV4} libraryId + * @param {string} fullPath + * @param {boolean} isSingleMedia + * @param {string[]} itemFiles + * @returns {Promise} library item that matches + */ async function findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingleMedia, itemFiles) { if (isSingleMedia) return null - // check if it was moved from the root folder by comparing the ino to the ino of the scanned files + // check if it was moved from the root folder by comparing the ino and deviceId to the ino and deviceId of the scanned files let itemFileInos = [] for (const itemFile of itemFiles) { const ino = await fileUtils.getIno(Path.posix.join(fullPath, itemFile)) - if (ino) itemFileInos.push(ino) + const deviceId = await fileUtils.getDeviceId(Path.posix.join(fullPath, itemFile)) + if (ino && deviceId) itemFileInos.push({ ino: ino, deviceId: deviceId }) } if (!itemFileInos.length) return null - const existingLibraryItem = await Database.libraryItemModel.findOneExpanded({ - libraryId: libraryId, - ino: { - [sequelize.Op.in]: itemFileInos + /** @type {import('../models/LibraryItem').LibraryItemExpanded | null} */ + let existingLibraryItem = null + for (let item in itemFileInos) { + existingLibraryItem = await Database.libraryItemModel.findOneExpanded({ + libraryId: libraryId, + ino: { + [sequelize.Op.in]: itemFileInos + } + }) + if (existingLibraryItem) { + break } - }) + } + if (existingLibraryItem) Logger.debug(`[LibraryScanner] Found library item with inode matching one of "${itemFileInos.join(',')}" at path "${existingLibraryItem.path}"`) return existingLibraryItem } diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 2da6b4c91..d542e8cb1 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -47,6 +47,10 @@ function getFileStat(path) { } } +/** + * @param {string} path + * @returns {Promise} + */ async function getFileTimestampsWithIno(path) { try { var stat = await fs.stat(path, { bigint: true }) @@ -55,11 +59,12 @@ async function getFileTimestampsWithIno(path) { mtimeMs: Number(stat.mtimeMs), ctimeMs: Number(stat.ctimeMs), birthtimeMs: Number(stat.birthtimeMs), - ino: String(stat.ino) + ino: String(stat.ino), + deviceId: String(stat.dev) } } catch (err) { Logger.error(`[fileUtils] Failed to getFileTimestampsWithIno for path "${path}"`, err) - return false + return null } } module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno @@ -92,7 +97,7 @@ module.exports.getFileMTimeMs = async (path) => { /** * * @param {string} filepath - * @returns {boolean} + * @returns {Promise} isFile */ async function checkPathIsFile(filepath) { try { @@ -104,6 +109,10 @@ async function checkPathIsFile(filepath) { } module.exports.checkPathIsFile = checkPathIsFile +/** + * @param {string} path + * @returns {string | null} inode + */ function getIno(path) { return fs .stat(path, { bigint: true }) @@ -115,10 +124,25 @@ function getIno(path) { } module.exports.getIno = getIno +/** + * @param {string} path + * @returns {Promise} deviceId + */ +async function getDeviceId(path) { + try { + var data = await fs.stat(path) + return String(data.dev) + } catch (error) { + Logger.error(`[Utils] Failed to get device Id for path "${path}": ${error}`) + return null + } +} +module.exports.getDeviceId = getDeviceId + /** * Read contents of file * @param {string} path - * @returns {string} + * @returns {Promise} file contents */ async function readTextFile(path) { try { @@ -135,7 +159,7 @@ module.exports.readTextFile = readTextFile * Check if file or directory should be ignored. Returns a string of the reason to ignore, or null if not ignored * * @param {string} path - * @returns {string} + * @returns {string | null} reason to ignore */ module.exports.shouldIgnoreFile = (path) => { // Check if directory or file name starts with "." @@ -178,8 +202,8 @@ module.exports.shouldIgnoreFile = (path) => { /** * Get array of files inside dir * @param {string} path - * @param {string} [relPathToReplace] - * @returns {FilePathItem[]} + * @param {string | null} [relPathToReplace] + * @returns {Promise} */ module.exports.recurseFiles = async (path, relPathToReplace = null) => { path = filePathToPOSIX(path) @@ -292,7 +316,7 @@ module.exports.getFilePathItemFromFileUpdate = (fileUpdate) => { * * @param {string} url * @param {string} filepath path to download the file to - * @param {Function} [contentTypeFilter] validate content type before writing + * @param {Function | null} [contentTypeFilter] validate content type before writing * @returns {Promise} */ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => { diff --git a/test/server/objects/LibraryItemScanData.test.js b/test/server/objects/LibraryItemScanData.test.js new file mode 100644 index 000000000..6322ef7db --- /dev/null +++ b/test/server/objects/LibraryItemScanData.test.js @@ -0,0 +1,23 @@ +// TODO - need to check +// compareUpdateLibraryFile +// checkEbookFileRemoved +// checkAudioFileRemoved +// libraryItemObject() +/* +new LibraryItemScanData({ + libraryFolderId: folder.id, + libraryId: library.id, + mediaType: library.mediaType, + ino: libraryItemStats.ino, + deviceId: libraryItemStats.dev, + mtimeMs: libraryItemStats.mtimeMs || 0, + ctimeMs: libraryItemStats.ctimeMs || 0, + birthtimeMs: libraryItemStats.birthtimeMs || 0, + path: libraryItemData.path, + relPath: libraryItemData.relPath, + isFile: isSingleMediaItem, + mediaMetadata: libraryItemData.mediaMetadata || null, + libraryFiles + }) + +*/ diff --git a/test/server/objects/SimilarLibraryFileObjects.test.js b/test/server/objects/SimilarLibraryFileObjects.test.js new file mode 100644 index 000000000..aa10465cd --- /dev/null +++ b/test/server/objects/SimilarLibraryFileObjects.test.js @@ -0,0 +1,9 @@ +const LibraryFile = require('../../../server/objects/files/LibraryFile') +const EBookFile = require('../../../server/objects/files/EBookFile') +const AudioFile = require('../../../server/objects/files/AudioFile') +const LibraryItem = require('../../../server/models/LibraryItem') +const LibraryItemScanData = require('../../../server/scanner/LibraryItemScanData') + +// TODO: all of these duplicate each other. Need to verify that deviceId is set on each when constructing. And that deviceId is populated when using toJSON() + +// TODO: check that any libraryFiles properties set to JSON contain a LibraryFile which has a deviceId property diff --git a/test/server/scanner/LibraryItemScanner.test.js b/test/server/scanner/LibraryItemScanner.test.js new file mode 100644 index 000000000..28c370b24 --- /dev/null +++ b/test/server/scanner/LibraryItemScanner.test.js @@ -0,0 +1 @@ +// TODO: test buildLibraryItemScanData diff --git a/test/server/scanner/LibraryScanner.test.js b/test/server/scanner/LibraryScanner.test.js new file mode 100644 index 000000000..06b7203a6 --- /dev/null +++ b/test/server/scanner/LibraryScanner.test.js @@ -0,0 +1,323 @@ +const chai = require('chai') +const expect = chai.expect +const sinon = require('sinon') +const rewire = require('rewire') +const fileUtils = require('../../../server/utils/fileUtils') +const Database = require('../../../server/Database') +const { Sequelize } = require('sequelize') +const LibraryFile = require('../../../server/objects/files/LibraryFile') +const LibraryItem = require('../../../server/models/LibraryItem') +const FileMetadata = require('../../../server/objects/metadata/FileMetadata') +const Path = require('path') + +describe('LibraryScanner', () => { + let getInoStub, getDeviceIdStub, getFileTimestampsWithInoStub, LibraryScanner, testLibrary + + beforeEach(async () => { + getInoStub = sinon.stub(fileUtils, 'getIno') + getInoStub.callsFake((path) => { + const normalizedPath = fileUtils.filePathToPOSIX(path).replace(/\/$/, '') + const stats = getMockFileInfo().get(normalizedPath) + if (stats) { + return stats.ino + } else { + return null + } + }) + + getDeviceIdStub = sinon.stub(fileUtils, 'getDeviceId') + getDeviceIdStub.callsFake(async (path) => { + const normalizedPath = fileUtils.filePathToPOSIX(path).replace(/\/$/, '') + const stats = getMockFileInfo().get(normalizedPath) + if (stats) { + return stats.dev + } else { + return null + } + }) + + getFileTimestampsWithInoStub = sinon.stub(fileUtils, 'getFileTimestampsWithIno') + getFileTimestampsWithInoStub.callsFake(async (path) => { + const normalizedPath = fileUtils.filePathToPOSIX(path).replace(/\/$/, '') + const stats = getMockFileInfo().get(normalizedPath) + if (stats) { + return stats + } else { + return null + } + }) + + LibraryScanner = rewire('../../../server/scanner/LibraryScanner') + }) + + afterEach(() => { + sinon.restore() + }) + + it('findsByInodeAndDeviceId', async function () { + // this.timeout(50000) // Note: don't use arrow function or timeout for debugging doesn't work + let findLibraryItemByItemToFileInoMatch = LibraryScanner.__get__('findLibraryItemByItemToFileInoMatch') + let fullPath = '/test/file.pdf' + + let mockFileInfo = getMockFileInfo() + testLibrary = await loadTestDatabase(mockFileInfo) + + const fileInfo = mockFileInfo.get(fullPath) + + /** @type {Promise} */ + const result = await findLibraryItemByItemToFileInoMatch(testLibrary.id, fullPath, true) + expect(result).to.not.be.null + expect(result.libraryFiles[0].metadata.path).to.equal(fullPath) + expect(result.libraryFiles[0].deviceId).to.equal(fileInfo.dev) + }) + + it('findsTheCorrectItemByInodeAndDeviceIdWhenThereAreDuplicateInodes', async () => { + let findLibraryItemByItemToFileInoMatch = LibraryScanner.__get__('findLibraryItemByItemToFileInoMatch') + let fullPath = '/mnt/drive/file-same-ino-different-dev.pdf' + + let mockFileInfo = getMockFileInfo() + testLibrary = await loadTestDatabase(mockFileInfo) + + const fileInfo = mockFileInfo.get(fullPath) + + /** @type {Promise} */ + const result = await findLibraryItemByItemToFileInoMatch(testLibrary.id, fullPath, true) + expect(result).to.not.be.null + expect(result.libraryFiles[0].metadata.path).to.equal(fullPath) + expect(result.libraryFiles[0].deviceId).to.equal(fileInfo.dev) + }) + + it('findLibraryItemByItemToItemInoMatch', async function () { + this.timeout(0) + // findLibraryItemByItemToItemInoMatch(libraryId, fullPath) + // findLibraryItemByFileToItemInoMatch(libraryId, fullPath, isSingleMedia, itemFiles) + let findLibraryItemByItemToItemInoMatch = LibraryScanner.__get__('findLibraryItemByItemToItemInoMatch') + + let fullPath = '/test/file.pdf' + + let mockFileInfo = getMockFileInfo() + testLibrary = await loadTestDatabase(mockFileInfo) + + const fileInfo = mockFileInfo.get(fullPath) + + /** @type {Promise} */ + const result = await findLibraryItemByItemToItemInoMatch(testLibrary.id, fullPath) + expect(result).to.not.be.null + expect(result.libraryFiles[0].metadata.path).to.equal(fullPath) + expect(result.libraryFiles[0].deviceId).to.equal(fileInfo.dev) + }) + + // ItemToFileInoMatch + it('ItemToFileInoMatch-ItemMatchesSelf', async function () { + this.timeout(0) + /** + * @param {import("../../../server/models/LibraryItem") | import("../../../server/scanner/LibraryItemScanData")} libraryItem1 + * @param {import("../../../server/models/LibraryItem") | import("../../../server/scanner/LibraryItemScanData")} libraryItem2 + */ + let ItemToFileInoMatch = LibraryScanner.__get__('ItemToFileInoMatch') + + // this compares the inode from the first library item to the second library item's library file inode + let mockFileInfo = getMockFileInfo() + testLibrary = await loadTestDatabase(mockFileInfo) + + const fileInfo = mockFileInfo.get('/test/file.pdf') + + let item1 = await Database.libraryItemModel.findOneExpanded({ + libraryId: testLibrary.id, + // @ts-ignore + ino: fileInfo.ino + }) + + expect(ItemToFileInoMatch(item1, item1)).to.be.true + }) + + it('ItemToFileInoMatch-TwoItemsWithSameInoButDifferentDeviceShouldNotMatch', async function () { + this.timeout(0) + /** + * @param {import("../../../server/models/LibraryItem") | import("../../../server/scanner/LibraryItemScanData")} libraryItem1 + * @param {import("../../../server/models/LibraryItem") | import("../../../server/scanner/LibraryItemScanData")} libraryItem2 + */ + let ItemToFileInoMatch = LibraryScanner.__get__('ItemToFileInoMatch') + + let mockFileInfo = getMockFileInfo() + testLibrary = await loadTestDatabase(mockFileInfo) + + // this compares the inode from the first library item to the second library item's library file inode + const item1 = await Database.libraryItemModel.findOneExpanded({ + libraryId: testLibrary.id, + path: '/test/file.pdf' + }) + + const item2 = await Database.libraryItemModel.findOneExpanded({ + libraryId: testLibrary.id, + path: '/mnt/drive/file-same-ino-different-dev.pdf' + }) + + expect(item1.path).to.not.equal(item2.path) + + expect(ItemToFileInoMatch(item1, item2)).to.be.false + }) + + it('ItemToFileInoMatch-RenamedFileShouldMatch', async function () { + this.timeout(0) + /** + * @param {import("../../../server/models/LibraryItem") | import("../../../server/scanner/LibraryItemScanData")} libraryItem1 + * @param {import("../../../server/models/LibraryItem") | import("../../../server/scanner/LibraryItemScanData")} libraryItem2 + */ + let ItemToFileInoMatch = LibraryScanner.__get__('ItemToFileInoMatch') + + let mockFileInfo = getMockFileInfo() + testLibrary = await loadTestDatabase(mockFileInfo) + + // this compares the inode from the first library item to the second library item's library file inode + const original = await Database.libraryItemModel.findOneExpanded({ + libraryId: testLibrary.id, + path: '/test/file.pdf' + }) + + const renamedMockFileInfo = getRenamedMockFileInfo().get('/test/file-renamed.pdf') + const renamedFile = new LibraryFile() + var fileMetadata = new FileMetadata() + fileMetadata.setData(renamedMockFileInfo) + fileMetadata.filename = Path.basename(renamedMockFileInfo.path) + fileMetadata.path = fileUtils.filePathToPOSIX(renamedMockFileInfo.path) + fileMetadata.relPath = fileUtils.filePathToPOSIX(renamedMockFileInfo.path) + fileMetadata.ext = Path.extname(renamedMockFileInfo.path) + renamedFile.ino = renamedMockFileInfo.ino + renamedFile.deviceId = renamedMockFileInfo.dev + renamedFile.metadata = fileMetadata + renamedFile.addedAt = Date.now() + renamedFile.updatedAt = Date.now() + renamedFile.metadata = fileMetadata + + const renamedItem = new LibraryItem(buildBookLibraryItemParams(renamedFile, null, testLibrary.id, null)) + + expect(ItemToFileInoMatch(original, renamedItem)).to.be.true + }) + + // ItemToItemInoMatch + it('ItemToItemInoMatch-ItemMatchesSelf', async function () { + this.timeout(0) + /** + * @param {import("../../../server/models/LibraryItem") | import("../../../server/scanner/LibraryItemScanData")} libraryItem1 + * @param {import("../../../server/models/LibraryItem") | import("../../../server/scanner/LibraryItemScanData")} libraryItem2 + */ + let ItemToItemInoMatch = LibraryScanner.__get__('ItemToItemInoMatch') + + // this compares the inode from the first library item to the second library item's library file inode + let mockFileInfo = getMockFileInfo() + testLibrary = await loadTestDatabase(mockFileInfo) + + const fileInfo = mockFileInfo.get('/test/file.pdf') + + let item1 = await Database.libraryItemModel.findOneExpanded({ + libraryId: testLibrary.id, + // @ts-ignore + ino: fileInfo.ino + }) + + expect(ItemToItemInoMatch(item1, item1)).to.be.true + }) + + it('ItemToItemInoMatch-TwoItemsWithSameInoButDifferentDeviceShouldNotMatch', async () => { + let ItemToItemInoMatch = LibraryScanner.__get__('ItemToItemInoMatch') + + let mockFileInfo = getMockFileInfo() + testLibrary = await loadTestDatabase(mockFileInfo) + + // this compares the inode from the first library item to the second library item's library file inode + const item1 = await Database.libraryItemModel.findOneExpanded({ + libraryId: testLibrary.id, + path: '/test/file.pdf' + }) + + const item2 = await Database.libraryItemModel.findOneExpanded({ + libraryId: testLibrary.id, + path: '/mnt/drive/file-same-ino-different-dev.pdf' + }) + + expect(item1.path).to.not.equal(item2.path) + + expect(ItemToItemInoMatch(item1, item2)).to.be.false + }) + + it('ItemToItemInoMatch-RenamedFileShouldMatch', () => { + let ItemToItemInoMatch = LibraryScanner.__get__('ItemToItemInoMatch') + }) +}) + +async function loadTestDatabase(mockFileInfo) { + let libraryItem1Id, libraryItem2Id + + let fileInfo = mockFileInfo || getMockFileInfo() + let bookLibraryFiles = fileInfo.keys().reduce((acc, key) => { + let bookfile = new LibraryFile() + bookfile.setDataFromPath(key, key) + acc.push(bookfile) + return acc + }, []) + + global.ServerSettings = {} + Database.sequelize = new Sequelize({ + dialect: 'sqlite', + storage: ':memory:', + // Choose one of the logging options + logging: (...msg) => console.log(msg), + logQueryParameters: true + }) + Database.sequelize.uppercaseFirst = (str) => (str ? `${str[0].toUpperCase()}${str.substr(1)}` : '') + await Database.buildModels() + + const newLibrary = await Database.libraryModel.create({ name: 'Test Library', mediaType: 'book' }) + const newLibraryFolder = await Database.libraryFolderModel.create({ path: '/test', libraryId: newLibrary.id }) + const newLibraryFolder2 = await Database.libraryFolderModel.create({ path: '/mnt/drive', libraryId: newLibrary.id }) + + const newBook = await Database.bookModel.create({ title: 'Test Book', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const newLibraryItem = await Database.libraryItemModel.create(buildBookLibraryItemParams(bookLibraryFiles[0], newBook.id, newLibrary.id, newLibraryFolder.id)) + libraryItem1Id = newLibraryItem.id + + const newBook2 = await Database.bookModel.create({ title: 'Test Book 2', audioFiles: [], tags: [], narrators: [], genres: [], chapters: [] }) + const newLibraryItem2 = await Database.libraryItemModel.create(buildBookLibraryItemParams(bookLibraryFiles[1], newBook2.id, newLibrary.id, newLibraryFolder2.id)) + libraryItem2Id = newLibraryItem2.id + + return newLibrary +} + +/** + * @param {LibraryFile} libraryFile + * @param {any} bookId + * @param {string} libraryId + * @param {any} libraryFolderId + */ +function buildBookLibraryItemParams(libraryFile, bookId, libraryId, libraryFolderId) { + return { + path: libraryFile.metadata.path, + isFile: true, + ino: libraryFile.ino, + deviceId: libraryFile.deviceId, + libraryFiles: [libraryFile.toJSON()], + mediaId: bookId, + mediaType: 'book', + libraryId: libraryId, + libraryFolderId: libraryFolderId + } +} + +/** @returns {Map} */ +function getMockFileInfo() { + // @ts-ignore + return new Map([ + ['/test/file.pdf', { path: '/test/file.pdf', isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '1', dev: '100' }], + ['/mnt/drive/file-same-ino-different-dev.pdf', { path: '/mnt/drive/file-same-ino-different-dev.pdf', isDirectory: () => false, size: 42, mtimeMs: Date.now(), ino: '1', dev: '200' }] + ]) +} + +/** @returns {Map} */ +// this has the same data as above except one file has been renamed +function getRenamedMockFileInfo() { + // @ts-ignore + return new Map([ + ['/test/file-renamed.pdf', { path: '/test/file-renamed.pdf', isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '1', dev: '100' }], + ['/mnt/drive/file-same-ino-different-dev.pdf', { path: '/mnt/drive/file-same-ino-different-dev.pdf', isDirectory: () => false, size: 42, mtimeMs: Date.now(), ino: '1', dev: '200' }] + ]) +} diff --git a/test/server/utils/fileUtils.test.js b/test/server/utils/fileUtils.test.js index b57a6fb86..77b088897 100644 --- a/test/server/utils/fileUtils.test.js +++ b/test/server/utils/fileUtils.test.js @@ -3,8 +3,13 @@ const expect = chai.expect const sinon = require('sinon') const fileUtils = require('../../../server/utils/fileUtils') const fs = require('fs') +const fsextra = require('../../../server/libs/fsExtra') const Logger = require('../../../server/Logger') +/** + * @typedef {import('../../../server/libs/fsExtra').fsExtra} fsextra + */ + describe('fileUtils', () => { it('shouldIgnoreFile', () => { global.isWin = process.platform === 'win32' @@ -39,6 +44,46 @@ describe('fileUtils', () => { }) }) + describe('fsextra', () => { + let statStub + + beforeEach(() => { + // two files with same indoe but different device ID + const mockStats = new Map([ + ['/test/file1.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '1', dev: '100' }], + ['/mnt/other/file2.txt', { isDirectory: () => false, size: 512, mtimeMs: Date.now(), ino: '1', dev: '200' }] + ]) + + statStub = sinon.stub(fsextra, 'stat') + statStub.callsFake((path) => { + const normalizedPath = fileUtils.filePathToPOSIX(path).replace(/\/$/, '') + const stats = mockStats.get(normalizedPath) + if (stats) { + return stats + } else { + new Error(`ENOENT: no such file or directory, stat '${normalizedPath}'`) + } + }) + }) + + after(() => { + fsextra.stat.restore() + sinon.restore() + }) + + it('shouldGetDeviceIdForFile', async () => { + const id = await fileUtils.getDeviceId('/test/file1.mp3') + + expect(id).to.be.an('string') + + const id2 = await fileUtils.getDeviceId('/mnt/other/file2.txt') + + expect(id2).to.be.an('string') + + expect(id).to.not.equal(id2) + }) + }) + describe('recurseFiles', () => { let readdirStub, realpathStub, statStub @@ -53,7 +98,7 @@ describe('fileUtils', () => { ]) const mockStats = new Map([ - ['/test/file1.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '1' }], + ['/test/file1.mp3', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '1', dev: '100' }], ['/test/subfolder', { isDirectory: () => true, size: 0, mtimeMs: Date.now(), ino: '2' }], ['/test/subfolder/file2.m4b', { isDirectory: () => false, size: 1024, mtimeMs: Date.now(), ino: '3' }], ['/test/ignoreme', { isDirectory: () => true, size: 0, mtimeMs: Date.now(), ino: '4' }], @@ -98,6 +143,9 @@ describe('fileUtils', () => { }) afterEach(() => { + fs.stat.restore() + fs.realpath.restore() + fs.readdir.restore() sinon.restore() }) @@ -105,6 +153,7 @@ describe('fileUtils', () => { const files = await fileUtils.recurseFiles('/test') expect(files).to.be.an('array') expect(files).to.have.lengthOf(3) + expect(statStub.called).to.be.true expect(files[0]).to.deep.equal({ name: 'file1.mp3',