perf: Arc<AppState>, streaming PROPFIND, spawn_blocking SHA-256

- Issue #4: Wrap AppState in Arc — eliminates 42 Arc::clone + 16 String::clone per request
- Issue #2: Reject Depth:infinity with 403 + streaming XML with paginated DB queries
- Issue #5: Move chunked upload assembly (SHA-256 hash-on-write) to spawn_blocking
- Remove ~270 lines dead code from di.rs (unused builders, Default impl, stubs)
- Clean up unused tokio imports in chunked_upload_service.rs
This commit is contained in:
Dionisio
2026-02-24 15:11:56 +01:00
parent cace61127f
commit 71c2cb5edb
20 changed files with 511 additions and 625 deletions
+42 -75
View File
@@ -213,81 +213,6 @@ impl WebDavAdapter {
Ok(PropFindRequest { prop_find_type })
}
/// Generate a PROPFIND response for files and folders
pub fn generate_propfind_response<W: Write>(
writer: W,
folder: Option<&FolderDto>,
files: &[FileDto],
subfolders: &[FolderDto],
request: &PropFindRequest,
_depth: &str,
base_href: &str,
) -> Result<()> {
let mut xml_writer = Writer::new(writer);
// Start multistatus response
xml_writer.write_event(Event::Start(
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
))?;
// Add response for current folder if provided
if let Some(folder) = folder {
Self::write_folder_response(&mut xml_writer, folder, request, base_href)?;
}
// If depth allows, add responses for files and subfolders
if _depth != "0" {
// Add responses for files
for file in files {
Self::write_file_response(
&mut xml_writer,
file,
request,
&format!("{}{}", base_href, file.name),
)?;
}
// Add responses for subfolders
for subfolder in subfolders {
Self::write_folder_response(
&mut xml_writer,
subfolder,
request,
&format!("{}{}/", base_href, subfolder.name),
)?;
}
}
// End multistatus
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
Ok(())
}
/// Generate a PROPFIND response for a single file
pub fn generate_propfind_response_for_file<W: Write>(
writer: W,
file: &FileDto,
request: &PropFindRequest,
_depth: &str,
href: &str,
) -> Result<()> {
let mut xml_writer = Writer::new(writer);
// Start multistatus response
xml_writer.write_event(Event::Start(
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
))?;
// Add response for file
Self::write_file_response(&mut xml_writer, file, request, href)?;
// End multistatus
xml_writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
Ok(())
}
/// Write folder properties as a response
fn write_folder_response<W: Write>(
xml_writer: &mut Writer<W>,
@@ -1102,4 +1027,46 @@ impl WebDavAdapter {
}
name.to_string()
}
// ─────────────────────────────────────────────────────────────
// Streaming PROPFIND helpers
//
// These methods write incremental XML fragments so the caller
// can flush chunks to the HTTP body without buffering the whole
// response in memory.
// ─────────────────────────────────────────────────────────────
/// Writes the opening `<D:multistatus>` tag.
pub fn write_multistatus_start<W: Write>(writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::Start(
BytesStart::new("D:multistatus").with_attributes([("xmlns:D", "DAV:")]),
))?;
Ok(())
}
/// Writes the closing `</D:multistatus>` tag.
pub fn write_multistatus_end<W: Write>(writer: &mut Writer<W>) -> Result<()> {
writer.write_event(Event::End(BytesEnd::new("D:multistatus")))?;
Ok(())
}
/// Writes a single `<D:response>` element for a folder.
pub fn write_folder_entry<W: Write>(
writer: &mut Writer<W>,
folder: &FolderDto,
request: &PropFindRequest,
href: &str,
) -> Result<()> {
Self::write_folder_response(writer, folder, request, href)
}
/// Writes a single `<D:response>` element for a file.
pub fn write_file_entry<W: Write>(
writer: &mut Writer<W>,
file: &FileDto,
request: &PropFindRequest,
href: &str,
) -> Result<()> {
Self::write_file_response(writer, file, request, href)
}
}
+14
View File
@@ -170,6 +170,20 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
) -> Result<Vec<FileDto>, DomainError> {
self.list_files(Some(folder_id)).await
}
/// Lists files in a folder with LIMIT/OFFSET pagination.
///
/// Used by streaming WebDAV PROPFIND to avoid loading all files at once.
/// Default: falls back to `list_files` (loads all, then slices in memory).
async fn list_files_batch(
&self,
folder_id: Option<&str>,
offset: i64,
limit: i64,
) -> Result<Vec<FileDto>, DomainError> {
let all = self.list_files(folder_id).await?;
Ok(all.into_iter().skip(offset as usize).take(limit as usize).collect())
}
}
// ─────────────────────────────────────────────────────
+16
View File
@@ -78,6 +78,22 @@ pub trait FileReadPort: Send + Sync + 'static {
Ok(None)
}
/// Lists files in a folder with LIMIT/OFFSET pagination.
///
/// Used by streaming WebDAV PROPFIND to avoid loading all files at once.
/// Default: falls back to `list_files` (loads all, then slices in memory).
async fn list_files_batch(
&self,
folder_id: Option<&str>,
offset: i64,
limit: i64,
) -> Result<Vec<File>, DomainError> {
let all = self.list_files(folder_id).await?;
let start = (offset as usize).min(all.len());
let end = (start + limit as usize).min(all.len());
Ok(all.into_iter().skip(start).take(end - start).collect())
}
/// Lists every file in the subtree rooted at `folder_id`.
///
/// Uses an ltree `<@` join against `storage.folders` so the entire
@@ -317,4 +317,14 @@ impl FileRetrievalUseCase for FileRetrievalService {
let files = self.file_read.list_files_in_subtree(folder_id).await?;
Ok(files.into_iter().map(FileDto::from).collect())
}
async fn list_files_batch(
&self,
folder_id: Option<&str>,
offset: i64,
limit: i64,
) -> Result<Vec<FileDto>, DomainError> {
let files = self.file_read.list_files_batch(folder_id, offset, limit).await?;
Ok(files.into_iter().map(FileDto::from).collect())
}
}