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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user