Files
Oxicloud/src/infrastructure/services/path_service.rs
T
Claude 221c1f31b0 perf: round 11 — StoragePath joined-only, classifier fusion, memoized bodies, query-shape pack, SPA fine-grained stars
Backend (each change benchmark-gated with BEFORE replicas + equivalence
gates; see examples/bench_round11_micro.rs, bench_round11_queries.rs,
bench_log_writer.rs and benches/ROUND11.md — final numbers land in the
follow-up doc commit):

- StoragePath re-representation: single canonical joined String, segments
  derived on demand; File/Folder drop the duplicated path_string field
  (4000→1000 allocs per 500-row listing page)
- Display classifier fusion: classify_display shares one stack-lowered
  extension across the three decision trees; call sites in FileDto,
  folder/favorites/recent handlers, trash, path-resolver (+ interning
  where Arc::from was still used)
- /status.php and /openapi.json memoized into OnceLock<Bytes> (openapi
  rebuilt a 171 KiB spec per request: 2.8 ms → 18 ns)
- NC upload-session PROPFIND: write! + pre-sized body + stack RFC2822
  dates (2.3-2.6x, 2582→772 allocs at 256 chunks)
- REST download: dead FileDto clone removed (capture mime/size + move)
- CalendarEventDto/TrashedItem into_parts moves (11 KiB ical_data memcpy
  gone per CalDAV row); CardDAV getlastmodified stack render
- 4xx path: borrowed ErrorResponse serialize, ErrorKind::as_str,
  not_found/already_exists clone kill
- vCard emit via write!; search page moved out with into_iter skip/take;
  content-hit UUIDs parsed once; group last-user check via HashSet
- RateLimiter: lock-free get + insert (and_upsert_with variant REJECTED
  by benchmark); CSRF token borrow-compare + borrowed cookie extraction
- Thumbnail/preview ETags built from as_str (Debug-identical bytes)
- Encrypted backend: encrypt_in_place_detached single-buffer write path,
  chunk-sized reserve in collect_stream; retry labels made lazy
- PG: deferred upload registration 3→1 round-trips (persist_file CTE
  template); direct_grant_cache for Calendar/AddressBook/Playlist authz
  (single-flight + set_role/clear_role invalidation); expand_user
  tokio::join!; geo clusters min(uuid)::text; recluster face assignment
  batched into one UNNEST update
- People recluster cosine: norms precomputed once (bit-identical gate)
- NC capabilities poll logs demoted to debug; tracing-appender dep added
  for the log-writer benchmark

Frontend:
- ResourceList.selectedEntries O(N)-per-toggle → id-index projection
  O(k log k); favorites/recent consume the batchToolbar snippet param and
  drop their duplicate filter + dead selectedIds mirror
- Recent: star state via new favoriteIds prop — a star click no longer
  rebuilds all N entries
- admin timeAgo >30d fallback uses the cached Intl.DateTimeFormat
- vitest gates in src/lib/components/round11.bench.test.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABhTEHuGujvwoodh67Kga7
2026-07-18 22:02:00 +00:00

335 lines
12 KiB
Rust

