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