fix: faulty upload overwrite checks

Massimo Melina committed May 6, 2026 at 00:05 UTC e92416ff95e88494098084133cc0f16087c10a5b
3 files changed +62 -5
src/upload.ts
+1 -1
@@ -180,7 +180,7 @@ export function uploadWriter(base: VfsNode, baseUri: string, filename: string, c
180 if (isPartial) // we are supposed to leave the unfinished upload as it is, with its temp name
181 return ctx.status = HTTP_NO_CONTENT // lockMiddleware contains an empty string, so we must take care of the status
182 let dest = fullPath // final destination, considering numbering if necessary
183 - if (dontOverwriteUploading.get() && !await overwriteAnyway() && fs.existsSync(dest)) {
183 + if (dontOverwriteUploading.get() && fs.existsSync(dest) && !await overwriteAnyway()) {
184 if (overwriteRequestedButForbidden) {
185 await rm(tempName).catch(e => console.warn(String(e)))
186 releaseFile()
src/webdav.ts
+2 -4
@@ -156,9 +156,7 @@ export const webdav: Koa.Middleware = async (ctx, next) => {
156 const overwriteGraceKey = path + prefix('|', getCurrentUsername(ctx)) // bind temporary overwrite grace to the authenticated user so accounts cannot reuse each other's grace window
157 // Finder first creates an empty file (a test?) then wants to overwrite it, which requires deletion permission, but the user may not have it, causing a renamed upload. To solve, so we give it special permission for a few seconds.
158 const x = ctx.get('x-expected-entity-length') // field used by Finder's webdav on actual upload, after
159 - if (!x && !ctx.length)
160 - allowWebdavOverwrite(overwriteGraceKey)
161 - else if (canOverwrite.has(overwriteGraceKey)) {
159 + if (isKnownWebdavAgent && canOverwrite.has(overwriteGraceKey)) {
160 canOverwrite.delete(overwriteGraceKey)
161 const node = await urlToNode(path, ctx)
162 if (node?.source)
@@ -170,7 +168,7 @@ export const webdav: Koa.Middleware = async (ctx, next) => {
168 if (isKnownWebdavAgent)
169 ctx.query.existing ??= 'overwrite' // with webdav this is our default
170 await next()
173 - if (ctx.status === HTTP_OK)
171 + if (isKnownWebdavAgent && ctx.body?.uri === path) // the upload middleware reports the final uri that can be different from the initial request
172 allowWebdavOverwrite(overwriteGraceKey)
173 }
174
tests/test.ts
+59
@@ -398,6 +398,65 @@ describe('webdav', () => {
398 await rmAny(destPath)
399 }
400 })
401 + test('webdav.put failed overwrite does not grant grace', async () => {
402 + const name = `wd-failed-grace-${randomId(6)}.txt`
403 + const uri = `${CANT_OVERWRITE_URI}${name}`
404 + const dir = await ensureCantOverwriteDir()
405 + const destPath = resolve(dir, name)
406 + await writeFile(destPath, 'dest')
407 + try {
408 + await req(uri, 403, {
409 + method: 'PUT',
410 + auth,
411 + jar,
412 + headers: { 'content-length': '0', 'user-agent': WEBDAV_UA },
413 + body: '',
414 + })()
415 + await webdavUpload(uri, 403, 'source')()
416 + if (readFileSync(destPath, 'utf8') !== 'dest')
417 + throw "destination changed"
418 + }
419 + finally {
420 + await rmAny(destPath)
421 + }
422 + })
423 + test('webdav.put grants grace after successful encoded empty upload', async () => {
424 + const name = `wd-grace-${randomId(6)} %#.txt`
425 + const uri = `${CANT_OVERWRITE_URI}${pathEncode(name)}`
426 + const dir = await ensureCantOverwriteDir()
427 + const destPath = resolve(dir, name)
428 + try {
429 + await req(uri, (x, res) => {
430 + if (res.statusCode !== 200)
431 + throw `expected first PUT 200, got ${res.statusCode}`
432 + if (x?.uri !== uri)
433 + throw "first PUT uri mismatch"
434 + }, {
435 + method: 'PUT',
436 + auth,
437 + jar,
438 + headers: { 'content-length': '0', 'user-agent': WEBDAV_UA },
439 + body: '',
440 + })()
441 + await req(uri, (x, res) => {
442 + if (res.statusCode !== 200)
443 + throw `expected second PUT 200, got ${res.statusCode}`
444 + if (x?.uri !== uri)
445 + throw "second PUT uri mismatch"
446 + }, {
447 + method: 'PUT',
448 + auth,
449 + jar,
450 + headers: { 'x-expected-entity-length': String(Buffer.byteLength('source')), 'user-agent': WEBDAV_UA },
451 + body: 'source',
452 + })()
453 + if (readFileSync(destPath, 'utf8') !== 'source')
454 + throw "destination not overwritten"
455 + }
456 + finally {
457 + await rmAny(destPath)
458 + }
459 + })
460 test('webdav.put grace is bound to username', async () => {
461 const firstUser = `wd-grace-a-${randomId(6)}`.toLowerCase()
462 const secondUser = `wd-grace-b-${randomId(6)}`.toLowerCase()