Files
Oxicloud/src/application/ports/file_ports.rs
T

248 lines
9.5 KiB
Rust
Raw Normal View History

2025-03-19 19:52:12 +01:00
use async_trait::async_trait;
use bytes::Bytes;
use futures::Stream;
use std::path::Path;
2026-02-14 01:29:34 +01:00
use std::pin::Pin;
use std::sync::Arc;
2025-03-19 19:52:12 +01:00
use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::storage_ports::CopyFolderTreeResult;
2025-03-19 19:52:12 +01:00
use crate::common::errors::DomainError;
// ─────────────────────────────────────────────────────
// Upload port
// ─────────────────────────────────────────────────────
/// Primary port for file upload operations.
///
/// **All upload paths converge on streaming-to-disk** — no method accepts
/// `Vec<u8>` for content. Even `create_file` / `update_file` (WebDAV
/// helpers that receive `&[u8]`) spool to a temp file internally so that
/// peak RAM stays at ~256 KB regardless of file size.
///
/// - Normal uploads: handler spools multipart to temp file → `upload_file_streaming`
/// - Chunked uploads: chunks already on disk → `upload_file_from_path`
/// - WebDAV PUT (new): handler streams to temp file → `update_file_streaming`
/// - WebDAV PUT (small/compat): `create_file` / `update_file` spool internally
2025-03-19 19:52:12 +01:00
#[async_trait]
pub trait FileUploadUseCase: Send + Sync + 'static {
/// Upload from a temp file already on disk (true streaming, ~256 KB RAM).
///
/// When `pre_computed_hash` is `Some`, the blob store skips the hash
/// re-read — the handler already computed it during the multipart spool.
async fn upload_file_streaming(
&self,
name: String,
folder_id: Option<String>,
content_type: String,
temp_path: &Path,
size: u64,
pre_computed_hash: Option<String>,
) -> Result<FileDto, DomainError>;
/// Upload from a file already assembled on disk (chunked uploads).
///
/// Same as `upload_file_streaming` but with a separate name for clarity.
async fn upload_file_from_path(
&self,
name: String,
folder_id: Option<String>,
content_type: String,
file_path: &Path,
pre_computed_hash: Option<String>,
) -> Result<FileDto, DomainError>;
/// Creates a new file at the specified path (for WebDAV)
2026-02-14 01:29:34 +01:00
async fn create_file(
&self,
parent_path: &str,
filename: &str,
content: &[u8],
content_type: &str,
) -> Result<FileDto, DomainError>;
/// Updates the content of an existing file (for WebDAV)
async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>;
/// Streaming update — spools body to a temp file with incremental hash,
/// then atomically replaces the file content via dedup store.
///
/// Peak RAM: ~256 KB regardless of file size.
/// Used by WebDAV PUT for large files.
async fn update_file_streaming(
&self,
path: &str,
temp_path: &Path,
size: u64,
content_type: &str,
pre_computed_hash: Option<String>,
) -> Result<(), DomainError>;
}
// ─────────────────────────────────────────────────────
// Retrieval / download port
// ─────────────────────────────────────────────────────
/// Optimized file content returned by the retrieval service.
///
/// The handler only needs to map each variant to the appropriate HTTP
/// response; all caching / transcoding / mmap decisions happen in the
/// application layer.
pub enum OptimizedFileContent {
/// Small-file content (possibly transcoded / compressed) already in RAM.
Bytes {
data: Bytes,
mime_type: String,
was_transcoded: bool,
},
/// Memory-mapped file (10–100 MB).
Mmap(Bytes),
/// Streaming download for very large files (≥100 MB).
Stream(Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>),
2025-03-19 19:52:12 +01:00
}
/// Primary port for file retrieval operations
2025-03-19 19:52:12 +01:00
#[async_trait]
pub trait FileRetrievalUseCase: Send + Sync + 'static {
/// Gets a file by its ID
2025-03-19 19:52:12 +01:00
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
2026-02-14 01:29:34 +01:00
/// Gets a file by its path (for WebDAV)
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
2026-02-14 01:29:34 +01:00
/// Lists files in a folder
2025-03-19 19:52:12 +01:00
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
2026-02-14 01:29:34 +01:00
/// Gets file content as a stream (for large files)
2026-02-14 01:29:34 +01:00
async fn get_file_stream(
&self,
id: &str,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
/// Optimized multi-tier download.
///
/// Internalises: write-behind lookup → content-cache → WebP transcode →
/// mmap → streaming, returning an `OptimizedFileContent` variant so the
/// handler only builds the HTTP response.
async fn get_file_optimized(
&self,
id: &str,
accept_webp: bool,
prefer_original: bool,
) -> Result<(FileDto, OptimizedFileContent), DomainError>;
/// Like `get_file_optimized` but accepts an already-fetched `FileDto`,
/// avoiding a redundant metadata query when the handler already has it.
async fn get_file_optimized_preloaded(
&self,
id: &str,
file_dto: FileDto,
accept_webp: bool,
prefer_original: bool,
) -> Result<(FileDto, OptimizedFileContent), DomainError> {
// Default: ignore pre-fetched meta, re-fetch everything.
let _ = file_dto;
self.get_file_optimized(id, accept_webp, prefer_original)
.await
}
/// Range-based streaming for HTTP Range Requests (video seek, resumable DL).
async fn get_file_range_stream(
&self,
id: &str,
start: u64,
end: Option<u64>,
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
/// Streams every file in the subtree rooted at `folder_id`.
///
/// Returns a streaming cursor — RAM stays O(1) per row. Callers
/// consume incrementally (e.g. group into a HashMap by folder_id)
/// without materializing the full result set.
async fn stream_files_in_subtree(
&self,
folder_id: &str,
) -> Result<Pin<Box<dyn Stream<Item = Result<FileDto, DomainError>> + Send>>, DomainError>;
/// 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())
}
2025-03-19 19:52:12 +01:00
}
// ─────────────────────────────────────────────────────
// Management port (delete, move)
// ─────────────────────────────────────────────────────
/// Primary port for file management operations
2025-03-19 19:52:12 +01:00
#[async_trait]
pub trait FileManagementUseCase: Send + Sync + 'static {
/// Moves a file to another folder
2026-02-14 01:29:34 +01:00
async fn move_file(
&self,
file_id: &str,
folder_id: Option<String>,
) -> Result<FileDto, DomainError>;
2026-02-14 20:22:19 +01:00
/// Copies a file to another folder (zero-copy with dedup).
async fn copy_file(
&self,
file_id: &str,
target_folder_id: Option<String>,
) -> Result<FileDto, DomainError>;
2026-02-14 01:29:34 +01:00
/// Renames a file
2026-02-08 22:44:42 +01:00
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>;
2026-02-14 01:29:34 +01:00
/// Deletes a file
2025-03-19 19:52:12 +01:00
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
/// Smart delete: trash-first with dedup reference cleanup.
///
/// 1. Tries to move to trash (soft delete).
/// 2. Falls back to permanent delete if trash unavailable/failed.
/// 3. Decrements the dedup reference count for the content hash.
///
/// Returns `Ok(true)` when trashed, `Ok(false)` when permanently deleted.
2026-02-14 01:29:34 +01:00
async fn delete_with_cleanup(&self, id: &str, user_id: &str) -> Result<bool, DomainError>;
/// Copies an entire folder subtree atomically (WebDAV COPY Depth: infinity).
///
/// Creates a copy of `source_folder_id` (with optional name override) under
/// `target_parent_id`, including ALL sub-folders and files. Files are
/// zero-copy (blob ref_counts incremented in batch).
///
/// Default: returns error (only available with PostgreSQL backend).
async fn copy_folder_tree(
&self,
_source_folder_id: &str,
_target_parent_id: Option<String>,
_dest_name: Option<String>,
) -> Result<CopyFolderTreeResult, DomainError> {
Err(DomainError::internal_error(
"FileManagement",
"copy_folder_tree not implemented",
))
}
2025-03-19 19:52:12 +01:00
}
/// Factory for creating file use case implementations
2025-03-20 09:22:31 +01:00
pub trait FileUseCaseFactory: Send + Sync + 'static {
2025-03-19 19:52:12 +01:00
fn create_file_upload_use_case(&self) -> Arc<dyn FileUploadUseCase>;
fn create_file_retrieval_use_case(&self) -> Arc<dyn FileRetrievalUseCase>;
fn create_file_management_use_case(&self) -> Arc<dyn FileManagementUseCase>;
2026-02-14 01:29:34 +01:00
}