webdav: better error code for MKCOL with missing or virtual parent
Massimo Melina committed
Mar 22, 2026 at 11:36 UTC
bafd41c0a39af2ae4b585a622b7f2a083823a13c
2 files changed
+32
-20
src/vfs.ts
+4
-13
@@ -108,7 +108,7 @@ export async function urlToNode(
108
url: string,
109
ctx?: Koa.Context,
110
parent: VfsNode=vfs,
111
- resolveMissing?: true | ((rest: string) => any) // true means missing path segments still resolve to temporary nodes with a computed source path (used by upload flows that create folders on write)
111
+ allowMissing?: boolean // true means missing path segments still resolve to temporary nodes with a computed source path
112
) : Promise<VfsNode | undefined> {
113
let initialSlashes = 0
114
while (url[initialSlashes] === '/')
@@ -122,25 +122,16 @@ export async function urlToNode(
122
return
123
const hasTrailingSlash = url.endsWith('/')
124
const rest = nextSlash < 0 ? '' : url.slice(nextSlash+1, hasTrailingSlash ? -1 : undefined)
125
- const allowMissing = resolveMissing === true
125
const assumeFolder = allowMissing && (rest > '' || hasTrailingSlash)
126
const ret = await getNodeByName(name, parent, assumeFolder)
127
if (!ret)
128
return
129
if (rest || ret?.original)
131
- return urlToNode(rest, ctx, ret, resolveMissing)
130
+ return urlToNode(rest, ctx, ret, allowMissing)
131
if (ret.source)
133
- if (!showHiddenFiles.get() && await isHiddenFile(ret.source))
132
+ if (!showHiddenFiles.get() && await isHiddenFile(ret.source)
133
+ || !allowMissing && await setIsFolder(ret) === undefined) // undefined = not found on disk
134
return
135
- else if (await setIsFolder(ret) === undefined) { // undefined = not found on disk
136
- if (!resolveMissing)
137
- return
138
- if (allowMissing)
139
- return ret
140
- const rest = ret.source!.slice(parent.source!.length) // we know parent has .source, otherwise !ret.source || ret.original
141
- resolveMissing(removeStarting('/', rest))
142
- return parent
143
- }
135
return ret
136
}
137
src/webdav.ts
+28
-7
@@ -4,9 +4,30 @@ import {
4
getNodeName, nodeIsFolder, nodeIsLink, nodeStats, statusCodeForMissingPerm, urlToNode, vfs, VfsNode, walkNode
5
} from './vfs'
6
import {
7
- HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_CREATED, HTTP_METHOD_NOT_ALLOWED, HTTP_NO_CONTENT, HTTP_NOT_FOUND,
8
- HTTP_OK, HTTP_PRECONDITION_FAILED, HTTP_SERVER_ERROR, HTTP_UNAUTHORIZED, HTTP_LOCKED, HTTP_FORBIDDEN,
9
- DAY, CFG, enforceFinal, pathEncode, prefix, getOrSet, Dict, Timeout, join as crossJoin, try_, safeDecodeURIComponent
7
+ HTTP_BAD_REQUEST,
8
+ HTTP_CONFLICT,
9
+ HTTP_CREATED,
10
+ HTTP_METHOD_NOT_ALLOWED,
11
+ HTTP_NO_CONTENT,
12
+ HTTP_NOT_FOUND,
13
+ HTTP_OK,
14
+ HTTP_PRECONDITION_FAILED,
15
+ HTTP_SERVER_ERROR,
16
+ HTTP_UNAUTHORIZED,
17
+ HTTP_LOCKED,
18
+ HTTP_FORBIDDEN,
19
+ DAY,
20
+ CFG,
21
+ enforceFinal,
22
+ pathEncode,
23
+ prefix,
24
+ getOrSet,
25
+ Dict,
26
+ Timeout,
27
+ join as crossJoin,
28
+ try_,
29
+ safeDecodeURIComponent,
30
+ pathDecode
31
} from './cross'
32
import { PassThrough } from 'stream'
33
import { mkdir, rm } from 'fs/promises'
@@ -127,10 +148,10 @@ export const webdav: Koa.Middleware = async (ctx, next) => {
148
ctx.status = HTTP_METHOD_NOT_ALLOWED
149
return
150
}
130
- let name = ''
131
- const parentNode = await urlToNode(path, ctx, vfs, v => name = v)
132
- if (!parentNode)
133
- return ctx.status = HTTP_NOT_FOUND
151
+ const parentNode = await urlToNode(dirname(path), ctx)
152
+ if (!parentNode) // this is a bit incoherent with the way we handle PUT, which doesn't stop in this case, but it's by RFC 4918 section 9.3
153
+ return ctx.status = HTTP_CONFLICT
154
+ const name = safeDecodeURIComponent(basename(path), '')
155
if (!isValidFileName(name))
156
return ctx.status = HTTP_BAD_REQUEST
157
if (statusCodeForMissingPerm(parentNode, 'can_upload', ctx)) {