use bytes::Bytes; use futures::Stream; 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 // ───────────────────────────────────────────────────── /// A blob already stored in the content-addressable chunk store, carrying /// exactly ONE reference that the receiving method takes ownership of. /// /// Produced by the upload-ingest layer (`interfaces::upload_ingest`), which /// streams the request body straight into the CDC dedup store. Methods that /// accept a `StoredBlob` either attach the reference to a file row or /// release it on failure — callers never need to compensate themselves. #[derive(Debug, Clone)] pub struct StoredBlob { /// BLAKE3 of the full content (manifest / blob key). pub hash: String, /// Content size in bytes. pub size: u64, /// `false` when the content already existed (dedup hit) — forwarded to /// lifecycle hooks so e.g. thumbnails aren't regenerated for known blobs. pub is_new_blob: bool, } /// Primary port for file upload operations. /// /// **All upload paths converge on streaming-into-the-chunk-store** — content /// never passes through this port; it is ingested by the interface layer /// (CDC chunking + hashing while the body arrives, no spool file) and only /// the resulting [`StoredBlob`] reference travels through here. pub trait FileUploadUseCase: Send + Sync + 'static { /// Register a new file row pointing at an already-ingested blob. /// /// Takes ownership of the blob's reference (released on failure). /// /// `caller_id` is plumbed down into /// `FileWritePort::save_file_with_blob` so the §14 `created_by` / /// `updated_by` columns record the principal performing the upload — /// not the parent folder's owner. D2 shared drives surface this /// most clearly: Adam upload into Alice's folder must record /// `created_by = adam.id`. async fn upload_file_streaming( &self, name: String, folder_id: Option, content_type: String, blob: StoredBlob, caller_id: Uuid, ) -> Result; /// Replace the content of the file at `path` with an already-ingested /// blob, or create the file when it doesn't exist (WebDAV/WOPI PUT). /// /// Takes ownership of the blob's reference (released on failure). /// /// `drive_id` scopes both the existence probe (`find_file_by_path`) /// and the parent-folder resolution (`get_parent_folder_id`) — the /// handler is responsible for deriving it from its protocol context /// (NC chroot, native default-drive lookup, WOPI default-drive). /// /// `caller_id` is plumbed down into /// `FileWritePort::update_file_content_with_blob` so the §14 /// `updated_by` column reflects the principal that performed the /// PUT — not the file's existing owner (D2 shared drives let /// non-owners overwrite content). async fn update_file_streaming( &self, path: &str, drive_id: Uuid, blob: StoredBlob, content_type: &str, modified_at: Option, caller_id: Uuid, ) -> 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 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, }, /// Streaming download for everything above the in-RAM cache threshold. 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; async fn get_file_or_trashed_with_perms( &self, id: &str, caller_id: Uuid, ) -> Result; /// Gets a file by its path (for WebDAV), scoped to a drive. /// /// Post-D0, `storage.files.path` is unique only within a single /// drive. The `drive_id` filter scopes the lookup to a specific /// drive (caller derives it from its protocol context: NC chroot, /// native default-drive lookup, WOPI default-drive lookup). async fn get_file_by_path(&self, path: &str, drive_id: Uuid) -> 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 require_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. /// /// `new_name`, when `Some(_)`, becomes the copy's filename — without it /// the copy keeps the source's name, which makes "same folder, different /// name" copies (classic WebDAV `COPY /a.txt → /b.txt`) collide on the /// `(folder, name, user)` unique index. async fn copy_file_with_perms( &self, file_id: &str, caller_id: Uuid, target_folder_id: Option, new_name: 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; }