//! PathService - Infrastructure service for storage path management
//!
//! This service was moved from domain/services because it implements application traits
//! (StoragePort) and has file system dependencies (tokio::fs).
//!
//! StoragePath (Value Object) remains in domain/services/path_service.rs
use std::path::{Path, PathBuf};
use tokio::fs;
use crate::application::ports::outbound::StoragePort;
use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::services::path_service::StoragePath;
/// Infrastructure service for handling storage path operations
pub struct PathService {
root_path: PathBuf,
}
impl PathService {
/// Creates a new path service with a specific root
pub fn new(root_path: PathBuf) -> Self {
Self { root_path }
}
/// Converts a domain path to an absolute physical path.
///
/// Returns an error if validation fails (defense-in-depth against traversal).
pub fn resolve_path(&self, storage_path: &StoragePath) -> Result<PathBuf, DomainError> {
self.validate_path(storage_path)?;
let mut path = self.root_path.clone();
for segment in storage_path.segments() {
path.push(segment);
}
// Final safety check: the resolved path must remain under root
if !path.starts_with(&self.root_path) {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Path",
format!("Resolved path escapes storage root: {}", path.display()),
));
}
Ok(path)
}
/// Converts a physical path to a domain path
pub fn to_storage_path(&self, physical_path: &Path) -> Option<StoragePath> {
physical_path
.strip_prefix(&self.root_path)
.ok()
.map(|rel_path| {
let segments: Vec<String> = rel_path
.components()
.filter_map(|c| match c {
std::path::Component::Normal(os_str) => {
Some(os_str.to_string_lossy().to_string())
}
_ => None,
})
.collect();
StoragePath::new(segments)
})
}
/// Creates a file path within a folder
pub fn create_file_path(&self, folder_path: &StoragePath, file_name: &str) -> StoragePath {
// `join` consumes its receiver to reuse the buffer; we only hold a
// borrow here, so clone first — the same copy the old `&self` join did.
folder_path.clone().join(file_name)
}
/// Checks if a path is a direct child of another
pub fn is_direct_child(
&self,
parent_path: &StoragePath,
potential_child: &StoragePath,
) -> bool {
if let Some(child_parent) = potential_child.parent() {
&child_parent == parent_path
} else {
parent_path.is_empty()
}
}
/// Checks if a path is at the root
pub fn is_in_root(&self, path: &StoragePath) -> bool {
path.parent().is_none_or(|p| p.is_empty())
}
/// Gets the root path used by this service
pub fn get_root_path(&self) -> &Path {
&self.root_path
}
/// Validates a path to ensure it doesn't contain dangerous components
pub fn validate_path(&self, path: &StoragePath) -> Result<(), DomainError> {
// Check for empty segments
if path.segments().any(|s| s.is_empty()) {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Path",
format!("Path contains empty segments: {}", path),
));
}
// Check for dangerous characters
let dangerous_chars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
for segment in path.segments() {
if segment.contains(&dangerous_chars[..]) {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Path",
format!("Path contains dangerous characters: {}", segment),
));
}
// Check that it doesn't start with . (hidden in Unix)
if segment.starts_with('.') && segment != ".well-known" {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Path",
format!("Path segments cannot start with dot: {}", segment),
));
}
}
Ok(())
}
}
impl StoragePort for PathService {
fn resolve_path(&self, storage_path: &StoragePath) -> Result<PathBuf, DomainError> {
// Delegate to inherent method which validates + bounds-checks
self.resolve_path(storage_path)
}
async fn ensure_directory(&self, storage_path: &StoragePath) -> Result<(), DomainError> {
// resolve_path already calls validate_path internally
let physical_path = self.resolve_path(storage_path)?;
// Check current state with a single async stat() — no worker blocking.
match fs::metadata(&physical_path).await {
Ok(meta) if meta.is_dir() => { /* already exists */ }
Ok(_) => {
return Err(DomainError::new(
ErrorKind::InvalidInput,
"Storage",
format!(
"Path exists but is not a directory: {}",
physical_path.display()
),
));
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir_all(&physical_path).await.map_err(|e| {
DomainError::new(
ErrorKind::AccessDenied,
"Storage",
format!("Failed to create directory: {}", physical_path.display()),
)
.with_source(e)
})?;
tracing::debug!("Created directory: {}", physical_path.display());
}
Err(e) => {
return Err(DomainError::new(
ErrorKind::InternalError,
"Storage",
format!("Cannot stat {}: {e}", physical_path.display()),
));
}
}
Ok(())
}
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
let physical_path = self.resolve_path(storage_path)?;
// Single async stat() — no worker blocking, one syscall instead of two.
match fs::metadata(&physical_path).await {
Ok(meta) => Ok(meta.is_file()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(DomainError::new(
ErrorKind::InternalError,
"Storage",
format!("Cannot stat {}: {e}", physical_path.display()),
)),
}
}
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError> {
let physical_path = self.resolve_path(storage_path)?;
// Single async stat() — no worker blocking, one syscall instead of two.
match fs::metadata(&physical_path).await {
Ok(meta) => Ok(meta.is_dir()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(DomainError::new(
ErrorKind::InternalError,
"Storage",
format!("Cannot stat {}: {e}", physical_path.display()),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_path() {
let service = PathService::new(PathBuf::from("/storage"));
let storage_path = StoragePath::from_string("test/file.txt");
let absolute = service.resolve_path(&storage_path).unwrap();
assert_eq!(absolute, PathBuf::from("/storage/test/file.txt"));
}
#[test]
fn test_to_storage_path() {
let service = PathService::new(PathBuf::from("/storage"));
let physical_path = PathBuf::from("/storage/folder/file.txt");
let storage_path = service.to_storage_path(&physical_path).unwrap();
assert_eq!(storage_path.to_string(), "/folder/file.txt");
}
#[test]
fn test_is_in_root() {
let service = PathService::new(PathBuf::from("/storage"));
let root_path = StoragePath::from_string("file.txt");
let nested_path = StoragePath::from_string("folder/file.txt");
assert!(service.is_in_root(&root_path));
assert!(!service.is_in_root(&nested_path));
}
#[test]
fn test_is_direct_child() {
let service = PathService::new(PathBuf::from("/storage"));
let parent = StoragePath::from_string("folder");
let child = StoragePath::from_string("folder/file.txt");
let not_child = StoragePath::from_string("folder2/file.txt");
assert!(service.is_direct_child(&parent, &child));
assert!(!service.is_direct_child(&parent, &not_child));
}
#[test]
fn test_create_file_path() {
let service = PathService::new(PathBuf::from("/storage"));
let folder_path = StoragePath::from_string("folder");
let file_path = service.create_file_path(&folder_path, "file.txt");
assert_eq!(file_path.to_string(), "/folder/file.txt");
}
// ── Path-traversal hardening tests (VULN-02) ──────────────
#[test]
fn test_resolve_path_traversal_stripped_by_domain() {
// StoragePath::from_string already strips ".." segments (Solution A+E),
// so resolve_path receives a clean path.
let service = PathService::new(PathBuf::from("/storage"));
let path = StoragePath::from_string("../../etc/passwd");
let resolved = service.resolve_path(&path).unwrap();
assert_eq!(resolved, PathBuf::from("/storage/etc/passwd"));
assert!(resolved.starts_with("/storage"));
}
#[test]
fn test_resolve_path_normal_path_ok() {
let service = PathService::new(PathBuf::from("/storage"));
let path = StoragePath::from_string("users/alice/documents/report.pdf");
let resolved = service.resolve_path(&path).unwrap();
assert_eq!(
resolved,
PathBuf::from("/storage/users/alice/documents/report.pdf")
);
}
#[test]
fn test_resolve_path_root_ok() {
let service = PathService::new(PathBuf::from("/storage"));
let path = StoragePath::root();
let resolved = service.resolve_path(&path).unwrap();
assert_eq!(resolved, PathBuf::from("/storage"));
}
#[test]
fn test_validate_path_rejects_dot_prefix() {
let service = PathService::new(PathBuf::from("/storage"));
// Manually construct a path with a dot-prefixed segment
// (from_string strips ".." but allows ".hidden")
let path = StoragePath::from_string("folder/.hidden/file.txt");
assert!(service.validate_path(&path).is_err());
}
#[test]
fn test_validate_path_allows_well_known() {
let service = PathService::new(PathBuf::from("/storage"));
let path = StoragePath::from_string("folder/.well-known/caldav");
assert!(service.validate_path(&path).is_ok());
}
#[test]
fn test_validate_path_rejects_dangerous_chars() {
let service = PathService::new(PathBuf::from("/storage"));
for dangerous in &[
"file:name",
"file*name",
"file?name",
"file<name",
"file>name",
"file|name",
"file\"name",
] {
let path = StoragePath::new(vec![dangerous.to_string()]);
assert!(
service.validate_path(&path).is_err(),
"validate_path should reject segment: {}",
dangerous
);
}
}
}