feat(webdav): add RFC 4331 quota-available-bytes/quota-used-bytes properties
Threads the caller's account-wide (used, available) storage figures through PROPFIND for the plain-file WebDAV surface, resolved once per request via StorageUsagePort::get_user_storage_info and reused for every folder entry in the response. Unlimited accounts (quota <= 0) omit quota-available-bytes entirely per RFC 4331 §3, rather than disclosing a sentinel value. Properties are only advertised as known when the quota subsystem is enabled and the lookup succeeds; otherwise they fall through to the standard 404 propstat.
This commit is contained in:
@@ -346,6 +346,33 @@ 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() {
|
||||
@@ -557,6 +584,7 @@ async fn handle_propfind(
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
};
|
||||
let quota = resolve_quota(&state, user.id).await;
|
||||
return build_streaming_propfind_response(
|
||||
root_folder,
|
||||
None, // folder_id = None → root children (drive-root folders)
|
||||
@@ -567,6 +595,7 @@ async fn handle_propfind(
|
||||
file_retrieval_service,
|
||||
user.id,
|
||||
state.webdav_dead_props.clone(),
|
||||
quota,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -595,6 +624,7 @@ async fn handle_propfind(
|
||||
)
|
||||
.await?;
|
||||
let folder_id = folder.id.clone();
|
||||
let quota = resolve_quota(&state, user.id).await;
|
||||
return build_streaming_propfind_response(
|
||||
folder,
|
||||
Some(folder_id),
|
||||
@@ -605,6 +635,7 @@ async fn handle_propfind(
|
||||
file_retrieval_service,
|
||||
user.id,
|
||||
state.webdav_dead_props.clone(),
|
||||
quota,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -659,6 +690,7 @@ async fn handle_propfind(
|
||||
)
|
||||
.await?;
|
||||
let folder_id = folder.id.clone();
|
||||
let quota = resolve_quota(&state, user.id).await;
|
||||
return build_streaming_propfind_response(
|
||||
folder,
|
||||
Some(folder_id),
|
||||
@@ -669,6 +701,7 @@ async fn handle_propfind(
|
||||
file_retrieval_service,
|
||||
user.id,
|
||||
state.webdav_dead_props.clone(),
|
||||
quota,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -732,6 +765,7 @@ async fn build_streaming_propfind_response(
|
||||
file_retrieval_service: std::sync::Arc<FileRetrievalService>,
|
||||
user_id: Uuid,
|
||||
dead_props_store: Arc<DeadPropertyStore>,
|
||||
quota: Option<(i64, Option<i64>)>,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
let depth = depth.to_string();
|
||||
let base_href = base_href.to_string();
|
||||
@@ -752,7 +786,7 @@ async fn build_streaming_propfind_response(
|
||||
let mut w = Writer::new(&mut buf);
|
||||
WebDavAdapter::write_multistatus_start(&mut w)
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
WebDavAdapter::write_folder_entry_with_dead_props(&mut w, &folder, &propfind_request, &base_href, &folder_dead)
|
||||
WebDavAdapter::write_folder_entry_with_dead_props(&mut w, &folder, &propfind_request, &base_href, &folder_dead, quota)
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
}
|
||||
yield Bytes::from(buf);
|
||||
@@ -795,7 +829,7 @@ async fn build_streaming_propfind_response(
|
||||
let mut w = Writer::new(&mut chunk);
|
||||
for (subfolder, child_dead) in result.items.iter().zip(subfolder_deads.iter()) {
|
||||
let href = format!("{}{}/", base_href, encode_path_segment(&subfolder.name));
|
||||
WebDavAdapter::write_folder_entry_with_dead_props(&mut w, subfolder, &propfind_request, &href, child_dead)
|
||||
WebDavAdapter::write_folder_entry_with_dead_props(&mut w, subfolder, &propfind_request, &href, child_dead, quota)
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user