adding webdav features

This commit is contained in:
DioCrafts
2025-04-04 21:31:41 +02:00
parent e3256bb3b5
commit ba3ef2a530
21 changed files with 3358 additions and 960 deletions
+3
View File
@@ -0,0 +1,3 @@
//! Adapters module for translating between external protocols and internal models
pub mod webdav_adapter;
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -2,5 +2,6 @@ pub mod dtos;
pub mod ports;
pub mod services;
pub mod transactions;
pub mod adapters;
// Re-exportaciones para facilitar el acceso a los principales puertos
// Re-exportaciones para facilitar el acceso a los principales puertos
+9
View File
@@ -23,6 +23,15 @@ pub trait FileUseCase: 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>;
/// 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>;
+6
View File
@@ -56,6 +56,12 @@ pub trait FileStoragePort: Send + Sync + 'static {
/// 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>;
}
/// Puerto secundario para persistencia de carpetas
+97
View File
@@ -140,6 +140,18 @@ impl FileService {
Ok(FileDto::empty())
}
async fn get_file_by_path(&self, _path: &str) -> Result<FileDto, DomainError> {
Ok(FileDto::empty())
}
async fn create_file(&self, _parent_path: &str, _filename: &str, _content: &[u8], _content_type: &str) -> Result<FileDto, DomainError> {
Ok(FileDto::empty())
}
async fn update_file(&self, _path: &str, _content: &[u8]) -> Result<(), DomainError> {
Ok(())
}
async fn list_files(&self, _folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError> {
Ok(vec![])
}
@@ -186,6 +198,76 @@ impl FileService {
Ok(FileDto::from(file))
}
/// Gets a file by path (needed for WebDAV)
pub async fn get_file_by_path(&self, path: &str) -> FileServiceResult<FileDto> {
// This is a simple implementation for WebDAV support
// First, normalize the path (remove leading/trailing slashes)
let path = path.trim_start_matches('/').trim_end_matches('/');
// List all files and find the one with matching path
let all_files = self.list_files(None).await?;
for file in all_files {
let file_path = file.path.trim_start_matches('/').trim_end_matches('/');
if file_path == path || file_path.ends_with(&format!("/{}", path)) || path.ends_with(&format!("/{}", file_path)) {
return Ok(file);
}
}
// If no file found, return an error
Err(FileServiceError::NotFound(format!("File not found at path: {}", path)))
}
/// Creates or updates a file at a specific path (needed for WebDAV)
pub async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> FileServiceResult<FileDto> {
// Get parent folder ID if parent path is not empty
let parent_id = if !parent_path.is_empty() {
match self.file_repository.get_parent_folder_id(parent_path).await {
Ok(id) => Some(id),
Err(_) => None // If parent doesn't exist, use root
}
} else {
None // Root folder
};
// Save the file with the provided filename and parent folder
let file = self.file_repository.save_file(
filename.to_string(),
parent_id,
content_type.to_string(),
content.to_vec()
).await.map_err(FileServiceError::from)?;
Ok(FileDto::from(file))
}
/// Updates an existing file (needed for WebDAV)
pub async fn update_file(&self, path: &str, content: &[u8]) -> FileServiceResult<()> {
// First, try to get the file by path
match self.get_file_by_path(path).await {
Ok(file) => {
// Update the file content
self.file_repository.update_file_content(&file.id, content.to_vec())
.await
.map_err(FileServiceError::from)
},
Err(_) => {
// If file doesn't exist, extract filename and parent path and create it
let path = path.trim_start_matches('/').trim_end_matches('/');
let (parent_path, filename) = if let Some(idx) = path.rfind('/') {
(&path[..idx], &path[idx+1..])
} else {
("", path)
};
// Create new file
self.create_file(parent_path, filename, content, "application/octet-stream").await?;
Ok(())
}
}
}
/// Lists files in a folder
pub async fn list_files(&self, folder_id: Option<&str>) -> FileServiceResult<Vec<FileDto>> {
let files = self.file_repository.list_files(folder_id).await
@@ -247,6 +329,21 @@ impl FileUseCase for FileService {
.map_err(DomainError::from)
}
async fn get_file_by_path(&self, path: &str) -> Result<FileDto, DomainError> {
FileService::get_file_by_path(self, path).await
.map_err(DomainError::from)
}
async fn create_file(&self, parent_path: &str, filename: &str, content: &[u8], content_type: &str) -> Result<FileDto, DomainError> {
FileService::create_file(self, parent_path, filename, content, content_type).await
.map_err(DomainError::from)
}
async fn update_file(&self, path: &str, content: &[u8]) -> Result<(), DomainError> {
FileService::update_file(self, path, content).await
.map_err(DomainError::from)
}
async fn list_files(&self, folder_id: Option<&str>) -> Result<Vec<FileDto>, DomainError> {
FileService::list_files(self, folder_id).await
.map_err(DomainError::from)
+20
View File
@@ -482,6 +482,14 @@ impl Default for AppState {
async fn get_file_path(&self, _id: &str) -> Result<crate::domain::services::path_service::StoragePath, crate::common::errors::DomainError> {
Ok(crate::domain::services::path_service::StoragePath::from_string("/"))
}
async fn get_parent_folder_id(&self, _path: &str) -> Result<String, crate::common::errors::DomainError> {
Ok("root".to_string())
}
async fn update_file_content(&self, _file_id: &str, _content: Vec<u8>) -> Result<(), crate::common::errors::DomainError> {
Ok(())
}
}
struct DummyFolderStoragePort;
@@ -618,6 +626,18 @@ impl Default for AppState {
Ok(crate::application::dtos::file_dto::FileDto::default())
}
async fn get_file_by_path(&self, _path: &str) -> Result<crate::application::dtos::file_dto::FileDto, crate::common::errors::DomainError> {
Ok(crate::application::dtos::file_dto::FileDto::default())
}
async fn create_file(&self, _parent_path: &str, _filename: &str, _content: &[u8], _content_type: &str) -> Result<crate::application::dtos::file_dto::FileDto, crate::common::errors::DomainError> {
Ok(crate::application::dtos::file_dto::FileDto::default())
}
async fn update_file(&self, _path: &str, _content: &[u8]) -> Result<(), crate::common::errors::DomainError> {
Ok(())
}
async fn list_files(&self, _folder_id: Option<&str>) -> Result<Vec<crate::application::dtos::file_dto::FileDto>, crate::common::errors::DomainError> {
Ok(Vec::new())
}
+12
View File
@@ -285,6 +285,18 @@ impl AppError {
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::INTERNAL_SERVER_ERROR, message, "InternalError")
}
pub fn method_not_allowed(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::METHOD_NOT_ALLOWED, message, "MethodNotAllowed")
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::CONFLICT, message, "Conflict")
}
pub fn unsupported_media_type(message: impl Into<String>) -> Self {
Self::new(axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE, message, "UnsupportedMediaType")
}
}
impl From<DomainError> for AppError {
@@ -224,4 +224,16 @@ pub trait FileRepository: Send + Sync + 'static {
* @return Success or error
*/
async fn delete_file_permanently(&self, file_id: &str) -> FileRepositoryResult<()>;
/**
* Updates the content of an existing file.
*
* This method replaces the binary content of a file while preserving its
* metadata like ID, creation timestamp, and location.
*
* @param file_id The unique identifier of the file to update
* @param content The new binary content for the file
* @return Success or error
*/
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> FileRepositoryResult<()>;
}
@@ -470,6 +470,89 @@ impl FileStoragePort for FileFsRepository {
.await
.map_err(|e| DomainError::internal_error("FileStorage", format!("Failed to get path for file with ID: {}: {}", id, e)))
}
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError> {
// Convert path string to StoragePath
let storage_path = StoragePath::from_string(path);
// Get parent path
let parent_path = match storage_path.parent() {
Some(parent) => parent,
None => return Ok("root".to_string()), // Root folder
};
// If it's an empty path (root), return root ID
if parent_path.is_empty() {
return Ok("root".to_string());
}
// Try to get the ID for the parent path from the ID mapping service
let parent_id = self.id_mapping_service.get_or_create_id(&parent_path).await
.map_err(|e| DomainError::internal_error("FileStorage",
format!("Failed to get parent folder ID for path: {}: {}", path, e)))?;
Ok(parent_id)
}
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> Result<(), DomainError> {
// First get the file to make sure it exists and to get its path
let file = self.get_file_by_id(file_id).await
.map_err(|e| DomainError::internal_error("FileStorage",
format!("Failed to get file for update: {}: {}", file_id, e)))?;
// Get the file path for writing
let file_path = FileStoragePort::get_file_path(self, file_id).await
.map_err(|e| DomainError::internal_error("FileStorage",
format!("Failed to get file path for update: {}: {}", file_id, e)))?;
// Resolve to actual filesystem path
let physical_path = self.storage_mediator.resolve_storage_path(&file_path);
// Write the content to the file
std::fs::write(&physical_path, content)
.map_err(|e| DomainError::internal_error("FileStorage",
format!("Failed to write updated content to file: {}: {}", file_id, e)))?;
// Get the metadata and add it to cache if available
if let Some(metadata) = std::fs::metadata(&physical_path).ok() {
// Create a FileMetadata instance and update the cache
use crate::infrastructure::services::file_metadata_cache::FileMetadata;
use crate::infrastructure::services::file_metadata_cache::CacheEntryType;
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::Duration;
// Get modified and created times
let created_at = metadata.created()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs());
let modified_at = metadata.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs());
// Default TTL
let ttl = Duration::from_secs(60); // 1 minute
// Create FileMetadata instance
let file_metadata = FileMetadata::new(
physical_path.clone(),
true, // exists
CacheEntryType::File,
Some(metadata.len()),
Some(file.mime_type().to_string()),
created_at,
modified_at,
ttl,
);
// Update the cache
self.metadata_cache.update_cache(file_metadata).await;
}
Ok(())
}
}
#[async_trait]
@@ -519,6 +602,63 @@ impl FileRepository for FileFsRepository {
}
}
}
#[instrument(skip(self, content))]
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> FileRepositoryResult<()> {
tracing::info!("FileRepository::update_file_content called for file ID: {}", file_id);
// Get the file info to verify it exists and get its path
let file = self.get_file_by_id(file_id).await?;
// Get the file path
let storage_path = FileRepository::get_file_path(self, file_id).await?;
let physical_path = self.path_service.resolve_path(&storage_path);
// Write the content to the file
std::fs::write(&physical_path, &content)
.map_err(|e| FileRepositoryError::IoError(e))?;
// Get the metadata and add it to cache if available
if let Some(metadata) = std::fs::metadata(&physical_path).ok() {
// Create a FileMetadata instance and update the cache
use crate::infrastructure::services::file_metadata_cache::FileMetadata;
use crate::infrastructure::services::file_metadata_cache::CacheEntryType;
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::Duration;
// Get modified and created times
let created_at = metadata.created()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs());
let modified_at = metadata.modified()
.ok()
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs());
// Default TTL
let ttl = Duration::from_secs(60); // 1 minute
// Create FileMetadata instance
let file_metadata = FileMetadata::new(
physical_path.clone(),
true, // exists
CacheEntryType::File,
Some(metadata.len()),
Some(file.mime_type().to_string()),
created_at,
modified_at,
ttl,
);
// Update the cache
self.metadata_cache.update_cache(file_metadata).await;
}
tracing::info!("File content updated successfully: {}", file_id);
Ok(())
}
async fn save_file_from_bytes(
&self,
name: String,
@@ -5,13 +5,9 @@ use axum::{
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use tracing::{error, info};
use uuid::Uuid;
use crate::application::ports::favorites_ports::FavoritesUseCase;
use crate::application::dtos::favorites_dto::FavoriteItemDto;
use crate::common::errors::{Result, DomainError, ErrorKind};
/// Handler for favorite-related API endpoints
pub async fn get_favorites(
+2 -2
View File
@@ -8,7 +8,7 @@ pub mod search_handler;
pub mod share_handler;
pub mod favorites_handler;
pub mod recent_handler;
pub mod webdav_handler;
/// Tipo de resultado para controladores de API
pub type ApiResult<T> = Result<T, (axum::http::StatusCode, String)>;
pub type ApiResult<T> = Result<T, (axum::http::StatusCode, String)>;
@@ -5,7 +5,7 @@ use axum::{
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use tracing::{error, info};
use crate::application::ports::recent_ports::RecentItemsUseCase;
File diff suppressed because it is too large Load Diff
+9
View File
@@ -630,6 +630,15 @@ pub fn create_api_routes(
if trash_service.is_some() {
tracing::info!("Trash service is available - trash view is functional");
}
// Add WebDAV routes if needed
let webdav_enabled = true; // In production, you'd read this from a config
let router = if webdav_enabled {
use crate::interfaces::api::handlers::webdav_handler;
router.merge(webdav_handler::webdav_routes())
} else {
router
};
router
.layer(CompressionLayer::new())
+11
View File
@@ -358,6 +358,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
}
async fn update_file_content(&self, file_id: &str, content: Vec<u8>) -> domain::repositories::file_repository::FileRepositoryResult<()> {
tracing::info!("Updating content for file: {}", file_id);
self.repo.update_file_content(file_id, content)
.await
.map_err(|e| {
tracing::error!("Failed to update file content: {}", e);
domain::repositories::file_repository::FileRepositoryError::Other(format!("Failed to update file content: {}", e))
})
}
}
struct DomainFolderRepoAdapter {