better error code for API move_files when the uri_to is not a folder
Massimo Melina committed
Jan 21, 2026 at 22:42 UTC
fd854bb5b3ed2ffe7ffdf04a3ccff63778b0263c
4 files changed
+18
-11
package.json
+1
-1
@@ -20,7 +20,7 @@
20
"server-for-test": "node dist/src --cwd tests/work --config tests --debug",
21
"server-for-test-dev": "cross-env DEV=1 FRONTEND_PROXY=3005 ADMIN_PROXY=3006 nodemon --ignore tests/ --watch src -e ts,tsx --exec tsx src -- --cwd tests/work --config tests",
22
"test": "node --import tsx --test tests/test.ts",
23
- "test-with-server": "npm run port-is-free && tsc && rm -rf tests/work && (node dist/src --cwd tests/work --config tests & echo $! > .server_pid) && sleep 2 && node --import tsx --test tests/test.ts; _exit=$?; if [ -f ./.server_pid ]; then SERVER_PID=$(cat ./.server_pid); kill \"$SERVER_PID\" 2>/dev/null || true; rm -f ./.server_pid; fi; exit $_exit",
23
+ "test-with-server": "npm run port-is-free && tsc && rm -rf tests/work tests/tmp && (node dist/src --cwd tests/work --config tests & echo $! > .server_pid) && sleep 2 && node --import tsx --test tests/test.ts; _exit=$?; if [ -f ./.server_pid ]; then SERVER_PID=$(cat ./.server_pid); kill \"$SERVER_PID\" 2>/dev/null || true; rm -f ./.server_pid; fi; exit $_exit",
24
"port-is-free": "node -e \"process.exit(await fetch('http://localhost:81').then(() => console.log('BUSY') || 1, () => 0))\"",
25
"test-ui": "npx playwright test frontend && npx playwright test serial",
26
"test-with-ui": "npx playwright test --ui",
src/frontEndApis.ts
+3
-1
@@ -114,7 +114,9 @@ export const frontEndApis: ApiHandlers = {
114
apiAssertTypes({ array: { uri_from }, string: { uri_to } })
115
ctx.logExtra(null, { target: uri_from.map(decodeURI), destination: decodeURI(uri_to) })
116
const destNode = await urlToNode(uri_to, ctx)
117
- const err = !destNode ? HTTP_NOT_FOUND : statusCodeForMissingPerm(destNode, 'can_upload', ctx)
117
+ const err = !destNode ? HTTP_NOT_FOUND
118
+ : !nodeIsFolder(destNode) ? HTTP_METHOD_NOT_ALLOWED
119
+ : statusCodeForMissingPerm(destNode, 'can_upload', ctx)
120
if (err)
121
return new ApiError(err)
122
return {
src/vfs.ts
+5
-1
@@ -161,7 +161,11 @@ async function isHiddenFile(path: string) {
161
162
export async function getNodeByName(name: string, parent: VfsNode, assumeMissingToBeFolder=false) {
163
// does the tree node have a child that goes by this name, otherwise attempt disk
164
- const child = parent.children?.find(isSameFilenameAs(name)) || await childFromDisk()
164
+ let child = parent.children?.find(isSameFilenameAs(name))
165
+ if (child) // found as vfs node
166
+ await setIsFolder(child) // in case it's pointing to a folder that didn't exist at loading time
167
+ else
168
+ child = await childFromDisk()
169
return child && applyParentToChild(child, parent, name)
170
171
async function childFromDisk() {
tests/test.ts
+9
-8
@@ -182,9 +182,9 @@ describe('basics', () => {
182
})
183
})
184
test('upload.post.absolute filename', () => {
185
- const absPath = resolve(ROOT, `abs-${randomId(6)}.txt`)
185
+ const absPath = resolve(__dirname, `abs-${randomId(6)}.txt`)
186
const absForBody = absPath.replace(/\\\\/g, '/')
187
- const storedPath = resolve(ROOT, 'tmp', basename(absPath))
187
+ const storedPath = resolve(__dirname, 'tmp', basename(absPath))
188
return execP(`curl -s -u ${auth} -H "x-hfs-wait: 1" -F "upload=@${SAMPLE_FILE_PATH};filename=${absForBody}" ${BASE_URL}${UPLOAD_ROOT} -w "\\nSTATUS:%{http_code}"`).then(async x => {
189
const out = x.trimEnd()
190
const idx = out.lastIndexOf('\nSTATUS:')
@@ -254,6 +254,7 @@ describe('after-login', () => {
254
})
255
test('upload.never', reqUpload('/random', 403))
256
test('upload.ok', reqUpload(UPLOAD_DEST, 200))
257
+ test('move.dest is file', reqApi('move_files', { uri_from: [UPLOAD_DEST], uri_to: UPLOAD_DEST }, 405))
258
test('upload.dot name', reqUpload(`${UPLOAD_ROOT}%2e`, 418))
259
test('upload.unreadable', reqUpload(`${UPLOAD_ROOT}%0a`, 418))
260
test('upload.temp hash traversal', req(`${UPLOAD_ROOT}%2e%2e?get=${UPLOAD_TEMP_HASH}`, 404))
@@ -270,7 +271,7 @@ describe('after-login', () => {
271
test('zip.no-list but archive', req('/zipNoList/?get=zip', 403, { jar: {} }))
272
test('upload but not delete', async () => {
273
const name = `cant-delete`
273
- await mkdir(resolve(ROOT, name), { recursive: true })
274
+ await mkdir(resolve(__dirname, name), { recursive: true })
275
await reqApi('add_vfs', { parent: UPLOAD_ROOT, source: `../${name}`, name, can_upload: ['admins'], can_delete: false }, 200)()
276
try {
277
const dest = `${UPLOAD_ROOT}${name}/no-delete.txt`
@@ -279,7 +280,7 @@ describe('after-login', () => {
280
}
281
finally {
282
await reqApi('del_vfs', { uris: [UPLOAD_ROOT + name] }, 200)().catch(() => {})
282
- await rmAny(resolve(ROOT, name))
283
+ await rmAny(resolve(__dirname, name))
284
}
285
})
286
test('move.overwrite needs delete', async () => {
@@ -297,13 +298,13 @@ describe('after-login', () => {
298
throw "file overwritten"
299
}
300
finally {
300
- await rmAny(resolve(ROOT, UPLOAD_DIR, destFile))
301
+ await rmAny(resolve(__dirname, UPLOAD_DIR, destFile))
302
await rmAny(destDir)
303
}
304
})
305
test('upload.path bypass', async () => {
306
const name = 'no-upload'
306
- const targetDir = resolve(ROOT, 'tmp', name)
307
+ const targetDir = resolve(__dirname, 'tmp', name)
308
try {
309
await execP(`curl -g -s -u ${auth} -F "upload=@${SAMPLE_FILE_PATH};filename=${name}/evil.txt" ${BASE_URL}${UPLOAD_ROOT}`)
310
if (existsSync(resolve(targetDir, 'evil.txt')))
@@ -365,7 +366,7 @@ describe('after-login', () => {
366
})
367
test('rename.backslash', async () => {
368
await reqApi('rename', { uri: UPLOAD_DEST, dest: 'sub\\file' }, process.platform === 'win32' ? 403 : 200)()
368
- const d = resolve(ROOT, UPLOAD_DIR)
369
+ const d = resolve(__dirname, UPLOAD_DIR)
370
await rename(resolve(d, 'sub\\file'), resolve(d, basename(UPLOAD_DEST))).catch(() => {})
371
})
372
const renameTo = 'z'
@@ -588,7 +589,7 @@ function throwIf(msg: any) {
589
}
590
591
async function ensureCantOverwriteDir() {
591
- const baseDir = resolve(ROOT, 'tmp', CANT_OVERWRITE_NAME)
592
+ const baseDir = resolve(__dirname, 'tmp', CANT_OVERWRITE_NAME)
593
await mkdir(baseDir, { recursive: true })
594
return baseDir
595
}