From e5c8d89da9a37b81e762a217b287c7bd3518b8a9 Mon Sep 17 00:00:00 2001 From: "M.Schmidt" Date: Mon, 13 Jul 2026 20:00:01 +0200 Subject: [PATCH] fix(webdav): bump storage usage on PUT, not just REST multipart upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update_file_streaming_with_perms (the method behind every WebDAV/ NextCloud/WOPI PUT) never called the storage-usage-delta hook, so drives.used_bytes and the RFC 4331 quota-used-bytes property never reflected content written via WebDAV — only the REST multipart upload path bumped usage. Extract apply_storage_usage_delta() from maybe_update_storage_usage() and wire it into both branches: the overwrite path applies new_size - old_size, the create path applies the full size. Also fixes the two RFC 4331 hurl tests that caught this: nc_webdav_quota_properties.hurl had a Hurl parse error ([BasicAuth] section keys can't mix literal+template, so the {user}~{folder} composite marker is now pre-resolved via [Options] variable: before being referenced as a single template), and both quota-properties tests now retry the post-upload PROPFIND (matching the existing drive_quota.hurl/user_envelope_quota.hurl pattern) since the delta is applied fire-and-forget on a background task. --- .../services/file_upload_service.rs | 25 +++++++++++++++---- tests/api/nc_webdav_quota_properties.hurl | 18 ++++++++++--- tests/api/webdav_quota_properties.hurl | 8 ++++++ 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/application/services/file_upload_service.rs b/src/application/services/file_upload_service.rs index a6ac8209..f3eb48f7 100644 --- a/src/application/services/file_upload_service.rs +++ b/src/application/services/file_upload_service.rs @@ -353,16 +353,24 @@ impl FileUploadService { /// the target drive is `kind='personal'`, so a shared-drive upload /// still doesn't touch any user envelope. fn maybe_update_storage_usage(&self, file: &FileDto, caller_id: Uuid) { + self.apply_storage_usage_delta(file.size as i64, &file.folder_id, caller_id); + } + + /// Same as [`Self::maybe_update_storage_usage`] but takes an explicit + /// `delta` instead of assuming "whole file size" — the overwrite path + /// (`update_file_streaming_with_perms`) needs `new_size - old_size`, + /// not the new size added a second time on top of what the old + /// content already contributed. + fn apply_storage_usage_delta(&self, delta: i64, folder_id: &Option, caller_id: Uuid) { let Some(storage_service) = &self.storage_usage_service else { return; }; - let delta = file.size as i64; + if delta == 0 { + return; + } let owner = Some(caller_id); - let folder = file - .folder_id - .as_deref() - .and_then(|s| Uuid::parse_str(s).ok()); + let folder = folder_id.as_deref().and_then(|s| Uuid::parse_str(s).ok()); // Per-user delta — only when the target drive is `kind='personal'`. // The user envelope (`auth.users.storage_quota_bytes`) caps the SUM @@ -496,6 +504,7 @@ impl FileUploadUseCase for FileUploadService { ) .await?; + let old_size = file.size(); let file_id = file.id().to_string(); let (new_hash, updated_at) = self .file_write @@ -531,6 +540,11 @@ impl FileUploadUseCase for FileUploadService { DomainError::internal_error("FileUpload", format!("rebuild entity: {e}")) })?; let dto = FileDto::from(updated); + self.apply_storage_usage_delta( + blob.size as i64 - old_size as i64, + &dto.folder_id, + caller_id, + ); if let Some(hook) = &self.file_lifecycle_hook { hook.on_file_updated(&file_id, &dto.content_hash, content_type); } @@ -603,6 +617,7 @@ impl FileUploadUseCase for FileUploadService { ) .await?; let dto = FileDto::from(created); + self.maybe_update_storage_usage(&dto, caller_id); if let Some(hook) = &self.file_lifecycle_hook { hook.on_file_created(&dto.id, &dto.content_hash, content_type, is_new_blob); } diff --git a/tests/api/nc_webdav_quota_properties.hurl b/tests/api/nc_webdav_quota_properties.hurl index 928f14c4..99775a07 100644 --- a/tests/api/nc_webdav_quota_properties.hurl +++ b/tests/api/nc_webdav_quota_properties.hurl @@ -120,8 +120,10 @@ ncq_root_folder_id: jsonpath "$.root_folder_id" # ───────────────────────────────────────────────────────────── PROPFIND {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/ Depth: 0 +[Options] +variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}} [BasicAuth] -{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}} +{{ncq_composite_user}}: {{ncq_nc_password}} HTTP 207 [Asserts] @@ -135,8 +137,10 @@ xpath "number(//*[local-name()='quota-available-bytes'])" == 500 # ───────────────────────────────────────────────────────────── PUT {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/quota-probe.txt Content-Type: text/plain +[Options] +variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}} [BasicAuth] -{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}} +{{ncq_composite_user}}: {{ncq_nc_password}} ``` 32-byte-ish payload for nc ``` @@ -144,10 +148,18 @@ Content-Type: text/plain HTTP 201 +# The drive-usage bump is fire-and-forget on a tokio task (see +# `file_upload_service.rs::maybe_update_storage_usage`), so retry +# until `used_bytes` catches up — same shape as `drive_quota.hurl` +# Step 5. PROPFIND {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/ Depth: 0 +[Options] +variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}} +retry: 10 +retry-interval: 200ms [BasicAuth] -{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}} +{{ncq_composite_user}}: {{ncq_nc_password}} HTTP 207 [Asserts] diff --git a/tests/api/webdav_quota_properties.hurl b/tests/api/webdav_quota_properties.hurl index e7d48f93..d092578f 100644 --- a/tests/api/webdav_quota_properties.hurl +++ b/tests/api/webdav_quota_properties.hurl @@ -203,10 +203,18 @@ Content-Type: text/plain HTTP 201 +# The drive-usage bump is fire-and-forget on a tokio task (see +# `file_upload_service.rs::maybe_update_storage_usage`), so the SQL +# UPDATE may not have landed yet when the PUT above returned. Retry +# the PROPFIND until `used_bytes` catches up — same shape as +# `drive_quota.hurl` Step 5. PROPFIND {{base_url}}/webdav/@drive/{{wq_drive_id}}/ Authorization: Bearer {{wq_owner_token}} Depth: 0 Content-Type: application/xml; charset=utf-8 +[Options] +retry: 10 +retry-interval: 200ms ```