@samitouri / QOSami-HFS / commits / bafd41c0

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)) {