fix: zipping exposes wrong path for nested folders
Massimo Melina committed
Jul 24, 2026 at 12:56 UTC
f93586863cd12ad1b2816efa435fec40bbed74ad
2 files changed
+12
-3
src/zip.ts
+3
-2
@@ -35,8 +35,9 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
35
continue
36
if (nodeIsFolder(subNode)) { // a directory needs to be walked
37
if (hasPermission(subNode, 'can_list', ctx) && hasPermission(subNode, 'can_archive', ctx)) {
38
- yield subNode // it could be empty
39
- yield* walkNode(subNode, { ctx, prefixPath: pathDecodeSegments(uri) + '/', requiredPerm: 'can_archive' })
38
+ const archivePath = pathDecodeSegments(uri)
39
+ yield { ...subNode, name: archivePath } // keep empty selected folders at the same path used for their contents
40
+ yield* walkNode(subNode, { ctx, prefixPath: archivePath + '/', requiredPerm: 'can_archive' })
41
}
42
continue
43
}
tests/test.ts
+9
-1
@@ -7,8 +7,9 @@ import { basename, dirname, join, resolve } from 'path'
7
import { exec } from 'child_process'
8
import _ from 'lodash'
9
import yaml from 'yaml'
10
+import unzipper from 'unzipper'
11
import { findDefined, pathEncode, randomId, try_, tryJson, UPLOAD_TEMP_HASH, wait, waitFor } from '../src/cross'
11
-import { httpStream, parseHttpUrl, stream2string, XRequestOptions } from '../src/util-http'
12
+import { httpStream, httpWithBody, parseHttpUrl, stream2string, XRequestOptions } from '../src/util-http'
13
import { ThrottledStream, ThrottleGroup } from '../src/ThrottledStream'
14
import { mkdir, rm, rename, writeFile, access } from 'fs/promises'
15
import { Readable } from 'stream'
@@ -297,6 +298,13 @@ describe('basics', () => {
298
test('zip.list.compacted folders', req('/f1/?get=zip&list=page%2Fgpl.png%2F%2F%00index.html', /page\/gpl.png.+page\/index.html/))
299
test('zip.list.selected folder decodes prefix', req('/tests/?get=zip&list=C%253A', data =>
300
String(data).includes('C:/gpl.png') && !String(data).includes('C%3A/gpl.png')))
301
+ test('zip.list.selected nested folder preserves path', async () => {
302
+ const url = '/?get=zip&list=f1%2Fpage'
303
+ const { body } = await httpWithBody(BASE_URL + url, { path: url })
304
+ const paths = (await unzipper.Open.buffer(body!)).files.map(x => x.path)
305
+ if (!paths.includes('f1/page/') || paths.includes('page/'))
306
+ throw Error('unexpected archive paths: ' + paths)
307
+ })
308
test('zip.list.bad encoding', req('/f1/?get=zip&list=%E0%A4%A//%00', { status: 200, length: 22 })) // basically empty
309
test('zip.list.null filename', req('/f1/?get=zip&list=%00', 400)) // tries to name the output with null-byte
310
test('zip.masked deep', req('/cantSearchForMasksDeep/?get=zip', {