fix: double slash in TUS upload path when readEntries returns multiple batches (#5848)

This commit is contained in:
fabb 2026-03-28 18:59:28 +01:00 committed by GitHub
parent ae72f93667
commit 432f3e60ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 336 additions and 3 deletions

View file

@ -27,6 +27,24 @@ jobs:
pnpm install --frozen-lockfile
pnpm run lint
test-frontend:
name: Test Frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
with:
package_json_file: "frontend/package.json"
- uses: actions/setup-node@v6
with:
node-version: "24.x"
cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml"
- working-directory: frontend
run: |
pnpm install --frozen-lockfile
pnpm run test
lint-backend:
name: Lint Backend
runs-on: ubuntu-latest

View file

@ -14,7 +14,8 @@
"typecheck": "vue-tsc -p ./tsconfig.app.json --noEmit",
"lint": "eslint src/",
"lint:fix": "eslint --fix src/",
"format": "prettier --write ."
"format": "prettier --write .",
"test": "vitest run"
},
"dependencies": {
"@chenfengyuan/vue-number-input": "^2.0.1",
@ -70,6 +71,7 @@
"typescript": "^5.9.3",
"vite": "^8.0.0",
"vite-plugin-compression2": "^2.3.1",
"vitest": "^4.1.0",
"vue-tsc": "^3.1.3"
},
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"

230
frontend/pnpm-lock.yaml generated
View file

