diff --git a/package-lock.json b/package-lock.json index d7eca51..82b1c81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@types/csv-parse": "^1.2.5", "@types/uuid": "^9.0.6", + "archiver": "^7.0.1", "bcrypt": "^5.1.0", "cookie-parser": "^1.4.6", "csv-parse": "^5.5.6", @@ -27,6 +28,7 @@ "yauzl": "^3.1.3" }, "devDependencies": { + "@types/archiver": "^6.0.3", "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.17", @@ -889,6 +891,95 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "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", @@ -1396,6 +1487,15 @@ "node": ">=10" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1453,6 +1553,15 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/archiver": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.3.tgz", + "integrity": "sha512-a6wUll6k3zX6qs5KlxIggs1P1JcYJaTCx2gnlr+f0S1yd2DoaEwoIK10HmBaLnZwWneBz+JBm0dwcZu0zECBcQ==", + "dev": true, + "dependencies": { + "@types/readdir-glob": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -1698,6 +1807,15 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -2184,6 +2302,17 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2351,7 +2480,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" }, @@ -2380,6 +2508,127 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -2465,6 +2714,11 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2486,6 +2740,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" + }, "node_modules/babel-jest": { "version": "29.6.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.0.tgz", @@ -2582,6 +2841,31 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bcrypt": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", @@ -2709,6 +2993,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -2972,7 +3279,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" }, @@ -2983,8 +3289,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", @@ -3012,6 +3317,36 @@ "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3086,6 +3421,49 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3096,7 +3474,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3340,6 +3717,11 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3862,6 +4244,22 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3970,6 +4368,11 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -4116,6 +4519,32 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -4428,8 +4857,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -4683,6 +5111,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -5057,7 +5504,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -5131,8 +5577,7 @@ "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.0", @@ -5223,6 +5668,20 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "29.6.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.0.tgz", @@ -5969,6 +6428,49 @@ "node": ">=6" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6654,7 +7156,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7008,6 +7509,11 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7076,7 +7582,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" } @@ -7087,6 +7592,34 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -7218,6 +7751,11 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -7327,6 +7865,11 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7394,6 +7937,33 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7703,7 +8273,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" }, @@ -7715,7 +8284,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" } @@ -7972,6 +8540,19 @@ "node": ">= 0.8" } }, + "node_modules/streamx": { + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.2.tgz", + "integrity": "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8006,6 +8587,20 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.padend": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", @@ -8084,6 +8679,18 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -8256,6 +8863,16 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -8283,6 +8900,11 @@ "node": ">=8" } }, + "node_modules/text-decoder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -8812,7 +9434,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" }, @@ -8883,6 +9504,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -8975,6 +9613,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } } } } diff --git a/package.json b/package.json index 7bb0143..dd2cb30 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@types/csv-parse": "^1.2.5", "@types/uuid": "^9.0.6", + "archiver": "^7.0.1", "bcrypt": "^5.1.0", "cookie-parser": "^1.4.6", "csv-parse": "^5.5.6", @@ -38,6 +39,7 @@ "yauzl": "^3.1.3" }, "devDependencies": { + "@types/archiver": "^6.0.3", "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.17", diff --git a/src/domain/entities/sample.ts b/src/domain/entities/sample.ts index d4f8db1..5fe7662 100644 --- a/src/domain/entities/sample.ts +++ b/src/domain/entities/sample.ts @@ -263,4 +263,59 @@ export interface ExportSampleModel extends PublicSampleModel { filter_removed_empty_slice: boolean,//TODO à déplacer : metadata à calculer au moment de export filter_filtered_rows: number,//TODO à déplacer : metadata à calculer au moment de export instrument_settings_acq_descent_filter: string,//TODO à déplacer +} + +/* FOR import UVP5 */ +export interface SampleFromMetaHeaderModel { + sample_name: string, // profilid + comment: string, // comment + instrument_serial_number: string, // in file title : uvp5_header_snXXX + station_id: string, // stationid + sampling_date: string, // filename????? + latitude_raw: string, // latitude + longitude_raw: string, // longitude + wind_direction: number, // winddir + wind_speed: number, // windspeed + sea_state: string, // seastate + nebulousness: number, // nebuloussness + bottom_depth: number, // bottomdepth + filename: string, // filename + filter_first_image: string, // firstimage + filter_last_image: string, // endimg + sampleType: string, // yoyo + instrument_settings_aa: number, // aa + instrument_settings_exp: number, // exp + instrument_settings_image_volume_l: number, // volimage + +} +export interface SampleFromWorkDatfileModel { + max_pressure: number, // maxpressure work/profileid/profileid_datfile.txt : 9; 20120520080214_203; ***00150***;00356;003 +} +export interface SampleFromWorkHDRModel { + instrument_settings_acq_gain: number, // Gain + instrument_settings_acq_description: string, // 2e ligne du fichier (retirer le ;) + instrument_settings_acq_task_type: string // TaskType + instrument_settings_acq_choice: string, // Choice + instrument_settings_acq_disk_type: string, // DiskType + instrument_settings_acq_appendices_ratio: number, // Ratio + instrument_settings_acq_erase_border: number, // EraseBorderBlobs + instrument_settings_acq_threshold: number, // Thresh + instrument_settings_particle_minimum_size_pixels: number, // SMbase + instrument_settings_vignettes_minimum_size_pixels: number, // SMzoo + + instrument_settings_acq_shutter_speed: number, // Exposure UVP5HD ????????? #TODO + instrument_settings_acq_exposure: number // ShutterSpeed UVP5SD ????????? #TODO + +} +export interface SampleFromCruiseInfoModel { + instrument_operator_email: string, // op_email +} +export interface SampleFromConfigurationDataModel { + instrument_settings_acq_xsize: number, // xsize + instrument_settings_acq_ysize: number, // ysize + instrument_settings_pixel_size_mm: number, // Pixel_Size +} +export interface SampleFromInstallConfigModel { + instrument_settings_process_gamma: number, // gamma + instrument_settings_vignettes_minimum_size_esd: number, // esdmin } \ No newline at end of file diff --git a/src/domain/interfaces/repositories/sample-repository.ts b/src/domain/interfaces/repositories/sample-repository.ts index af44160..e8999a2 100644 --- a/src/domain/interfaces/repositories/sample-repository.ts +++ b/src/domain/interfaces/repositories/sample-repository.ts @@ -7,7 +7,8 @@ export interface SampleRepository { createManySamples(samples: SampleRequestCreationModel[]): Promise; ensureFolderExists(root_folder_path: string): Promise; listImportableSamples(root_folder_path: string, instrument_model: string, dest_folder: string, project_id: number): Promise; - copySamplesToImportFolder(source_folder: string, dest_folder: string, samples_names_to_import: string[]): Promise + UVP6copySamplesToImportFolder(source_folder: string, dest_folder: string, samples_names_to_import: string[]): Promise + UVP5copySamplesToImportFolder(source_folder: string, dest_folder: string, samples_names_to_import: string[]): Promise deleteSamplesFromImportFolder(dest_folder: string, samples_names_to_import: string[]): Promise getSample(sample: SampleRequestModel): Promise; deleteSample(sample: SampleIdModel): Promise; diff --git a/src/domain/repositories/sample-repository.ts b/src/domain/repositories/sample-repository.ts index 343735a..63190a9 100644 --- a/src/domain/repositories/sample-repository.ts +++ b/src/domain/repositories/sample-repository.ts @@ -6,17 +6,19 @@ import { SampleDataSource } from "../../data/interfaces/data-sources/sample-data // import { PreparedSearchOptions, SearchResult } from "../entities/search"; // import { SampleRepository } from "../interfaces/repositories/sample-repository"; -import { ComputeVignettesModel, HeaderSampleModel, MetadataIniSampleModel, MinimalSampleRequestModel, PublicHeaderSampleResponseModel, PublicSampleModel, SampleIdModel, SampleRequestCreationModel, SampleRequestModel, SampleTypeModel, SampleTypeRequestModel, VisualQualityCheckStatusModel, VisualQualityCheckStatusRequestModel } from "../entities/sample"; +import { ComputeVignettesModel, HeaderSampleModel, MetadataIniSampleModel, MinimalSampleRequestModel, PublicHeaderSampleResponseModel, PublicSampleModel, SampleFromConfigurationDataModel, SampleFromCruiseInfoModel, SampleFromInstallConfigModel, SampleFromMetaHeaderModel, SampleFromWorkDatfileModel, SampleFromWorkHDRModel, SampleIdModel, SampleRequestCreationModel, SampleRequestModel, SampleTypeModel, SampleTypeRequestModel, VisualQualityCheckStatusModel, VisualQualityCheckStatusRequestModel } from "../entities/sample"; import { PreparedSearchOptions, SearchResult } from "../entities/search"; import { SampleRepository } from "../interfaces/repositories/sample-repository"; -import { promises as fs } from 'fs'; -//import fs from 'fs'; // Correct import for `createReadStream` +import * as fs from 'fs'; // For createWriteStream +import * as fsPromises from 'fs/promises'; // For promise-based file operations//import fs from 'fs'; // Correct import for `createReadStream` //import { parse, Options } from 'csv-parse'; // Use named import for `parse` import path from 'path'; import yauzl from 'yauzl'; +import archiver from 'archiver'; + @@ -42,6 +44,49 @@ export class SampleRepositoryImpl implements SampleRepository { const sample_to_return = await this.getSampleFromFsStorage(file_system_storage_project_folder, base_sample, instrument_model); return sample_to_return; } + // Generic method to read a file from a ZIP archive + private async readFileFromZip(zipPath: string, targetFileName?: string, filePathPattern?: RegExp): Promise { + return new Promise((resolve, reject) => { + yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { + if (err || !zipfile) { + return reject(err || new Error('Failed to open zip file')); + } + + zipfile.readEntry(); + + zipfile.on('entry', (entry) => { + if ((targetFileName && entry.fileName === targetFileName) || (filePathPattern && filePathPattern.test(entry.fileName))) { + zipfile.openReadStream(entry, (err, readStream) => { + if (err || !readStream) { + return reject(err || new Error('Failed to open read stream')); + } + + let data = ''; + readStream.on('data', (chunk) => (data += chunk)); + readStream.on('end', () => resolve(data)); + }); + } else { + zipfile.readEntry(); + } + }); + + zipfile.on('end', () => reject(new Error(`${targetFileName} or ${filePathPattern} not found in zip file`))); + }); + }); + } + // Method to parse and return ComputeVignettes data + async readComputeVignettes(file_system_storage_project_folder: string, sample_name: string): Promise { + const zipPath = path.join(file_system_storage_project_folder, sample_name, `${sample_name}_Images.zip`); + const fileContent = await this.readFileFromZip(zipPath, 'compute_vignette.txt', undefined); + return this.parseComputeVignettes(fileContent); + } + + // Method to parse and return pressures from particules.csv + async getPressuresFromParticulesCsv(file_system_storage_project_folder: string, sample_name: string): Promise<(number | "NaN")[]> { + const zipPath = path.join(file_system_storage_project_folder, sample_name, `${sample_name}_Particule.zip`); + const fileContent = await this.readFileFromZip(zipPath, 'particules.csv', undefined); + return this.extractPressures(fileContent); + } async getSampleFromFsStorage(file_system_storage_project_folder: string, base_sample: Partial, instrument_model: string): Promise { let sample_fss = {}; @@ -50,41 +95,98 @@ export class SampleRepositoryImpl implements SampleRepository { // read from file_system_storage_project_folder/ecodata and return the list of samples sample_fss = await this.getSampleFromFsStorageUVP6(file_system_storage_project_folder, base_sample.sample_name as string); } - // else if (instrument_model.startsWith('UVP5')) { - // // read from file_system_storage_project_folder/work and return the list of samples - // sample_fss = await this.getSampleFromFsStorageUVP5(file_system_storage_project_folder, base_sample.sample_name as string); - // } + else if (instrument_model.startsWith('UVP5')) { + // read from file_system_storage_project_folder/work and return the list of samples + sample_fss = await this.getSampleFromFsStorageUVP5(file_system_storage_project_folder, base_sample.sample_name as string); + } const sample = this.constructSampleFromBaseAndFsStorage(sample_fss, base_sample); return sample; } - // async getSampleFromFsStorageUVP5(file_system_storage_project_folder: string, sample_name: string): Promise { - // // Complete/reprocess the sample object - // // Get sample info from meta/uvp5_header_sn - // const sample_metadata_ini = await this.getSampleFromMetaHeader(file_system_storage_project_folder, sample_name); - // // get sample from work/profileid/profileid_datfile.txt - // // get sample from config\cruise_info.txt - // // get sample from work/profileid/HDRfilename.txt - // // get sample from config/uvp5_settings/uvp5_configurationdata.txt - // // get sample from config/process_install_config.txt - - // //hardcode "zooprocess" - - - // // Process sample_type_id - // const sample_type_id = await this.computeSampleTypeId(sample_metadata_ini); - // // Process latitude and longitude - // const coords = this.computeLatitudeAndLongitude(sample_metadata_ini); - // // Construct the sample object - // const sample_to_return: Partial = { - // ...sample_metadata_ini, - // sample_type_id, - // latitude: coords.latitude, - // longitude: coords.longitude - // }; - // return sample_to_return; - // } + async getSampleFromFsStorageUVP5(file_system_storage_project_folder: string, sample_name: string): Promise> { + // Complete the sample object + // Get sample info from meta/uvp5_header_sn + const sample_header = await this.getSampleFromMetaHeader(file_system_storage_project_folder, sample_name); + // get sample from work/profileid/profileid_datfile.txt + const sample_work_datfile = await this.getSampleFromWorkDatfile(file_system_storage_project_folder, sample_name); + // get sample from work/profileid/HDRfilename.txt + const sample_work_hdr = await this.getSampleFromWorkHDR(file_system_storage_project_folder, sample_name); + // get sample from config/cruise_info.txt + const sample_cruise_info = await this.getSampleFromCruiseInfo(file_system_storage_project_folder, sample_name); + // get sample from config/uvp5_settings/uvp5_configurationdata.txt + const sample_configurationdata = await this.getSampleFromConfigurationData(file_system_storage_project_folder, sample_name); + // get sample from config/process_install_config.txt + const sample_install_config = await this.getSampleFromInstallConfig(file_system_storage_project_folder, sample_name); + + //TODO hardcode "zooprocess" + const instrument_settings_images_post_process = "zooprocess"; + + // Process some fields + + // Process sample_type_id + const sample_type_id = await this.computeSampleTypeId(sample_header.sampleType); + // Process latitude and longitude + const coords = this.computeLatitudeAndLongitude(sample_header.latitude_raw, sample_header.longitude_raw); + + // Construct the sample object + delete (sample_header as any).sampleType; + delete (sample_header as any).latitude_raw; + delete (sample_header as any).longitude_raw; + + // Construct the sample object + const sample_to_return: Partial = { + ...sample_header, + ...sample_work_datfile, + ...sample_work_hdr, + ...sample_cruise_info, + ...sample_configurationdata, + ...sample_install_config, + sample_type_id, + instrument_settings_images_post_process, + latitude: coords.latitude, + longitude: coords.longitude + }; + return sample_to_return; + } + + async getSampleFromInstallConfig(file_system_storage_project_folder: string, sample_name: string): Promise { + const filePath = 'config/process_install_config.txt'; + const zipPath = path.join(file_system_storage_project_folder, `${sample_name}.zip`); + const fileContent = await this.readFileFromZip(zipPath, filePath, undefined); + return this.parseInstallConfig(fileContent); + } + async getSampleFromConfigurationData(file_system_storage_project_folder: string, sample_name: string): Promise { + const filePath = 'config/uvp5_settings/uvp5_configurationdata.txt'; + const zipPath = path.join(file_system_storage_project_folder, `${sample_name}.zip`); + const fileContent = await this.readFileFromZip(zipPath, filePath, undefined); + return this.parseConfigurationData(fileContent); + } + async getSampleFromCruiseInfo(file_system_storage_project_folder: string, sample_name: string): Promise { + const filePath = 'config/cruise_info.txt'; + const zipPath = path.join(file_system_storage_project_folder, `${sample_name}.zip`); + const fileContent = await this.readFileFromZip(zipPath, filePath, undefined); + return this.parseCruiseInfo(fileContent); + } + async getSampleFromWorkHDR(file_system_storage_project_folder: string, sample_name: string): Promise { + // Construct the file path to match files starting with "HDR" and ending with ".txt" + const filePathPattern = new RegExp(`^work/${sample_name}/'HDR.*\\.txt$`); + const zipPath = path.join(file_system_storage_project_folder, `${sample_name}.zip`); + const fileContent = await this.readFileFromZip(zipPath, undefined, filePathPattern); + return this.parseWorkHDR(fileContent); + } + async getSampleFromWorkDatfile(file_system_storage_project_folder: string, sample_name: string): Promise { + const filePath = 'work/' + sample_name + '/' + sample_name + '_datfile.txt'; // perle3_003_datfile.txt + const zipPath = path.join(file_system_storage_project_folder, `${sample_name}.zip`); + const fileContent = await this.readFileFromZip(zipPath, filePath, undefined); + return this.parseWorkDatfile(fileContent); + } + async getSampleFromMetaHeader(file_system_storage_project_folder: string, sample_name: string): Promise { + const filePathPattern = new RegExp(`^meta/uvp5_header_sn.*\\.txt$`);//uvp5_header_sn205_perle_03_2020.txt + const zipPath = path.join(file_system_storage_project_folder, `${sample_name}.zip`); + const fileContent = await this.readFileFromZip(zipPath, undefined, filePathPattern); + return this.parseMetaHeader(fileContent); + } async getSampleFromFsStorageUVP6(file_system_storage_project_folder: string, sample_name: string): Promise> { @@ -95,22 +197,22 @@ export class SampleRepositoryImpl implements SampleRepository { process already fetch data: latitude: this.computeLatitude(ini_content.sample_metadata['latitude'] as number), longitude: this.computeLongitude(ini_content.sample_metadata['longitude'] as number), - - sample_type_id: this.getSampleTypeFromLetter(ini_content.sample_metadata['sampletype']), //TODO à calculer checket si T ou D si non erreur - + + sample_type_id: this.getSampleTypeFromLetter(ini_content.sample_metadata['sampletype']), + go to another file to get the data: - max_pressure: this.computeMaxPressure(),//TODO à calculer - instrument_settings_process_gamma: this.getGammaForUVP6(),//TODO à aller chercher image.zip compute_vignette.txt - + max_pressure: this.computeMaxPressure(), + instrument_settings_process_gamma: this.getGammaForUVP6(), + * ***/ // Complete/reprocess the sample object // Get sample info from metadata.ini const sample_metadata_ini = await this.getSampleFromMetadataIni(file_system_storage_project_folder, sample_name); // Process sample_type_id - const sample_type_id = await this.computeSampleTypeId(sample_metadata_ini); + const sample_type_id = await this.computeSampleTypeId(sample_metadata_ini.sampleType); // Process latitude and longitude - const coords = this.computeLatitudeAndLongitude(sample_metadata_ini); + const coords = this.computeLatitudeAndLongitude(sample_metadata_ini.latitude_raw, sample_metadata_ini.longitude_raw); // Compute max_pressure const max_pressure = await this.computeMaxPressure(sample_metadata_ini, file_system_storage_project_folder, sample_name); @@ -144,57 +246,6 @@ export class SampleRepositoryImpl implements SampleRepository { return gamma; } - async readComputeVignettes(file_system_storage_project_folder: string, sample_name: string): Promise { - return new Promise((resolve, reject) => { - // Define the path to the .zip file based on the project folder and sample name - const zipPath = path.join(file_system_storage_project_folder, sample_name, `${sample_name}_Images.zip`); - - // Open the zip file without extracting it to disk - yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { - if (err || !zipfile) { - return reject(err || new Error('Failed to open zip file')); - } - - // Start reading entries in the zip file - zipfile.readEntry(); - - // Handle each entry found in the zip file - zipfile.on('entry', (entry) => { - // Check if the current entry is the metadata.ini file - if (entry.fileName === 'compute_vignette.txt') { - zipfile.openReadStream(entry, (err, readStream) => { - if (err || !readStream) { - return reject(err || new Error('Failed to open read stream')); - } - - let data = ''; - // Collect data chunks from the read stream - readStream.on('data', (chunk) => { - data += chunk; - }); - - // When the read stream ends, parse the collected data manually - readStream.on('end', () => { - try { - const parsedData = this.parseComputeVignettes(data); - resolve(parsedData); - } catch (parseError) { - reject(parseError); - } - }); - }); - } else { - zipfile.readEntry(); - } - }); - - zipfile.on('end', () => { - reject(new Error('metadata.ini not found in zip file')); - }); - }); - }); - } - parseComputeVignettes(data: string): ComputeVignettesModel { const result: Partial = {}; const lines = data.split('\n'); @@ -261,57 +312,6 @@ export class SampleRepositoryImpl implements SampleRepository { return Math.max(...numericPressures); } - getPressuresFromParticulesCsv(file_system_storage_project_folder: string, sample_name: string): Promise<(number | "NaN")[]> { - return new Promise((resolve, reject) => { - // Define the path to the .zip file based on the project folder and sample name - const zipPath = path.join(file_system_storage_project_folder, sample_name, `${sample_name}_Particule.zip`); - - // Open the zip file without extracting it to disk - yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { - if (err || !zipfile) { - return reject(err || new Error('Failed to open zip file')); - } - - // Start reading entries in the zip file - zipfile.readEntry(); - - // Handle each entry found in the zip file - zipfile.on('entry', (entry) => { - // Check if the current entry is the particules.csv file - if (entry.fileName === 'particules.csv') { - zipfile.openReadStream(entry, (err, readStream) => { - if (err || !readStream) { - return reject(err || new Error('Failed to open read stream')); - } - - let data = ''; - // Collect data chunks from the read stream - readStream.on('data', (chunk) => { - data += chunk; - }); - - // When the read stream ends, parse the collected data manually - readStream.on('end', () => { - try { - const parsedData = this.extractPressures(data); - resolve(parsedData); - } catch (parseError) { - reject(parseError); - } - }); - }); - } else { - zipfile.readEntry(); - } - }); - - zipfile.on('end', () => { - reject(new Error('particules.csv not found in zip file')); - }); - }); - }); - } - extractPressures(input: string): (number | "NaN")[] { // Split the input by lines const lines = input.split("\n"); @@ -331,10 +331,14 @@ export class SampleRepositoryImpl implements SampleRepository { } - computeLatitudeAndLongitude(sample: MetadataIniSampleModel): { latitude: number, longitude: number } { + computeLatitudeAndLongitude(latitude_raw: string | undefined, longitude_raw: string | undefined): { latitude: number, longitude: number } { + if (latitude_raw === undefined || longitude_raw === undefined) { + throw new Error("Latitude or longitude not found"); + } // #TODO what to do if not found???? + // Compute latitude and longitude - const latitude = this.convTextDegreeDotMinuteToDecimalDegree(sample.latitude_raw, 'uvp6'); - const longitude = this.convTextDegreeDotMinuteToDecimalDegree(sample.longitude_raw, 'uvp6'); + const latitude = this.convTextDegreeDotMinuteToDecimalDegree(latitude_raw, 'uvp6'); + const longitude = this.convTextDegreeDotMinuteToDecimalDegree(longitude_raw, 'uvp6'); return { latitude, longitude }; } @@ -375,17 +379,16 @@ export class SampleRepositoryImpl implements SampleRepository { return parseFloat(v); } - - async computeSampleTypeId(sample: MetadataIniSampleModel): Promise { + async computeSampleTypeId(sampleType: string | undefined): Promise { let sample_type_id: number | undefined; //throw error if unknown sample type - if (sample.sampleType === undefined) throw new Error("Undefined sample type"); + if (sampleType === undefined) throw new Error("Undefined sample type"); // Compute sample_type_id - if (sample.sampleType == "T") { + if (sampleType == "T") { sample_type_id = (await this.getSampleType({ sample_type_label: "Time" }))?.sample_type_id; - } else if (sample.sampleType == "D") { + } else if (sampleType == "D") { sample_type_id = (await this.getSampleType({ sample_type_label: "Depth" }))?.sample_type_id; } else throw new Error("Unknown sample type"); @@ -393,55 +396,274 @@ export class SampleRepositoryImpl implements SampleRepository { return sample_type_id } - getSampleFromMetadataIni(file_system_storage_project_folder: string, sample_name: string): Promise { - return new Promise((resolve, reject) => { - // Define the path to the .zip file based on the project folder and sample name - const zipPath = path.join(file_system_storage_project_folder, sample_name, `${sample_name}_Particule.zip`); + // Method to read and parse the metadata.ini file + async getSampleFromMetadataIni(file_system_storage_project_folder: string, sample_name: string): Promise { + const zipPath = path.join(file_system_storage_project_folder, sample_name, `${sample_name}_Particule.zip`); + const fileContent = await this.readFileFromZip(zipPath, 'metadata.ini', undefined); + return this.parseIniContent(fileContent); + } - // Open the zip file without extracting it to disk - yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { - if (err || !zipfile) { - return reject(err || new Error('Failed to open zip file')); - } + parseInstallConfig(data: string): SampleFromInstallConfigModel { + const install_config_content: any = {}; + console.log("******************************parseInstallConfig******************************"); + console.log(data); + // TODO + // let currentSection: string | null = null; + + // // Split the data into lines and process each line + // const lines = data.split(/\r?\n/); + // lines.forEach((line) => { + // line = line.trim(); + + // // Ignore empty lines or comments + // if (!line || line.startsWith(';') || line.startsWith('#')) { + // return; + // } + + // // Check if the line is a section header + // if (line.startsWith('[') && line.endsWith(']')) { + // currentSection = line.slice(1, -1).trim(); + // ini_content[currentSection] = {}; + // } + // // Otherwise, it's a key-value pair + // else if (currentSection) { + // const [key, value] = line.split('=').map((part) => part.trim()); + // if (key) { + // // Convert the value to a number if possible, otherwise keep as string + // (ini_content[currentSection] as any)[key] = isNaN(Number(value)) ? value : Number(value); + // } + // } + // }); + + const sample: SampleFromInstallConfigModel = { + instrument_settings_process_gamma: install_config_content.sample_metadata['gamma'], + instrument_settings_vignettes_minimum_size_esd: install_config_content.sample_metadata['esdmin'] + } - // Start reading entries in the zip file - zipfile.readEntry(); + return sample; + } + parseConfigurationData(data: string): SampleFromConfigurationDataModel { + const configuration_data_content: any = {}; + console.log("******************************parseConfigurationData******************************"); + console.log(data); + // TODO + // let currentSection: string | null = null; + + // // Split the data into lines and process each line + // const lines = data.split(/\r?\n/); + // lines.forEach((line) => { + // line = line.trim(); + + // // Ignore empty lines or comments + // if (!line || line.startsWith(';') || line.startsWith('#')) { + // return; + // } + + // // Check if the line is a section header + // if (line.startsWith('[') && line.endsWith(']')) { + // currentSection = line.slice(1, -1).trim(); + // ini_content[currentSection] = {}; + // } + // // Otherwise, it's a key-value pair + // else if (currentSection) { + // const [key, value] = line.split('=').map((part) => part.trim()); + // if (key) { + // // Convert the value to a number if possible, otherwise keep as string + // (ini_content[currentSection] as any)[key] = isNaN(Number(value)) ? value : Number(value); + // } + // } + // }); + + const sample: SampleFromConfigurationDataModel = { + instrument_settings_acq_xsize: configuration_data_content.sample_metadata['xsize'], // xsize + instrument_settings_acq_ysize: configuration_data_content.sample_metadata['ysize'], // ysize + instrument_settings_pixel_size_mm: configuration_data_content.sample_metadata['Pixel_Size'] // Pixel_Size + } - // Handle each entry found in the zip file - zipfile.on('entry', (entry) => { - // Check if the current entry is the metadata.ini file - if (entry.fileName === 'metadata.ini') { - zipfile.openReadStream(entry, (err, readStream) => { - if (err || !readStream) { - return reject(err || new Error('Failed to open read stream')); - } + return sample; + } + parseCruiseInfo(data: string): SampleFromCruiseInfoModel { + const cruise_info_content: any = {}; + console.log("******************************parseCruiseInfo******************************"); + console.log(data); + // TODO + // let currentSection: string | null = null; + + // // Split the data into lines and process each line + // const lines = data.split(/\r?\n/); + // lines.forEach((line) => { + // line = line.trim(); + + // // Ignore empty lines or comments + // if (!line || line.startsWith(';') || line.startsWith('#')) { + // return; + // } + + // // Check if the line is a section header + // if (line.startsWith('[') && line.endsWith(']')) { + // currentSection = line.slice(1, -1).trim(); + // ini_content[currentSection] = {}; + // } + // // Otherwise, it's a key-value pair + // else if (currentSection) { + // const [key, value] = line.split('=').map((part) => part.trim()); + // if (key) { + // // Convert the value to a number if possible, otherwise keep as string + // (ini_content[currentSection] as any)[key] = isNaN(Number(value)) ? value : Number(value); + // } + // } + // }); + + const sample: SampleFromCruiseInfoModel = { + instrument_operator_email: cruise_info_content.sample_metadata['op_email'],// op_email + } - let data = ''; - // Collect data chunks from the read stream - readStream.on('data', (chunk) => { - data += chunk; - }); - - // When the read stream ends, parse the collected data manually - readStream.on('end', () => { - try { - const parsedData = this.parseIniContent(data); - resolve(parsedData); - } catch (parseError) { - reject(parseError); - } - }); - }); - } else { - zipfile.readEntry(); - } - }); + return sample; + } + parseWorkHDR(data: string): SampleFromWorkHDRModel { + console.log("******************************parseWorkHDR******************************"); + console.log(data); + const work_hdr_content: any = {}; + // TODO + // let currentSection: string | null = null; + + // // Split the data into lines and process each line + // const lines = data.split(/\r?\n/); + // lines.forEach((line) => { + // line = line.trim(); + + // // Ignore empty lines or comments + // if (!line || line.startsWith(';') || line.startsWith('#')) { + // return; + // } + + // // Check if the line is a section header + // if (line.startsWith('[') && line.endsWith(']')) { + // currentSection = line.slice(1, -1).trim(); + // ini_content[currentSection] = {}; + // } + // // Otherwise, it's a key-value pair + // else if (currentSection) { + // const [key, value] = line.split('=').map((part) => part.trim()); + // if (key) { + // // Convert the value to a number if possible, otherwise keep as string + // (ini_content[currentSection] as any)[key] = isNaN(Number(value)) ? value : Number(value); + // } + // } + // }); + + + const sample: SampleFromWorkHDRModel = { + instrument_settings_acq_gain: work_hdr_content.sample_metadata['Gain'], // Gain + instrument_settings_acq_description: work_hdr_content.sample_metadata['description'], // 2e ligne du fichier (retirer le ;) + instrument_settings_acq_task_type: work_hdr_content.sample_metadata['TaskType'], // TaskType + instrument_settings_acq_choice: work_hdr_content.sample_metadata['Choice'], // Choice + instrument_settings_acq_disk_type: work_hdr_content.sample_metadata['DiskType'], // DiskType + instrument_settings_acq_appendices_ratio: work_hdr_content.sample_metadata['Ratio'], // Ratio + instrument_settings_acq_erase_border: work_hdr_content.sample_metadata['EraseBorderBlobs'], // EraseBorderBlobs + instrument_settings_acq_threshold: work_hdr_content.sample_metadata['Thresh'], // Thresh + instrument_settings_particle_minimum_size_pixels: work_hdr_content.sample_metadata['SMbase'], // SMbase + instrument_settings_vignettes_minimum_size_pixels: work_hdr_content.sample_metadata['SMzoo'], // SMzoo + instrument_settings_acq_shutter_speed: work_hdr_content.sample_metadata['Exposure'], // Exposure UVP5HD ????????? #TODO + instrument_settings_acq_exposure: work_hdr_content.sample_metadata['ShutterSpeed'] // ShutterSpeed UVP5SD ????????? #TODO + } - zipfile.on('end', () => { - reject(new Error('metadata.ini not found in zip file')); - }); - }); - }); + return sample; + } + parseWorkDatfile(data: string): SampleFromWorkDatfileModel { + console.log("******************************parseWorkDatfile******************************"); + console.log(data); + const work_datfile_content: any = {}; + // TODO + // let currentSection: string | null = null; + + // // Split the data into lines and process each line + // const lines = data.split(/\r?\n/); + // lines.forEach((line) => { + // line = line.trim(); + + // // Ignore empty lines or comments + // if (!line || line.startsWith(';') || line.startsWith('#')) { + // return; + // } + + // // Check if the line is a section header + // if (line.startsWith('[') && line.endsWith(']')) { + // currentSection = line.slice(1, -1).trim(); + // ini_content[currentSection] = {}; + // } + // // Otherwise, it's a key-value pair + // else if (currentSection) { + // const [key, value] = line.split('=').map((part) => part.trim()); + // if (key) { + // // Convert the value to a number if possible, otherwise keep as string + // (ini_content[currentSection] as any)[key] = isNaN(Number(value)) ? value : Number(value); + // } + // } + // }); + + const sample: SampleFromWorkDatfileModel = { + max_pressure: work_datfile_content.sample_metadata['max_pressure'] //maxpressure work/profileid/profileid_datfile.txt : 9; //TODO à calculer ? + } + + return sample; + } + + parseMetaHeader(data: string): SampleFromMetaHeaderModel { + const meta_header_content: any = {}; + console.log("******************************parseMetaHeader******************************"); + console.log(data); + // TODO + // let currentSection: string | null = null; + + // // Split the data into lines and process each line + // const lines = data.split(/\r?\n/); + // lines.forEach((line) => { + // line = line.trim(); + + // // Ignore empty lines or comments + // if (!line || line.startsWith(';') || line.startsWith('#')) { + // return; + // } + + // // Check if the line is a section header + // if (line.startsWith('[') && line.endsWith(']')) { + // currentSection = line.slice(1, -1).trim(); + // ini_content[currentSection] = {}; + // } + // // Otherwise, it's a key-value pair + // else if (currentSection) { + // const [key, value] = line.split('=').map((part) => part.trim()); + // if (key) { + // // Convert the value to a number if possible, otherwise keep as string + // (ini_content[currentSection] as any)[key] = isNaN(Number(value)) ? value : Number(value); + // } + // } + // }); + + const sample: SampleFromMetaHeaderModel = { + sample_name: meta_header_content.sample_metadata['profileid'], + comment: meta_header_content.sample_metadata['comment'], + instrument_serial_number: meta_header_content.sample_metadata['Camera_ref'], + station_id: meta_header_content.sample_metadata['stationid'], + sampling_date: meta_header_content.sample_metadata['filename'], + latitude_raw: meta_header_content.sample_metadata['latitude'], + longitude_raw: meta_header_content.sample_metadata['longitude'], + wind_direction: meta_header_content.sample_metadata['winddir'], + wind_speed: meta_header_content.sample_metadata['windspeed'], + sea_state: meta_header_content.sample_metadata['seastate'], + nebulousness: meta_header_content.sample_metadata['nebuloussness'], + bottom_depth: meta_header_content.sample_metadata['bottomdepth'], + filename: meta_header_content.sample_metadata['filename'], + filter_first_image: meta_header_content.sample_metadata['firstimage'], + filter_last_image: meta_header_content.sample_metadata['endimg'], + sampleType: meta_header_content.sample_metadata['yoyo'], + instrument_settings_aa: meta_header_content.sample_metadata['aa'], + instrument_settings_exp: meta_header_content.sample_metadata['exp'], + instrument_settings_image_volume_l: meta_header_content.sample_metadata['volimage'] + } + + return sample; } parseIniContent(data: string): MetadataIniSampleModel { @@ -543,7 +765,7 @@ export class SampleRepositoryImpl implements SampleRepository { const folderPath = path.join(root_folder_path); try { - await fs.access(folderPath); + await fsPromises.access(folderPath); } catch (error) { throw new Error(`Folder does not exist at path: ${folderPath}`); } @@ -602,7 +824,7 @@ export class SampleRepositoryImpl implements SampleRepository { // list folders names in folderPath const samples: string[] = []; try { - const files = await fs.readdir(folderPath); + const files = await fsPromises.readdir(folderPath); for (const file of files) { samples.push(file); @@ -636,11 +858,11 @@ export class SampleRepositoryImpl implements SampleRepository { const samples: HeaderSampleModel[] = []; try { const header_path = path.join(folderPath, 'meta'); - const files = await fs.readdir(header_path); + const files = await fsPromises.readdir(header_path); for (const file of files) { if (file.includes('header') && file.endsWith('.txt')) { const filePath = path.join(header_path, file); - const content = await fs.readFile(filePath, 'utf8'); + const content = await fsPromises.readFile(filePath, 'utf8'); const lines = content.trim().split('\n'); for (let i = 1; i < lines.length; i++) { @@ -693,7 +915,7 @@ export class SampleRepositoryImpl implements SampleRepository { async getSamplesFromEcodata(folderPath: string): Promise { const samples: string[] = []; try { - const files = await fs.readdir(path.join(folderPath, 'ecodata')); + const files = await fsPromises.readdir(path.join(folderPath, 'ecodata')); for (const file of files) { samples.push(file); @@ -709,7 +931,7 @@ export class SampleRepositoryImpl implements SampleRepository { async getSamplesFromWork(folderPath: string): Promise { const samples: string[] = []; try { - const files = await fs.readdir(path.join(folderPath, 'work')); + const files = await fsPromises.readdir(path.join(folderPath, 'work')); for (const file of files) { samples.push(file); @@ -725,7 +947,7 @@ export class SampleRepositoryImpl implements SampleRepository { for (const sample of samples_names_to_import) { const destPath = path.join(dest_folder, sample); try { - await fs.access(destPath); + await fsPromises.access(destPath); throw new Error(`Sample folder already exists: ${destPath}`); } catch (error) { if (error.code === 'ENOENT') { @@ -737,15 +959,85 @@ export class SampleRepositoryImpl implements SampleRepository { } } } + async UVP5copySamplesToImportFolder(source_folder: string, dest_folder: string, samples_names_to_import: string[]): Promise { + const base_folder = path.join(__dirname, '..', '..', '..'); + + // Ensure that none of the samples folder already exists + await this.ensureSampleFolderDoNotExists(samples_names_to_import, path.join(base_folder, dest_folder)); + + // Create destination folder + await fsPromises.mkdir(path.join(base_folder, dest_folder), { recursive: true }); - async copySamplesToImportFolder(source_folder: string, dest_folder: string, samples_names_to_import: string[]): Promise { + // Iterate over each sample name, create the sample folder, copy files, and zip the folder + for (const sample of samples_names_to_import) { + const sourcePath = path.join(base_folder, source_folder); + const destPath = path.join(base_folder, dest_folder, sample); + + // Ensure the destination sample folder exists + await fsPromises.mkdir(destPath, { recursive: true }); + + // Copy specific files and directories + const filesToCopy = [ + { source: 'work/' + sample, dest: 'work/' + sample }, + { source: 'meta', dest: 'meta' }, + { source: 'config/cruise_info.txt', dest: 'config/cruise_info.txt' }, + { source: 'config/uvp5_settings/uvp5_configuration_data.txt', dest: 'config/uvp5_settings/uvp5_configuration_data.txt' }, + { source: 'config/process_install_config.txt', dest: 'config/process_install_config.txt' }, + ]; + + for (const file of filesToCopy) { + const sourceFilePath = path.join(sourcePath, file.source); + const destFilePath = path.join(destPath, file.dest); + + try { + // Ensure the parent folder exists for nested files + await fsPromises.mkdir(path.dirname(destFilePath), { recursive: true }); + + // Copy the file or directory + await fsPromises.cp(sourceFilePath, destFilePath, { recursive: true }); + console.log(`Copied ${file.source} for sample ${sample}`); + } catch (error) { + throw new Error(`Error copying ${file.source} for sample ${sample}: ${error.message}`); + } + } + + // Zip the sample folder + const zipFilePath = path.join(base_folder, dest_folder, `${sample}.zip`); + try { + await this.zipFolder(destPath, zipFilePath); + console.log(`Zipped folder for sample ${sample} to ${zipFilePath}`); + + // Remove the unzipped folder after zipping + await fsPromises.rm(destPath, { recursive: true, force: true }); + console.log(`Removed unzipped folder for sample ${sample}`); + } catch (error) { + throw new Error(`Error zipping folder for sample ${sample}: ${error.message}`); + } + } + } + + async zipFolder(folderPath: string, zipFilePath: string): Promise { + return new Promise((resolve, reject) => { + const output = fs.createWriteStream(zipFilePath); + const archive = archiver('zip', { zlib: { level: 9 } }); // High compression level + + output.on('close', resolve); + archive.on('error', reject); + + archive.pipe(output); + archive.directory(folderPath, false); // Add folder contents + archive.finalize(); + }); + } + + async UVP6copySamplesToImportFolder(source_folder: string, dest_folder: string, samples_names_to_import: string[]): Promise { const base_folder = path.join(__dirname, '..', '..', '..'); // Ensure that non of the samples folder already exists await this.ensureSampleFolderDoNotExists(samples_names_to_import, path.join(base_folder, dest_folder)); // Ensure destination folder exists - await fs.mkdir(path.join(base_folder, dest_folder), { recursive: true }); + await fsPromises.mkdir(path.join(base_folder, dest_folder), { recursive: true }); // Iterate over each sample name and copy .zip files only for (const sample of samples_names_to_import) { @@ -753,7 +1045,7 @@ export class SampleRepositoryImpl implements SampleRepository { const destPath = path.join(base_folder, dest_folder, sample); // Check if the sample directory exists and list files - const files = await fs.readdir(sourcePath); + const files = await fsPromises.readdir(sourcePath); // Filter and copy only .zip files for (const file of files) { @@ -762,9 +1054,9 @@ export class SampleRepositoryImpl implements SampleRepository { const destFilePath = path.join(destPath, file); // Ensure destination subfolder exists - await fs.mkdir(destPath, { recursive: true }); + await fsPromises.mkdir(destPath, { recursive: true }); //TODO à noter dans la tache console.log("Copying:", sourceFilePath, "to", destFilePath); - await fs.copyFile(sourceFilePath, destFilePath); + await fsPromises.copyFile(sourceFilePath, destFilePath); } } } @@ -772,22 +1064,20 @@ export class SampleRepositoryImpl implements SampleRepository { async deleteSamplesFromImportFolder(dest_folder: string, samples_names_to_import: string[]): Promise { const base_folder = path.join(__dirname, '..', '..', '..'); - // Iterate over each sample name and delete it + for (const sample of samples_names_to_import) { - const destPath = path.join(base_folder, dest_folder, sample); + // Construct paths for the .zip file and the folder + const zipPath = path.join(base_folder, dest_folder, `${sample}.zip`); + const folderPath = path.join(base_folder, dest_folder, sample); + + // Delete the .zip file + await fsPromises.rm(zipPath, { force: true }); - // Delete the sample folder recurcively from destination - await fs.rm(destPath, { recursive: true, force: true }); + // Delete the folder and its contents recursively + await fsPromises.rm(folderPath, { recursive: true, force: true }); } } - - - // async createSample(sample: SampleRequestCreationModel): Promise { - // const result = await this.sampleDataSource.create(sample) - // return result; - // } - async getSample(sample: SampleRequestModel): Promise { // Clean the sample to keep only sample_id, sample_name, project_id if they are defined const cleaned_sample: MinimalSampleRequestModel = {}; @@ -811,7 +1101,7 @@ export class SampleRepositoryImpl implements SampleRepository { const folderPath = path.join(this.DATA_STORAGE_FS_STORAGE, `${project_id}`, `${sample_name}`); try { console.log(`Deleting sample from storage: ${folderPath}`); - await fs.rm(folderPath, { recursive: true, force: true }); + await fsPromises.rm(folderPath, { recursive: true, force: true }); return 1; } catch (error) { throw new Error(`Error deleting sample from storage: ${error.message}`); @@ -928,5 +1218,4 @@ export class SampleRepositoryImpl implements SampleRepository { const result = await this.sampleDataSource.getVisualQCStatus(cleaned_visual_qc_status); return result; } - } \ No newline at end of file diff --git a/src/domain/use-cases/sample/import-samples.ts b/src/domain/use-cases/sample/import-samples.ts index afeacbe..bed740b 100644 --- a/src/domain/use-cases/sample/import-samples.ts +++ b/src/domain/use-cases/sample/import-samples.ts @@ -151,12 +151,12 @@ export class ImportSamples implements ImportSamplesUseCase { if (instrument_model.startsWith('UVP6')) { source_folder = path.join(root_folder_path, 'ecodata'); + await this.sampleRepository.UVP6copySamplesToImportFolder(source_folder, dest_folder, samples_names_to_import); } else if (instrument_model.startsWith('UVP5')) { - source_folder = path.join(root_folder_path, 'work'); + await this.sampleRepository.UVP5copySamplesToImportFolder(root_folder_path, dest_folder, samples_names_to_import); } else { throw new Error("Unknown instrument model"); } - await this.sampleRepository.copySamplesToImportFolder(source_folder, dest_folder, samples_names_to_import); await this.taskRepository.updateTaskProgress({ task_id: task_id }, 50, "Step 2/4 sample folders copy : done"); @@ -174,7 +174,7 @@ export class ImportSamples implements ImportSamplesUseCase { } async importSamples(task_id: number, project: ProjectResponseModel, current_user_id: number, samples_names_to_import: string[]): Promise { - await this.taskRepository.updateTaskProgress({ task_id: task_id }, 75, "Step 4/4 samples creation : start"); + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 75, "Step 4/4 samples db creation : start"); // Common sample data const base_sample: Partial = { @@ -196,7 +196,7 @@ export class ImportSamples implements ImportSamplesUseCase { // Create samples const created_samples_ids = await this.sampleRepository.createManySamples(formated_samples); - await this.taskRepository.updateTaskProgress({ task_id: task_id }, 100, "Step 4/4 samples creation done"); + await this.taskRepository.updateTaskProgress({ task_id: task_id }, 100, "Step 4/4 samples db creation done"); return created_samples_ids; } } \ No newline at end of file