use bytes::Bytes; use futures::Stream; use std::path::Path; use std::pin::Pin; use std::sync::Arc; use uuid::Uuid; use crate::application::dtos::file_dto::FileDto; use crate::application::ports::storage_ports::CopyFolderTreeResult; use crate::application::services::file_management_service::FileManagementService; use crate::application::services::file_retrieval_service::FileRetrievalService; use crate::application::services::file_upload_service::FileUploadService; use crate::common::errors::DomainError; use crate::domain::services::authorization::Permission; // ───────────────────────────────────────────────────── // Upload port // ───────────────────────────────────────────────────── /// Primary port for file upload operations. /// /// **All upload paths converge on streaming-to-disk** — no method accepts /// `Vec` 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 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, content_type: String, temp_path: &Path, size: u64, pre_computed_hash: Option, ) -> Result; /// 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, content_type: String, file_path: &Path, pre_computed_hash: Option, ) -> Result; /// Creates a new file at the specified path (for WebDAV) async fn create_file( &self, parent_path: &str, filename: &str, content: &[u8], content_type: &str, ) -> Result; /// Updates the content of an existing file (for WebDAV) async fn update_file( &self, path: &str, content: &[u8], content_type: &str, modified_at: Option, ) -> Result; /// 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, modified_at: Option, ) -> Result; } // ───────────────────────────────────────────────────── // 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: Arc, was_transcoded: bool, }, /// Memory-mapped file (10–100 MB). Mmap(Bytes), /// Streaming download for very large files (≥100 MB). Stream(Pin> + Send>>), } /// Primary port for file retrieval operations pub trait FileRetrievalUseCase: Send + Sync + 'static { /// Gets a file by its ID (system/internal — no ownership check). async fn get_file(&self, id: &str) -> Result; /// Gets a file by its ID, enforcing that `caller_id` is the owner. /// /// Returns `NotFound` if the file does not exist **or** belongs to /// another user. All user-facing handlers should use this method. async fn get_file_with_perms(&self, id: &str, caller_id: Uuid) -> Result; /// Gets a file by its path (for WebDAV) async fn get_file_by_path(&self, path: &str) -> Result; /// Lists files in a folder async fn list_files(&self, folder_id: Option<&str>) -> Result, DomainError>; /// Lists files in a folder, scoped to the authenticated user. /// /// Uses SQL-level `AND user_id` filtering — no in-memory post-filter. /// All user-facing list handlers should use this method. async fn list_files_with_perms( &self, folder_id: Option<&str>, owner_id: Uuid, ) -> Result, DomainError>; /// Gets file content as a stream (for large files) async fn get_file_stream( &self, id: &str, ) -> Result> + Send>, DomainError>; /// Gets file content as a stream, enforcing that `caller_id` is the owner. async fn get_file_stream_with_perms( &self, id: &str, caller_id: Uuid, ) -> Result> + 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>; /// Ownership-scoped optimized download. /// /// Verifies `caller_id` owns the file before returning content. /// All user-facing download handlers should use this. async fn get_file_optimized_with_perms( &self, id: &str, caller_id: Uuid, 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, ) -> Result> + Send>, DomainError>; /// Ownership-scoped range stream — verifies caller owns the file first. async fn get_file_range_stream_with_perms( &self, id: &str, caller_id: Uuid, start: u64, end: Option, ) -> Result> + 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> + 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, DomainError> { let all = self.list_files(folder_id).await?; Ok(all .into_iter() .skip(offset as usize) .take(limit as usize) .collect()) } /// Like [`list_files_batch`], but scoped to a specific owner. /// /// Used by streaming WebDAV PROPFIND so that each user only sees their /// own files, even in shared folder_id namespaces. async fn list_files_batch_with_perms( &self, folder_id: Option<&str>, owner_id: Uuid, offset: i64, limit: i64, ) -> Result, DomainError> { let all = self.list_files_batch(folder_id, offset, limit).await?; let owner_str = owner_id.to_string(); Ok(all .into_iter() .filter(|f| f.owner_id.as_deref().is_some_and(|o| o == owner_str)) .collect()) } } /// Primary port for file management operations pub trait FileManagementUseCase: Send + Sync + 'static { async fn has_permission( &self, caller_id: Uuid, permission: Permission, file_id: &str, ) -> Result<(), DomainError>; /// Moves a file, enforcing that `caller_id` is the owner. async fn move_file_with_perms( &self, file_id: &str, caller_id: Uuid, folder_id: Option, ) -> Result; /// Copies a file, enforcing that `caller_id` is the owner. async fn copy_file_with_perms( &self, file_id: &str, caller_id: Uuid, target_folder_id: Option, ) -> Result; /// Renames a file, enforcing that `caller_id` is the owner. async fn rename_file_with_perms( &self, file_id: &str, caller_id: Uuid, new_name: &str, ) -> Result; /// Deletes a file, enforcing that `caller_id` is the owner. async fn delete_file_with_perms(&self, id: &str, caller_id: Uuid) -> 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. async fn delete_and_cleanup_with_perms( &self, id: &str, user_id: Uuid, ) -> Result; /// Copies an entire folder subtree atomically (WebDAV COPY Depth: infinity). /// enforcing that `caller_id` owns both the source folder /// and the target parent folder. /// /// 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_with_perms( &self, source_folder_id: &str, caller_id: Uuid, target_parent_id: Option, dest_name: Option, ) -> Result; } /// Factory for creating file use case implementations pub trait FileUseCaseFactory: Send + Sync + 'static { fn create_file_upload_use_case(&self) -> Arc; fn create_file_retrieval_use_case(&self) -> Arc; fn create_file_management_use_case(&self) -> Arc; }