adding webdav features
This commit is contained in:
@@ -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,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
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user