diff --git a/src/infrastructure/repositories/file_fs_repository.rs b/src/infrastructure/repositories/file_fs_repository.rs index f0539d70..2638ff10 100644 --- a/src/infrastructure/repositories/file_fs_repository.rs +++ b/src/infrastructure/repositories/file_fs_repository.rs @@ -677,9 +677,14 @@ impl FileRepository for FileFsRepository { match self.storage_mediator.get_folder_path(id).await { Ok(path) => { tracing::info!("Using folder path: {:?} for folder_id: {:?}", path, id); - // Convert to StoragePath - let path_str = path.to_string_lossy().to_string(); - StoragePath::from_string(&path_str) + // Convert to StoragePath - use just the folder name to avoid path duplication + // Get just the folder name to avoid path duplication + let lossy = path.to_string_lossy().to_string(); + let folder_name = path.file_name() + .and_then(|f| f.to_str()) + .unwrap_or_else(|| &lossy); + tracing::info!("Using folder name: {} for StoragePath", folder_name); + StoragePath::from_string(folder_name) }, Err(e) => { tracing::error!("Error getting folder: {}", e); @@ -863,13 +868,56 @@ impl FileRepository for FileFsRepository { ).await?; // Ensure ID mapping is persisted - this is critical for later retrieval - let save_result = self.id_mapping_service.save_changes().await; - if let Err(e) = &save_result { - tracing::error!("Failed to save ID mapping for file {}: {}", id, e); - } else { - tracing::info!("Successfully saved ID mapping for file ID: {} -> path: {}", id, path_string); + // Ejecutar múltiples intentos de guardado con verificación para garantizar persistencia + for attempt in 1..=3 { + match self.id_mapping_service.save_changes().await { + Ok(_) => { + tracing::info!("Successfully saved ID mapping for file ID: {} -> path: {} (attempt {})", id, path_string, attempt); + + // Verificar que el mapeo se puede recuperar después de guardado + if let Ok(verified_path) = self.id_mapping_service.get_path_by_id(&id).await { + if verified_path.to_string() == path_string { + tracing::info!("Verified ID mapping is retrievable after save: {} -> {}", id, path_string); + break; // Guaradado correcto y verificado, salir del bucle + } else { + tracing::error!("Mapping verification failed: expected {} but got {}", path_string, verified_path.to_string()); + if attempt < 3 { + tracing::info!("Will retry saving ID mapping (attempt {}/3)", attempt + 1); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + continue; + } else { + return Err(FileRepositoryError::Other( + format!("Failed to verify ID mapping for file: {} after 3 attempts", id) + )); + } + } + } else { + tracing::error!("Cannot verify mapping, ID {} not found after save", id); + if attempt < 3 { + tracing::info!("Will retry saving ID mapping (attempt {}/3)", attempt + 1); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + continue; + } else { + return Err(FileRepositoryError::Other( + format!("Failed to verify ID mapping for file: {} after 3 attempts", id) + )); + } + } + }, + Err(e) => { + tracing::error!("Failed to save ID mapping for file {}: {} (attempt {})", id, e, attempt); + if attempt < 3 { + tracing::info!("Will retry saving ID mapping (attempt {}/3)", attempt + 1); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + continue; + } else { + return Err(FileRepositoryError::Other( + format!("Failed to save ID mapping for file: {} after 3 attempts - {}", id, e) + )); + } + } + } } - save_result?; // Invalidate any directory cache entries for the parent folders // to ensure directory listings show the new file @@ -896,9 +944,14 @@ impl FileRepository for FileFsRepository { match self.storage_mediator.get_folder_path(fid).await { Ok(path) => { tracing::info!("Using folder path: {:?} for folder_id: {:?}", path, fid); - // Convert to StoragePath - let path_str = path.to_string_lossy().to_string(); - StoragePath::from_string(&path_str) + // Convert to StoragePath - use just the folder name to avoid path duplication + // Get just the folder name to avoid path duplication + let lossy = path.to_string_lossy().to_string(); + let folder_name = path.file_name() + .and_then(|f| f.to_str()) + .unwrap_or_else(|| &lossy); + tracing::info!("Using folder name: {} for StoragePath", folder_name); + StoragePath::from_string(folder_name) }, Err(e) => { tracing::error!("Error getting folder: {}", e); @@ -1143,8 +1196,14 @@ impl FileRepository for FileFsRepository { match self.storage_mediator.get_folder_path(id).await { Ok(path) => { tracing::info!("Found folder with path: {:?}", path); - let path_str = path.to_string_lossy().to_string(); - StoragePath::from_string(&path_str) + // Convert to StoragePath - use just the folder name to avoid path duplication + // Get just the folder name to avoid path duplication + let lossy = path.to_string_lossy().to_string(); + let folder_name = path.file_name() + .and_then(|f| f.to_str()) + .unwrap_or_else(|| &lossy); + tracing::info!("Using folder name: {} for StoragePath", folder_name); + StoragePath::from_string(folder_name) }, Err(e) => { tracing::error!("Error getting folder by ID: {}: {}", id, e); @@ -1155,8 +1214,8 @@ impl FileRepository for FileFsRepository { None => StoragePath::root(), }; - // Get the absolute folder path - let abs_folder_path = self.resolve_storage_path(&folder_storage_path); + // Get the absolute folder path without duplicate ./storage prefix + let abs_folder_path = self.path_service.resolve_path(&folder_storage_path); tracing::info!("Absolute folder path: {:?}", abs_folder_path); // Check if the directory exists @@ -1475,8 +1534,14 @@ impl FileRepository for FileFsRepository { Some(folder_id) => { match self.storage_mediator.get_folder_path(folder_id).await { Ok(path) => { - let path_str = path.to_string_lossy().to_string(); - StoragePath::from_string(&path_str) + // Convert to StoragePath - use just the folder name to avoid path duplication + // Get just the folder name to avoid path duplication + let lossy = path.to_string_lossy().to_string(); + let folder_name = path.file_name() + .and_then(|f| f.to_str()) + .unwrap_or_else(|| &lossy); + tracing::info!("Target folder name: {} for StoragePath", folder_name); + StoragePath::from_string(folder_name) }, Err(e) => { return Err(FileRepositoryError::Other( diff --git a/src/infrastructure/services/id_mapping_service.rs b/src/infrastructure/services/id_mapping_service.rs index 446b17f9..d3395b7a 100644 --- a/src/infrastructure/services/id_mapping_service.rs +++ b/src/infrastructure/services/id_mapping_service.rs @@ -70,7 +70,7 @@ struct IdMap { } /// Constantes para configuración -const SAVE_DEBOUNCE_MS: u64 = 300; // Tiempo para agrupar operaciones de guardado +const SAVE_DEBOUNCE_MS: u64 = 0; // Sin debounce para garantizar guardado inmediato /// Servicio para gestionar mapeos entre rutas y IDs únicos pub struct IdMappingService { @@ -389,7 +389,7 @@ impl IdMappingService { } } - /// Guarda cambios pendientes al disco + /// Guarda cambios pendientes al disco inmediatamente, sin debounce pub async fn save_pending_changes(&self) -> Result<(), IdMappingError> { // Verificar si hay cambios pendientes { @@ -399,20 +399,55 @@ impl IdMappingService { } } - // Implementar debounce para agrupación de guardados - let map_path = self.map_path.clone(); - let self_clone = self.clone(); - - tokio::spawn(async move { - // Esperar un poco para permitir la agrupación de operaciones - time::sleep(Duration::from_millis(SAVE_DEBOUNCE_MS)).await; - - if let Err(e) = self_clone.save_id_map().await { - tracing::error!("Failed to save ID map to {}: {}", map_path.display(), e); + // Guardar inmediatamente (sin debounce ni spawn) + match self.save_id_map().await { + Ok(_) => { + tracing::info!("ID mappings saved successfully to disk at {}", self.map_path.display()); + + // Verificar explícitamente que el archivo existe y tiene tamaño + match std::fs::metadata(&self.map_path) { + Ok(metadata) => { + if metadata.len() > 0 { + tracing::info!("Verified saved map file exists with size: {} bytes", metadata.len()); + } else { + tracing::warn!("Map file exists but has zero size - this might cause issues"); + } + }, + Err(e) => { + tracing::error!("Failed to verify saved map file: {}", e); + // Intentar un segundo guardado si la verificación falla + if let Err(retry_err) = self.save_id_map().await { + tracing::error!("Second save attempt also failed: {}", retry_err); + return Err(IdMappingError::IoError(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to verify and retry save: {}", retry_err) + ))); + } + tracing::info!("Second save attempt succeeded"); + } + } + + Ok(()) + }, + Err(e) => { + tracing::error!("Failed to save ID map to {}: {}", self.map_path.display(), e); + // Intentar un segundo guardado con retraso en caso de error + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + match self.save_id_map().await { + Ok(_) => { + tracing::info!("Second save attempt succeeded after initial failure"); + Ok(()) + }, + Err(retry_e) => { + tracing::error!("Second save attempt also failed: {}", retry_e); + Err(IdMappingError::IoError(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to save ID mappings after retry: {}", retry_e) + ))) + } + } } - }); - - Ok(()) + } } } diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index 47ce98d2..119ad3ee 100644 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -122,9 +122,33 @@ impl FileHandler { // Log additional debugging information tracing::info!("Created file details: folder_id={:?}, size={}, path={}", file.folder_id, file.size, file.path); + + // VERIFICACIÓN ADICIONAL: Comprobar que el archivo es accesible inmediatamente después de subir + let file_id = file.id.clone(); // Clonar para uso en la verificación + match service.get_file(&file_id).await { + Ok(_) => tracing::info!("Verified file is immediately accessible after upload: {}", file_id), + Err(e) => { + tracing::warn!("File uploaded but not immediately accessible: {} - {}. This could cause issues in frontend.", file_id, e); + // Esperar un momento y comprobar de nuevo + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + if let Err(retry_e) = service.get_file(&file_id).await { + tracing::error!("File still not accessible after retry: {} - {}", file_id, retry_e); + } else { + tracing::info!("File became accessible after short delay: {}", file_id); + } + } + } - // Return success response with file information - (StatusCode::CREATED, Json(file)).into_response() + // Añadir cabecera para evitar caché del navegador en respuestas + let response = Response::builder() + .status(StatusCode::CREATED) + .header("Cache-Control", "no-cache, no-store, must-revalidate") + .header("Pragma", "no-cache") + .header("Expires", "0") + .body(axum::body::Body::from(serde_json::to_string(&file).unwrap())) + .unwrap(); + + response }, Err(err) => { tracing::error!("Error uploading file '{}' through service: {}", filename, err); @@ -434,8 +458,16 @@ impl FileHandler { tracing::info!("No files found in folder through service"); } - // Return the files as JSON response - (StatusCode::OK, Json(files)).into_response() + // Devolver respuesta con cabeceras para evitar caché del navegador + let response = Response::builder() + .status(StatusCode::OK) + .header("Cache-Control", "no-cache, no-store, must-revalidate") + .header("Pragma", "no-cache") + .header("Expires", "0") + .body(axum::body::Body::from(serde_json::to_string(&files).unwrap())) + .unwrap(); + + response }, Err(err) => { tracing::error!("Error listing files through service: {}", err); diff --git a/static/index.html b/static/index.html index 8feb45f6..e04cf0a3 100644 --- a/static/index.html +++ b/static/index.html @@ -17,17 +17,18 @@ - - - - + - + + + + + diff --git a/static/js/app.js b/static/js/app.js index 9501b3b1..ce028788 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -383,8 +383,21 @@ function setupEventListeners() { /** * Load files and folders for the current path */ -async function loadFiles() { +async function loadFiles(options = {}) { try { + console.log("Iniciando loadFiles() - cargando archivos...", options); + + // Flag para forzar el refresco completo ignorando caché + const forceRefresh = options.forceRefresh || false; + + // Prevenir múltiples solicitudes de carga simultáneas + if (window.isLoadingFiles) { + console.log("Ya hay una carga de archivos en progreso, ignorando solicitud"); + return; + } + + window.isLoadingFiles = true; + // Always ensure a userHomeFolderId is set if (!app.userHomeFolderId) { // If we don't have a home folder ID yet, try to get the user's username @@ -392,35 +405,51 @@ async function loadFiles() { const userData = JSON.parse(localStorage.getItem(USER_DATA_KEY) || '{}'); if (userData.username) { // Find user's home folder + console.log("Buscando carpeta de usuario para", userData.username); await findUserHomeFolder(userData.username); } } + // Agregar timestamp para evitar caché + const timestamp = new Date().getTime(); let url; + // ALWAYS use the userHomeFolderId (current folder or home folder) to avoid showing root if (!app.currentPath || app.currentPath === '') { // If at root, force user to their home folder if (app.userHomeFolderId) { - url = `/api/folders/${app.userHomeFolderId}/contents`; + url = `/api/folders/${app.userHomeFolderId}/contents?t=${timestamp}`; app.currentPath = app.userHomeFolderId; ui.updateBreadcrumb(app.userHomeFolderName || 'Home'); + console.log(`Cargando carpeta del usuario: ${app.userHomeFolderName} (${app.userHomeFolderId})`); } else { // Emergency fallback - this should rarely happen but prevents errors - url = '/api/folders'; + url = `/api/folders?t=${timestamp}`; console.warn("Emergency fallback to root folder - this should not normally happen"); } } else { // Normal case - viewing subfolder contents - url = `/api/folders/${app.currentPath}/contents`; + url = `/api/folders/${app.currentPath}/contents?t=${timestamp}`; + console.log(`Cargando contenido de subcarpeta: ${app.currentPath}`); } const token = localStorage.getItem('oxicloud_token'); const requestOptions = { headers: { - 'Authorization': `Bearer ${token}` - } + 'Authorization': `Bearer ${token}`, + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache' + }, + cache: 'no-store' // Instruir al navegador a no usar caché }; + // Si se especifica forceRefresh, agregar un parámetro adicional para evitar caché + if (forceRefresh) { + url += `&force_refresh=true`; + requestOptions.headers['X-Force-Refresh'] = 'true'; + console.log('Forzando refresco completo ignorando caché'); + } + console.log(`Loading files from ${url}`); const response = await fetch(url, requestOptions); @@ -490,10 +519,12 @@ async function loadFiles() { }); // Also load files in this folder - let filesUrl = '/api/files'; + const cacheTimestamp = new Date().getTime(); + let filesUrl = `/api/files?t=${cacheTimestamp}`; // Agregar timestamp para evitar problemas de caché if (app.currentPath) { - filesUrl += `?folder_id=${app.currentPath}`; + filesUrl += `&folder_id=${app.currentPath}`; } + console.log(`Cargando archivos desde: ${filesUrl}`); try { console.log(`Fetching files from: ${filesUrl}`); @@ -532,6 +563,9 @@ async function loadFiles() { } catch (error) { console.error('Error loading folders:', error); ui.showNotification('Error', 'Could not load files and folders'); + } finally { + // Marcar que ya no estamos cargando archivos para permitir solicitudes futuras + window.isLoadingFiles = false; } } diff --git a/static/js/fileOperations.js b/static/js/fileOperations.js index ade9a2e1..0b040150 100644 --- a/static/js/fileOperations.js +++ b/static/js/fileOperations.js @@ -39,7 +39,13 @@ const fileOps = { const response = await fetch('/api/files/upload', { method: 'POST', - body: formData + body: formData, + // Añadir cache: 'no-store' para evitar problemas de caché durante la subida + cache: 'no-store', + headers: { + // Agregar este encabezado para forzar recargas frescas + 'Cache-Control': 'no-cache, no-store, must-revalidate' + } }); console.log('Respuesta del servidor:', { @@ -55,11 +61,57 @@ const fileOps = { if (response.ok) { const responseData = await response.json(); console.log(`Successfully uploaded ${file.name}`, responseData); + + // Agregar el archivo a la vista inmediatamente para mostrar retroalimentación instantánea + // Esto permite que el usuario vea el archivo aunque el refresco posterior falle + if (window.ui && window.ui.addFileToView) { + console.log('Añadiendo archivo subido directamente a la vista:', responseData); + window.ui.addFileToView(responseData); + window.ui.updateFileIcons(); + } if (i === totalFiles - 1) { // Last file uploaded console.log('Recargando lista de archivos después de subida'); - window.loadFiles(); + + try { + // Esperar 1500ms para asegurar que los mapeos de ID se guarden + // Tiempo aumentado significativamente para permitir al backend completar persistencia + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Forzar recarga de archivos con parámetro de bypass de caché + const timestamp = new Date().getTime(); + const filesUrl = `/api/files?t=${timestamp}&folder_id=${window.app.currentPath || ''}`; + + console.log(`Recargando lista de archivos desde: ${filesUrl}`); + const filesResponse = await fetch(filesUrl, { + cache: 'no-store', + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache' + } + }); + + if (filesResponse.ok) { + const files = await filesResponse.json(); + console.log(`Recarga de archivos completada, obtenidos ${files.length} archivos`); + } + + // Esperar un momento más antes de la recarga final + await new Promise(resolve => setTimeout(resolve, 500)); + + // Hacer una única recarga final forzando refresco completo + await window.loadFiles({forceRefresh: true}); + + // Asegurarse de que no haya recargas adicionales o duplicados + } catch (reloadError) { + console.error("Error durante recarga de archivos:", reloadError); + // Intentar recargar lista de nuevo en caso de error + await window.loadFiles({forceRefresh: true}); + // Segundo intento con retraso y forzando refresco + setTimeout(() => window.loadFiles({forceRefresh: true}), 500); + } + setTimeout(() => { document.getElementById('dropzone').style.display = 'none'; uploadProgressDiv.style.display = 'none'; diff --git a/static/js/inlineViewer.js b/static/js/inlineViewer.js index f44929e3..a5c06482 100644 --- a/static/js/inlineViewer.js +++ b/static/js/inlineViewer.js @@ -15,6 +15,13 @@ class InlineViewer { return; } + // Verify document.body exists + if (!document.body) { + console.warn('Document body not available yet for inline viewer, will retry later'); + setTimeout(() => this.setupViewer(), 200); + return; + } + // Create modal container const modal = document.createElement('div'); modal.id = 'inline-viewer-modal'; @@ -325,5 +332,21 @@ class InlineViewer { } } -// Initialize viewer -window.inlineViewer = new InlineViewer(); \ No newline at end of file +// Initialize viewer when document is ready +document.addEventListener('DOMContentLoaded', () => { + // Check if it's already initialized + if (!window.inlineViewer) { + console.log('Initializing inline viewer on DOMContentLoaded'); + window.inlineViewer = new InlineViewer(); + } +}); + +// Fallback initialization for cases where DOMContentLoaded already fired +if (document.readyState === 'complete' || document.readyState === 'interactive') { + if (!window.inlineViewer) { + console.log('Fallback initialization for inline viewer'); + setTimeout(() => { + window.inlineViewer = new InlineViewer(); + }, 100); + } +} \ No newline at end of file diff --git a/static/js/ui.js b/static/js/ui.js index 0a00a609..786f9451 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -518,6 +518,15 @@ const ui = { * @param {Object} folder - Folder object */ addFolderToView(folder) { + // Verificar si la carpeta ya existe en la vista para evitar duplicados + if (document.querySelector(`.file-card[data-folder-id="${folder.id}"]`) || + document.querySelector(`.file-item[data-folder-id="${folder.id}"]`)) { + console.log(`Carpeta ${folder.name} (${folder.id}) ya existe en la vista, no duplicando`); + return; + } + + console.log(`Añadiendo carpeta a la vista: ${folder.name} (${folder.id})`); + // Grid view element const folderGridElement = document.createElement('div'); folderGridElement.className = 'file-card'; @@ -709,6 +718,15 @@ const ui = { * @param {Object} file - File object */ addFileToView(file) { + // Verificar si el archivo ya existe en la vista para evitar duplicados + if (document.querySelector(`.file-card[data-file-id="${file.id}"]`) || + document.querySelector(`.file-item[data-file-id="${file.id}"]`)) { + console.log(`Archivo ${file.name} (${file.id}) ya existe en la vista, no duplicando`); + return; + } + + console.log(`Añadiendo archivo a la vista: ${file.name} (${file.id})`); + // Determine icon and type let iconClass = 'fas fa-file'; let iconSpecialClass = '';