refactoring hexagonal and clean architecture
This commit is contained in:
@@ -89,9 +89,11 @@ impl BufferPool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Obtiene un buffer del pool o crea uno nuevo si es necesario
|
||||
/// Obtiene un buffer del pool o crea uno nuevo si es necesario.
|
||||
/// This version takes an Arc<Self> to ensure the BorrowedBuffer keeps a proper
|
||||
/// reference to the shared pool (not a clone).
|
||||
#[allow(unused_variables)]
|
||||
pub async fn get_buffer(&self) -> BorrowedBuffer {
|
||||
pub async fn get_buffer(self: &Arc<Self>) -> BorrowedBuffer {
|
||||
// Incrementar contador de gets
|
||||
{
|
||||
let mut stats = self.stats.lock().await;
|
||||
@@ -99,27 +101,31 @@ impl BufferPool {
|
||||
}
|
||||
|
||||
// Control de concurrencia
|
||||
// Usando el mecanismo RAII de Rust para gestión automática
|
||||
// de recursos al finalizar la función
|
||||
let _ = match self.limit.try_acquire() {
|
||||
Ok(_permit) => _permit, // _ prefix para indicar que es intencional
|
||||
// Acquire a semaphore permit. If none available, wait.
|
||||
// We forget() the permit so it doesn't auto-release on drop.
|
||||
// Instead, the permit is manually released in return_buffer/Drop via add_permits(1).
|
||||
match self.limit.try_acquire() {
|
||||
Ok(permit) => permit.forget(),
|
||||
Err(_) => {
|
||||
// No hay permisos disponibles, esperamos
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.waits += 1;
|
||||
stats.max_buffers_reached += 1;
|
||||
drop(stats);
|
||||
{
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.waits += 1;
|
||||
stats.max_buffers_reached += 1;
|
||||
}
|
||||
|
||||
debug!("Buffer pool: waiting for available buffer");
|
||||
let _permit = self.limit.acquire().await.expect("Semaphore should not be closed");
|
||||
let permit = self.limit.acquire().await.expect("Semaphore should not be closed");
|
||||
debug!("Buffer pool: acquired buffer after waiting");
|
||||
_permit
|
||||
permit.forget();
|
||||
}
|
||||
};
|
||||
|
||||
// Intentar obtener un buffer existente del pool
|
||||
let mut pool_locked = self.pool.lock().await;
|
||||
|
||||
let pool_arc = Arc::clone(self);
|
||||
|
||||
if let Some(mut pooled_buffer) = pool_locked.pop_front() {
|
||||
// Verificar si el buffer ha expirado
|
||||
if pooled_buffer.last_used.elapsed() > self.buffer_ttl {
|
||||
@@ -137,7 +143,7 @@ impl BufferPool {
|
||||
BorrowedBuffer {
|
||||
buffer: vec![0; self.buffer_size],
|
||||
used_size: 0,
|
||||
pool: Arc::new(self.clone()),
|
||||
pool: pool_arc,
|
||||
return_to_pool: true,
|
||||
}
|
||||
} else {
|
||||
@@ -155,7 +161,7 @@ impl BufferPool {
|
||||
BorrowedBuffer {
|
||||
buffer: pooled_buffer.buffer,
|
||||
used_size: 0,
|
||||
pool: Arc::new(self.clone()),
|
||||
pool: pool_arc,
|
||||
return_to_pool: true,
|
||||
}
|
||||
}
|
||||
@@ -173,7 +179,7 @@ impl BufferPool {
|
||||
BorrowedBuffer {
|
||||
buffer: vec![0; self.buffer_size],
|
||||
used_size: 0,
|
||||
pool: Arc::new(self.clone()),
|
||||
pool: pool_arc,
|
||||
return_to_pool: true,
|
||||
}
|
||||
}
|
||||
@@ -185,6 +191,8 @@ impl BufferPool {
|
||||
if buffer.capacity() != self.buffer_size {
|
||||
debug!("Buffer pool: discarding buffer of wrong size: {} (expected {})",
|
||||
buffer.capacity(), self.buffer_size);
|
||||
// Release the semaphore permit even if we discard the buffer
|
||||
self.limit.add_permits(1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,6 +210,11 @@ impl BufferPool {
|
||||
// Actualizar estadísticas
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.returns += 1;
|
||||
|
||||
// Release the semaphore permit so another caller can acquire a buffer
|
||||
drop(pool_locked);
|
||||
drop(stats);
|
||||
self.limit.add_permits(1);
|
||||
}
|
||||
|
||||
/// Limpia buffers expirados del pool
|
||||
@@ -331,9 +344,13 @@ impl Drop for BorrowedBuffer {
|
||||
let pool = self.pool.clone();
|
||||
|
||||
// Spawn del return para que el drop no bloquee
|
||||
// return_buffer will release the semaphore permit
|
||||
tokio::spawn(async move {
|
||||
pool.return_buffer(buffer).await;
|
||||
});
|
||||
} else {
|
||||
// Buffer not returned to pool, but we still need to release the semaphore permit
|
||||
self.pool.limit.add_permits(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time;
|
||||
use futures::future::BoxFuture;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Representación de metadatos en caché
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedMetadata {
|
||||
/// Si el archivo o directorio existe
|
||||
pub exists: bool,
|
||||
/// Tamaño en bytes (para archivos)
|
||||
pub size: Option<u64>,
|
||||
/// Timestamp de creación
|
||||
pub created_at: Option<u64>,
|
||||
/// Timestamp de modificación
|
||||
pub modified_at: Option<u64>,
|
||||
/// Tiempo de expiración de la caché
|
||||
expires_at: Instant,
|
||||
}
|
||||
|
||||
/// Estructura para gestionar la caché de metadatos de archivos y directorios
|
||||
pub struct StorageCacheManager {
|
||||
/// Caché de existencia y metadatos
|
||||
cache: RwLock<HashMap<PathBuf, CachedMetadata>>,
|
||||
/// TTL para entradas de archivos (milisegundos)
|
||||
file_ttl_ms: u64,
|
||||
/// TTL para entradas de directorios (milisegundos)
|
||||
dir_ttl_ms: u64,
|
||||
/// Tamaño máximo de caché
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl StorageCacheManager {
|
||||
/// Crea una nueva instancia del gestor de caché
|
||||
pub fn new(file_ttl_ms: u64, dir_ttl_ms: u64, max_entries: usize) -> Self {
|
||||
Self {
|
||||
cache: RwLock::new(HashMap::with_capacity(max_entries)),
|
||||
file_ttl_ms,
|
||||
dir_ttl_ms,
|
||||
max_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea una instancia por defecto del gestor de caché
|
||||
pub fn default() -> Self {
|
||||
Self::new(
|
||||
60_000, // 1 minuto para archivos
|
||||
300_000, // 5 minutos para directorios
|
||||
10_000, // máximo 10,000 entradas
|
||||
)
|
||||
}
|
||||
|
||||
/// Verifica si un archivo o directorio existe en caché
|
||||
pub async fn check_exists(&self, path: &PathBuf, _is_dir: bool) -> Result<bool, ()> {
|
||||
// Intentar obtener de la caché
|
||||
if let Some(metadata) = self.get_cached_metadata(path).await {
|
||||
return Ok(metadata.exists);
|
||||
}
|
||||
|
||||
// No está en caché
|
||||
Err(())
|
||||
}
|
||||
|
||||
/// Obtiene los metadatos de un path desde la caché
|
||||
async fn get_cached_metadata(&self, path: &PathBuf) -> Option<CachedMetadata> {
|
||||
let cache = self.cache.read().await;
|
||||
|
||||
if let Some(metadata) = cache.get(path) {
|
||||
// Verificar si la entrada expiró
|
||||
if Instant::now() < metadata.expires_at {
|
||||
return Some(metadata.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Actualiza la caché con los metadatos de un path
|
||||
pub async fn update_cache(&self, path: &PathBuf, exists: bool, size: Option<u64>,
|
||||
created_at: Option<u64>, modified_at: Option<u64>, is_dir: bool) {
|
||||
let mut cache = self.cache.write().await;
|
||||
|
||||
// Si la caché está llena, eliminar entradas aleatorias antes de agregar
|
||||
if cache.len() >= self.max_entries {
|
||||
self.evict_entries(&mut cache, 100).await;
|
||||
}
|
||||
|
||||
// Determinar TTL basado en si es archivo o directorio
|
||||
let ttl = if is_dir {
|
||||
Duration::from_millis(self.dir_ttl_ms)
|
||||
} else {
|
||||
Duration::from_millis(self.file_ttl_ms)
|
||||
};
|
||||
|
||||
// Crear metadatos y agregar a la caché
|
||||
let metadata = CachedMetadata {
|
||||
exists,
|
||||
size,
|
||||
created_at,
|
||||
modified_at,
|
||||
expires_at: Instant::now() + ttl,
|
||||
};
|
||||
|
||||
cache.insert(path.clone(), metadata);
|
||||
}
|
||||
|
||||
/// Elimina entradas aleatorias de la caché cuando está llena
|
||||
async fn evict_entries(&self, cache: &mut HashMap<PathBuf, CachedMetadata>, count: usize) {
|
||||
// Obtener las entradas más antiguas para eliminar
|
||||
let mut entries: Vec<_> = cache.keys().cloned().collect();
|
||||
|
||||
// Limitar el número de entradas a eliminar
|
||||
let to_remove = count.min(entries.len() / 10);
|
||||
|
||||
if to_remove == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Eliminar las primeras entradas (implementación simple)
|
||||
entries.truncate(to_remove);
|
||||
|
||||
for path in entries {
|
||||
cache.remove(&path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inicia una tarea de limpieza periódica
|
||||
pub fn start_cleanup_task(cache_manager: Arc<Self>) -> BoxFuture<'static, ()> {
|
||||
Box::pin(async move {
|
||||
let interval = Duration::from_secs(60); // Ejecutar cada minuto
|
||||
|
||||
loop {
|
||||
time::sleep(interval).await;
|
||||
|
||||
// Limpiar entradas expiradas
|
||||
let now = Instant::now();
|
||||
let mut cache = cache_manager.cache.write().await;
|
||||
|
||||
// Encontrar entradas expiradas
|
||||
let expired: Vec<_> = cache
|
||||
.iter()
|
||||
.filter(|(_, metadata)| now > metadata.expires_at)
|
||||
.map(|(path, _)| path.clone())
|
||||
.collect();
|
||||
|
||||
// Eliminar entradas expiradas
|
||||
for path in expired {
|
||||
cache.remove(&path);
|
||||
}
|
||||
|
||||
// Registrar estadísticas
|
||||
let cache_size = cache.len();
|
||||
drop(cache);
|
||||
|
||||
tracing::debug!("Cache cleanup completed. Entries remaining: {}", cache_size);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Invalida una entrada específica de la caché
|
||||
pub async fn invalidate(&self, path: &PathBuf) {
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.remove(path);
|
||||
}
|
||||
|
||||
/// Invalida todas las entradas de la caché relacionadas con una carpeta
|
||||
pub async fn invalidate_folder(&self, folder_path: &PathBuf) {
|
||||
let mut cache = self.cache.write().await;
|
||||
|
||||
// Eliminar entradas que sean descendientes de la carpeta
|
||||
let folder_str = folder_path.to_string_lossy().to_string();
|
||||
|
||||
// Encontrar entradas a eliminar
|
||||
let to_remove: Vec<_> = cache
|
||||
.keys()
|
||||
.filter_map(|path| {
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
if path_str.starts_with(&folder_str) {
|
||||
Some(path.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Eliminar las entradas
|
||||
for path in to_remove {
|
||||
cache.remove(&path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene el número actual de entradas en la caché
|
||||
pub async fn cache_size(&self) -> usize {
|
||||
let cache = self.cache.read().await;
|
||||
cache.len()
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,15 @@ use tokio::fs::{self, File, OpenOptions};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::chunked_upload_ports::{
|
||||
ChunkedUploadPort,
|
||||
CreateUploadResponseDto,
|
||||
ChunkUploadResponseDto,
|
||||
UploadStatusResponseDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Minimum file size to use chunked upload (10MB)
|
||||
pub const CHUNKED_UPLOAD_THRESHOLD: usize = 10 * 1024 * 1024;
|
||||
@@ -507,6 +516,93 @@ impl ChunkedUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
impl ChunkedUploadPort for ChunkedUploadService {
|
||||
async fn create_session(
|
||||
&self,
|
||||
filename: String,
|
||||
folder_id: Option<String>,
|
||||
content_type: String,
|
||||
total_size: u64,
|
||||
chunk_size: Option<usize>,
|
||||
) -> Result<CreateUploadResponseDto, DomainError> {
|
||||
let resp = self.create_session(filename, folder_id, content_type, total_size, chunk_size).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))?;
|
||||
Ok(CreateUploadResponseDto {
|
||||
upload_id: resp.upload_id,
|
||||
chunk_size: resp.chunk_size,
|
||||
total_chunks: resp.total_chunks,
|
||||
expires_at: resp.expires_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn upload_chunk(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
chunk_index: usize,
|
||||
data: bytes::Bytes,
|
||||
checksum: Option<String>,
|
||||
) -> Result<ChunkUploadResponseDto, DomainError> {
|
||||
let resp = self.upload_chunk(upload_id, chunk_index, data, checksum).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))?;
|
||||
Ok(ChunkUploadResponseDto {
|
||||
chunk_index: resp.chunk_index,
|
||||
bytes_received: resp.bytes_received,
|
||||
progress: resp.progress,
|
||||
is_complete: resp.is_complete,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_status(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<UploadStatusResponseDto, DomainError> {
|
||||
let resp = self.get_status(upload_id).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::NotFound, "ChunkedUpload", e))?;
|
||||
Ok(UploadStatusResponseDto {
|
||||
upload_id: resp.upload_id,
|
||||
filename: resp.filename,
|
||||
total_size: resp.total_size,
|
||||
bytes_received: resp.bytes_received,
|
||||
progress: resp.progress,
|
||||
total_chunks: resp.total_chunks,
|
||||
completed_chunks: resp.completed_chunks,
|
||||
pending_chunks: resp.pending_chunks,
|
||||
is_complete: resp.is_complete,
|
||||
})
|
||||
}
|
||||
|
||||
async fn complete_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(PathBuf, String, Option<String>, String, u64), DomainError> {
|
||||
self.complete_upload(upload_id).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||
}
|
||||
|
||||
async fn finalize_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
self.finalize_upload(upload_id).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||
}
|
||||
|
||||
async fn cancel_upload(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<(), DomainError> {
|
||||
self.cancel_upload(upload_id).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ChunkedUpload", e))
|
||||
}
|
||||
|
||||
fn should_use_chunked(&self, size: u64) -> bool {
|
||||
ChunkedUploadService::should_use_chunked(size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -9,6 +9,11 @@ use flate2::Compression;
|
||||
use flate2::read::GzEncoder as GzEncoderRead;
|
||||
use flate2::bufread::GzDecoder;
|
||||
|
||||
use crate::application::ports::compression_ports::{
|
||||
CompressionPort,
|
||||
CompressionLevel as PortCompressionLevel,
|
||||
};
|
||||
use crate::domain::errors::DomainError;
|
||||
use crate::infrastructure::services::buffer_pool::BufferPool;
|
||||
|
||||
/// Nivel de compresión para ficheros
|
||||
@@ -254,7 +259,7 @@ impl CompressionService for GzipCompressionService {
|
||||
}
|
||||
|
||||
// Compress collected data
|
||||
match self.compress_data(&data, compression_level).await {
|
||||
match CompressionService::compress_data(self, &data, compression_level).await {
|
||||
Ok(compressed) => {
|
||||
// Return compressed data as a single chunk
|
||||
yield Ok(Bytes::from(compressed));
|
||||
@@ -293,7 +298,7 @@ impl CompressionService for GzipCompressionService {
|
||||
}
|
||||
|
||||
// Decompress collected data
|
||||
match self.decompress_data(&compressed_data).await {
|
||||
match CompressionService::decompress_data(self, &compressed_data).await {
|
||||
Ok(decompressed) => {
|
||||
// Return decompressed data as a single chunk
|
||||
yield Ok(Bytes::from(decompressed));
|
||||
@@ -345,6 +350,35 @@ impl CompressionService for GzipCompressionService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Convert application-layer CompressionLevel to infrastructure CompressionLevel.
|
||||
impl From<PortCompressionLevel> for CompressionLevel {
|
||||
fn from(level: PortCompressionLevel) -> Self {
|
||||
match level {
|
||||
PortCompressionLevel::None => CompressionLevel::None,
|
||||
PortCompressionLevel::Fast => CompressionLevel::Fast,
|
||||
PortCompressionLevel::Default => CompressionLevel::Default,
|
||||
PortCompressionLevel::Best => CompressionLevel::Best,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CompressionPort for GzipCompressionService {
|
||||
async fn compress_data(&self, data: &[u8], level: PortCompressionLevel) -> Result<Vec<u8>, DomainError> {
|
||||
CompressionService::compress_data(self, data, level.into()).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn decompress_data(&self, compressed_data: &[u8]) -> Result<Vec<u8>, DomainError> {
|
||||
CompressionService::decompress_data(self, compressed_data).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
fn should_compress(&self, mime_type: &str, size: u64) -> bool {
|
||||
CompressionService::should_compress(self, mime_type, size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -359,13 +393,13 @@ mod tests {
|
||||
let data = "Hello, world! ".repeat(1000).into_bytes();
|
||||
|
||||
// Comprimir
|
||||
let compressed = service.compress_data(&data, CompressionLevel::Default).await.unwrap();
|
||||
let compressed = CompressionService::compress_data(&service, &data, CompressionLevel::Default).await.unwrap();
|
||||
|
||||
// Verificar que la compresión reduce el tamaño
|
||||
assert!(compressed.len() < data.len());
|
||||
|
||||
// Descomprimir
|
||||
let decompressed = service.decompress_data(&compressed).await.unwrap();
|
||||
let decompressed = CompressionService::decompress_data(&service, &compressed).await.unwrap();
|
||||
|
||||
// Verificar que los datos originales se recuperan correctamente
|
||||
assert_eq!(decompressed, data);
|
||||
@@ -396,7 +430,7 @@ mod tests {
|
||||
}).await.unwrap();
|
||||
|
||||
// Descomprimir los datos
|
||||
let decompressed = service.decompress_data(&compressed_bytes).await.unwrap();
|
||||
let decompressed = CompressionService::decompress_data(&service, &compressed_bytes).await.unwrap();
|
||||
|
||||
// Verificar resultado
|
||||
let expected = "Hello, world! This is a test of streaming compression.";
|
||||
@@ -408,16 +442,16 @@ mod tests {
|
||||
let service = GzipCompressionService::new();
|
||||
|
||||
// Casos que no deberían comprimirse
|
||||
assert!(!service.should_compress("image/jpeg", 100 * 1024));
|
||||
assert!(!service.should_compress("video/mp4", 10 * 1024 * 1024));
|
||||
assert!(!service.should_compress("application/zip", 5 * 1024 * 1024));
|
||||
assert!(!CompressionService::should_compress(&service, "image/jpeg", 100 * 1024));
|
||||
assert!(!CompressionService::should_compress(&service, "video/mp4", 10 * 1024 * 1024));
|
||||
assert!(!CompressionService::should_compress(&service, "application/zip", 5 * 1024 * 1024));
|
||||
|
||||
// Casos que sí deberían comprimirse
|
||||
assert!(service.should_compress("text/html", 100 * 1024));
|
||||
assert!(service.should_compress("application/json", 200 * 1024));
|
||||
assert!(service.should_compress("text/plain", 1024 * 1024));
|
||||
assert!(CompressionService::should_compress(&service, "text/html", 100 * 1024));
|
||||
assert!(CompressionService::should_compress(&service, "application/json", 200 * 1024));
|
||||
assert!(CompressionService::should_compress(&service, "text/plain", 1024 * 1024));
|
||||
|
||||
// Archivos pequeños no deberían comprimirse independientemente del tipo
|
||||
assert!(!service.should_compress("text/html", 10 * 1024));
|
||||
assert!(!CompressionService::should_compress(&service, "text/html", 10 * 1024));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
//! to the same physical blob.
|
||||
//!
|
||||
//! Architecture:
|
||||
//! ```
|
||||
//! ```text
|
||||
//! ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
//! │ User Files │────▶│ Dedup Index │────▶│ Blob Store │
|
||||
//! │ (references) │ │ (hash→metadata) │ │ (actual data) │
|
||||
@@ -26,6 +26,15 @@ use tokio::sync::RwLock;
|
||||
use sha2::{Sha256, Digest};
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::dedup_ports::{
|
||||
DedupPort,
|
||||
BlobMetadataDto,
|
||||
DedupResultDto,
|
||||
DedupStatsDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Chunk size for streaming hash calculation (256KB)
|
||||
const HASH_CHUNK_SIZE: usize = 256 * 1024;
|
||||
@@ -664,6 +673,120 @@ impl DedupService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Convert infra DedupResult to port DedupResultDto.
|
||||
impl From<DedupResult> for DedupResultDto {
|
||||
fn from(result: DedupResult) -> Self {
|
||||
match result {
|
||||
DedupResult::NewBlob { hash, size, blob_path } => {
|
||||
DedupResultDto::NewBlob { hash, size, blob_path }
|
||||
}
|
||||
DedupResult::ExistingBlob { hash, size, blob_path, saved_bytes } => {
|
||||
DedupResultDto::ExistingBlob { hash, size, blob_path, saved_bytes }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert infra BlobMetadata to port BlobMetadataDto.
|
||||
impl From<BlobMetadata> for BlobMetadataDto {
|
||||
fn from(m: BlobMetadata) -> Self {
|
||||
BlobMetadataDto {
|
||||
hash: m.hash,
|
||||
size: m.size,
|
||||
ref_count: m.ref_count,
|
||||
content_type: m.content_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert infra DedupStats to port DedupStatsDto.
|
||||
impl From<DedupStats> for DedupStatsDto {
|
||||
fn from(s: DedupStats) -> Self {
|
||||
DedupStatsDto {
|
||||
total_blobs: s.total_blobs,
|
||||
total_bytes_stored: s.total_bytes_stored,
|
||||
total_bytes_referenced: s.total_bytes_referenced,
|
||||
bytes_saved: s.bytes_saved,
|
||||
dedup_hits: s.dedup_hits,
|
||||
dedup_ratio: s.dedup_ratio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DedupPort for DedupService {
|
||||
async fn store_bytes(
|
||||
&self,
|
||||
content: &[u8],
|
||||
content_type: Option<String>,
|
||||
) -> Result<DedupResultDto, DomainError> {
|
||||
self.store_bytes(content, content_type).await
|
||||
.map(Into::into)
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Dedup", e))
|
||||
}
|
||||
|
||||
async fn store_from_file(
|
||||
&self,
|
||||
source_path: &Path,
|
||||
content_type: Option<String>,
|
||||
) -> Result<DedupResultDto, DomainError> {
|
||||
self.store_from_file(source_path, content_type).await
|
||||
.map(Into::into)
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Dedup", e))
|
||||
}
|
||||
|
||||
async fn blob_exists(&self, hash: &str) -> bool {
|
||||
self.blob_exists(hash).await
|
||||
}
|
||||
|
||||
async fn get_blob_metadata(&self, hash: &str) -> Option<BlobMetadataDto> {
|
||||
self.get_blob_metadata(hash).await.map(Into::into)
|
||||
}
|
||||
|
||||
async fn read_blob(&self, hash: &str) -> Result<Vec<u8>, DomainError> {
|
||||
self.read_blob(hash).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::NotFound, "Blob", e))
|
||||
}
|
||||
|
||||
async fn read_blob_bytes(&self, hash: &str) -> Result<Bytes, DomainError> {
|
||||
self.read_blob_bytes(hash).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::NotFound, "Blob", e))
|
||||
}
|
||||
|
||||
async fn add_reference(&self, hash: &str) -> Result<(), DomainError> {
|
||||
self.add_reference(hash).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Blob", e))
|
||||
}
|
||||
|
||||
async fn remove_reference(&self, hash: &str) -> Result<bool, DomainError> {
|
||||
self.remove_reference(hash).await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Blob", e))
|
||||
}
|
||||
|
||||
fn hash_bytes(&self, content: &[u8]) -> String {
|
||||
DedupService::hash_bytes(content)
|
||||
}
|
||||
|
||||
async fn hash_file(&self, path: &Path) -> Result<String, DomainError> {
|
||||
DedupService::hash_file(path).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> DedupStatsDto {
|
||||
self.get_stats().await.into()
|
||||
}
|
||||
|
||||
async fn flush(&self) -> Result<(), DomainError> {
|
||||
self.flush().await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn verify_integrity(&self) -> Result<Vec<String>, DomainError> {
|
||||
self.verify_integrity().await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Dedup", e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -675,7 +798,8 @@ mod tests {
|
||||
let service = DedupService::new(temp_dir.path());
|
||||
service.initialize().await.unwrap();
|
||||
|
||||
let content = b"Hello, World! This is test content.";
|
||||
// Content must be >= MIN_DEDUP_SIZE (4096 bytes) for dedup to kick in
|
||||
let content = &b"Hello, World! This is test content for dedup. ".repeat(100);
|
||||
|
||||
// First store
|
||||
let result1 = service.store_bytes(content, None).await.unwrap();
|
||||
@@ -698,7 +822,8 @@ mod tests {
|
||||
let service = DedupService::new(temp_dir.path());
|
||||
service.initialize().await.unwrap();
|
||||
|
||||
let content = b"Test content for reference counting";
|
||||
// Content must be >= MIN_DEDUP_SIZE (4096 bytes) for dedup to kick in
|
||||
let content = &b"Test content for reference counting. ".repeat(120);
|
||||
|
||||
// Store twice
|
||||
let result1 = service.store_bytes(content, None).await.unwrap();
|
||||
|
||||
@@ -213,6 +213,34 @@ pub struct CacheStats {
|
||||
/// Thread-safe wrapper for sharing across handlers
|
||||
pub type SharedFileContentCache = Arc<FileContentCache>;
|
||||
|
||||
// ─── ContentCachePort implementation ─────────────────────────
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::application::ports::cache_ports::ContentCachePort;
|
||||
|
||||
#[async_trait]
|
||||
impl ContentCachePort for FileContentCache {
|
||||
fn should_cache(&self, size: usize) -> bool {
|
||||
FileContentCache::should_cache(self, size)
|
||||
}
|
||||
|
||||
async fn get(&self, file_id: &str) -> Option<(Bytes, String, String)> {
|
||||
FileContentCache::get(self, file_id).await
|
||||
}
|
||||
|
||||
async fn put(&self, file_id: String, content: Bytes, etag: String, content_type: String) {
|
||||
FileContentCache::put(self, file_id, content, etag, content_type).await
|
||||
}
|
||||
|
||||
async fn invalidate(&self, file_id: &str) {
|
||||
FileContentCache::invalidate(self, file_id).await
|
||||
}
|
||||
|
||||
async fn clear(&self) {
|
||||
FileContentCache::clear(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -598,6 +598,56 @@ impl FileMetadataCache {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MetadataCachePort implementation ────────────────────────
|
||||
|
||||
use async_trait::async_trait;
|
||||
use crate::application::ports::cache_ports::{MetadataCachePort, CachedMetadataDto};
|
||||
use crate::common::errors::DomainError;
|
||||
|
||||
#[async_trait]
|
||||
impl MetadataCachePort for FileMetadataCache {
|
||||
async fn get_metadata(&self, path: &Path) -> Option<CachedMetadataDto> {
|
||||
// Delegate to the existing rich get_metadata, then project into the DTO.
|
||||
let fm = FileMetadataCache::get_metadata(self, path).await?;
|
||||
Some(CachedMetadataDto {
|
||||
path: fm.path,
|
||||
exists: fm.exists,
|
||||
is_file: fm.entry_type == CacheEntryType::File,
|
||||
size: fm.size,
|
||||
mime_type: fm.mime_type,
|
||||
created_at: fm.created_at,
|
||||
modified_at: fm.modified_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn is_file(&self, path: &Path) -> Option<bool> {
|
||||
FileMetadataCache::is_file(self, path).await
|
||||
}
|
||||
|
||||
async fn refresh_metadata(&self, path: &Path) -> Result<CachedMetadataDto, DomainError> {
|
||||
let fm = FileMetadataCache::refresh_metadata(self, path)
|
||||
.await
|
||||
.map_err(|e| DomainError::internal_error("MetadataCache", e.to_string()))?;
|
||||
Ok(CachedMetadataDto {
|
||||
path: fm.path,
|
||||
exists: fm.exists,
|
||||
is_file: fm.entry_type == CacheEntryType::File,
|
||||
size: fm.size,
|
||||
mime_type: fm.mime_type,
|
||||
created_at: fm.created_at,
|
||||
modified_at: fm.modified_at,
|
||||
})
|
||||
}
|
||||
|
||||
async fn invalidate(&self, path: &Path) {
|
||||
FileMetadataCache::invalidate(self, path).await
|
||||
}
|
||||
|
||||
async fn invalidate_directory(&self, dir_path: &Path) {
|
||||
FileMetadataCache::invalidate_directory(self, dir_path).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -648,10 +698,12 @@ mod tests {
|
||||
async fn test_directory_operations() {
|
||||
// Crear estructura de directorios para pruebas
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let sub_dir = temp_dir.path().join("subdir");
|
||||
// Canonicalize to handle macOS /var -> /private/var symlinks
|
||||
let base_path = temp_dir.path().canonicalize().unwrap();
|
||||
let sub_dir = base_path.join("subdir");
|
||||
fs::create_dir(&sub_dir).await.unwrap();
|
||||
|
||||
let file1 = temp_dir.path().join("file1.txt");
|
||||
let file1 = base_path.join("file1.txt");
|
||||
let file2 = sub_dir.join("file2.txt");
|
||||
|
||||
File::create(&file1).await.unwrap();
|
||||
@@ -662,20 +714,19 @@ mod tests {
|
||||
let cache = FileMetadataCache::new(config, 1000);
|
||||
|
||||
// Precargar directorio recursivamente
|
||||
let count = cache.preload_directory(temp_dir.path(), true, 2).await.unwrap();
|
||||
assert_eq!(count, 3); // dir, subdir, 2 files
|
||||
// preload_directory caches the *contents* of the directory, not the root itself
|
||||
let count = cache.preload_directory(&base_path, true, 2).await.unwrap();
|
||||
assert_eq!(count, 3); // subdir, file1, file2
|
||||
|
||||
// Verificar existencia en caché
|
||||
assert_eq!(cache.is_dir(temp_dir.path()).await, Some(true));
|
||||
// Verificar existencia en caché (solo contenido, no la raíz)
|
||||
assert_eq!(cache.is_dir(&sub_dir).await, Some(true));
|
||||
assert_eq!(cache.is_file(&file1).await, Some(true));
|
||||
assert_eq!(cache.is_file(&file2).await, Some(true));
|
||||
|
||||
// Invalidar directorio y contenido
|
||||
cache.invalidate_directory(temp_dir.path()).await;
|
||||
cache.invalidate_directory(&base_path).await;
|
||||
|
||||
// Verificar que nada existe en caché
|
||||
assert!(cache.exists(temp_dir.path()).await.is_none());
|
||||
assert!(cache.exists(&sub_dir).await.is_none());
|
||||
assert!(cache.exists(&file1).await.is_none());
|
||||
assert!(cache.exists(&file2).await.is_none());
|
||||
|
||||
@@ -515,6 +515,7 @@ impl Clone for IdMappingService {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -18,6 +18,14 @@ use bytes::Bytes;
|
||||
use lru::LruCache;
|
||||
use std::num::NonZeroUsize;
|
||||
use image::{ImageFormat, DynamicImage};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::transcode_ports::{
|
||||
ImageTranscodePort,
|
||||
OutputFormat as PortOutputFormat,
|
||||
TranscodeStatsDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Maximum file size for transcoding (5MB - larger files stream directly)
|
||||
pub const MAX_TRANSCODE_SIZE: u64 = 5 * 1024 * 1024;
|
||||
@@ -354,6 +362,59 @@ impl ImageTranscodeService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Convert port OutputFormat to infra OutputFormat.
|
||||
impl From<PortOutputFormat> for OutputFormat {
|
||||
fn from(fmt: PortOutputFormat) -> Self {
|
||||
match fmt {
|
||||
PortOutputFormat::WebP => OutputFormat::WebP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ImageTranscodePort for ImageTranscodeService {
|
||||
fn can_transcode(&self, mime_type: &str) -> bool {
|
||||
ImageTranscodeService::can_transcode(mime_type)
|
||||
}
|
||||
|
||||
fn should_transcode(&self, mime_type: &str, file_size: u64) -> bool {
|
||||
ImageTranscodeService::should_transcode(mime_type, file_size)
|
||||
}
|
||||
|
||||
async fn get_transcoded(
|
||||
&self,
|
||||
file_id: &str,
|
||||
original_content: &[u8],
|
||||
original_mime: &str,
|
||||
target_format: PortOutputFormat,
|
||||
) -> Result<(Bytes, String, bool), DomainError> {
|
||||
self.get_transcoded(file_id, original_content, original_mime, target_format.into())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ImageTranscode", e))
|
||||
}
|
||||
|
||||
async fn invalidate(&self, file_id: &str) {
|
||||
self.invalidate(file_id).await
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> TranscodeStatsDto {
|
||||
let stats = self.get_stats().await;
|
||||
TranscodeStatsDto {
|
||||
cache_hits: stats.cache_hits,
|
||||
disk_hits: stats.disk_hits,
|
||||
transcodes: stats.transcodes,
|
||||
bytes_saved: stats.bytes_saved,
|
||||
transcode_errors: stats.transcode_errors,
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear_cache(&self) -> Result<(), DomainError> {
|
||||
self.clear_cache().await.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -2,7 +2,6 @@ pub mod file_system_i18n_service;
|
||||
pub mod file_system_utils;
|
||||
pub mod id_mapping_service;
|
||||
pub mod id_mapping_optimizer;
|
||||
pub mod cache_manager;
|
||||
pub mod file_metadata_cache;
|
||||
pub mod file_content_cache;
|
||||
pub mod compression_service;
|
||||
|
||||
@@ -19,6 +19,14 @@ use image::{ImageFormat, imageops::FilterType};
|
||||
use lru::LruCache;
|
||||
use std::num::NonZeroUsize;
|
||||
use bytes::Bytes;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::thumbnail_ports::{
|
||||
ThumbnailPort,
|
||||
ThumbnailSize as PortThumbnailSize,
|
||||
ThumbnailStatsDto,
|
||||
};
|
||||
use crate::domain::errors::{DomainError, ErrorKind};
|
||||
|
||||
/// Thumbnail sizes supported by the system
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
@@ -329,6 +337,60 @@ impl ThumbnailService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Convert port ThumbnailSize to infra ThumbnailSize.
|
||||
impl From<PortThumbnailSize> for ThumbnailSize {
|
||||
fn from(size: PortThumbnailSize) -> Self {
|
||||
match size {
|
||||
PortThumbnailSize::Icon => ThumbnailSize::Icon,
|
||||
PortThumbnailSize::Preview => ThumbnailSize::Preview,
|
||||
PortThumbnailSize::Large => ThumbnailSize::Large,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ThumbnailPort for ThumbnailService {
|
||||
fn is_supported_image(&self, mime_type: &str) -> bool {
|
||||
ThumbnailService::is_supported_image(mime_type)
|
||||
}
|
||||
|
||||
async fn get_thumbnail(
|
||||
&self,
|
||||
file_id: &str,
|
||||
size: PortThumbnailSize,
|
||||
original_path: &Path,
|
||||
) -> Result<Bytes, DomainError> {
|
||||
self.get_thumbnail(file_id, size.into(), original_path)
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Thumbnail", e.to_string()))
|
||||
}
|
||||
|
||||
fn generate_all_sizes_background(
|
||||
self: Arc<Self>,
|
||||
file_id: String,
|
||||
original_path: PathBuf,
|
||||
) {
|
||||
ThumbnailService::generate_all_sizes_background(self, file_id, original_path)
|
||||
}
|
||||
|
||||
async fn delete_thumbnails(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
self.delete_thumbnails(file_id)
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "Thumbnail", e.to_string()))
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> ThumbnailStatsDto {
|
||||
let stats = self.get_stats().await;
|
||||
ThumbnailStatsDto {
|
||||
cached_thumbnails: stats.cached_thumbnails,
|
||||
cache_size_bytes: stats.cache_size_bytes,
|
||||
max_cache_bytes: stats.max_cache_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Thumbnail service errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ThumbnailError {
|
||||
|
||||
@@ -78,8 +78,8 @@ impl TrashCleanupService {
|
||||
|
||||
// Eliminar cada elemento expirado
|
||||
for item in expired_items {
|
||||
let trash_id = item.id.to_string();
|
||||
let user_id = item.user_id.to_string();
|
||||
let trash_id = item.id().to_string();
|
||||
let user_id = item.user_id().to_string();
|
||||
|
||||
debug!("Eliminando elemento expirado: id={}, user={}", trash_id, user_id);
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ use tokio::sync::{RwLock, mpsc};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use bytes::Bytes;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::application::ports::cache_ports::{WriteBehindCachePort, WriteBehindStatsDto};
|
||||
use crate::domain::errors::DomainError;
|
||||
|
||||
/// Maximum size for write-behind cache (files larger bypass cache)
|
||||
const WRITE_BEHIND_MAX_SIZE: usize = 1024 * 1024; // 1MB
|
||||
@@ -364,6 +368,56 @@ impl WriteBehindCache {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
impl WriteBehindCachePort for WriteBehindCache {
|
||||
fn is_eligible_size(&self, size: usize) -> bool {
|
||||
WriteBehindCache::is_eligible(size)
|
||||
}
|
||||
|
||||
async fn put_pending(
|
||||
&self,
|
||||
file_id: String,
|
||||
content: Bytes,
|
||||
target_path: PathBuf,
|
||||
) -> Result<bool, DomainError> {
|
||||
self.put_pending(file_id, content, target_path).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_pending(&self, file_id: &str) -> Option<Bytes> {
|
||||
self.get_pending(file_id).await
|
||||
}
|
||||
|
||||
async fn is_pending(&self, file_id: &str) -> bool {
|
||||
self.is_pending(file_id).await
|
||||
}
|
||||
|
||||
async fn force_flush(&self, file_id: &str) -> Result<(), DomainError> {
|
||||
self.force_flush(file_id).await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn flush_all(&self) -> Result<(), DomainError> {
|
||||
self.flush_all().await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), DomainError> {
|
||||
self.shutdown().await.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> WriteBehindStatsDto {
|
||||
let stats = self.get_stats().await;
|
||||
WriteBehindStatsDto {
|
||||
pending_count: stats.pending_count,
|
||||
pending_bytes: stats.pending_bytes,
|
||||
total_writes: stats.total_writes,
|
||||
total_bytes_written: stats.total_bytes_written,
|
||||
cache_hits: stats.cache_hits,
|
||||
avg_flush_time_us: stats.avg_flush_time_us,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WriteBehindCache {
|
||||
fn default() -> Self {
|
||||
// Note: This creates a non-Arc version, prefer using new()
|
||||
|
||||
@@ -2,10 +2,13 @@ use std::io::{Cursor, Read, Write};
|
||||
use zip::{ZipWriter, write::SimpleFileOptions};
|
||||
use thiserror::Error;
|
||||
use tracing::*;
|
||||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
application::dtos::file_dto::FileDto,
|
||||
application::dtos::folder_dto::FolderDto,
|
||||
application::ports::inbound::{FileUseCase, FolderUseCase},
|
||||
application::ports::inbound::FolderUseCase,
|
||||
application::ports::file_ports::FileRetrievalUseCase,
|
||||
application::ports::zip_ports::ZipPort,
|
||||
common::errors::{Result, DomainError, ErrorKind},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -45,13 +48,13 @@ impl From<zip::result::ZipError> for DomainError {
|
||||
|
||||
/// Servicio para crear archivos ZIP
|
||||
pub struct ZipService {
|
||||
file_service: Arc<dyn FileUseCase>,
|
||||
file_service: Arc<dyn FileRetrievalUseCase>,
|
||||
folder_service: Arc<dyn FolderUseCase>,
|
||||
}
|
||||
|
||||
impl ZipService {
|
||||
/// Crea una nueva instancia del servicio ZIP con una referencia al servicio de archivos
|
||||
pub fn new(file_service: Arc<dyn FileUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self {
|
||||
pub fn new(file_service: Arc<dyn FileRetrievalUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self {
|
||||
Self {
|
||||
file_service,
|
||||
folder_service,
|
||||
@@ -225,4 +228,17 @@ impl ZipService {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Port implementation ─────────────────────────────────────────────────────
|
||||
|
||||
#[async_trait]
|
||||
impl ZipPort for ZipService {
|
||||
async fn create_folder_zip(
|
||||
&self,
|
||||
folder_id: &str,
|
||||
folder_name: &str,
|
||||
) -> std::result::Result<Vec<u8>, DomainError> {
|
||||
self.create_folder_zip(folder_id, folder_name).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user