Skip to content

fix(dav): abort chunked v2 writes when deleting upload folder - #64110

Open
joshtrichards wants to merge 2 commits into
masterfrom
jtr/fix-chunkedV2-deletions
Open

fix(dav): abort chunked v2 writes when deleting upload folder#64110
joshtrichards wants to merge 2 commits into
masterfrom
jtr/fix-chunkedV2-deletions

Conversation

@joshtrichards

Copy link
Copy Markdown
Member

Summary

Restore cancellation of Chunking v2 backend writes when an upload folder is deleted.

The original Chunking v2 implementation in #27034 resolved DELETE requests against the upload folder itself and did not require a Destination header. This behavior inadvertently regressed in #38100, which began resolving the parent directory and applying prerequisites intended for PUT and MOVE requests, including the Destination header requirement,

As a result, deleting an upload folder did not call cancelChunkedWrite(). For object-storage-backed uploads, this could leave the underlying multipart upload active even though the DAV upload folder was deleted.

This fix allows uploads without v2 metadata to proceed through normal DAV deletion, rejects inconsistent v2 metadata, propagates backend cancellation failures, and removes the cached session after successful cancellation.

Specifically, this change:

  • resolves DELETE requests against the upload folder itself;
  • uses the target path and write token stored in the v2 upload metadata;
  • does not require a Destination header for DELETE;
  • rejects partial (incomplete not fully missing) v2 metadata instead of silently deleting the upload folder;
  • still allows uploads lacking v2 metadata to proceed through normal DAV deletion;
  • still validates that the storage supports chunked writes and, when applicable, multipart object-store uploads;
  • propagates target-resolution and backend cancellation failures so normal DAV deletion does not continue after a failed cancellation;
  • removes the cached upload session after successful backend cancellation; and
  • adds unit coverage for successful cancellation, fallback behavior, invalid metadata, unsupported storage, and failure propagation.

This restores the intended behavior of the documented abort operation:

curl -X DELETE -u roeland:pass \
    https://server/remote.php/dav/uploads/roeland/<upload-id>/

No client changes required.

TODO

  • Backport to v35-v32
  • Backport <v32, optionally (applicable all the way back to v27)

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Resolve DELETE requests against the upload folder itself and use the stored upload metadata rather than requiring a `Destination` header. The header was not required by the original implementation but became required due to an inadvertent regression in #38100.

Allow uploads without v2 metadata to proceed through normal DAV deletion. Reject inconsistent v2 metadata, propagate backend cancellation failures, and remove the cached session after successful cancellation.

Assisted-by: Copilot:gpt-5.6-sol

Signed-off-by: Josh <josh.t.richards@gmail.com>
Add unit coverage for deleting chunked v2 upload folders, including:

- cancellation without a Destination header;
- resolution of existing and temporary upload targets;
- cache cleanup after successful cancellation;
- missing and incomplete upload metadata;
- unsupported storage backends;
- multipart capability failures; and
- propagation of cancellation and target-resolution errors.

Assisted-by: Copilot:gpt-5.6-sol

Signed-off-by: Josh <josh.t.richards@gmail.com>
@solracsf

solracsf commented Sep 7, 2026

Copy link
Copy Markdown
Member

Nice catch on the dirname() regression, this was dead code before. 👍

My worry is that the new error paths can leave the upload folder impossible to delete at all. getUploadStorage() re-resolves the target through the tree and can throw: when the upload overwrites an existing file, afterMkcol returns early and never creates .target, so if that target is deleted or renamed while the upload is open (24h TTL), the abort goes target NotFound > getChild('.target') NotFound > out of beforeDelete > 404. Folder stays, multipart upload still open, every retry the same. getUploadFile() can also throw Forbidden, which its own catch (NotFound) doesn't catch. And both StorageInvalidException throws come out as 500, since it extends plain \Exception rather than Sabre\DAV\Exception.

Could the abort be best effort? Catch those, log, and let normal DAV deletion continue. Failing to cancel is bad but it's what happens today anyway, whereas a folder the client can't remove is new.

Separately, maybe out of scope: UploadCleanup deletes stale folders via IRootFolder, outside the Sabre tree, so this hook never runs there. That's the path abandoned uploads actually take (crashed client, closed tab, no DELETE sent) and probably where most of #29841 comes from. Follow-up? 🤔

@@
 use OCP\Lock\ILockingProvider;
+use Psr\Log\LoggerInterface;
 use Sabre\DAV\Exception\BadRequest;
@@
 		$storage = $this->uploadFolder->getStorage();
-		if (!$storage->instanceOfStorage(IChunkedFileWrite::class)) {
-			throw new StorageInvalidException(
-				'Storage does not support chunked file writing'
-			);
-		}
-
-		if (
-			$storage->instanceOfStorage(ObjectStoreStorage::class)
-			&& !$storage->getObjectStore() instanceof IObjectStoreMultiPartUpload
-		) {
-			throw new StorageInvalidException(
-				'Storage does not support multi part uploads'
-			);
-		}
-
-		[$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);
+		if (!$storage->instanceOfStorage(IChunkedFileWrite::class)
+			|| ($storage->instanceOfStorage(ObjectStoreStorage::class)
+				&& !$storage->getObjectStore() instanceof IObjectStoreMultiPartUpload)) {
+			return true;
+		}
+
+		try {
+			[$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);
+		} catch (NotFound|Forbidden $e) {
+			// Without a resolvable target there is no way left to reach the
+			// backend write, and refusing the delete would leave the client
+			// with an upload folder it can never remove.
+			\OCP\Server::get(LoggerInterface::class)->warning(
+				'Could not resolve the target of chunked upload ' . $this->uploadFolder->getName(),
+				['exception' => $e, 'app' => 'dav'],
+			);
+			return true;
+		}
+
 		$storage->cancelChunkedWrite($storagePath, $this->uploadId);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants