feat(webdav): drive-aware RFC 4331 quota properties

resolve_quota only ever reported the caller's personal envelope,
ignoring the drive_id already resolved at every PROPFIND call site —
shared drives with their own quota showed the wrong numbers. Adds
AppState::resolve_webdav_quota, shared by both WebDAV surfaces:
nil drive_id or personal drive -> account envelope, shared drive ->
its own storage.drives quota/used_bytes.

Also adds quota-used-bytes/quota-available-bytes to the NextCloud-
compatible surface, which previously had no RFC 4331 support at all.

Registers webdav_quota_properties.hurl and the new
nc_webdav_quota_properties.hurl in tests/api/run.sh — neither was
wired into the suite before this change.
This commit is contained in:
M.Schmidt
2026-07-13 00:34:17 +02:00
parent fdef73380f
commit c07aeabd85
7 changed files with 352 additions and 38 deletions
+3 -30
View File
@@ -346,33 +346,6 @@ async fn resolve_webdav_scope_or_405(
}
}
/// Resolve `(used_bytes, available_bytes)` for RFC 4331 quota properties.
///
/// `None` when the quota subsystem is disabled (`storage_usage_service` is
/// only wired up behind its feature flag) or the lookup fails — callers
/// treat that as "quota properties aren't known", not an error, since
/// PROPFIND must still succeed for the rest of the response. Quota is
/// account-wide, not per-folder, so this is resolved once per PROPFIND
/// request and reused for every folder entry in the response.
///
/// `available_bytes` is itself `None` for unlimited accounts (quota <= 0):
/// RFC 4331 §3 says a server MAY omit `quota-available-bytes` when there's
/// no enforced/finite quota rather than disclose a made-up value, so
/// callers drop the property (404 propstat) instead of reporting a
/// sentinel like `i64::MAX`. `quota-used-bytes` is unaffected — it's a real
/// measured value regardless of whether a limit exists.
async fn resolve_quota(state: &Arc<AppState>, user_id: Uuid) -> Option<(i64, Option<i64>)> {
let storage_svc = state.storage_usage_service.as_ref()?;
let (used, quota_bytes) = storage_svc.get_user_storage_info(user_id).await.ok()?;
// Quota <= 0 means unlimited (see `StorageUsageService::check_storage_quota`).
let available = if quota_bytes <= 0 {
None
} else {
Some((quota_bytes - used).max(0))
};
Some((used, available))
}
fn join_drive_path(root_name: &str, subpath: &str) -> String {
let subpath = subpath.trim_start_matches('/').trim_end_matches('/');
if subpath.is_empty() {
@@ -584,7 +557,7 @@ async fn handle_propfind(
created_by: None,
updated_by: None,
};
let quota = resolve_quota(&state, user.id).await;
let quota = state.resolve_webdav_quota(user.id, Uuid::nil()).await;
return build_streaming_propfind_response(
root_folder,
None, // folder_id = None → root children (drive-root folders)
@@ -624,7 +597,7 @@ async fn handle_propfind(
)
.await?;
let folder_id = folder.id.clone();
let quota = resolve_quota(&state, user.id).await;
let quota = state.resolve_webdav_quota(user.id, drive_id).await;
return build_streaming_propfind_response(
folder,
Some(folder_id),
@@ -690,7 +663,7 @@ async fn handle_propfind(
)
.await?;
let folder_id = folder.id.clone();
let quota = resolve_quota(&state, user.id).await;
let quota = state.resolve_webdav_quota(user.id, drive_id).await;
return build_streaming_propfind_response(
folder,
Some(folder_id),