diff --git a/src/infrastructure/services/webdav_dead_property_store.rs b/src/infrastructure/services/webdav_dead_property_store.rs index 1e123d0c..50e1347f 100644 --- a/src/infrastructure/services/webdav_dead_property_store.rs +++ b/src/infrastructure/services/webdav_dead_property_store.rs @@ -3,10 +3,18 @@ //! RFC 4918 ยง4.2 defines "dead properties" as those stored verbatim by the //! server without interpreting their value. Properties are persisted to //! `storage.webdav_dead_properties` and survive server restarts. +//! +//! Queries here use `sqlx::query()` (runtime-bound) rather than the +//! compile-time-checked `sqlx::query!()` macro. The macro would require either +//! a live DB at compile time OR committed `.sqlx/` offline metadata; the rest +//! of this codebase consistently uses the runtime variant (see +//! `user_pg_repository.rs` for the canonical style), so a fresh checkout +//! compiles without any DB connection. Trading the macro's compile-time column +//! check for that bootstrap-friendliness is the project's standing convention. use std::sync::Arc; -use sqlx::PgPool; +use sqlx::{PgPool, Row}; use uuid::Uuid; use crate::application::adapters::webdav_adapter::QualifiedName; @@ -29,7 +37,7 @@ impl DeadPropertyStore { name: QualifiedName, value: Option, ) -> Result<(), DomainError> { - sqlx::query!( + sqlx::query( r#" INSERT INTO storage.webdav_dead_properties (resource_path, user_id, namespace, local_name, value) @@ -37,12 +45,12 @@ impl DeadPropertyStore { ON CONFLICT (resource_path, user_id, namespace, local_name) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP "#, - path, - user_id, - name.namespace, - name.name, - value, ) + .bind(path) + .bind(user_id) + .bind(&name.namespace) + .bind(&name.name) + .bind(&value) .execute(&*self.pool) .await .map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("set: {e}")))?; @@ -56,15 +64,15 @@ impl DeadPropertyStore { user_id: Uuid, name: &QualifiedName, ) -> Result<(), DomainError> { - sqlx::query!( + sqlx::query( "DELETE FROM storage.webdav_dead_properties WHERE resource_path = $1 AND user_id = $2 AND namespace = $3 AND local_name = $4", - path, - user_id, - name.namespace, - name.name, ) + .bind(path) + .bind(user_id) + .bind(&name.namespace) + .bind(&name.name) .execute(&*self.pool) .await .map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("remove: {e}")))?; @@ -77,20 +85,25 @@ impl DeadPropertyStore { path: &str, user_id: Uuid, ) -> Result)>, DomainError> { - let rows = sqlx::query!( + let rows = sqlx::query( "SELECT namespace, local_name, value FROM storage.webdav_dead_properties WHERE resource_path = $1 AND user_id = $2", - path, - user_id, ) + .bind(path) + .bind(user_id) .fetch_all(&*self.pool) .await .map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("get_all: {e}")))?; Ok(rows .into_iter() - .map(|r| (QualifiedName::new(r.namespace, r.local_name), r.value)) + .map(|r| { + let namespace: String = r.get("namespace"); + let local_name: String = r.get("local_name"); + let value: Option = r.get("value"); + (QualifiedName::new(namespace, local_name), value) + }) .collect()) } @@ -102,30 +115,30 @@ impl DeadPropertyStore { user_id: Uuid, name: &QualifiedName, ) -> Result>, DomainError> { - let row = sqlx::query!( + let row = sqlx::query( "SELECT value FROM storage.webdav_dead_properties WHERE resource_path = $1 AND user_id = $2 AND namespace = $3 AND local_name = $4", - path, - user_id, - name.namespace, - name.name, ) + .bind(path) + .bind(user_id) + .bind(&name.namespace) + .bind(&name.name) .fetch_optional(&*self.pool) .await .map_err(|e| DomainError::internal_error("DeadPropertyStore", format!("get: {e}")))?; - Ok(row.map(|r| r.value)) + Ok(row.map(|r| r.get::, _>("value"))) } /// Delete all dead properties for `path` (called on DELETE). pub async fn remove_resource(&self, path: &str, user_id: Uuid) -> Result<(), DomainError> { - sqlx::query!( + sqlx::query( "DELETE FROM storage.webdav_dead_properties WHERE resource_path = $1 AND user_id = $2", - path, - user_id, ) + .bind(path) + .bind(user_id) .execute(&*self.pool) .await .map_err(|e| { @@ -146,26 +159,26 @@ impl DeadPropertyStore { DomainError::internal_error("DeadPropertyStore", format!("rename_resource tx: {e}")) })?; - sqlx::query!( + sqlx::query( "DELETE FROM storage.webdav_dead_properties WHERE resource_path = $1 AND user_id = $2", - new_path, - user_id, ) + .bind(new_path) + .bind(user_id) .execute(&mut *tx) .await .map_err(|e| { DomainError::internal_error("DeadPropertyStore", format!("rename_resource delete: {e}")) })?; - sqlx::query!( + sqlx::query( "UPDATE storage.webdav_dead_properties SET resource_path = $2 WHERE resource_path = $1 AND user_id = $3", - old_path, - new_path, - user_id, ) + .bind(old_path) + .bind(new_path) + .bind(user_id) .execute(&mut *tx) .await .map_err(|e| { diff --git a/src/interfaces/api/handlers/webdav_handler.rs b/src/interfaces/api/handlers/webdav_handler.rs index 23fde19e..0357c39e 100644 --- a/src/interfaces/api/handlers/webdav_handler.rs +++ b/src/interfaces/api/handlers/webdav_handler.rs @@ -487,7 +487,10 @@ async fn handle_propfind( .await; } Ok(ResolvedResource::File(file)) => { - let dead_props = state.webdav_dead_props.get_all(&path, user.id).await + let dead_props = state + .webdav_dead_props + .get_all(&path, user.id) + .await .unwrap_or_default(); let file_href = webdav_href(&client_path); let mut buf = Vec::with_capacity(1024); @@ -541,7 +544,10 @@ async fn handle_propfind( .await { assert_owner(file.owner_id.as_deref(), &user.id.to_string(), &path)?; - let dead_props = state.webdav_dead_props.get_all(&path, user.id).await + let dead_props = state + .webdav_dead_props + .get_all(&path, user.id) + .await .unwrap_or_default(); let file_href = webdav_href(&client_path); let mut buf = Vec::with_capacity(1024); @@ -785,13 +791,18 @@ async fn handle_proppatch( for op in &ops { match op { PropPatchOp::Set(pv) => { - dead_props.set(&path, user.id, pv.name.clone(), pv.value.clone()).await - .map_err(|e| AppError::internal_error(format!("Failed to store dead property: {e}")))?; + dead_props + .set(&path, user.id, pv.name.clone(), pv.value.clone()) + .await + .map_err(|e| { + AppError::internal_error(format!("Failed to store dead property: {e}")) + })?; results.push((&pv.name, true)); } PropPatchOp::Remove(name) => { - dead_props.remove(&path, user.id, name).await - .map_err(|e| AppError::internal_error(format!("Failed to remove dead property: {e}")))?; + dead_props.remove(&path, user.id, name).await.map_err(|e| { + AppError::internal_error(format!("Failed to remove dead property: {e}")) + })?; results.push((name, true)); } } @@ -1112,7 +1123,9 @@ fn enforce_native_lock( if p.is_empty() { return None; } - if let Some(e) = lock_store.get_by_path(p) && e.info.depth.eq_ignore_ascii_case("infinity") { + if let Some(e) = lock_store.get_by_path(p) + && e.info.depth.eq_ignore_ascii_case("infinity") + { return Some(e); } }