fdf445d2b0
Benchmark-gated round (benches/ROUND9.md): every change carries a BEFORE/AFTER bench with equivalence/safety gates; verdicts below are from the committed harnesses on 4 cores / local PG 16. Backend: - Blob decorators (Retry/Cached) now forward put_blob_from_bytes_unsynced + sync_blobs — the trait default had silently reinstated HEAD-before-PUT per chunk on decorated remote stacks, undoing ROUND3 §8. Full production stack: 500 probes -> 0, 1.9x wall at 10 ms RTT (bench_s3_put §3). - NC PROPFIND per-page enrichment triple (favorites / oc:fileid / dead props) overlapped with tokio::join!: 2.07x local, 2.86x at 5 ms RTT (bench_nc_enrich_join, injected-latency decide-by-bench). - Search enrichment consumes its DTOs and carries the interned Arc<str> display fields end-to-end (SearchFileResultDto type change, OpenAPI shape preserved): enrich_file 2.0x, 11.6 -> 2.2 allocs/row; the NC REPORT conversion stops re-running all three classifiers per row (bench_search_enrich). - NC session Arc end-to-end: SharedNcSession extractor (8 -> 0 allocs), Arc<FolderDto> chroot cache (4 -> 0/hit), single shared Arc<CurrentUser> + lazy span render (11 -> 6/build) (bench_nc_session). - Storage micro-pack: atomic create_new chunk writes (2.1x fresh), stream_chunks over the manifest Arc (4097 -> 0 allocs/read incl. the Range path), manifest single-flight (herd 64 -> 1 loads), hex_lower for chunk Content-MD5 (18 -> 1 allocs) (bench_storage_micro). - OCS capabilities memoized into OnceLock<[Bytes;2]>: 237x, 102 -> 0 allocs/poll, byte-identical (bench_capabilities_static). - Drive::is_empty COUNT(*) sum -> EXISTS: 34.4x on a 100k-file drive (bench_drive_is_empty). - favorites/recents row-map ROUND7 port: path/name/blob_hash moved, -2.75 allocs/row (bench_resource_row_map §2). - Folder rows decode binary UUIDs (ROUND6 §10 port): 1.03-1.07x page fetch, honest verdict incl. one noise-band wash documented (bench_folder_uuid_decode). - Authz: file cascade decision decomposed into memoized folder-level decision + direct-grant lookup (ROUND8 deferred item). Cold shared-album first view 592 -> 418 µs/thumb; warm path unchanged; safety gates incl. new direct-grant sibling isolation, revoke-flush re-verified, full integration authz suite green (bench_thumbnail_cascade_cache). Frontend (vitest gates committed beside the code): - resolveLabel/resolveRecipient O(directory) scan -> id-keyed Map: 13.9x (recipients.bench.test.ts). - ResourceList selection-prune effect skips when nothing is selected (100 -> 0 Set builds per drain) and the photos timeline reads a listener-fed mobile flag instead of matchMedia per recompute (listDerives.bench.test.ts). Verification: cargo fmt + clippy --all-features --all-targets -D warnings clean; 524 unit + 554 integration (--cfg integration_tests) tests pass; frontend npm run check clean with 293 vitest tests green. Deferred with rationale in ROUND9.md: CalDAV authz-before-fetch reorder (maintainer sign-off), per-page batched parent resolution, JWT-claims Arc<str>, batch_operations signature widening. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XDc9VtXvskJ6dnMRraSndn
284 lines
8.9 KiB
Rust
284 lines
8.9 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
use utoipa::ToSchema;
|
|
|
|
/**
|
|
* Data Transfer Object for file search criteria.
|
|
*
|
|
* This structure represents all possible search parameters that can be used
|
|
* to filter files and folders in the system. It supports various filter types
|
|
* including name matching, file types, date ranges, and size constraints.
|
|
*/
|
|
#[derive(Debug, Clone, Hash, Serialize, Deserialize, ToSchema)]
|
|
pub struct SearchCriteriaDto {
|
|
/// Optional text to search in file/folder names
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub name_contains: Option<String>,
|
|
|
|
/// Optional list of file extensions to include (e.g., "pdf", "jpg")
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub file_types: Option<Vec<String>>,
|
|
|
|
/// Optional minimum creation date (seconds since epoch)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub created_after: Option<u64>,
|
|
|
|
/// Optional maximum creation date (seconds since epoch)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub created_before: Option<u64>,
|
|
|
|
/// Optional minimum modification date (seconds since epoch)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub modified_after: Option<u64>,
|
|
|
|
/// Optional maximum modification date (seconds since epoch)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub modified_before: Option<u64>,
|
|
|
|
/// Optional minimum file size in bytes
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub min_size: Option<u64>,
|
|
|
|
/// Optional maximum file size in bytes
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub max_size: Option<u64>,
|
|
|
|
/// Optional folder ID to limit search scope
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub folder_id: Option<String>,
|
|
|
|
/// Whether to search recursively within subfolders (default: true)
|
|
#[serde(default = "default_recursive")]
|
|
pub recursive: bool,
|
|
|
|
/// Maximum number of results to return
|
|
#[serde(default = "default_limit")]
|
|
pub limit: usize,
|
|
|
|
/// Offset for pagination
|
|
#[serde(default)]
|
|
pub offset: usize,
|
|
|
|
/// Sort order for results: "relevance", "name", "name_desc", "date", "date_desc", "size", "size_desc"
|
|
#[serde(default = "default_sort_by")]
|
|
pub sort_by: String,
|
|
}
|
|
|
|
/// Default value for recursive search (true)
|
|
fn default_recursive() -> bool {
|
|
true
|
|
}
|
|
|
|
/// Default limit for search results (100)
|
|
fn default_limit() -> usize {
|
|
100
|
|
}
|
|
|
|
/// Default sort_by value
|
|
fn default_sort_by() -> String {
|
|
"relevance".to_string()
|
|
}
|
|
|
|
impl Default for SearchCriteriaDto {
|
|
fn default() -> Self {
|
|
Self {
|
|
name_contains: None,
|
|
file_types: None,
|
|
created_after: None,
|
|
created_before: None,
|
|
modified_after: None,
|
|
modified_before: None,
|
|
min_size: None,
|
|
max_size: None,
|
|
folder_id: None,
|
|
recursive: default_recursive(),
|
|
limit: default_limit(),
|
|
offset: 0,
|
|
sort_by: default_sort_by(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A file search result enriched with server-computed metadata
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
|
pub struct SearchFileResultDto {
|
|
/// File ID
|
|
pub id: String,
|
|
/// File name
|
|
pub name: String,
|
|
/// Path to the file (relative)
|
|
pub path: String,
|
|
/// Size in bytes
|
|
pub size: u64,
|
|
/// MIME type — `Arc<str>` so enrichment reuses `FileDto`'s interned
|
|
/// value (an atomic increment) instead of allocating per result row.
|
|
#[schema(value_type = String)]
|
|
pub mime_type: Arc<str>,
|
|
/// Parent folder ID
|
|
pub folder_id: Option<String>,
|
|
/// Creation timestamp
|
|
pub created_at: u64,
|
|
/// Last modification timestamp
|
|
pub modified_at: u64,
|
|
/// Relevance score (0-100) computed server-side
|
|
pub relevance_score: u32,
|
|
/// Human-readable file size (e.g., "2.5 MB")
|
|
pub size_formatted: String,
|
|
/// CSS icon class for the file type (e.g., "fas fa-file-pdf")
|
|
#[schema(value_type = String)]
|
|
pub icon_class: Arc<str>,
|
|
/// Extra CSS class for icon styling (e.g., "pdf-icon", "code-icon js-icon")
|
|
#[schema(value_type = String)]
|
|
pub icon_special_class: Arc<str>,
|
|
/// Content category: "document", "image", "video", "audio", "archive", "code", "other"
|
|
#[schema(value_type = String)]
|
|
pub category: Arc<str>,
|
|
/// Raw BLAKE3 content hash. Feeds `FileDto::content_hash` and
|
|
/// `File::compute_etag` when search results are converted to
|
|
/// `FileDto` (NC REPORT/SEARCH response). Defaults to `String::new()`
|
|
/// for backward-compatible deserialisation of cached results
|
|
/// that pre-date the column.
|
|
#[serde(default)]
|
|
pub blob_hash: String,
|
|
/// Plain-text fragment around the first content match, present only for
|
|
/// hits discovered through the full-text content index.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub snippet: Option<String>,
|
|
/// Where the match came from: "name" (filename matched the query) or
|
|
/// "content" (discovered via the full-text content index).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub match_source: Option<String>,
|
|
}
|
|
|
|
/// A folder search result enriched with server-computed metadata
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
|
pub struct SearchFolderResultDto {
|
|
/// Folder ID
|
|
pub id: String,
|
|
/// Folder name
|
|
pub name: String,
|
|
/// Path to the folder (relative)
|
|
pub path: String,
|
|
/// Parent folder ID
|
|
pub parent_id: Option<String>,
|
|
/// Drive that owns this folder. Same column as `storage.folders.drive_id`,
|
|
/// carried through so downstream callers (e.g. the NC search REPORT
|
|
/// handler) can populate `FolderDto::drive_id` without a fallback sentinel.
|
|
pub drive_id: uuid::Uuid,
|
|
/// Creation timestamp
|
|
pub created_at: u64,
|
|
/// Last modification timestamp
|
|
pub modified_at: u64,
|
|
/// Whether it is a root folder
|
|
pub is_root: bool,
|
|
/// Relevance score (0-100) computed server-side
|
|
pub relevance_score: u32,
|
|
}
|
|
|
|
/**
|
|
* Data Transfer Object for search results.
|
|
*
|
|
* This structure encapsulates the results of a search operation, including
|
|
* both files and folders that match the search criteria, along with pagination
|
|
* information and server-computed metadata.
|
|
*/
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
|
pub struct SearchResultsDto {
|
|
/// Files matching the search criteria (enriched with metadata)
|
|
pub files: Vec<SearchFileResultDto>,
|
|
|
|
/// Folders matching the search criteria (enriched with metadata)
|
|
pub folders: Vec<SearchFolderResultDto>,
|
|
|
|
/// Total count of matching items (for pagination)
|
|
pub total_count: Option<usize>,
|
|
|
|
/// Limit used in the search
|
|
pub limit: usize,
|
|
|
|
/// Offset used in the search
|
|
pub offset: usize,
|
|
|
|
/// Whether there are more results available
|
|
pub has_more: bool,
|
|
|
|
/// Query execution time in milliseconds (server-side)
|
|
pub query_time_ms: u64,
|
|
|
|
/// Sort order used
|
|
pub sort_by: String,
|
|
}
|
|
|
|
impl SearchResultsDto {
|
|
/// Creates a new empty search results object
|
|
pub fn empty() -> Self {
|
|
Self {
|
|
files: Vec::new(),
|
|
folders: Vec::new(),
|
|
total_count: None,
|
|
limit: 0,
|
|
offset: 0,
|
|
has_more: false,
|
|
query_time_ms: 0,
|
|
sort_by: "relevance".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Creates a new search results object from files and folders
|
|
pub fn new(
|
|
files: Vec<SearchFileResultDto>,
|
|
folders: Vec<SearchFolderResultDto>,
|
|
limit: usize,
|
|
offset: usize,
|
|
total_count: Option<usize>,
|
|
query_time_ms: u64,
|
|
sort_by: String,
|
|
) -> Self {
|
|
let has_more = match total_count {
|
|
Some(total) => (offset + files.len() + folders.len()) < total,
|
|
None => false,
|
|
};
|
|
|
|
Self {
|
|
files,
|
|
folders,
|
|
total_count,
|
|
limit,
|
|
offset,
|
|
has_more,
|
|
query_time_ms,
|
|
sort_by,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// DTO for search suggestion results (quick prefix search)
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
|
pub struct SearchSuggestionsDto {
|
|
/// Suggested file/folder names matching the query prefix
|
|
pub suggestions: Vec<SearchSuggestionItem>,
|
|
/// Query execution time in milliseconds
|
|
pub query_time_ms: u64,
|
|
}
|
|
|
|
/// Individual search suggestion item
|
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
|
pub struct SearchSuggestionItem {
|
|
/// The suggested name
|
|
pub name: String,
|
|
/// Type: "file" or "folder"
|
|
pub item_type: String,
|
|
/// Item ID for navigation
|
|
pub id: String,
|
|
/// Path for context
|
|
pub path: String,
|
|
/// CSS icon class
|
|
#[schema(value_type = String)]
|
|
pub icon_class: Arc<str>,
|
|
/// Extra CSS class for icon styling
|
|
#[schema(value_type = String)]
|
|
pub icon_special_class: Arc<str>,
|
|
/// Relevance score
|
|
pub relevance_score: u32,
|
|
}
|