modernizing frontend

This commit is contained in:
Diocrafts
2026-02-08 22:44:42 +01:00
parent 03b409bbbc
commit 5bd505ccd7
31 changed files with 3262 additions and 451 deletions
+3
View File
@@ -125,6 +125,9 @@ pub trait FileManagementUseCase: Send + Sync + 'static {
/// Mueve un archivo a otra carpeta
async fn move_file(&self, file_id: &str, folder_id: Option<String>) -> Result<FileDto, DomainError>;
/// Renombra un archivo
async fn rename_file(&self, file_id: &str, new_name: &str) -> Result<FileDto, DomainError>;
/// Elimina un archivo
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
+7
View File
@@ -91,6 +91,13 @@ pub trait FileWritePort: Send + Sync + 'static {
target_folder_id: Option<String>,
) -> Result<File, DomainError>;
/// Renombra un archivo (same folder, different name).
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError>;
/// Elimina un archivo.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
@@ -109,6 +109,27 @@ impl FileManagementUseCase for FileManagementService {
Ok(FileDto::from(moved_file))
}
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<FileDto, DomainError> {
info!("Renaming file with ID: {} to \"{}\"", file_id, new_name);
let renamed_file = self.file_repository.rename_file(file_id, new_name).await.map_err(|e| {
error!("Error renaming file (ID: {}): {}", file_id, e);
e
})?;
info!(
"File renamed successfully: {} (ID: {})",
renamed_file.name(),
renamed_file.id()
);
Ok(FileDto::from(renamed_file))
}
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
self.file_repository.delete_file(id).await
}
+16
View File
@@ -314,6 +314,14 @@ impl FileWritePort for StubFileWritePort {
Ok(File::default())
}
async fn rename_file(
&self,
_file_id: &str,
_new_name: &str,
) -> Result<File, DomainError> {
Ok(File::default())
}
async fn delete_file(&self, _id: &str) -> Result<(), DomainError> {
Ok(())
}
@@ -640,6 +648,14 @@ impl FileManagementUseCase for StubFileManagementUseCase {
Ok(FileDto::default())
}
async fn rename_file(
&self,
_file_id: &str,
_new_name: &str,
) -> Result<FileDto, DomainError> {
Ok(FileDto::default())
}
async fn delete_file(&self, _id: &str) -> Result<(), DomainError> {
Ok(())
}
@@ -97,6 +97,13 @@ pub trait FileWriteRepository: Send + Sync + 'static {
target_folder_id: Option<String>,
) -> Result<File, DomainError>;
/// Renombra un archivo (same folder, different name).
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError>;
/// Elimina un archivo.
async fn delete_file(&self, id: &str) -> Result<(), DomainError>;
@@ -107,6 +107,14 @@ impl FileWritePort for CompositeFileRepository {
self.write.move_file(file_id, target_folder_id).await
}
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError> {
self.write.rename_file(file_id, new_name).await
}
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
self.write.delete_file(id).await
}
@@ -372,6 +372,55 @@ impl FileWritePort for FileFsWriteRepository {
.map_err(|e| DomainError::internal_error("File", e.to_string()))
}
async fn rename_file(
&self,
file_id: &str,
new_name: &str,
) -> Result<File, DomainError> {
// 1. Get current file info
let original_path = self.id_mapping_service.get_path_by_id(file_id).await?;
let old_abs = self.resolve_storage_path(&original_path);
if !old_abs.exists() || !old_abs.is_file() {
return Err(DomainError::not_found("File", file_id.to_string()));
}
let (size, created_at, modified_at) = self.get_file_metadata_raw(&old_abs).await.map_err(map_repo_err)?;
// 2. Build new path (same parent directory, different filename)
let parent = original_path.parent()
.unwrap_or_else(|| StoragePath::new(vec![]));
let new_storage_path = parent.join(new_name);
if self.file_exists_at_storage_path(&new_storage_path).await.map_err(map_repo_err)? {
return Err(DomainError::already_exists("File",
format!("File already exists: {}", new_name)));
}
let new_abs = self.resolve_storage_path(&new_storage_path);
let mime = from_path(&new_abs).first_or_octet_stream().to_string();
// 3. Rename on disk
time::timeout(
self.config.timeouts.file_timeout(),
FileSystemUtils::rename_with_sync(&old_abs, &new_abs),
).await
.map_err(|_| DomainError::internal_error("File", "Timeout renaming file"))?
.map_err(|e| DomainError::internal_error("File", e.to_string()))?;
// 4. Update id→path mapping
self.id_mapping_service.update_path(file_id, &new_storage_path).await?;
let _ = self.id_mapping_service.save_changes().await;
File::with_timestamps(
file_id.to_string(),
new_name.to_string(),
new_storage_path,
size,
mime,
None,
created_at,
modified_at,
)
.map_err(|e| DomainError::internal_error("File", e.to_string()))
}
async fn delete_file(&self, id: &str) -> Result<(), DomainError> {
let storage_path = self.id_mapping_service.get_path_by_id(id).await?;
let abs_path = self.resolve_storage_path(&storage_path);
@@ -540,6 +540,41 @@ impl FileHandler {
// MOVE
// ═══════════════════════════════════════════════════════════════════════
/// Renames a file
pub async fn rename_file(
State(state): State<GlobalState>,
Path(id): Path<String>,
Json(payload): Json<serde_json::Value>,
) -> impl IntoResponse {
let new_name = match payload.get("name").and_then(|v| v.as_str()) {
Some(name) if !name.trim().is_empty() => name.trim().to_string(),
_ => {
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
"error": "Missing or empty 'name' field"
}))).into_response();
}
};
tracing::info!("Renaming file {} to \"{}\"", id, new_name);
let mgmt = &state.applications.file_management_service;
match mgmt.rename_file(&id, &new_name).await {
Ok(file_dto) => (StatusCode::OK, Json(file_dto)).into_response(),
Err(err) => {
tracing::error!("Error renaming file: {}", err);
let status = if err.to_string().contains("not found") || err.to_string().contains("NotFound") {
StatusCode::NOT_FOUND
} else if err.to_string().contains("already exists") {
StatusCode::CONFLICT
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
(status, Json(serde_json::json!({
"error": format!("Error renaming file: {}", err)
}))).into_response()
}
}
}
/// Moves a file to a different folder
pub async fn move_file(
State(state): State<GlobalState>,
+15 -1
View File
@@ -2,13 +2,23 @@ use std::sync::Arc;
use axum::{
routing::{get, post, put, delete},
Router,
response::Json as AxumJson,
};
use serde_json::json;
use tower_http::{
compression::CompressionLayer,
trace::TraceLayer,
};
use crate::common::di::AppState;
/// Returns the application version from Cargo.toml (compile-time constant)
async fn get_version() -> AxumJson<serde_json::Value> {
AxumJson(json!({
"name": "OxiCloud",
"version": env!("CARGO_PKG_VERSION")
}))
}
use crate::interfaces::middleware::cache::{HttpCache, start_cache_cleanup_task};
use crate::application::services::batch_operations::BatchOperationService;
@@ -57,6 +67,9 @@ pub fn create_public_api_routes(app_state: &AppState) -> Router<AppState> {
router = router.nest("/i18n", i18n_router);
}
// Version endpoint — public, no auth required
router = router.route("/version", get(get_version));
router
}
@@ -134,7 +147,8 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
// File operations with trash support
let file_operations_router = Router::new()
.route("/{id}", delete(FileHandler::delete_file))
.route("/{id}/move", put(FileHandler::move_file_simple));
.route("/{id}/move", put(FileHandler::move_file_simple))
.route("/{id}/rename", put(FileHandler::rename_file));
// Merge the routers
let files_router = basic_file_router.merge(file_operations_router);