From bf6ee7b00ad6f50d106ef85e81ed370aa0b38631 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Mon, 15 Jun 2026 15:37:44 -0500 Subject: [PATCH 1/3] fix: downgrade @internxt/inxt-js version to 2.2.13 --- package-lock.json | 239 ++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 221 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab88c849c5..b548262dee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "AGPL-3.0", "dependencies": { "@internxt/drive-desktop-core": "0.1.7", - "@internxt/inxt-js": "^3.2.2", + "@internxt/inxt-js": "^2.2.13", "@internxt/scan": "^1.0.7", "@internxt/sdk": "1.17.4", "async": "^3.2.4", @@ -3556,33 +3556,51 @@ } }, "node_modules/@internxt/inxt-js": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@internxt/inxt-js/-/inxt-js-3.2.2.tgz", - "integrity": "sha512-URoQYmpv+iDqGSwAL5UfH+hy0hpEhLsnidREU8KHbQKsD4Nmmmn0ZCZKFEQAzPeyhsTPI2/sh5NcLCO/bSEPOg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@internxt/inxt-js/-/inxt-js-2.3.1.tgz", + "integrity": "sha512-Ta2yMItwSuYPqhVE5Or7RIgbwticWtIjU9xgNNkD5HmcNhvnbMYj+R6DphSTP0++yJhrCg42lIbDG03oggr2bw==", "license": "ISC", "dependencies": { - "@internxt/sdk": "1.16.2", - "async": "^3.2.6", - "axios": "^1.16.0", - "bip39": "^3.1.0", - "undici": "^7.25.0", - "winston": "^3.19.0" + "@internxt/lib": "1.4.1", + "@internxt/sdk": "1.15.1", + "async": "3.2.6", + "axios": "1.13.5", + "bip39": "3.1.0", + "undici": "7.22.0", + "winston": "3.19.0" } }, "node_modules/@internxt/inxt-js/node_modules/@internxt/sdk": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.16.2.tgz", - "integrity": "sha512-hTtOxR94v1MsFbV2eX1CNDf5L7BNoGFScn+SNDipzT17PY0LhHm2vVOa1ncRFjqR0FUBqGi3Hpm+T8/4aQefxQ==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.15.1.tgz", + "integrity": "sha512-CEH/fNjDWenmFAl8NHaykb85AY4uEZDO5pA2ap8/GD4SXmxC3rcWsKnMsYO9jc8R2vpOPxmE3oBlNqOVcI/llQ==", "license": "MIT", "dependencies": { - "axios": "^1.16.0" + "axios": "1.13.5", + "internxt-crypto": "0.0.13" + } + }, + "node_modules/@internxt/inxt-js/node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" } }, + "node_modules/@internxt/inxt-js/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/@internxt/lib": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@internxt/lib/-/lib-1.4.1.tgz", "integrity": "sha512-sWNp57IKCk0HjzTdPSuxOgZWvrSDWGYrzNOq90LIZTzr1HwkxObicUaZqSzmw4uDKrJhsdFdzwdywk3g8gwDDA==", - "dev": true, "license": "MIT", "dependencies": { "uuid": "^11.1.0" @@ -3592,7 +3610,6 @@ "version": "11.1.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -4362,6 +4379,33 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -4374,6 +4418,34 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/post-quantum": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.5.4.tgz", + "integrity": "sha512-leww0zzIirrvwaYMPI9fj6aRIlA/c6Y0/lifQQ1YOOyHEr0MNH3yYpjXeiVG+tWdPps4XxGclFWX2INPO3Yo5w==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~2.0.0", + "@noble/hashes": "~2.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5966,6 +6038,40 @@ "dev": true, "license": "MIT" }, + "node_modules/@scure/base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", + "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0", + "@scure/base": "2.2.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -12956,6 +13062,34 @@ "dev": true, "license": "ISC" }, + "node_modules/flexsearch": { + "version": "0.8.212", + "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.8.212.tgz", + "integrity": "sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/ts-thomas" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=GEVR88FC9BWRW" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/flexsearch" + }, + { + "type": "patreon", + "url": "https://patreon.com/user?u=96245532" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/ts-thomas" + } + ], + "license": "Apache-2.0" + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -13683,6 +13817,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", + "license": "MIT" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -14126,6 +14266,12 @@ "postcss": "^8.1.0" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -14278,6 +14424,61 @@ "node": ">= 0.4" } }, + "node_modules/internxt-crypto": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/internxt-crypto/-/internxt-crypto-0.0.13.tgz", + "integrity": "sha512-V8Epf4oFZQZwMyIt8mcw7w36M+tryQ9VcfaiKtcruNW/129bawZnPXEeoOfLvAV+9OYlY2ZAwIYP0EtOD2ec6g==", + "dependencies": { + "@noble/hashes": "^2.0.1", + "@noble/post-quantum": "^0.5.2", + "@scure/bip39": "^2.0.1", + "flexsearch": "^0.8.205", + "hash-wasm": "^4.12.0", + "husky": "^9.1.7", + "idb": "^8.0.3", + "uuid": "^13.0.0" + } + }, + "node_modules/internxt-crypto/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/internxt-crypto/node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/internxt-crypto/node_modules/uuid": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -22284,9 +22485,9 @@ } }, "node_modules/undici": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.0.tgz", - "integrity": "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==", + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", "license": "MIT", "engines": { "node": ">=20.18.1" diff --git a/package.json b/package.json index 314008f6be..837cf73b39 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,7 @@ }, "dependencies": { "@internxt/drive-desktop-core": "0.1.7", - "@internxt/inxt-js": "^3.2.2", + "@internxt/inxt-js": "^2.2.13", "@internxt/scan": "^1.0.7", "@internxt/sdk": "1.17.4", "async": "^3.2.4", From 2dca4bbbd2ec97de09f31a1ca179e771066a0f51 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Tue, 16 Jun 2026 14:45:02 -0500 Subject: [PATCH 2/3] chore: update @internxt/inxt-js dependency to version 3.3.1 --- package-lock.json | 243 ++---------------- package.json | 2 +- .../upload-content-to-environment.test.ts | 147 +++++------ .../upload/upload-content-to-environment.ts | 74 +++--- .../upload-thumbnail-to-bucket.test.ts | 45 ++-- .../thumbnails/upload-thumbnail-to-bucket.ts | 41 +-- .../EnvironmentContentFileDownloader.ts | 84 +++--- ...onmentRemoteFileContentsManagersFactory.ts | 2 +- .../upload/EnvironmentTemporalFileUploader.ts | 48 ++-- .../EnvironmentTemporalFileUploaderFactory.ts | 7 +- 10 files changed, 243 insertions(+), 450 deletions(-) diff --git a/package-lock.json b/package-lock.json index b548262dee..2f58896b27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "internxt", - "version": "2.6.0", + "version": "2.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "internxt", - "version": "2.6.0", + "version": "2.6.1", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { "@internxt/drive-desktop-core": "0.1.7", - "@internxt/inxt-js": "^2.2.13", + "@internxt/inxt-js": "^3.3.1", "@internxt/scan": "^1.0.7", "@internxt/sdk": "1.17.4", "async": "^3.2.4", @@ -3556,51 +3556,33 @@ } }, "node_modules/@internxt/inxt-js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@internxt/inxt-js/-/inxt-js-2.3.1.tgz", - "integrity": "sha512-Ta2yMItwSuYPqhVE5Or7RIgbwticWtIjU9xgNNkD5HmcNhvnbMYj+R6DphSTP0++yJhrCg42lIbDG03oggr2bw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@internxt/inxt-js/-/inxt-js-3.3.1.tgz", + "integrity": "sha512-aqfhm77lvobWNzkofSid9k8xDWpT/VGrhe4yyjkVZ6LAhadIELny4kMk2aNC8uwZkdUFH9Kjx/jyxgy5q1naag==", "license": "ISC", "dependencies": { - "@internxt/lib": "1.4.1", - "@internxt/sdk": "1.15.1", - "async": "3.2.6", - "axios": "1.13.5", - "bip39": "3.1.0", - "undici": "7.22.0", - "winston": "3.19.0" + "@internxt/sdk": "1.16.2", + "async": "^3.2.6", + "axios": "^1.16.0", + "bip39": "^3.1.0", + "undici": "^7.25.0", + "winston": "^3.19.0" } }, "node_modules/@internxt/inxt-js/node_modules/@internxt/sdk": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.15.1.tgz", - "integrity": "sha512-CEH/fNjDWenmFAl8NHaykb85AY4uEZDO5pA2ap8/GD4SXmxC3rcWsKnMsYO9jc8R2vpOPxmE3oBlNqOVcI/llQ==", - "license": "MIT", - "dependencies": { - "axios": "1.13.5", - "internxt-crypto": "0.0.13" - } - }, - "node_modules/@internxt/inxt-js/node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.16.2.tgz", + "integrity": "sha512-hTtOxR94v1MsFbV2eX1CNDf5L7BNoGFScn+SNDipzT17PY0LhHm2vVOa1ncRFjqR0FUBqGi3Hpm+T8/4aQefxQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "axios": "^1.16.0" } }, - "node_modules/@internxt/inxt-js/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/@internxt/lib": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@internxt/lib/-/lib-1.4.1.tgz", "integrity": "sha512-sWNp57IKCk0HjzTdPSuxOgZWvrSDWGYrzNOq90LIZTzr1HwkxObicUaZqSzmw4uDKrJhsdFdzwdywk3g8gwDDA==", + "dev": true, "license": "MIT", "dependencies": { "uuid": "^11.1.0" @@ -3610,6 +3592,7 @@ "version": "11.1.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "dev": true, "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -4379,33 +4362,6 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@noble/curves": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", - "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.0.1" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -4418,34 +4374,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/post-quantum": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.5.4.tgz", - "integrity": "sha512-leww0zzIirrvwaYMPI9fj6aRIlA/c6Y0/lifQQ1YOOyHEr0MNH3yYpjXeiVG+tWdPps4XxGclFWX2INPO3Yo5w==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~2.0.0", - "@noble/hashes": "~2.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/post-quantum/node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6038,40 +5966,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@scure/base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", - "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz", - "integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "2.2.0", - "@scure/base": "2.2.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -13062,34 +12956,6 @@ "dev": true, "license": "ISC" }, - "node_modules/flexsearch": { - "version": "0.8.212", - "resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.8.212.tgz", - "integrity": "sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/ts-thomas" - }, - { - "type": "paypal", - "url": "https://www.paypal.com/donate/?hosted_button_id=GEVR88FC9BWRW" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/flexsearch" - }, - { - "type": "patreon", - "url": "https://patreon.com/user?u=96245532" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/ts-thomas" - } - ], - "license": "Apache-2.0" - }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -13817,12 +13683,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hash-wasm": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", - "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", - "license": "MIT" - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -14266,12 +14126,6 @@ "postcss": "^8.1.0" } }, - "node_modules/idb": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", - "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", - "license": "ISC" - }, "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", @@ -14424,61 +14278,6 @@ "node": ">= 0.4" } }, - "node_modules/internxt-crypto": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/internxt-crypto/-/internxt-crypto-0.0.13.tgz", - "integrity": "sha512-V8Epf4oFZQZwMyIt8mcw7w36M+tryQ9VcfaiKtcruNW/129bawZnPXEeoOfLvAV+9OYlY2ZAwIYP0EtOD2ec6g==", - "dependencies": { - "@noble/hashes": "^2.0.1", - "@noble/post-quantum": "^0.5.2", - "@scure/bip39": "^2.0.1", - "flexsearch": "^0.8.205", - "hash-wasm": "^4.12.0", - "husky": "^9.1.7", - "idb": "^8.0.3", - "uuid": "^13.0.0" - } - }, - "node_modules/internxt-crypto/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/internxt-crypto/node_modules/husky": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", - "license": "MIT", - "bin": { - "husky": "bin.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/internxt-crypto/node_modules/uuid": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", - "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -22485,9 +22284,9 @@ } }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" diff --git a/package.json b/package.json index 837cf73b39..6ffc37f947 100644 --- a/package.json +++ b/package.json @@ -197,7 +197,7 @@ }, "dependencies": { "@internxt/drive-desktop-core": "0.1.7", - "@internxt/inxt-js": "^2.2.13", + "@internxt/inxt-js": "^3.3.1", "@internxt/scan": "^1.0.7", "@internxt/sdk": "1.17.4", "async": "^3.2.4", diff --git a/src/backend/features/backup/upload/upload-content-to-environment.test.ts b/src/backend/features/backup/upload/upload-content-to-environment.test.ts index 6fe9a99b90..30a0c53a0d 100644 --- a/src/backend/features/backup/upload/upload-content-to-environment.test.ts +++ b/src/backend/features/backup/upload/upload-content-to-environment.test.ts @@ -1,5 +1,4 @@ import { Environment } from '@internxt/inxt-js'; -import { UploadOptions } from '@internxt/inxt-js/build/lib/core'; import { Readable } from 'stream'; import { deepMocked } from '../../../../../tests/vitest/utils.helper'; import { uploadContentToEnvironment } from './upload-content-to-environment'; @@ -13,38 +12,39 @@ vi.mock('../../../../infra/local-file-system/safe-access', () => ({ })); describe('upload-content-to-environment', () => { + type UploadOptions = { + source: NodeJS.ReadableStream; + fileSize: number; + progressCallback: (progress: number) => void; + abortSignal?: AbortSignal; + }; + const createReadStreamMock = deepMocked(fs.createReadStream); const safeAccessMock = vi.mocked(safeAccessModule.safeAccess); const SMALL_SIZE = 1024; - const LARGE_SIZE = 200 * 1024 * 1024; // > 100MB threshold let environment: Environment; let abortController: AbortController; let fakeStream: Readable; - let capturedOpts: UploadOptions; - - function makeActionState() { - return { stop: vi.fn() }; - } - - function makeUploadFn(actionState = makeActionState()) { - return vi.fn((_bucket: string, opts: UploadOptions) => { - capturedOpts = opts; - return actionState; - }); - } + let capturedOpts: UploadOptions | null; + let uploadMock: ReturnType Promise>>; beforeEach(() => { abortController = new AbortController(); - capturedOpts = undefined as unknown as UploadOptions; + capturedOpts = null; fakeStream = Object.assign(new Readable({ read() {} }), { close: vi.fn(), destroy: vi.fn() }); createReadStreamMock.mockReturnValue(fakeStream as ReturnType); safeAccessMock.mockResolvedValue({ data: undefined }); + uploadMock = vi.fn(async (_bucket: string, opts: UploadOptions) => { + capturedOpts = opts; + return 'default-contents-id'; + }); + environment = { - upload: makeUploadFn(), - uploadMultipartFile: makeUploadFn(), + upload: uploadMock, + uploadMultipartFile: vi.fn(), } as unknown as Environment; }); @@ -58,105 +58,75 @@ describe('upload-content-to-environment', () => { }); } - async function startUpload(size = SMALL_SIZE) { - const promise = callUpload(size); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - return { promise }; - } - - function triggerFinished(err: Error | null, contentsId: string | null) { - capturedOpts.finishedCallback(err, contentsId); - } - it('should resolve with contentsId on successful upload', async () => { const contentsId = 'abc123'; - const { promise } = await startUpload(); - expect(capturedOpts).toBeDefined(); - triggerFinished(null, contentsId); + uploadMock.mockResolvedValue(contentsId); - const result = await promise; + const result = await callUpload(); expect(result.data).toBe(contentsId); expect(result.error).toBeUndefined(); }); - it('should use upload for files below multipart threshold', async () => { - const { promise } = await startUpload(SMALL_SIZE); - triggerFinished(null, 'id'); + it('should call upload with file size and abortSignal', async () => { + uploadMock.mockResolvedValue('id'); - await promise; + await callUpload(SMALL_SIZE); - expect(environment.upload).toHaveBeenCalled(); - expect(environment.uploadMultipartFile).not.toHaveBeenCalled(); - }); - - it('should use uploadMultipartFile for files above multipart threshold', async () => { - const { promise } = await startUpload(LARGE_SIZE); - triggerFinished(null, 'id'); - - await promise; - - expect(environment.uploadMultipartFile).toHaveBeenCalled(); - expect(environment.upload).not.toHaveBeenCalled(); + expect(capturedOpts?.fileSize).toBe(SMALL_SIZE); + expect(capturedOpts?.abortSignal).toBe(abortController.signal); + expect(uploadMock).toHaveBeenCalled(); }); it('should return NOT_ENOUGH_SPACE error when upload fails with "Max space used"', async () => { - const { promise } = await startUpload(); - triggerFinished(new Error('Max space used'), null); + uploadMock.mockRejectedValue(new Error('Max space used')); - const result = await promise; + const result = await callUpload(); expect(result.error?.cause).toBe('NOT_ENOUGH_SPACE'); }); it('should return RATE_LIMITED error on 429 with retry_after from message', async () => { - const { promise } = await startUpload(); const err = Object.assign(new Error(JSON.stringify({ retry_after: 10 })), { status: 429 }); - triggerFinished(err, null); + uploadMock.mockRejectedValue(err); - const result = await promise; + const result = await callUpload(); expect(result.error?.cause).toBe('RATE_LIMITED'); expect(result.error?.message).toBe('10000'); }); it('should return RATE_LIMITED with default delay when retry_after is missing', async () => { - const { promise } = await startUpload(); const err = Object.assign(new Error('{}'), { status: 429 }); - triggerFinished(err, null); + uploadMock.mockRejectedValue(err); - const result = await promise; + const result = await callUpload(); expect(result.error?.cause).toBe('RATE_LIMITED'); expect(result.error?.message).toBe('30000'); }); it('should return INTERNAL_SERVER_ERROR on 500+ errors', async () => { - const { promise } = await startUpload(); const err = Object.assign(new Error('Server error'), { status: 500 }); - triggerFinished(err, null); + uploadMock.mockRejectedValue(err); - const result = await promise; + const result = await callUpload(); expect(result.error?.cause).toBe('INTERNAL_SERVER_ERROR'); }); it('should return UNKNOWN error for generic errors', async () => { - const { promise } = await startUpload(); - triggerFinished(new Error('Something went wrong'), null); + uploadMock.mockRejectedValue(new Error('Something went wrong')); - const result = await promise; + const result = await callUpload(); expect(result.error?.cause).toBe('UNKNOWN'); }); - it('should return UNKNOWN error when contentsId is null on success', async () => { - const { promise } = await startUpload(); - triggerFinished(null, null); + it('should return UNKNOWN error when contentsId is empty on success', async () => { + uploadMock.mockResolvedValue(''); - const result = await promise; + const result = await callUpload(); expect(result.error?.cause).toBe('UNKNOWN'); }); @@ -179,8 +149,7 @@ describe('upload-content-to-environment', () => { expect(result.error).toBe(accessError); expect(createReadStreamMock).not.toHaveBeenCalled(); - expect(environment.upload).not.toHaveBeenCalled(); - expect(environment.uploadMultipartFile).not.toHaveBeenCalled(); + expect(uploadMock).not.toHaveBeenCalled(); }); it('should return ACTION_NOT_PERMITTED when createReadStream throws EACCES', async () => { @@ -193,35 +162,49 @@ describe('upload-content-to-environment', () => { expect(result.error?.cause).toBe('ACTION_NOT_PERMITTED'); expect(safeAccessMock).toHaveBeenCalledWith({ absolutePath: '/some/file.txt' }); expect(createReadStreamMock).toHaveBeenCalledWith('/some/file.txt'); - expect(environment.upload).not.toHaveBeenCalled(); - expect(environment.uploadMultipartFile).not.toHaveBeenCalled(); + expect(uploadMock).not.toHaveBeenCalled(); expect(result.data).toBeUndefined(); }); it('should return ACTION_NOT_PERMITTED when the read stream emits EACCES', async () => { - const { promise } = await startUpload(); + uploadMock.mockImplementation( + async (_bucket: string, opts: UploadOptions) => + await new Promise((_resolve, reject) => { + opts.source.once('error', reject); + }), + ); + + const promise = callUpload(); fakeStream.emit('error', Object.assign(new Error('permission denied'), { code: 'EACCES' })); - triggerFinished(null, 'contents-id-after-stream-error'); const result = await promise; expect(result.error?.cause).toBe('ACTION_NOT_PERMITTED'); expect(safeAccessMock).toHaveBeenCalledWith({ absolutePath: '/some/file.txt' }); expect(createReadStreamMock).toHaveBeenCalledWith('/some/file.txt'); - expect(environment.upload).toHaveBeenCalledTimes(1); - expect(environment.uploadMultipartFile).not.toHaveBeenCalled(); + expect(uploadMock).toHaveBeenCalledTimes(1); expect(result.data).toBeUndefined(); }); - it('should stop the upload and destroy the stream when signal is aborted', async () => { - const actionState = makeActionState(); - (environment.upload as unknown as ReturnType) = makeUploadFn(actionState); - - await startUpload(); + it('should abort the upload and destroy the stream when signal is aborted', async () => { + uploadMock.mockImplementation( + async (_bucket: string, opts: UploadOptions) => + await new Promise((_resolve, reject) => { + opts.abortSignal?.addEventListener( + 'abort', + () => { + reject(new Error('aborted')); + }, + { once: true }, + ); + }), + ); + + const promise = callUpload(); abortController.abort(); + await promise; - expect(actionState.stop).toHaveBeenCalled(); expect(fakeStream.destroy).toHaveBeenCalled(); }); }); diff --git a/src/backend/features/backup/upload/upload-content-to-environment.ts b/src/backend/features/backup/upload/upload-content-to-environment.ts index f072b39c66..3038267862 100644 --- a/src/backend/features/backup/upload/upload-content-to-environment.ts +++ b/src/backend/features/backup/upload/upload-content-to-environment.ts @@ -1,8 +1,6 @@ import { Environment } from '@internxt/inxt-js'; import { DriveDesktopError } from '../../../../context/shared/domain/errors/DriveDesktopError'; -import { MULTIPART_UPLOAD_SIZE_THRESHOLD } from '../../../../context/shared/domain/UploadConstants'; import { createReadStream } from 'node:fs'; -import { UploadStrategyFunction } from '@internxt/inxt-js/build/lib/core'; import { Result } from '../../../../context/shared/domain/Result'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { isError } from '../../../../shared/errors/is-error'; @@ -25,63 +23,53 @@ export async function uploadContentToEnvironment({ signal, }: ContentUploadParams): Promise> { try { - const uploadFn: UploadStrategyFunction = - size > MULTIPART_UPLOAD_SIZE_THRESHOLD - ? environment.uploadMultipartFile.bind(environment) - : environment.upload.bind(environment); - const accessResult = await safeAccess({ absolutePath: path }); if (accessResult.error) { return { error: accessResult.error }; } const readable = createReadStream(path); + const abortHandler = () => { + readable.destroy(); + }; - return new Promise>((resolve) => { - let settled = false; - const resolveOnce = (result: Result) => { - if (settled) return; - settled = true; - resolve(result); - }; + readable.on('error', (err: Error & { code?: unknown; status?: unknown }) => { + logger.error({ tag: 'BACKUPS', msg: '[ENVLFU READ STREAM ERROR]', err, path }); + }); - readable.on('error', (err: Error & { code?: unknown; status?: unknown }) => { - logger.error({ tag: 'BACKUPS', msg: '[ENVLFU READ STREAM ERROR]', err, path }); - resolveOnce({ error: mapEnvironmentUploadError(err) }); - }); + signal.addEventListener('abort', abortHandler, { once: true }); - const state = uploadFn(bucket, { + try { + const contentsId = await environment.upload(bucket, { source: readable, fileSize: size, - finishedCallback: (err, contentsId) => { - readable.close(); - - if (err) { - logger.error({ tag: 'BACKUPS', msg: '[ENVLFU UPLOAD ERROR]', err }); - return resolveOnce({ error: mapEnvironmentUploadError(err) }); - } - - if (!contentsId) { - logger.error({ tag: 'BACKUPS', msg: '[ENVLFU UPLOAD ERROR] No contentsId returned' }); - return resolveOnce({ error: new DriveDesktopError('UNKNOWN') }); - } - - resolveOnce({ data: contentsId }); - }, + abortSignal: signal, progressCallback: (progress: number) => { logger.debug({ tag: 'SYNC-ENGINE', msg: '[UPLOAD PROGRESS]', progress }); }, }); - signal.addEventListener( - 'abort', - () => { - state.stop(); - readable.destroy(); - }, - { once: true }, - ); - }); + if (!contentsId) { + logger.error({ tag: 'BACKUPS', msg: '[ENVLFU UPLOAD ERROR] No contentsId returned' }); + return { error: new DriveDesktopError('UNKNOWN') }; + } + + return { data: contentsId }; + } catch (err) { + logger.error({ tag: 'BACKUPS', msg: '[ENVLFU UPLOAD ERROR]', err }); + + if (isError(err)) { + return { error: mapEnvironmentUploadError(err) }; + } + + return { error: new DriveDesktopError('UNKNOWN') }; + } finally { + signal.removeEventListener('abort', abortHandler); + + if (!readable.destroyed) { + readable.close(); + } + } } catch (err) { if (isError(err)) { return { error: mapEnvironmentUploadError(err) }; diff --git a/src/backend/features/thumbnails/upload-thumbnail-to-bucket.test.ts b/src/backend/features/thumbnails/upload-thumbnail-to-bucket.test.ts index 85915f488a..7dfe64db81 100644 --- a/src/backend/features/thumbnails/upload-thumbnail-to-bucket.test.ts +++ b/src/backend/features/thumbnails/upload-thumbnail-to-bucket.test.ts @@ -1,60 +1,71 @@ import { Environment } from '@internxt/inxt-js'; import { uploadThumbnailToBucket } from './upload-thumbnail-to-bucket'; import { UPLOAD_TIMEOUT_MS } from './thumbnail.constants'; -import { UploadOptions } from '@internxt/inxt-js/build/lib/core'; import { partialSpyOn } from '../../../../tests/vitest/utils.helper'; -function environmentMock(upload: (bucket: string, options: UploadOptions) => void): Environment { +type UploadMock = ( + bucket: string, + options: { source: NodeJS.ReadableStream; fileSize: number; abortSignal?: AbortSignal }, +) => Promise; + +function environmentMock(upload: UploadMock): Environment { return { upload } as unknown as Environment; } describe('upload-thumbnail-to-bucket', () => { const bucket = 'test-bucket'; const buffer = Buffer.from('image-data'); + const setTimeoutMock = partialSpyOn(global, 'setTimeout'); const clearTimeoutMock = partialSpyOn(global, 'clearTimeout'); it('should return data with contentsId on successful upload', async () => { const contentsId = 'contents-id-123'; - const environment = environmentMock((_bucket, { finishedCallback }) => { - finishedCallback(null, contentsId); - }); + const environment = environmentMock(async () => contentsId); const { data, error } = await uploadThumbnailToBucket(environment, bucket, buffer); expect(error).toBeUndefined(); expect(data).toBe(contentsId); + expect(setTimeoutMock).toBeCalled(); expect(clearTimeoutMock).toBeCalled(); }); - it('should return error when finishedCallback receives an error', async () => { + it('should return error when upload rejects', async () => { const uploadError = new Error('bucket error'); - const environment = environmentMock((_bucket, { finishedCallback }) => { - finishedCallback(uploadError, null); - }); + const environment = environmentMock(async () => Promise.reject(uploadError)); const { error } = await uploadThumbnailToBucket(environment, bucket, buffer); expect(error).toBe(uploadError); + expect(setTimeoutMock).toHaveBeenCalled(); expect(clearTimeoutMock).toHaveBeenCalled(); }); - it('should return error when finishedCallback has no contentsId', async () => { - const environment = environmentMock((_bucket, { finishedCallback }) => { - finishedCallback(null, null); - }); + it('should return error when upload resolves with empty contentsId', async () => { + const environment = environmentMock(async () => ''); const { error } = await uploadThumbnailToBucket(environment, bucket, buffer); expect(error).toBeInstanceOf(Error); + expect(setTimeoutMock).toHaveBeenCalled(); expect(clearTimeoutMock).toHaveBeenCalled(); }); it('should return error when upload times out', async () => { vi.useFakeTimers(); - const environment = environmentMock(() => { - // never calls finishedCallback - }); + const environment = environmentMock( + (_bucket, { abortSignal }) => + new Promise((_resolve, reject) => { + abortSignal?.addEventListener( + 'abort', + () => { + reject(new Error('aborted')); + }, + { once: true }, + ); + }), + ); const resultPromise = uploadThumbnailToBucket(environment, bucket, buffer); vi.advanceTimersByTime(UPLOAD_TIMEOUT_MS); @@ -62,6 +73,8 @@ describe('upload-thumbnail-to-bucket', () => { const { error } = await resultPromise; expect(error).toBeInstanceOf(Error); + expect(error?.message).toBe('Thumbnail bucket upload timed out'); + expect(clearTimeoutMock).toHaveBeenCalled(); vi.useRealTimers(); }); diff --git a/src/backend/features/thumbnails/upload-thumbnail-to-bucket.ts b/src/backend/features/thumbnails/upload-thumbnail-to-bucket.ts index 6a952e5eba..03d4712c2f 100644 --- a/src/backend/features/thumbnails/upload-thumbnail-to-bucket.ts +++ b/src/backend/features/thumbnails/upload-thumbnail-to-bucket.ts @@ -3,34 +3,37 @@ import { Readable } from 'node:stream'; import { Result } from '../../../context/shared/domain/Result'; import { UPLOAD_TIMEOUT_MS } from './thumbnail.constants'; -export function uploadThumbnailToBucket( +export async function uploadThumbnailToBucket( environment: Environment, bucket: string, buffer: Buffer, ): Promise> { - let timeoutId: ReturnType; + const abortController = new AbortController(); + const timeoutId = setTimeout(() => { + abortController.abort(); + }, UPLOAD_TIMEOUT_MS); - const upload = new Promise>((resolve) => { - environment.upload(bucket, { + try { + const contentsId = await environment.upload(bucket, { source: Readable.from(buffer), fileSize: buffer.length, - finishedCallback: (err: Error | null, contentsId: string | null) => { - clearTimeout(timeoutId); - if (err) { - return resolve({ error: err }); - } - if (!contentsId) { - return resolve({ error: new Error('Upload finished but no contentsId returned') }); - } - resolve({ data: contentsId }); - }, + abortSignal: abortController.signal, progressCallback: () => {}, }); - }); - const timeout = new Promise>((resolve) => { - timeoutId = setTimeout(() => resolve({ error: new Error('Thumbnail bucket upload timed out') }), UPLOAD_TIMEOUT_MS); - }); + if (!contentsId) { + return { error: new Error('Upload finished but no contentsId returned') }; + } - return Promise.race([upload, timeout]); + return { data: contentsId }; + } catch (error: unknown) { + if (abortController.signal.aborted) { + return { error: new Error('Thumbnail bucket upload timed out') }; + } + + const uploadError = error instanceof Error ? error : new Error('Thumbnail bucket upload failed'); + return { error: uploadError }; + } finally { + clearTimeout(timeoutId); + } } diff --git a/src/context/storage/StorageFiles/infrastructure/download/EnvironmentContentFileDownloader.ts b/src/context/storage/StorageFiles/infrastructure/download/EnvironmentContentFileDownloader.ts index abd2b5c47e..2b6e3ad611 100644 --- a/src/context/storage/StorageFiles/infrastructure/download/EnvironmentContentFileDownloader.ts +++ b/src/context/storage/StorageFiles/infrastructure/download/EnvironmentContentFileDownloader.ts @@ -1,6 +1,4 @@ -import { ActionState } from '@internxt/inxt-js/build/api'; -import { DownloadOneShardStrategy } from '@internxt/inxt-js/build/lib/core'; -import { DownloadStrategyFunction } from '@internxt/inxt-js/build/lib/core/download/strategy'; +import { Environment } from '@internxt/inxt-js'; import { EventEmitter, Readable } from 'stream'; import { Stopwatch } from '../../../../../apps/shared/types/Stopwatch'; import { StorageFile } from '../../domain/StorageFile'; @@ -10,20 +8,19 @@ import { logger } from '@internxt/drive-desktop-core/build/backend'; export class EnvironmentContentFileDownloader implements DownloaderHandler { private eventEmitter: EventEmitter; private stopwatch: Stopwatch; - - private state: ActionState | null; + private abortController: AbortController | null; constructor( - private readonly fn: DownloadStrategyFunction, + private readonly environment: Environment, private readonly bucket: string, ) { this.eventEmitter = new EventEmitter(); this.stopwatch = new Stopwatch(); - this.state = null; + this.abortController = null; } forceStop(): void { - this.state?.stop(); + this.abortController?.abort(); } download(file: StorageFile): Promise { @@ -34,44 +31,53 @@ export class EnvironmentContentFileDownloader implements DownloaderHandler { return this.executeDownload(fileId); } - private executeDownload(fileId: string): Promise { + private async executeDownload(fileId: string): Promise { this.stopwatch.start(); this.eventEmitter.emit('start'); - return new Promise((resolve, reject) => { - try { - this.state = this.fn( - this.bucket, - fileId, - { - progressCallback: (progress: number) => { - this.eventEmitter.emit('progress', progress, this.elapsedTime()); - }, - finishedCallback: async (err: Error, stream: Readable) => { - this.stopwatch.finish(); - if (err) { - this.eventEmitter.emit('error', err); - return reject(err); - } - this.eventEmitter.emit('finish', this.elapsedTime()); + this.abortController = new AbortController(); - resolve(stream); - }, + try { + const result = Reflect.apply(this.environment.download, this.environment, [ + this.bucket, + fileId, + { + progressCallback: (progress: number) => { + this.eventEmitter.emit('progress', progress, this.elapsedTime()); }, - { - label: 'Dynamic', - params: { - useProxy: false, - chunkSize: 4096 * 1024, - }, + abortSignal: this.abortController.signal, + }, + { + label: 'Dynamic', + params: { + useProxy: false, + chunkSize: 4096 * 1024, }, - ); - } catch (err) { - logger.error({ msg: 'Error in downloader:', err }); - this.eventEmitter.emit('error', err); - return reject(err); + }, + ]); + + if (!(result instanceof Promise)) { + throw new Error('Environment download strategy must return a Promise'); } - }); + + const stream = await result; + + if (!(stream instanceof Readable)) { + throw new Error('Download stream not available'); + } + + this.eventEmitter.emit('finish', this.elapsedTime()); + + return stream; + } catch (err: unknown) { + const downloadError = err instanceof Error ? err : new Error('Download failed'); + logger.error({ msg: 'Error in downloader', err: downloadError }); + this.eventEmitter.emit('error', downloadError); + throw downloadError; + } finally { + this.stopwatch.finish(); + this.abortController = null; + } } on(event: keyof DownloadEvents, handler: DownloadEvents[keyof DownloadEvents]): void { diff --git a/src/context/storage/StorageFiles/infrastructure/download/EnvironmentRemoteFileContentsManagersFactory.ts b/src/context/storage/StorageFiles/infrastructure/download/EnvironmentRemoteFileContentsManagersFactory.ts index 5e304adee7..d3afd26e83 100644 --- a/src/context/storage/StorageFiles/infrastructure/download/EnvironmentRemoteFileContentsManagersFactory.ts +++ b/src/context/storage/StorageFiles/infrastructure/download/EnvironmentRemoteFileContentsManagersFactory.ts @@ -12,6 +12,6 @@ export class EnvironmentFileDownloaderHandlerFactory implements DownloaderHandle ) {} downloader(): DownloaderHandler { - return new EnvironmentContentFileDownloader(this.environment.download, this.bucket); + return new EnvironmentContentFileDownloader(this.environment, this.bucket); } } diff --git a/src/context/storage/TemporalFiles/infrastructure/upload/EnvironmentTemporalFileUploader.ts b/src/context/storage/TemporalFiles/infrastructure/upload/EnvironmentTemporalFileUploader.ts index cba89a392f..b7f86e8b8f 100644 --- a/src/context/storage/TemporalFiles/infrastructure/upload/EnvironmentTemporalFileUploader.ts +++ b/src/context/storage/TemporalFiles/infrastructure/upload/EnvironmentTemporalFileUploader.ts @@ -26,36 +26,42 @@ export class EnvironmentTemporalFileUploader { this.eventEmitter.emit('start'); this.stopwatch.start(); - return new Promise((resolve, reject) => { - const state = this.fn(this.bucket, { + const abortHandler = () => { + contents.destroy(); + }; + + if (this.abortSignal) { + this.abortSignal.addEventListener('abort', abortHandler, { once: true }); + } + + try { + const contentsId = await this.fn(this.bucket, { source: contents, fileSize: size, - finishedCallback: (err: Error | null, contentsId: string) => { - this.stopwatch.finish(); - - if (err) { - this.eventEmitter.emit('error', err); - return reject(err); - } - this.eventEmitter.emit('finish', contentsId); - resolve(contentsId); - }, + abortSignal: this.abortSignal, progressCallback: (progress: number) => { this.eventEmitter.emit('progress', progress); }, }); + this.eventEmitter.emit('finish', contentsId); + return contentsId; + } catch (error) { + const uploadError = error instanceof Error ? error : new Error('Upload failed'); + + if (!contents.destroyed) { + contents.destroy(uploadError); + } + + this.eventEmitter.emit('error', uploadError); + throw uploadError; + } finally { + this.stopwatch.finish(); + if (this.abortSignal) { - this.abortSignal.addEventListener( - 'abort', - () => { - state.stop(); - contents.destroy(); - }, - { once: true }, - ); + this.abortSignal.removeEventListener('abort', abortHandler); } - }); + } } on(event: keyof UploadEvents, handler: UploadEvents[keyof UploadEvents]): void { diff --git a/src/context/storage/TemporalFiles/infrastructure/upload/EnvironmentTemporalFileUploaderFactory.ts b/src/context/storage/TemporalFiles/infrastructure/upload/EnvironmentTemporalFileUploaderFactory.ts index becd588edb..efc16e220f 100644 --- a/src/context/storage/TemporalFiles/infrastructure/upload/EnvironmentTemporalFileUploaderFactory.ts +++ b/src/context/storage/TemporalFiles/infrastructure/upload/EnvironmentTemporalFileUploaderFactory.ts @@ -7,7 +7,6 @@ import { EnvironmentTemporalFileUploader } from './EnvironmentTemporalFileUpload import { TemporalFileUploaderFactory } from '../../domain/upload/TemporalFileUploaderFactory'; import { Replaces } from '../../domain/upload/Replaces'; import { TemporalFile } from '../../domain/TemporalFile'; -import { MULTIPART_UPLOAD_SIZE_THRESHOLD } from '../../../../shared/domain/UploadConstants'; @Service() export class EnvironmentTemporalFileUploaderFactory implements TemporalFileUploaderFactory { @@ -92,11 +91,7 @@ export class EnvironmentTemporalFileUploaderFactory implements TemporalFileUploa throw new Error('Readable is needed to upload a file'); } - const fn = - document.size.value > MULTIPART_UPLOAD_SIZE_THRESHOLD - ? this.environment.uploadMultipartFile.bind(this.environment) - : this.environment.upload.bind(this.environment); - + const fn = this.environment.upload.bind(this.environment); const uploader = new EnvironmentTemporalFileUploader(fn, this.bucket, this._abortController?.signal); this.registerEvents(uploader); From 101c8e7550bf5966438399171eb0b846948d14e1 Mon Sep 17 00:00:00 2001 From: Esteban Galvis Date: Tue, 16 Jun 2026 14:58:00 -0500 Subject: [PATCH 3/3] fix: refactor upload handling to improve promise resolution and error management --- .../upload/upload-content-to-environment.test.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/backend/features/backup/upload/upload-content-to-environment.test.ts b/src/backend/features/backup/upload/upload-content-to-environment.test.ts index 30a0c53a0d..7aca18427d 100644 --- a/src/backend/features/backup/upload/upload-content-to-environment.test.ts +++ b/src/backend/features/backup/upload/upload-content-to-environment.test.ts @@ -58,6 +58,12 @@ describe('upload-content-to-environment', () => { }); } + async function startUpload(size = SMALL_SIZE) { + const promise = callUpload(size); + await Promise.resolve(); + return { promise }; + } + it('should resolve with contentsId on successful upload', async () => { const contentsId = 'abc123'; uploadMock.mockResolvedValue(contentsId); @@ -69,7 +75,10 @@ describe('upload-content-to-environment', () => { }); it('should call upload with file size and abortSignal', async () => { - uploadMock.mockResolvedValue('id'); + uploadMock.mockImplementation(async (_bucket: string, opts: UploadOptions) => { + capturedOpts = opts; + return 'id'; + }); await callUpload(SMALL_SIZE); @@ -174,7 +183,7 @@ describe('upload-content-to-environment', () => { }), ); - const promise = callUpload(); + const { promise } = await startUpload(); fakeStream.emit('error', Object.assign(new Error('permission denied'), { code: 'EACCES' })); @@ -201,7 +210,7 @@ describe('upload-content-to-environment', () => { }), ); - const promise = callUpload(); + const { promise } = await startUpload(); abortController.abort(); await promise;