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; /// `_with_perms` variant of `upload_file_streaming` — enforces /// `Create` on the target folder before registering the row. /// /// AuthZ audit #17 (2026-07-12): the chunked-upload `complete` /// path called plain `upload_file_streaming` at finalize; a grant /// revoked between session open and finalize stayed effective /// until the caller landed the final chunk (up to 24h JWT TTL, /// forever with app-passwords). Handlers now call this variant /// so the engine re-checks at finalize regardless of how long /// the session was open. async fn upload_file_streaming_with_perms( &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). /// `_with_perms` suffix (AGENTS.md AuthZ convention): the /// implementation calls `authz.require(caller, Update, File(id))` /// on the overwrite branch and `authz.require(caller, Create, /// Folder|Drive(id))` on the new-file branch. Handlers just plumb /// `caller_id` through — no protocol-layer authz. /// /// `expected_hash`: forwarded to /// `FileWritePort::update_file_content_with_blob` on the overwrite /// branch for compare-and-swap; ignored on the new-file branch /// (nothing to compare against). Pass `None` for plain PUT/WOPI/ /// chunked-upload last-write-wins semantics; pass the pre-write /// snapshot's content hash for PATCH, where a concurrent write /// during the (potentially slow) splice must be rejected rather /// than silently clobbered. #[allow(clippy::too_many_arguments)] async fn update_file_streaming_with_perms( &self, path: &str, drive_id: Uuid, blob: StoredBlob, content_type: &str, modified_at: Option, caller_id: Uuid, expected_hash: Option<&str>, ) -> 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>>), } /// Result of a cache-aware HTTP-Range read /// (`FileRetrievalService::get_file_range_preloaded`). Same split as /// [`OptimizedFileContent`]: handlers map each variant onto a response body. pub enum RangeContent { /// Zero-copy slice out of the RAM content cache (a `Bytes::slice` is a /// refcount bump — no allocation, no I/O, no DB). Bytes(Bytes), /// Streaming range read from the blob store (cache miss / large file). Stream(Box> + 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>, after_name: Option<&str>, limit: i64, ) -> Result, DomainError> { let mut all = self.list_files(folder_id).await?; all.sort_by(|a, b| a.name.cmp(&b.name)); Ok(all .into_iter() .filter(|f| after_name.is_none_or(|a| f.name.as_str() > a)) .take(limit as usize) .collect()) } /// Like [`list_files_batch`], but scoped to a specific caller. /// /// Used by streaming WebDAV PROPFIND. Post-D7 the concrete /// implementation in `FileRetrievalService` uses drive-membership /// grants; this default falls back to the unscoped listing (the /// caller passes through `owner_id` for interface parity but the /// stub can't apply a real filter without a repo lookup). async fn list_files_batch_with_perms( &self, folder_id: Option<&str>, _owner_id: Uuid, after_name: Option<&str>, limit: i64, ) -> Result, DomainError> { self.list_files_batch(folder_id, after_name, limit).await } } /// 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; }