@ -162,6 +162,9 @@ importers:
vite-plugin-compression2:
specifier: ^2.3.1
version: 2.5.1(rollup@4.57.1)
vitest:
specifier: ^4.1.0
version: 4.1.0(@types/node@24.12.0)(vite@8.0.0(@types/node@24.12.0)(esbuild@0.27.3)(terser@5.46.0)(yaml@2.8.2))
vue-tsc:
specifier: ^3.1.3
version: 3.2.5(typescript@5.9.3)
@ -1400,12 +1403,21 @@ packages:
cpu: [x64]
os: [win32]
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@tsconfig/node24@24.0.4':
resolution: {integrity: sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/esrecurse@4.3.1':
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
@ -1572,6 +1584,35 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
vue: ^3.2.25
'@vitest/expect@4.1.0':
resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==}
'@vitest/mocker@4.1.0':
resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@4.1.0':
resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==}
'@vitest/runner@4.1.0':
resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==}
'@vitest/snapshot@4.1.0':
resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==}
'@vitest/spy@4.1.0':
resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==}
'@vitest/utils@4.1.0':
resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==}
'@volar/language-core@2.4.28':
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
@ -1777,6 +1818,10 @@ packages:
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
ast-kit@2.2.0:
resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==}
engines: {node: '>=20.19.0'}
@ -1853,6 +1898,10 @@ packages:
caniuse-lite@1.0.30001774:
resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'}
@ -1946,6 +1995,9 @@ packages:
epubjs@0.3.93:
resolution: {integrity: sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw==}
es-module-lexer@2.0.0:
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
es5-ext@0.10.64:
resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
engines: {node: '>=0.10'}
@ -2068,6 +2120,9 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@ -2075,6 +2130,10 @@ packages:
event-emitter@0.3.5:
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
@ -2487,6 +2546,9 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@ -2700,6 +2762,9 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@ -2718,6 +2783,12 @@ packages:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@4.0.0:
resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@ -2747,10 +2818,21 @@ packages:
engines: {node: '>=10'}
hasBin: true
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@1.0.4:
resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==}
engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tinyrainbow@3.1.0:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -2909,6 +2991,41 @@ packages:
yaml:
optional: true
vitest@4.1.0:
resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 4.1.0
'@vitest/browser-preview': 4.1.0
'@vitest/browser-webdriverio': 4.1.0
'@vitest/ui': 4.1.0
happy-dom: '*'
jsdom: '*'
vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@opentelemetry/api':
optional: true
'@types/node':
optional: true
'@vitest/browser-playwright':
optional: true
'@vitest/browser-preview':
optional: true
'@vitest/browser-webdriverio':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
@ -2972,6 +3089,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@ -4135,6 +4257,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.57.1':
optional: true
'@standard-schema/spec@1.1.0': {}
'@tsconfig/node24@24.0.4': {}
'@tybys/wasm-util@0.10.1':
@ -4142,6 +4266,13 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/deep-eql@4.0.2': {}
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {}
@ -4384,6 +4515,47 @@ snapshots:
vite: 8.0.0(@types/node@24.12.0)(esbuild@0.27.3)(terser@5.46.0)(yaml@2.8.2)
vue: 3.5.30(typescript@5.9.3)
'@vitest/expect@4.1.0':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.1.0
'@vitest/utils': 4.1.0
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.0(vite@8.0.0(@types/node@24.12.0)(esbuild@0.27.3)(terser@5.46.0)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 4.1.0
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.0(@types/node@24.12.0)(esbuild@0.27.3)(terser@5.46.0)(yaml@2.8.2)
'@vitest/pretty-format@4.1.0':
dependencies:
tinyrainbow: 3.1.0
'@vitest/runner@4.1.0':
dependencies:
'@vitest/utils': 4.1.0
pathe: 2.0.3
'@vitest/snapshot@4.1.0':
dependencies:
'@vitest/pretty-format': 4.1.0
'@vitest/utils': 4.1.0
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.1.0': {}
'@vitest/utils@4.1.0':
dependencies:
'@vitest/pretty-format': 4.1.0
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
'@volar/language-core@2.4.28':
dependencies:
'@volar/source-map': 2.4.28
@ -4619,6 +4791,8 @@ snapshots:
alien-signals@3.1.2: {}
assertion-error@2.0.1: {}
ast-kit@2.2.0:
dependencies:
'@babel/parser': 7.29.0
@ -4701,6 +4875,8 @@ snapshots:
caniuse-lite@1.0.30001774: {}
chai@6.2.2: {}
chokidar@5.0.0:
dependencies:
readdirp: 5.0.0
@ -4783,6 +4959,8 @@ snapshots:
marks-pane: 1.0.9
path-webpack: 0.0.3
es-module-lexer@2.0.0: {}
es5-ext@0.10.64:
dependencies:
es6-iterator: 2.0.3
@ -4977,6 +5155,10 @@ snapshots:
estree-walker@2.0.2: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
esutils@2.0.3: {}
event-emitter@0.3.5:
@ -4984,6 +5166,8 @@ snapshots:
d: 1.0.2
es5-ext: 0.10.64
expect-type@1.3.0: {}
exsolve@1.0.8: {}
ext@1.7.0:
@ -5338,6 +5522,8 @@ snapshots:
dependencies:
boolbase: 1.0.0
obug@2.1.1: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@ -5569,6 +5755,8 @@ snapshots:
shebang-regex@3.0.0: {}
siginfo@2.0.0: {}
signal-exit@3.0.7: {}
source-map-js@1.2.1: {}
@ -5582,6 +5770,10 @@ snapshots:
speakingurl@14.0.1: {}
stackback@0.0.2: {}
std-env@4.0.0: {}
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
@ -5610,11 +5802,17 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
tinybench@2.9.0: {}
tinyexec@1.0.4: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinyrainbow@3.1.0: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@ -5764,6 +5962,33 @@ snapshots:
terser: 5.46.0
yaml: 2.8.2
vitest@4.1.0(@types/node@24.12.0)(vite@8.0.0(@types/node@24.12.0)(esbuild@0.27.3)(terser@5.46.0)(yaml@2.8.2)):
dependencies:
'@vitest/expect': 4.1.0
'@vitest/mocker': 4.1.0(vite@8.0.0(@types/node@24.12.0)(esbuild@0.27.3)(terser@5.46.0)(yaml@2.8.2))
'@vitest/pretty-format': 4.1.0
'@vitest/runner': 4.1.0
'@vitest/snapshot': 4.1.0
'@vitest/spy': 4.1.0
'@vitest/utils': 4.1.0
es-module-lexer: 2.0.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.1
pathe: 2.0.3
picomatch: 4.0.3
std-env: 4.0.0
tinybench: 2.9.0
tinyexec: 1.0.4
tinyglobby: 0.2.15
tinyrainbow: 3.1.0
vite: 8.0.0(@types/node@24.12.0)(esbuild@0.27.3)(terser@5.46.0)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.12.0
transitivePeerDependencies:
- msw
vscode-uri@3.1.0: {}
vue-eslint-parser@10.4.0(eslint@10.0.3):
@ -5842,6 +6067,11 @@ snapshots:
dependencies:
isexe: 2.0.0
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
word-wrap@1.2.5: {}
xml-name-validator@4.0.0: {}

View file

@ -0,0 +1,80 @@
import { describe, it, expect } from "vitest";
/**
* Reproduces the path-building logic from scanFiles() in upload.ts (lines 118-138).
*
* readReaderContent() is called when traversing a dropped folder. The browser's
* FileSystemDirectoryReader.readEntries() returns entries in batches you must
* call it repeatedly until it returns an empty array. Each recursive call in the
* current code appends "/" to the directory, so the second batch and beyond get
* double (or triple, etc.) slashes in the constructed fullPath.
*/
type Entry = { name: string; isFile: boolean; isDirectory: boolean };
function simulateScanFiles(
dirName: string,
entryBatches: Entry[][]
): string[] {
const paths: string[] = [];
let batchIndex = 0;
// Mirrors readEntry() for files — records fullPath as `${directory}${file.name}`
function readEntry(entry: Entry, directory = ""): void {
if (entry.isFile) {
paths.push(`${directory}${entry.name}`);
}
}
// Mirrors readReaderContent() from upload.ts lines 118-138
function readReaderContent(directory: string): void {
const entries = batchIndex < entryBatches.length ? entryBatches[batchIndex] : [];
batchIndex++;
if (entries.length > 0) {
const dirWithSlash = directory.endsWith("/")
? directory
: `${directory}/`;
for (const entry of entries) {
readEntry(entry, dirWithSlash);
}
readReaderContent(dirWithSlash);
}
}
// Initial call mirrors readEntry() for a directory — upload.ts line 111-114
readReaderContent(dirName);
return paths;
}
describe("scanFiles path construction", () => {
it("should not produce double slashes when readEntries returns multiple batches", () => {
// Two batches: simulates a large directory where the browser splits
// readEntries() results across multiple calls
const paths = simulateScanFiles("TestFolder", [
[
{ name: "file1.xlsx", isFile: true, isDirectory: false },
{ name: "file2.xlsx", isFile: true, isDirectory: false },
],
[
{ name: "file3.xlsx", isFile: true, isDirectory: false },
],
]);
expect(paths).toHaveLength(3);
for (const p of paths) {
expect(p, `path "${p}" contains double slash`).not.toContain("//");
}
});
it("single batch should work fine (no regression)", () => {
const paths = simulateScanFiles("TestFolder", [
[
{ name: "file1.xlsx", isFile: true, isDirectory: false },
],
]);
expect(paths).toEqual(["TestFolder/file1.xlsx"]);
});
});

View file

@ -124,11 +124,14 @@ export function scanFiles(dt: DataTransfer): Promise<UploadList | FileList> {
reader.readEntries((entries) => {
reading--;
if (entries.length > 0) {
const dirWithSlash = directory.endsWith("/")
? directory
: `${directory}/`;
for (const entry of entries) {
readEntry(entry, `${directory}/`);
readEntry(entry, dirWithSlash);
}
readReaderContent(reader, `${directory}/`);
readReaderContent(reader, dirWithSlash);
}
if (reading === 0) {