fix(webdav): bump storage usage on PUT, not just REST multipart upload
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.
This commit is contained in:
@@ -353,16 +353,24 @@ impl FileUploadService {
|
|||||||
/// the target drive is `kind='personal'`, so a shared-drive upload
|
/// the target drive is `kind='personal'`, so a shared-drive upload
|
||||||
/// still doesn't touch any user envelope.
|
/// still doesn't touch any user envelope.
|
||||||
fn maybe_update_storage_usage(&self, file: &FileDto, caller_id: Uuid) {
|
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<String>, caller_id: Uuid) {
|
||||||
let Some(storage_service) = &self.storage_usage_service else {
|
let Some(storage_service) = &self.storage_usage_service else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let delta = file.size as i64;
|
if delta == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let owner = Some(caller_id);
|
let owner = Some(caller_id);
|
||||||
let folder = file
|
let folder = folder_id.as_deref().and_then(|s| Uuid::parse_str(s).ok());
|
||||||
.folder_id
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|s| Uuid::parse_str(s).ok());
|
|
||||||
|
|
||||||
// Per-user delta — only when the target drive is `kind='personal'`.
|
// Per-user delta — only when the target drive is `kind='personal'`.
|
||||||
// The user envelope (`auth.users.storage_quota_bytes`) caps the SUM
|
// The user envelope (`auth.users.storage_quota_bytes`) caps the SUM
|
||||||
@@ -496,6 +504,7 @@ impl FileUploadUseCase for FileUploadService {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let old_size = file.size();
|
||||||
let file_id = file.id().to_string();
|
let file_id = file.id().to_string();
|
||||||
let (new_hash, updated_at) = self
|
let (new_hash, updated_at) = self
|
||||||
.file_write
|
.file_write
|
||||||
@@ -531,6 +540,11 @@ impl FileUploadUseCase for FileUploadService {
|
|||||||
DomainError::internal_error("FileUpload", format!("rebuild entity: {e}"))
|
DomainError::internal_error("FileUpload", format!("rebuild entity: {e}"))
|
||||||
})?;
|
})?;
|
||||||
let dto = FileDto::from(updated);
|
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 {
|
if let Some(hook) = &self.file_lifecycle_hook {
|
||||||
hook.on_file_updated(&file_id, &dto.content_hash, content_type);
|
hook.on_file_updated(&file_id, &dto.content_hash, content_type);
|
||||||
}
|
}
|
||||||
@@ -603,6 +617,7 @@ impl FileUploadUseCase for FileUploadService {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let dto = FileDto::from(created);
|
let dto = FileDto::from(created);
|
||||||
|
self.maybe_update_storage_usage(&dto, caller_id);
|
||||||
if let Some(hook) = &self.file_lifecycle_hook {
|
if let Some(hook) = &self.file_lifecycle_hook {
|
||||||
hook.on_file_created(&dto.id, &dto.content_hash, content_type, is_new_blob);
|
hook.on_file_created(&dto.id, &dto.content_hash, content_type, is_new_blob);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}}/
|
PROPFIND {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/
|
||||||
Depth: 0
|
Depth: 0
|
||||||
|
[Options]
|
||||||
|
variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}}
|
||||||
[BasicAuth]
|
[BasicAuth]
|
||||||
{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}}
|
{{ncq_composite_user}}: {{ncq_nc_password}}
|
||||||
|
|
||||||
HTTP 207
|
HTTP 207
|
||||||
[Asserts]
|
[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
|
PUT {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/quota-probe.txt
|
||||||
Content-Type: text/plain
|
Content-Type: text/plain
|
||||||
|
[Options]
|
||||||
|
variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}}
|
||||||
[BasicAuth]
|
[BasicAuth]
|
||||||
{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}}
|
{{ncq_composite_user}}: {{ncq_nc_password}}
|
||||||
```
|
```
|
||||||
32-byte-ish payload for nc
|
32-byte-ish payload for nc
|
||||||
```
|
```
|
||||||
@@ -144,10 +148,18 @@ Content-Type: text/plain
|
|||||||
HTTP 201
|
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}}/
|
PROPFIND {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/
|
||||||
Depth: 0
|
Depth: 0
|
||||||
|
[Options]
|
||||||
|
variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}}
|
||||||
|
retry: 10
|
||||||
|
retry-interval: 200ms
|
||||||
[BasicAuth]
|
[BasicAuth]
|
||||||
{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}}
|
{{ncq_composite_user}}: {{ncq_nc_password}}
|
||||||
|
|
||||||
HTTP 207
|
HTTP 207
|
||||||
[Asserts]
|
[Asserts]
|
||||||
|
|||||||
@@ -203,10 +203,18 @@ Content-Type: text/plain
|
|||||||
HTTP 201
|
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}}/
|
PROPFIND {{base_url}}/webdav/@drive/{{wq_drive_id}}/
|
||||||
Authorization: Bearer {{wq_owner_token}}
|
Authorization: Bearer {{wq_owner_token}}
|
||||||
Depth: 0
|
Depth: 0
|
||||||
Content-Type: application/xml; charset=utf-8
|
Content-Type: application/xml; charset=utf-8
|
||||||
|
[Options]
|
||||||
|
retry: 10
|
||||||
|
retry-interval: 200ms
|
||||||
```
|
```
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<D:propfind xmlns:D="DAV:">
|
<D:propfind xmlns:D="DAV:">
|
||||||
|
|||||||
Reference in New Issue
Block a user