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:
@@ -1,9 +1,12 @@
|
||||
use sqlx::PgPool;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::ports::blob_storage_ports::BlobStorageBackend;
|
||||
use crate::application::ports::storage_ports::StorageUsagePort;
|
||||
use crate::common::config::StorageBackendType;
|
||||
use crate::domain::repositories::drive_repository::DriveRepository;
|
||||
use crate::infrastructure::db::DbPools;
|
||||
|
||||
use crate::application::services::admin_settings_service::AdminSettingsService;
|
||||
@@ -2134,6 +2137,48 @@ pub struct AppState {
|
||||
|
||||
// All AppState construction is done via struct literal in build_app_state().
|
||||
|
||||
impl AppState {
|
||||
/// Drive-aware RFC 4331 quota resolution — shared by the native and
|
||||
/// NextCloud-compatible WebDAV PROPFIND handlers so both surfaces
|
||||
/// report the same numbers for the same drive.
|
||||
///
|
||||
/// - `drive_id == Uuid::nil()`: synthetic drive-listing pseudo-root —
|
||||
/// no single drive, so the account envelope is the only defensible
|
||||
/// answer.
|
||||
/// - Personal drives carry no quota of their own (`Drive::quota_bytes`
|
||||
/// is NULL post-migration) — the account envelope in `auth.users`
|
||||
/// caps them.
|
||||
/// - Shared drives carry their own finite quota on `storage.drives` —
|
||||
/// report that, not the owner's unrelated personal envelope.
|
||||
///
|
||||
/// `available` is `None` for unlimited accounts/drives (quota <= 0 or
|
||||
/// unset) — RFC 4331 §3 lets a server omit `quota-available-bytes`
|
||||
/// rather than disclose a made-up value. Any lookup failure (quota
|
||||
/// subsystem disabled, drive gone) is treated the same way: quota is
|
||||
/// silently omitted rather than failing the whole PROPFIND.
|
||||
pub async fn resolve_webdav_quota(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
drive_id: Uuid,
|
||||
) -> Option<(i64, Option<i64>)> {
|
||||
let storage_svc = self.storage_usage_service.as_ref()?;
|
||||
|
||||
if drive_id.is_nil() {
|
||||
let (used, quota) = storage_svc.get_user_storage_info(user_id).await.ok()?;
|
||||
return Some((used, (quota > 0).then(|| (quota - used).max(0))));
|
||||
}
|
||||
|
||||
let drive = self.drive_repo.get_by_id(drive_id).await.ok()?.drive;
|
||||
if drive.is_personal() {
|
||||
let (used, quota) = storage_svc.get_user_storage_info(user_id).await.ok()?;
|
||||
Some((used, (quota > 0).then(|| (quota - used).max(0))))
|
||||
} else {
|
||||
let used = drive.used_bytes;
|
||||
Some((used, drive.quota_bytes.map(|q| (q - used).max(0))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the authorization engine. Today this only constructs `PgAclEngine`;
|
||||
/// the `OXICLOUD_AUTHZ_ENGINE` env var is reserved for future alternate
|
||||
/// implementations (e.g. `openfga`).
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -211,6 +211,10 @@ async fn handle_filter_files(
|
||||
oc_id.as_deref(),
|
||||
&user.username,
|
||||
&favorite_ids,
|
||||
// REPORT results are a flat filter/search listing, not a
|
||||
// PROPFIND on a specific collection — quota isn't
|
||||
// meaningful here (see `AppState::resolve_webdav_quota`).
|
||||
None,
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
}
|
||||
@@ -342,6 +346,10 @@ async fn handle_search(
|
||||
oc_id.as_deref(),
|
||||
&user.username,
|
||||
&favorite_ids,
|
||||
// REPORT results are a flat filter/search listing, not a
|
||||
// PROPFIND on a specific collection — quota isn't
|
||||
// meaningful here (see `AppState::resolve_webdav_quota`).
|
||||
None,
|
||||
)
|
||||
.map_err(|e| AppError::internal_error(format!("XML write error: {}", e)))?;
|
||||
}
|
||||
|
||||
@@ -336,6 +336,7 @@ async fn handle_propfind(
|
||||
// function's username arg. Refining the owner-id usages
|
||||
// back to the canonical username is deferred to the
|
||||
// NcSession commit.
|
||||
let quota = state.resolve_webdav_quota(user.id, chroot.drive_id).await;
|
||||
Ok(build_nc_streaming_propfind(
|
||||
state.clone(),
|
||||
folder,
|
||||
@@ -343,6 +344,7 @@ async fn handle_propfind(
|
||||
user.id,
|
||||
url_user.to_string(),
|
||||
subpath.to_string(),
|
||||
quota,
|
||||
))
|
||||
}
|
||||
ResolvedResource::File(file) => {
|
||||
@@ -1495,6 +1497,7 @@ fn build_nc_streaming_propfind(
|
||||
user_id: Uuid,
|
||||
username: String,
|
||||
subpath: String,
|
||||
quota: Option<(i64, Option<i64>)>,
|
||||
) -> Response<Body> {
|
||||
let stream = async_stream::try_stream! {
|
||||
let file_id_svc = state.nextcloud.as_ref().map(|n| &n.file_ids);
|
||||
@@ -1522,7 +1525,7 @@ fn build_nc_streaming_propfind(
|
||||
let href = nc_collection_href(&username, &subpath);
|
||||
let fid = folder_id_map.get(&folder.id).copied();
|
||||
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
|
||||
write_folder_response(&mut xml, &folder, &href, fid, oc_id.as_deref(), &username, &folder_favs)
|
||||
write_folder_response(&mut xml, &folder, &href, fid, oc_id.as_deref(), &username, &folder_favs, quota)
|
||||
.map_err(std::io::Error::other)?;
|
||||
}
|
||||
yield Bytes::from(buf);
|
||||
@@ -1613,7 +1616,7 @@ fn build_nc_streaming_propfind(
|
||||
let href = nc_collection_href(&username, &child_sub);
|
||||
let fid = sub_id_map.get(&sf.id).copied();
|
||||
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
|
||||
write_folder_response(&mut xml, sf, &href, fid, oc_id.as_deref(), &username, &favs)
|
||||
write_folder_response(&mut xml, sf, &href, fid, oc_id.as_deref(), &username, &favs, quota)
|
||||
.map_err(std::io::Error::other)?;
|
||||
}
|
||||
}
|
||||
@@ -1648,6 +1651,7 @@ fn build_nc_streaming_propfind(
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn write_folder_response<W: std::io::Write>(
|
||||
xml: &mut Writer<W>,
|
||||
folder: &FolderDto,
|
||||
@@ -1656,6 +1660,7 @@ pub fn write_folder_response<W: std::io::Write>(
|
||||
oc_id: Option<&str>,
|
||||
owner: &str,
|
||||
favorite_ids: &HashSet<String>,
|
||||
quota: Option<(i64, Option<i64>)>,
|
||||
) -> Result<(), String> {
|
||||
xml.write_event(Event::Start(BytesStart::new("d:response")))
|
||||
.xml_err()?;
|
||||
@@ -1705,6 +1710,16 @@ pub fn write_folder_response<W: std::io::Write>(
|
||||
// Numeric share-permissions bitmask: Read=1 + Update=2 + Create=4 + Delete=8 + Share=16 = 31
|
||||
write_text_element(xml, "ocs:share-permissions", "31")?;
|
||||
write_text_element(xml, "oc:size", "0")?;
|
||||
// RFC 4331 — same account/drive-wide value regardless of which
|
||||
// folder entry is being described, mirroring the native WebDAV
|
||||
// surface's `write_folder_standard_props` (see
|
||||
// `AppState::resolve_webdav_quota`).
|
||||
if let Some((used, available)) = quota {
|
||||
write_text_element(xml, "d:quota-used-bytes", &used.to_string())?;
|
||||
if let Some(avail) = available {
|
||||
write_text_element(xml, "d:quota-available-bytes", &avail.to_string())?;
|
||||
}
|
||||
}
|
||||
write_text_element(xml, "oc:owner-id", owner)?;
|
||||
write_text_element(xml, "oc:owner-display-name", owner)?;
|
||||
write_text_element(xml, "nc:has-preview", "false")?;
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# =============================================================
|
||||
# OxiCloud — NC WebDAV quota properties (RFC 4331)
|
||||
# =============================================================
|
||||
# The NextCloud-compatible WebDAV surface
|
||||
# (`interfaces/nextcloud/webdav_handler.rs`) previously had NO
|
||||
# `d:quota-used-bytes`/`d:quota-available-bytes` support at all. This
|
||||
# pins the new coverage added alongside the native surface's
|
||||
# drive-awareness fix (`AppState::resolve_webdav_quota`):
|
||||
#
|
||||
# 1. Personal-drive PROPFIND (no drive marker in the Basic Auth
|
||||
# username) reports the caller's account envelope.
|
||||
# 2. A SHARED drive with its own finite quota reports THAT quota —
|
||||
# not the owner's personal envelope — when addressed via the
|
||||
# multi-drive POC's `{user}~{root_folder_id}` Basic Auth marker
|
||||
# (see `basic_auth_middleware.rs` — the marker is the drive's
|
||||
# ROOT FOLDER id, not the drive's own id).
|
||||
# 3. quota-used-bytes on the shared drive increases after a PUT.
|
||||
#
|
||||
# This file always emits the full property set regardless of the
|
||||
# PROPFIND request body (see `handle_propfind`'s doc comment — "the
|
||||
# NC response always emits the full property set"), so no XML body
|
||||
# is needed to request the props; a bare PROPFIND suffices.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 1 — Admin login + mint admin's own NC app password (for
|
||||
# the personal-envelope case).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "{{username}}", "password": "{{password}}" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
admin_token: jsonpath "$.access_token"
|
||||
|
||||
POST {{base_url}}/api/auth/app-passwords
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "label": "nc_webdav_quota_properties personal" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
admin_nc_username: jsonpath "$.username"
|
||||
admin_nc_password: jsonpath "$.password"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 2 — Personal-drive PROPFIND (no `~` marker) surfaces the
|
||||
# account envelope. `>= 0` / `> 0` rather than exact
|
||||
# numbers since admin's envelope already has content
|
||||
# from earlier tests in the suite.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
|
||||
Depth: 0
|
||||
[BasicAuth]
|
||||
{{admin_nc_username}}: {{admin_nc_password}}
|
||||
|
||||
HTTP 207
|
||||
[Asserts]
|
||||
xpath "number(//*[local-name()='quota-used-bytes'])" >= 0
|
||||
xpath "number(//*[local-name()='quota-available-bytes'])" > 0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3 — Fresh user + a 500-byte shared drive owned by them.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/admin/users
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"username": "ncq_owner",
|
||||
"password": "NcqOwnerPwd1!",
|
||||
"email": "ncq_owner@example.com",
|
||||
"role": "user"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "ncq_owner", "password": "NcqOwnerPwd1!" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
ncq_owner_jwt: jsonpath "$.access_token"
|
||||
ncq_owner_id: jsonpath "$.user.id"
|
||||
|
||||
POST {{base_url}}/api/auth/app-passwords
|
||||
Authorization: Bearer {{ncq_owner_jwt}}
|
||||
Content-Type: application/json
|
||||
{ "label": "nc_webdav_quota_properties shared" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
ncq_nc_username: jsonpath "$.username"
|
||||
ncq_nc_password: jsonpath "$.password"
|
||||
|
||||
|
||||
POST {{base_url}}/api/drives
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"kind": "shared",
|
||||
"name": "ncq-shared",
|
||||
"owner": { "type": "user", "id": "{{ncq_owner_id}}" },
|
||||
"quota_bytes": 500
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
ncq_root_folder_id: jsonpath "$.root_folder_id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 4 — PROPFIND the shared drive via the multi-drive POC's
|
||||
# `{user}~{root_folder_id}` Basic Auth marker. Brand-new
|
||||
# drive → used == 0, available == quota exactly.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/
|
||||
Depth: 0
|
||||
[BasicAuth]
|
||||
{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}}
|
||||
|
||||
HTTP 207
|
||||
[Asserts]
|
||||
xpath "number(//*[local-name()='quota-used-bytes'])" == 0
|
||||
xpath "number(//*[local-name()='quota-available-bytes'])" == 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 5 — PUT a file into the shared drive; quota-used-bytes
|
||||
# must reflect it, quota-available-bytes must shrink.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/quota-probe.txt
|
||||
Content-Type: text/plain
|
||||
[BasicAuth]
|
||||
{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}}
|
||||
```
|
||||
32-byte-ish payload for nc
|
||||
```
|
||||
|
||||
HTTP 201
|
||||
|
||||
|
||||
PROPFIND {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/
|
||||
Depth: 0
|
||||
[BasicAuth]
|
||||
{{ncq_nc_username}}~{{ncq_root_folder_id}}: {{ncq_nc_password}}
|
||||
|
||||
HTTP 207
|
||||
[Asserts]
|
||||
xpath "number(//*[local-name()='quota-used-bytes'])" > 0
|
||||
xpath "number(//*[local-name()='quota-available-bytes'])" < 500
|
||||
|
||||
|
||||
# No further cleanup needed — `tests/api/storage_cleanup_check.sh`
|
||||
# drains/deletes every non-admin-default drive at suite end.
|
||||
@@ -187,6 +187,8 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
|
||||
"$API_DIR/cross_drive_move.hurl" \
|
||||
"$API_DIR/cross_drive_copy.hurl" \
|
||||
"$API_DIR/webdav_dead_properties.hurl" \
|
||||
"$API_DIR/webdav_quota_properties.hurl" \
|
||||
"$API_DIR/nc_webdav_quota_properties.hurl" \
|
||||
"$API_DIR/webdav_drive_root.hurl" \
|
||||
"$API_DIR/webdav_permissions.hurl" \
|
||||
"$API_DIR/webdav_nested_move_cascade.hurl" \
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# =============================================================
|
||||
# OxiCloud — WebDAV quota properties (RFC 4331)
|
||||
# =============================================================
|
||||
# `DAV:quota-available-bytes` / `DAV:quota-used-bytes` are account-wide
|
||||
# (not per-folder) live properties resolved once per PROPFIND request
|
||||
# from the storage-usage service — see
|
||||
# webdav_handler.rs::resolve_quota / webdav_adapter.rs::write_quota_props.
|
||||
# Unlimited accounts (quota <= 0) omit quota-available-bytes entirely per
|
||||
# RFC 4331 §3, rather than reporting a sentinel value.
|
||||
# `DAV:quota-available-bytes` / `DAV:quota-used-bytes` are resolved once
|
||||
# per PROPFIND request via `AppState::resolve_webdav_quota` — see
|
||||
# webdav_handler.rs / webdav_adapter.rs::write_quota_props.
|
||||
# Unlimited accounts/drives (quota <= 0 or unset) omit
|
||||
# quota-available-bytes entirely per RFC 4331 §3, rather than reporting
|
||||
# a sentinel value.
|
||||
#
|
||||
# Coverage:
|
||||
# 1. Named-prop PROPFIND for both properties on the WebDAV root → 207,
|
||||
@@ -14,6 +14,10 @@
|
||||
# 2. allprop PROPFIND also includes both properties.
|
||||
# 3. quota-used-bytes increases by (at least) the size of a file
|
||||
# just uploaded through WebDAV.
|
||||
# 4. A SHARED drive with its own finite quota reports THAT quota on
|
||||
# `/webdav/@drive/<id>/`, distinct from the caller's personal
|
||||
# envelope — the drive-awareness fix (previously `resolve_quota`
|
||||
# ignored `drive_id` entirely and always reported the envelope).
|
||||
# =============================================================
|
||||
|
||||
|
||||
@@ -113,3 +117,111 @@ DELETE {{base_url}}/webdav/quota-probe.txt
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 204
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 5 — Shared drive with its own finite quota reports THAT
|
||||
# quota, not the owner's personal envelope.
|
||||
#
|
||||
# Provision a fresh user + a 500-byte shared drive owned
|
||||
# by them, then PROPFIND `/webdav/@drive/<id>/` (see
|
||||
# `webdav_drive_root.hurl` for the URL-scheme contract).
|
||||
# A brand-new drive has `used_bytes == 0`, so
|
||||
# quota-available-bytes must equal the quota exactly —
|
||||
# a value that cannot coincide with the personal envelope
|
||||
# asserted above (that account already had files on it
|
||||
# from earlier steps).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/admin/users
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"username": "wq_owner",
|
||||
"password": "WqOwnerPwd1!",
|
||||
"email": "wq_owner@example.com",
|
||||
"role": "user"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "wq_owner", "password": "WqOwnerPwd1!" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
wq_owner_token: jsonpath "$.access_token"
|
||||
wq_owner_id: jsonpath "$.user.id"
|
||||
|
||||
|
||||
POST {{base_url}}/api/drives
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"kind": "shared",
|
||||
"name": "wq-shared",
|
||||
"owner": { "type": "user", "id": "{{wq_owner_id}}" },
|
||||
"quota_bytes": 500
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
wq_drive_id: jsonpath "$.id"
|
||||
|
||||
|
||||
PROPFIND {{base_url}}/webdav/@drive/{{wq_drive_id}}/
|
||||
Authorization: Bearer {{wq_owner_token}}
|
||||
Depth: 0
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:quota-available-bytes/>
|
||||
<D:quota-used-bytes/>
|
||||
</D:prop>
|
||||
</D:propfind>
|
||||
```
|
||||
|
||||
HTTP 207
|
||||
[Asserts]
|
||||
xpath "number(//*[local-name()='quota-used-bytes'])" == 0
|
||||
xpath "number(//*[local-name()='quota-available-bytes'])" == 500
|
||||
|
||||
|
||||
# Upload a 32-byte file into the shared drive; quota-used-bytes must
|
||||
# reflect it and quota-available-bytes must shrink accordingly —
|
||||
# confirms the shared-drive branch reads `storage.drives` live, not
|
||||
# a cached/stale value.
|
||||
PUT {{base_url}}/webdav/@drive/{{wq_drive_id}}/quota-probe.txt
|
||||
Authorization: Bearer {{wq_owner_token}}
|
||||
Content-Type: text/plain
|
||||
```
|
||||
32-byte-ish payload for drv
|
||||
```
|
||||
|
||||
HTTP 201
|
||||
|
||||
|
||||
PROPFIND {{base_url}}/webdav/@drive/{{wq_drive_id}}/
|
||||
Authorization: Bearer {{wq_owner_token}}
|
||||
Depth: 0
|
||||
Content-Type: application/xml; charset=utf-8
|
||||
```
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:quota-available-bytes/>
|
||||
<D:quota-used-bytes/>
|
||||
</D:prop>
|
||||
</D:propfind>
|
||||
```
|
||||
|
||||
HTTP 207
|
||||
[Asserts]
|
||||
xpath "number(//*[local-name()='quota-used-bytes'])" > 0
|
||||
xpath "number(//*[local-name()='quota-available-bytes'])" < 500
|
||||
|
||||
|
||||
# No further cleanup needed — `tests/api/storage_cleanup_check.sh`
|
||||
# drains/deletes every non-admin-default drive at suite end.
|
||||
|
||||
Reference in New Issue
Block a user