refactoring hexagonal and clean architecture
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
//! Cache Ports — Application-layer abstractions for all caching concerns.
|
||||
//!
|
||||
//! This module defines ports (traits) for:
|
||||
//! - **WriteBehindCachePort**: deferred write caching for zero-latency uploads.
|
||||
//! - **MetadataCachePort**: file/directory metadata caching (existence, size, timestamps).
|
||||
//! - **ContentCachePort**: hot file content caching (small files served from RAM).
|
||||
//!
|
||||
//! The application and interface layers remain independent of the caching
|
||||
//! implementation details.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Statistics for monitoring write-behind cache status.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct WriteBehindStatsDto {
|
||||
pub pending_count: usize,
|
||||
pub pending_bytes: usize,
|
||||
pub total_writes: u64,
|
||||
pub total_bytes_written: u64,
|
||||
pub cache_hits: u64,
|
||||
pub avg_flush_time_us: u64,
|
||||
}
|
||||
|
||||
/// Port for write-behind cache operations.
|
||||
///
|
||||
/// Provides deferred write semantics: small files are held in memory
|
||||
/// and the response is returned immediately, while actual disk writes
|
||||
/// happen asynchronously in the background.
|
||||
#[async_trait]
|
||||
pub trait WriteBehindCachePort: Send + Sync + 'static {
|
||||
/// Check if a file size is eligible for write-behind caching.
|
||||
fn is_eligible_size(&self, size: usize) -> bool;
|
||||
|
||||
/// Put a file in the pending write cache.
|
||||
///
|
||||
/// Returns `Ok(true)` if cached successfully, `Ok(false)` if cache is full.
|
||||
async fn put_pending(
|
||||
&self,
|
||||
file_id: String,
|
||||
content: Bytes,
|
||||
target_path: PathBuf,
|
||||
) -> Result<bool, DomainError>;
|
||||
|
||||
/// Get content from cache if the file is still pending flush.
|
||||
async fn get_pending(&self, file_id: &str) -> Option<Bytes>;
|
||||
|
||||
/// Check if a file is pending flush.
|
||||
async fn is_pending(&self, file_id: &str) -> bool;
|
||||
|
||||
/// Force immediate flush of a specific file.
|
||||
async fn force_flush(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Flush all pending writes immediately.
|
||||
async fn flush_all(&self) -> Result<(), DomainError>;
|
||||
|
||||
/// Gracefully shutdown the cache, flushing all pending writes.
|
||||
async fn shutdown(&self) -> Result<(), DomainError>;
|
||||
|
||||
/// Get current cache statistics.
|
||||
async fn get_stats(&self) -> WriteBehindStatsDto;
|
||||
}
|
||||
|
||||
// ─── Metadata Cache ──────────────────────────────────────────
|
||||
|
||||
/// Lightweight DTO for cached file/directory metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedMetadataDto {
|
||||
pub path: PathBuf,
|
||||
pub exists: bool,
|
||||
pub is_file: bool,
|
||||
pub size: Option<u64>,
|
||||
pub mime_type: Option<String>,
|
||||
pub created_at: Option<u64>,
|
||||
pub modified_at: Option<u64>,
|
||||
}
|
||||
|
||||
/// Port for file/directory metadata caching.
|
||||
///
|
||||
/// Provides fast lookups for existence, size, timestamps and MIME types
|
||||
/// without hitting the filesystem on every request.
|
||||
#[async_trait]
|
||||
pub trait MetadataCachePort: Send + Sync + 'static {
|
||||
/// Get cached metadata for a path, or `None` on miss / expired.
|
||||
async fn get_metadata(&self, path: &Path) -> Option<CachedMetadataDto>;
|
||||
|
||||
/// Check whether a path is a file (cached). Returns `None` on miss.
|
||||
async fn is_file(&self, path: &Path) -> Option<bool>;
|
||||
|
||||
/// Read actual filesystem metadata and update the cache entry.
|
||||
async fn refresh_metadata(&self, path: &Path) -> Result<CachedMetadataDto, DomainError>;
|
||||
|
||||
/// Invalidate a single cache entry.
|
||||
async fn invalidate(&self, path: &Path);
|
||||
|
||||
/// Invalidate all entries under a directory (recursive prefix match).
|
||||
async fn invalidate_directory(&self, dir_path: &Path);
|
||||
}
|
||||
|
||||
// ─── Content Cache ───────────────────────────────────────────
|
||||
|
||||
/// Port for hot file content caching (small frequently-accessed files in RAM).
|
||||
///
|
||||
/// Implementations should use LRU eviction and respect size limits so that
|
||||
/// the application layer never needs to know the concrete cache type.
|
||||
#[async_trait]
|
||||
pub trait ContentCachePort: Send + Sync + 'static {
|
||||
/// Check whether a file of the given size should be cached.
|
||||
fn should_cache(&self, size: usize) -> bool;
|
||||
|
||||
/// Get cached content. Returns `(content, etag, content_type)` on hit.
|
||||
async fn get(&self, file_id: &str) -> Option<(Bytes, String, String)>;
|
||||
|
||||
/// Store content in the cache (may evict older entries).
|
||||
async fn put(&self, file_id: String, content: Bytes, etag: String, content_type: String);
|
||||
|
||||
/// Remove a file from the cache (e.g. on delete/update).
|
||||
async fn invalidate(&self, file_id: &str);
|
||||
|
||||
/// Clear the entire cache.
|
||||
async fn clear(&self);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Chunked Upload Port - Application layer abstraction for resumable chunked uploads.
|
||||
//!
|
||||
//! This module defines the port (trait) and DTOs for chunked/resumable upload
|
||||
//! operations, keeping the application and interface layers independent of
|
||||
//! the specific upload implementation (TUS-like protocol, S3 multipart, etc.).
|
||||
|
||||
use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use serde::Serialize;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Default chunk size (5 MB) — optimised for parallel transfers.
|
||||
pub const DEFAULT_CHUNK_SIZE: usize = 5 * 1024 * 1024;
|
||||
|
||||
/// Minimum file size to use chunked upload (10 MB).
|
||||
pub const CHUNKED_UPLOAD_THRESHOLD: usize = 10 * 1024 * 1024;
|
||||
|
||||
/// Response returned when a new upload session is created.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CreateUploadResponseDto {
|
||||
pub upload_id: String,
|
||||
pub chunk_size: usize,
|
||||
pub total_chunks: usize,
|
||||
pub expires_at: u64,
|
||||
}
|
||||
|
||||
/// Response returned after a single chunk is uploaded.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ChunkUploadResponseDto {
|
||||
pub chunk_index: usize,
|
||||
pub bytes_received: u64,
|
||||
pub progress: f64,
|
||||
pub is_complete: bool,
|
||||
}
|
||||
|
||||
/// Response for querying upload session status.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct UploadStatusResponseDto {
|
||||
pub upload_id: String,
|
||||
pub filename: String,
|
||||
pub total_size: u64,
|
||||
pub bytes_received: u64,
|
||||
pub progress: f64,
|
||||
pub total_chunks: usize,
|
||||
pub completed_chunks: usize,
|
||||
pub pending_chunks: Vec<usize>,
|
||||
pub is_complete: bool,
|
||||
}
|
||||
|
||||
/// Port for chunked/resumable upload operations.
|
||||
///
|
||||
/// Implementations manage upload sessions, chunk storage, reassembly,
|
||||
/// and cleanup, while the application layer only interacts through
|
||||
/// this abstraction.
|
||||
#[async_trait]
|
||||
pub trait ChunkedUploadPort: Send + Sync + 'static {
|
||||
/// Create a new upload session.
|
||||
///
|
||||
/// Returns session metadata including the upload ID, chunk size,
|
||||
/// total number of chunks, and expiration timestamp.
|
||||
async fn create_session(
|
||||
&self,
|
||||
filename: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
total_size: u64,
|
||||
chunk_size: Option<usize>,
|
||||
) -> Result<CreateUploadResponseDto, DomainError>;
|
||||
|
||||
/// Upload a single chunk.
|
||||
///
|
||||
/// `checksum` is an optional MD5 hex string for integrity verification.
|
||||
async fn upload_chunk(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
chunk_index: usize,
|
||||
data: Bytes,
|
||||
checksum: Option<String>,
|
||||
) -> Result<ChunkUploadResponseDto, DomainError>;
|
||||
|
||||
/// Get the current status of an upload session.
|
||||
async fn get_status(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<UploadStatusResponseDto, DomainError>;
|
||||
|
||||
/// Assemble all chunks into the final file.
|
||||
///
|
||||
/// Returns `(assembled_file_path, filename, folder_id, content_type, total_size)`.
|
||||
async fn complete_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(PathBuf, String, Option<String>, String, u64), DomainError>;
|
||||
|
||||
/// Finalize upload: clean up the session and temporary files.
|
||||
async fn finalize_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(), DomainError>;
|
||||
|
||||
/// Cancel an upload and clean up all temporary data.
|
||||
async fn cancel_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(), DomainError>;
|
||||
|
||||
/// Check if a file size qualifies for chunked upload.
|
||||
fn should_use_chunked(&self, size: u64) -> bool;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Compression Port - Application layer abstraction for compression services.
|
||||
//!
|
||||
//! This module defines the port (trait) for compression operations,
|
||||
//! keeping the application and interface layers independent of specific
|
||||
//! compression implementations (gzip, zstd, etc.).
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Compression level settings for file compression operations.
|
||||
///
|
||||
/// These levels control the trade-off between compression speed and ratio.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CompressionLevel {
|
||||
/// No compression (passthrough)
|
||||
None = 0,
|
||||
/// Fast compression with lower ratio
|
||||
Fast = 1,
|
||||
/// Balanced compression (default)
|
||||
Default = 6,
|
||||
/// Maximum compression (slower)
|
||||
Best = 9,
|
||||
}
|
||||
|
||||
/// Port for compression/decompression operations.
|
||||
///
|
||||
/// Implementations of this trait provide the actual compression logic
|
||||
/// (e.g., gzip, zstd) while the application layer remains agnostic
|
||||
/// of the specific algorithm used.
|
||||
#[async_trait]
|
||||
pub trait CompressionPort: Send + Sync + 'static {
|
||||
/// Compress data in memory.
|
||||
async fn compress_data(&self, data: &[u8], level: CompressionLevel) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Decompress data in memory.
|
||||
async fn decompress_data(&self, compressed_data: &[u8]) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Determine if a file should be compressed based on its MIME type and size.
|
||||
fn should_compress(&self, mime_type: &str, size: u64) -> bool;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! Deduplication Port - Application layer abstraction for content-addressable storage.
|
||||
//!
|
||||
//! This module defines the port (trait) and DTOs for deduplication operations,
|
||||
//! keeping the application and interface layers independent of the specific
|
||||
//! content-addressable storage implementation.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use serde::Serialize;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Metadata of a stored blob in the dedup system.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct BlobMetadataDto {
|
||||
/// SHA-256 hash of the content.
|
||||
pub hash: String,
|
||||
/// Size in bytes.
|
||||
pub size: u64,
|
||||
/// Number of references to this blob.
|
||||
pub ref_count: u32,
|
||||
/// Original content type (for serving).
|
||||
pub content_type: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of a deduplication store operation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DedupResultDto {
|
||||
/// New content was stored (first occurrence).
|
||||
NewBlob {
|
||||
hash: String,
|
||||
size: u64,
|
||||
blob_path: PathBuf,
|
||||
},
|
||||
/// Content already existed; a reference was added instead.
|
||||
ExistingBlob {
|
||||
hash: String,
|
||||
size: u64,
|
||||
blob_path: PathBuf,
|
||||
saved_bytes: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl DedupResultDto {
|
||||
pub fn hash(&self) -> &str {
|
||||
match self {
|
||||
DedupResultDto::NewBlob { hash, .. } => hash,
|
||||
DedupResultDto::ExistingBlob { hash, .. } => hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u64 {
|
||||
match self {
|
||||
DedupResultDto::NewBlob { size, .. } => *size,
|
||||
DedupResultDto::ExistingBlob { size, .. } => *size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blob_path(&self) -> &Path {
|
||||
match self {
|
||||
DedupResultDto::NewBlob { blob_path, .. } => blob_path,
|
||||
DedupResultDto::ExistingBlob { blob_path, .. } => blob_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn was_deduplicated(&self) -> bool {
|
||||
matches!(self, DedupResultDto::ExistingBlob { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics for the deduplication service.
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct DedupStatsDto {
|
||||
/// Total number of unique blobs.
|
||||
pub total_blobs: u64,
|
||||
/// Total bytes stored (actual disk usage).
|
||||
pub total_bytes_stored: u64,
|
||||
/// Total bytes referenced (logical size).
|
||||
pub total_bytes_referenced: u64,
|
||||
/// Bytes saved through deduplication.
|
||||
pub bytes_saved: u64,
|
||||
/// Number of deduplication hits.
|
||||
pub dedup_hits: u64,
|
||||
/// Deduplication ratio (referenced / stored).
|
||||
pub dedup_ratio: f64,
|
||||
}
|
||||
|
||||
/// Port for content-addressable deduplication operations.
|
||||
///
|
||||
/// Implementations store files by their content hash, eliminating
|
||||
/// duplicate storage automatically. Multiple file references can
|
||||
/// point to the same physical blob.
|
||||
#[async_trait]
|
||||
pub trait DedupPort: Send + Sync + 'static {
|
||||
/// Store content with deduplication (from bytes).
|
||||
///
|
||||
/// If content with the same hash already exists, a reference is added
|
||||
/// instead of storing a duplicate.
|
||||
async fn store_bytes(
|
||||
&self,
|
||||
content: &[u8],
|
||||
content_type: Option<String>,
|
||||
) -> Result<DedupResultDto, DomainError>;
|
||||
|
||||
/// Store content with deduplication (streaming from file).
|
||||
async fn store_from_file(
|
||||
&self,
|
||||
source_path: &Path,
|
||||
content_type: Option<String>,
|
||||
) -> Result<DedupResultDto, DomainError>;
|
||||
|
||||
/// Check if a blob with the given hash exists.
|
||||
async fn blob_exists(&self, hash: &str) -> bool;
|
||||
|
||||
/// Get metadata for a blob.
|
||||
async fn get_blob_metadata(&self, hash: &str) -> Option<BlobMetadataDto>;
|
||||
|
||||
/// Read blob content as raw bytes.
|
||||
async fn read_blob(&self, hash: &str) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Read blob content as `Bytes`.
|
||||
async fn read_blob_bytes(&self, hash: &str) -> Result<Bytes, DomainError>;
|
||||
|
||||
/// Add a reference to a blob (increment ref_count).
|
||||
async fn add_reference(&self, hash: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Remove a reference from a blob.
|
||||
///
|
||||
/// Returns `true` if the blob was deleted (ref_count reached 0).
|
||||
async fn remove_reference(&self, hash: &str) -> Result<bool, DomainError>;
|
||||
|
||||
/// Calculate SHA-256 hash of in-memory content.
|
||||
fn hash_bytes(&self, content: &[u8]) -> String;
|
||||
|
||||
/// Calculate SHA-256 hash of a file (streaming).
|
||||
async fn hash_file(&self, path: &Path) -> Result<String, DomainError>;
|
||||
|
||||
/// Get deduplication statistics.
|
||||
async fn get_stats(&self) -> DedupStatsDto;
|
||||
|
||||
/// Flush the index to persistent storage.
|
||||
async fn flush(&self) -> Result<(), DomainError>;
|
||||
|
||||
/// Verify integrity of all stored blobs.
|
||||
///
|
||||
/// Returns a list of issues found (empty if everything is OK).
|
||||
async fn verify_integrity(&self) -> Result<Vec<String>, DomainError>;
|
||||
}
|
||||
@@ -16,4 +16,28 @@ pub trait FavoritesUseCase: Send + Sync {
|
||||
|
||||
/// Check if an item is in user's favorites
|
||||
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// Outbound port — persistence abstraction
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Puerto secundario (outbound) para persistencia de favoritos.
|
||||
///
|
||||
/// Los servicios de aplicación dependen de este trait en lugar de
|
||||
/// acceder directamente a `PgPool`. La implementación concreta
|
||||
/// vive en `infrastructure::repositories::pg`.
|
||||
#[async_trait]
|
||||
pub trait FavoritesRepositoryPort: Send + Sync + 'static {
|
||||
/// Obtiene todos los favoritos de un usuario.
|
||||
async fn get_favorites(&self, user_id: &str) -> Result<Vec<FavoriteItemDto>>;
|
||||
|
||||
/// Añade un ítem a favoritos. Devuelve `Ok(())` si ya existía (idempotente).
|
||||
async fn add_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
||||
|
||||
/// Elimina un ítem de favoritos. Devuelve `true` si existía.
|
||||
async fn remove_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||
|
||||
/// Comprueba si un ítem está en favoritos.
|
||||
async fn is_favorite(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::pin::Pin;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
@@ -6,6 +7,21 @@ use futures::Stream;
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// Upload port
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Strategy chosen by the upload service based on file size.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UploadStrategy {
|
||||
/// Instant (<256KB): write-behind cache, ~0ms latency
|
||||
WriteBehind,
|
||||
/// Buffered (256KB–1MB): full bytes in memory then write
|
||||
Buffered,
|
||||
/// Streaming (≥1MB): pipe chunks directly to disk
|
||||
Streaming,
|
||||
}
|
||||
|
||||
/// Puerto primario para operaciones de subida de archivos
|
||||
#[async_trait]
|
||||
pub trait FileUploadUseCase: Send + Sync + 'static {
|
||||
@@ -17,6 +33,47 @@ pub trait FileUploadUseCase: Send + Sync + 'static {
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Smart upload: picks the best strategy (write-behind / buffered / streaming)
|
||||
/// and handles dedup automatically.
|
||||
///
|
||||
/// Returns `(FileDto, UploadStrategy)` so the handler can log the chosen tier.
|
||||
async fn smart_upload(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
chunks: Vec<Bytes>,
|
||||
total_size: usize,
|
||||
) -> Result<(FileDto, UploadStrategy), DomainError>;
|
||||
|
||||
/// Crea un nuevo archivo en la ruta especificada (para WebDAV)
|
||||
async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Actualiza el contenido de un archivo existente (para WebDAV)
|
||||
async fn update_file(&self, path: &str, content: &[u8]) -> 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>>),
|
||||
}
|
||||
|
||||
/// Puerto primario para operaciones de recuperación de archivos
|
||||
@@ -25,6 +82,9 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
||||
/// Obtiene un archivo por su ID
|
||||
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Obtiene un archivo por su ruta (para WebDAV)
|
||||
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Lista archivos en una carpeta
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
|
||||
|
||||
@@ -33,8 +93,32 @@ pub trait FileRetrievalUseCase: Send + Sync + 'static {
|
||||
|
||||
/// Obtiene contenido de archivo como stream (para archivos grandes)
|
||||
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>;
|
||||
|
||||
/// 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>;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// Management port (delete, move)
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Puerto primario para operaciones de gestión de archivos
|
||||
#[async_trait]
|
||||
pub trait FileManagementUseCase: Send + Sync + 'static {
|
||||
@@ -43,6 +127,19 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
|
||||
|
||||
/// Elimina un archivo
|
||||
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.
|
||||
async fn delete_with_cleanup(
|
||||
&self,
|
||||
id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<bool, DomainError>;
|
||||
}
|
||||
|
||||
/// Factory para crear implementaciones de casos de uso de archivos
|
||||
|
||||
@@ -1,53 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto};
|
||||
use crate::application::dtos::search_dto::{SearchCriteriaDto, SearchResultsDto};
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Puerto primario para operaciones de archivos
|
||||
#[async_trait]
|
||||
pub trait FileUseCase: Send + Sync + 'static {
|
||||
/// Sube un nuevo archivo desde bytes
|
||||
async fn upload_file(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Obtiene un archivo por su ID
|
||||
async fn get_file(&self, id: &str) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Obtiene un archivo por su ruta (para WebDAV)
|
||||
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Crea un nuevo archivo en la ruta especificada (para WebDAV)
|
||||
async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result<FileDto, DomainError>;
|
||||
|
||||
/// Actualiza el contenido de un archivo existente (para WebDAV)
|
||||
async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError>;
|
||||
|
||||
/// Lista archivos en una carpeta
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError>;
|
||||
|
||||
/// Elimina un archivo
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como bytes (para archivos pequeños)
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como stream (para archivos grandes)
|
||||
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Mueve un archivo a otra carpeta
|
||||
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
|
||||
}
|
||||
|
||||
/// Puerto primario para operaciones de carpetas
|
||||
#[async_trait]
|
||||
pub trait FolderUseCase: Send + Sync + 'static {
|
||||
@@ -102,11 +58,4 @@ pub trait SearchUseCase: Send + Sync + 'static {
|
||||
* @return Resultado indicando éxito o error
|
||||
*/
|
||||
async fn clear_search_cache(&self) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/// Factory para crear implementaciones de casos de uso
|
||||
pub trait UseCaseFactory {
|
||||
fn create_file_use_case(&self) -> Arc<dyn FileUseCase>;
|
||||
fn create_folder_use_case(&self) -> Arc<dyn FolderUseCase>;
|
||||
fn create_search_use_case(&self) -> Arc<dyn SearchUseCase>;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
pub mod auth_ports;
|
||||
pub mod cache_ports;
|
||||
pub mod calendar_ports;
|
||||
pub mod carddav_ports;
|
||||
pub mod chunked_upload_ports;
|
||||
pub mod compression_ports;
|
||||
pub mod dedup_ports;
|
||||
pub mod favorites_ports;
|
||||
pub mod file_ports;
|
||||
pub mod inbound;
|
||||
@@ -8,4 +12,7 @@ pub mod outbound;
|
||||
pub mod recent_ports;
|
||||
pub mod share_ports;
|
||||
pub mod storage_ports;
|
||||
pub mod trash_ports;
|
||||
pub mod thumbnail_ports;
|
||||
pub mod transcode_ports;
|
||||
pub mod trash_ports;
|
||||
pub mod zip_ports;
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
|
||||
use crate::domain::entities::file::File;
|
||||
use crate::domain::entities::folder::Folder;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
// Re-export domain repository traits for backward compatibility
|
||||
pub use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
|
||||
use super::storage_ports::{FileReadPort, FileWritePort};
|
||||
|
||||
/// Puerto secundario para operaciones de almacenamiento
|
||||
#[async_trait]
|
||||
pub trait StoragePort: Send + Sync + 'static {
|
||||
@@ -24,124 +25,29 @@ pub trait StoragePort: Send + Sync + 'static {
|
||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||
}
|
||||
|
||||
/// Puerto secundario para persistencia de archivos
|
||||
#[async_trait]
|
||||
pub trait FileStoragePort: Send + Sync + 'static {
|
||||
/// Guarda un nuevo archivo desde bytes
|
||||
async fn save_file(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Guarda un archivo desde stream (streaming upload)
|
||||
/// Escribe chunks directamente al disco sin acumular en memoria
|
||||
async fn save_file_from_stream(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Obtiene un archivo por su ID
|
||||
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
||||
|
||||
/// Lista archivos en una carpeta
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
||||
|
||||
/// Elimina un archivo
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como bytes
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como stream
|
||||
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Obtiene un rango de contenido como stream (para HTTP Range Requests)
|
||||
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>;
|
||||
|
||||
/// Memory-maps archivo para acceso zero-copy (ideal para 10-100MB)
|
||||
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
|
||||
|
||||
/// Mueve un archivo a otra carpeta
|
||||
async fn move_file(&self, file_id: &str, target_folder_id: Option<String>) -> Result<File, DomainError>;
|
||||
|
||||
/// Obtiene la ruta de almacenamiento de un archivo
|
||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
|
||||
/// Obtiene el ID de la carpeta padre para una ruta dada (necesario para WebDAV)
|
||||
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
|
||||
|
||||
/// Actualiza el contenido de un archivo existente
|
||||
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>;
|
||||
|
||||
/// Registra metadatos de archivo SIN escribir contenido a disco (write-behind)
|
||||
///
|
||||
/// Este método:
|
||||
/// 1. Genera ID único para el archivo
|
||||
/// 2. Calcula la ruta de destino
|
||||
/// 3. Registra el mapping ID->path
|
||||
/// 4. Devuelve File entity con path real (pero archivo no existe aún)
|
||||
///
|
||||
/// El contenido se escribe posteriormente via WriteBehindCache
|
||||
/// Beneficio: Respuesta ~0ms para uploads pequeños
|
||||
async fn register_file_deferred(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
size: u64,
|
||||
) -> Result<(File, PathBuf), DomainError>;
|
||||
}
|
||||
/// Puerto unificado para persistencia de archivos (backward-compatible).
|
||||
///
|
||||
/// Ahora es un **supertrait** de `FileReadPort + FileWritePort`.
|
||||
/// Cualquier tipo que implemente ambos ports obtiene `FileStoragePort`
|
||||
/// automáticamente via blanket impl. Esto permite migrar consumidores
|
||||
/// gradualmente a los ports granulares mientras los existentes siguen
|
||||
/// funcionando sin cambios.
|
||||
pub trait FileStoragePort: FileReadPort + FileWritePort {}
|
||||
|
||||
/// Puerto secundario para persistencia de carpetas
|
||||
#[async_trait]
|
||||
pub trait FolderStoragePort: Send + Sync + 'static {
|
||||
/// Crea una nueva carpeta
|
||||
async fn create_folder(&self, name: String, parent_id: Option<String>) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Obtiene una carpeta por su ID
|
||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Obtiene una carpeta por su ruta
|
||||
async fn get_folder_by_path(&self, storage_path: &StoragePath) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Lista carpetas dentro de una carpeta padre
|
||||
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<Folder>, DomainError>;
|
||||
|
||||
/// Lista carpetas con paginación
|
||||
async fn list_folders_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
||||
|
||||
/// Renombra una carpeta
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Mueve una carpeta a otro padre
|
||||
async fn move_folder(&self, id: &str, new_parent_id: Option<&str>) -> Result<Folder, DomainError>;
|
||||
|
||||
/// Elimina una carpeta
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Verifica si existe una carpeta en la ruta dada
|
||||
async fn folder_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||
|
||||
/// Obtiene la ruta de una carpeta
|
||||
async fn get_folder_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
}
|
||||
/// Blanket implementation: cualquier tipo que implemente ambos ports
|
||||
/// es automáticamente un FileStoragePort.
|
||||
impl<T: FileReadPort + FileWritePort> FileStoragePort for T {}
|
||||
|
||||
/// Puerto secundario para persistencia de carpetas (application layer).
|
||||
///
|
||||
/// Tiene la misma firma que `FolderRepository` del dominio.
|
||||
/// Las implementaciones concretas deben implementar `FolderRepository`,
|
||||
/// obteniendo `FolderStoragePort` automáticamente vía blanket impl.
|
||||
pub trait FolderStoragePort: FolderRepository {}
|
||||
|
||||
/// Blanket implementation: cualquier tipo que implemente FolderRepository
|
||||
/// es automáticamente un FolderStoragePort.
|
||||
impl<T: FolderRepository> FolderStoragePort for T {}
|
||||
|
||||
/// Puerto secundario para mapeo de IDs
|
||||
#[async_trait]
|
||||
|
||||
@@ -16,4 +16,30 @@ pub trait RecentItemsUseCase: Send + Sync {
|
||||
|
||||
/// Limpiar toda la lista de elementos recientes
|
||||
async fn clear_recent_items(&self, user_id: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// Outbound port — persistence abstraction
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Puerto secundario (outbound) para persistencia de elementos recientes.
|
||||
///
|
||||
/// Abstrae el acceso a la tabla `auth.user_recent_files` para que
|
||||
/// `RecentService` no dependa directamente de `PgPool`.
|
||||
#[async_trait]
|
||||
pub trait RecentItemsRepositoryPort: Send + Sync + 'static {
|
||||
/// Obtiene los últimos elementos recientes de un usuario (ordenados por fecha desc).
|
||||
async fn get_recent_items(&self, user_id: &str, limit: i32) -> Result<Vec<RecentItemDto>>;
|
||||
|
||||
/// Registra/actualiza el acceso a un ítem (upsert por user+item+type).
|
||||
async fn upsert_access(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<()>;
|
||||
|
||||
/// Elimina un ítem de recientes. Devuelve `true` si existía.
|
||||
async fn remove_item(&self, user_id: &str, item_id: &str, item_type: &str) -> Result<bool>;
|
||||
|
||||
/// Elimina todos los elementos recientes de un usuario.
|
||||
async fn clear_all(&self, user_id: &str) -> Result<()>;
|
||||
|
||||
/// Elimina elementos que excedan `max_items` (los más antiguos).
|
||||
async fn prune(&self, user_id: &str, max_items: i32) -> Result<()>;
|
||||
}
|
||||
@@ -8,26 +8,65 @@ use crate::domain::entities::file::File;
|
||||
use crate::domain::services::path_service::StoragePath;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Puerto secundario para lectura de archivos
|
||||
// Re-export domain repository traits for backward compatibility.
|
||||
// The canonical definitions now live in domain/repositories/.
|
||||
pub use crate::domain::repositories::file_repository::{FileReadRepository, FileWriteRepository, FileRepository};
|
||||
pub use crate::domain::repositories::folder_repository::FolderRepository;
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FileReadPort — application-layer alias for FileReadRepository
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Puerto secundario para **lectura** de archivos.
|
||||
///
|
||||
/// Encapsula toda operación que consulta estado sin modificarlo:
|
||||
/// get, list, content, stream, mmap, range, resolución de rutas.
|
||||
#[async_trait]
|
||||
pub trait FileReadPort: Send + Sync + 'static {
|
||||
/// Obtiene un archivo por su ID
|
||||
/// Obtiene un archivo por su ID.
|
||||
async fn get_file(&self, id: &str) -> Result<File, DomainError>;
|
||||
|
||||
/// Lista archivos en una carpeta
|
||||
|
||||
/// Lista archivos en una carpeta.
|
||||
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<File>, DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como bytes
|
||||
|
||||
/// Obtiene contenido completo como bytes (solo archivos pequeños/medianos).
|
||||
async fn get_file_content(&self, id: &str) -> Result<Vec<u8>, DomainError>;
|
||||
|
||||
/// Obtiene contenido de archivo como stream
|
||||
async fn get_file_stream(&self, id: &str) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Obtiene contenido como stream (ideal para archivos grandes).
|
||||
async fn get_file_stream(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>, DomainError>;
|
||||
|
||||
/// Stream de un rango de bytes (HTTP Range Requests, video seek).
|
||||
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>;
|
||||
|
||||
/// Memory-map de archivo para acceso zero-copy (10–100 MB).
|
||||
async fn get_file_mmap(&self, id: &str) -> Result<Bytes, DomainError>;
|
||||
|
||||
/// Obtiene la ruta de almacenamiento lógica de un archivo.
|
||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
|
||||
/// Obtiene el ID de la carpeta padre a partir de una ruta (WebDAV).
|
||||
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
|
||||
}
|
||||
|
||||
/// Puerto secundario para escritura de archivos
|
||||
// ─────────────────────────────────────────────────────
|
||||
// FileWritePort — all write / mutate operations
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Puerto secundario para **escritura** de archivos.
|
||||
///
|
||||
/// Cubre: upload (buffered + streaming), move, delete, update,
|
||||
/// y el registro diferido para write-behind cache.
|
||||
#[async_trait]
|
||||
pub trait FileWritePort: Send + Sync + 'static {
|
||||
/// Guarda un nuevo archivo desde bytes
|
||||
/// Guarda un nuevo archivo desde bytes.
|
||||
async fn save_file(
|
||||
&self,
|
||||
name: String,
|
||||
@@ -35,26 +74,63 @@ pub trait FileWritePort: Send + Sync + 'static {
|
||||
content_type: String,
|
||||
content: Vec<u8>,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Mueve un archivo a otra carpeta
|
||||
async fn move_file(&self, file_id: &str, target_folder_id: Option<String>) -> Result<File, DomainError>;
|
||||
|
||||
/// Elimina un archivo
|
||||
|
||||
/// Upload en streaming — escribe chunks a disco sin acumular en RAM.
|
||||
async fn save_file_from_stream(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
stream: std::pin::Pin<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send>>,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Mueve un archivo a otra carpeta.
|
||||
async fn move_file(
|
||||
&self,
|
||||
file_id: &str,
|
||||
target_folder_id: Option<String>,
|
||||
) -> Result<File, DomainError>;
|
||||
|
||||
/// Elimina un archivo.
|
||||
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Obtiene detalles de una carpeta
|
||||
async fn get_folder_details(&self, folder_id: &str) -> Result<File, DomainError>;
|
||||
|
||||
/// Obtiene la ruta de una carpeta como string
|
||||
async fn get_folder_path_str(&self, folder_id: &str) -> Result<String, DomainError>;
|
||||
|
||||
/// Actualiza el contenido de un archivo existente.
|
||||
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError>;
|
||||
|
||||
/// Registra metadatos de archivo SIN escribir contenido a disco (write-behind).
|
||||
///
|
||||
/// Devuelve `(File, PathBuf)` donde `PathBuf` es la ruta destino para la
|
||||
/// escritura diferida que realizará el `WriteBehindCache`.
|
||||
async fn register_file_deferred(
|
||||
&self,
|
||||
name: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
size: u64,
|
||||
) -> Result<(File, PathBuf), DomainError>;
|
||||
|
||||
// ── Trash operations ──
|
||||
|
||||
/// Mueve un archivo a la papelera
|
||||
async fn move_to_trash(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Restaura un archivo desde la papelera a su ubicación original
|
||||
async fn restore_from_trash(&self, file_id: &str, original_path: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Elimina un archivo permanentemente (usado por la papelera)
|
||||
async fn delete_file_permanently(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// Auxiliary ports (unchanged)
|
||||
// ─────────────────────────────────────────────────────
|
||||
|
||||
/// Puerto secundario para resolución de rutas de archivos
|
||||
#[async_trait]
|
||||
pub trait FilePathResolutionPort: Send + Sync + 'static {
|
||||
/// Obtiene la ruta de almacenamiento de un archivo
|
||||
async fn get_file_path(&self, id: &str) -> Result<StoragePath, DomainError>;
|
||||
|
||||
|
||||
/// Resuelve una ruta de dominio a una ruta física
|
||||
fn resolve_path(&self, storage_path: &StoragePath) -> PathBuf;
|
||||
}
|
||||
@@ -64,7 +140,7 @@ pub trait FilePathResolutionPort: Send + Sync + 'static {
|
||||
pub trait StorageVerificationPort: Send + Sync + 'static {
|
||||
/// Verifica si existe un archivo en la ruta dada
|
||||
async fn file_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||
|
||||
|
||||
/// Verifica si existe un directorio en la ruta dada
|
||||
async fn directory_exists(&self, storage_path: &StoragePath) -> Result<bool, DomainError>;
|
||||
}
|
||||
@@ -81,7 +157,7 @@ pub trait DirectoryManagementPort: Send + Sync + 'static {
|
||||
pub trait StorageUsagePort: Send + Sync + 'static {
|
||||
/// Actualiza estadísticas de uso de almacenamiento para un usuario
|
||||
async fn update_user_storage_usage(&self, user_id: &str) -> Result<i64, DomainError>;
|
||||
|
||||
|
||||
/// Actualiza estadísticas de uso de almacenamiento para todos los usuarios
|
||||
async fn update_all_users_storage_usage(&self) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Thumbnail Port - Application layer abstraction for thumbnail generation.
|
||||
//!
|
||||
//! This module defines the port (trait) for thumbnail operations,
|
||||
//! keeping the application and interface layers independent of specific
|
||||
//! image processing implementations.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Thumbnail sizes supported by the system.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ThumbnailSize {
|
||||
/// Small icon for file listings (150×150)
|
||||
Icon,
|
||||
/// Medium preview for gallery view (400×400)
|
||||
Preview,
|
||||
/// Large preview for detail view (800×800)
|
||||
Large,
|
||||
}
|
||||
|
||||
impl ThumbnailSize {
|
||||
/// Get the maximum dimension for this size.
|
||||
pub fn max_dimension(&self) -> u32 {
|
||||
match self {
|
||||
ThumbnailSize::Icon => 150,
|
||||
ThumbnailSize::Preview => 400,
|
||||
ThumbnailSize::Large => 800,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the directory name for this size.
|
||||
pub fn dir_name(&self) -> &'static str {
|
||||
match self {
|
||||
ThumbnailSize::Icon => "icon",
|
||||
ThumbnailSize::Preview => "preview",
|
||||
ThumbnailSize::Large => "large",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all thumbnail sizes.
|
||||
pub fn all() -> &'static [ThumbnailSize] {
|
||||
&[ThumbnailSize::Icon, ThumbnailSize::Preview, ThumbnailSize::Large]
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about the thumbnail cache.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThumbnailStatsDto {
|
||||
pub cached_thumbnails: usize,
|
||||
pub cache_size_bytes: usize,
|
||||
pub max_cache_bytes: usize,
|
||||
}
|
||||
|
||||
/// Port for thumbnail generation and retrieval.
|
||||
///
|
||||
/// Implementations handle the actual image processing, caching,
|
||||
/// and storage of thumbnails, while the application layer only
|
||||
/// interacts through this abstraction.
|
||||
#[async_trait]
|
||||
pub trait ThumbnailPort: Send + Sync + 'static {
|
||||
/// Check if a file is an image that can have thumbnails.
|
||||
fn is_supported_image(&self, mime_type: &str) -> bool;
|
||||
|
||||
/// Get a thumbnail, generating it on-demand if needed.
|
||||
///
|
||||
/// Returns the thumbnail bytes in WebP format.
|
||||
async fn get_thumbnail(
|
||||
&self,
|
||||
file_id: &str,
|
||||
size: ThumbnailSize,
|
||||
original_path: &Path,
|
||||
) -> Result<Bytes, DomainError>;
|
||||
|
||||
/// Generate all thumbnail sizes for a file in the background.
|
||||
///
|
||||
/// Called after file upload to pre-generate thumbnails.
|
||||
fn generate_all_sizes_background(
|
||||
self: Arc<Self>,
|
||||
file_id: String,
|
||||
original_path: PathBuf,
|
||||
);
|
||||
|
||||
/// Delete all thumbnails for a file.
|
||||
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Get cache statistics.
|
||||
async fn get_stats(&self) -> ThumbnailStatsDto;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Image Transcode Port - Application layer abstraction for image transcoding.
|
||||
//!
|
||||
//! This module defines the port (trait) for on-demand image format conversion
|
||||
//! (e.g., JPEG/PNG → WebP), keeping the application and interface layers
|
||||
//! independent of specific image processing implementations.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Supported output formats for image transcoding.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum OutputFormat {
|
||||
/// WebP format — best current browser support with good compression.
|
||||
WebP,
|
||||
// Future: Avif, JpegXl
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
/// Get the file extension for this format.
|
||||
pub fn extension(&self) -> &'static str {
|
||||
match self {
|
||||
OutputFormat::WebP => "webp",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the MIME type for this format.
|
||||
pub fn mime_type(&self) -> &'static str {
|
||||
match self {
|
||||
OutputFormat::WebP => "image/webp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Browser image format capabilities detected from the Accept header.
|
||||
#[derive(Debug)]
|
||||
pub struct BrowserCapabilities {
|
||||
pub supports_webp: bool,
|
||||
pub supports_avif: bool,
|
||||
}
|
||||
|
||||
impl BrowserCapabilities {
|
||||
/// Parse the HTTP Accept header to determine browser image format support.
|
||||
pub fn from_accept_header(accept: Option<&str>) -> Self {
|
||||
let accept = accept.unwrap_or("");
|
||||
Self {
|
||||
supports_webp: accept.contains("image/webp"),
|
||||
supports_avif: accept.contains("image/avif"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the best output format supported by the browser.
|
||||
pub fn best_format(&self) -> Option<OutputFormat> {
|
||||
if self.supports_webp {
|
||||
Some(OutputFormat::WebP)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about transcoding operations.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TranscodeStatsDto {
|
||||
pub cache_hits: u64,
|
||||
pub disk_hits: u64,
|
||||
pub transcodes: u64,
|
||||
pub bytes_saved: u64,
|
||||
pub transcode_errors: u64,
|
||||
}
|
||||
|
||||
/// Port for image transcoding operations.
|
||||
///
|
||||
/// Implementations handle the actual image conversion, caching,
|
||||
/// and format detection, while the application layer only interacts
|
||||
/// through this abstraction.
|
||||
#[async_trait]
|
||||
pub trait ImageTranscodePort: Send + Sync + 'static {
|
||||
/// Check if a MIME type can be transcoded.
|
||||
fn can_transcode(&self, mime_type: &str) -> bool;
|
||||
|
||||
/// Check if transcoding should be attempted based on file size and type.
|
||||
fn should_transcode(&self, mime_type: &str, file_size: u64) -> bool;
|
||||
|
||||
/// Get a transcoded version of an image.
|
||||
///
|
||||
/// Returns `(content, mime_type, was_transcoded)`.
|
||||
/// If transcoding is not beneficial (output larger than input), returns the
|
||||
/// original content with `was_transcoded = false`.
|
||||
async fn get_transcoded(
|
||||
&self,
|
||||
file_id: &str,
|
||||
original_content: &[u8],
|
||||
original_mime: &str,
|
||||
target_format: OutputFormat,
|
||||
) -> Result<(Bytes, String, bool), DomainError>;
|
||||
|
||||
/// Invalidate cached transcodes for a file.
|
||||
async fn invalidate(&self, file_id: &str);
|
||||
|
||||
/// Get transcoding statistics.
|
||||
async fn get_stats(&self) -> TranscodeStatsDto;
|
||||
|
||||
/// Clear all caches.
|
||||
async fn clear_cache(&self) -> Result<(), DomainError>;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//! ZIP Port - Application layer abstraction for ZIP archive creation.
|
||||
//!
|
||||
//! This module defines the port (trait) for ZIP operations,
|
||||
//! keeping the interface layer independent of specific ZIP
|
||||
//! implementation details.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
/// Port for ZIP archive operations.
|
||||
///
|
||||
/// Implementations handle the actual ZIP file creation, compression,
|
||||
/// and recursive folder traversal.
|
||||
#[async_trait]
|
||||
pub trait ZipPort: Send + Sync + 'static {
|
||||
/// Create a ZIP archive containing the contents of a folder (recursively).
|
||||
///
|
||||
/// Returns the ZIP file bytes.
|
||||
async fn create_folder_zip(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
folder_name: &str,
|
||||
) -> Result<Vec<u8>, DomainError>;
|
||||
}
|
||||
Reference in New Issue
Block a user