diff --git a/src/handlers/file_handler.rs b/src/handlers/file_handler.rs index afb592d..504f31d 100644 --- a/src/handlers/file_handler.rs +++ b/src/handlers/file_handler.rs @@ -307,12 +307,7 @@ impl FileHandler { match self .app_state .file - .find_file_by_local_path( - &req.account_hash, - &normalized_file_path, - &req.filename, - req.revision, - ) + .find_file_by_local_path(&req.account_hash, &normalized_file_path, &req.filename) .await { Ok(Some(info)) => { diff --git a/src/services/file_service.rs b/src/services/file_service.rs index b60851e..87cb4d9 100644 --- a/src/services/file_service.rs +++ b/src/services/file_service.rs @@ -606,14 +606,13 @@ impl FileService { // Try to find actual active file by path/name // This handles the case where client has wrong file_id (from previous deleted file) - // revision=0 means get the latest active file + // Now searches by updated_time (latest active file) match self .storage .find_file_by_path_and_name( &file_info.account_hash, &file_info.file_path, &file_info.filename, - 0, // revision=0: get latest active file (highest revision, not deleted) ) .await { @@ -808,11 +807,10 @@ impl FileService { account_hash: &str, file_path: &str, filename: &str, - revision: i64, ) -> Result, StorageError> { debug!( - "Finding file by local path: account={}, path={}, filename={}, revision={}", - account_hash, file_path, filename, revision + "Finding file by local path (by updated_time): account={}, path={}, filename={}", + account_hash, file_path, filename ); // Normalize file path to preserve tilde (~) prefix for home directory @@ -822,9 +820,9 @@ impl FileService { file_path, normalized_file_path ); - // Query file information from storage + // Query file information from storage (searches by updated_time for latest active file) self.storage - .find_file_by_path_and_name(account_hash, &normalized_file_path, filename, revision) + .find_file_by_path_and_name(account_hash, &normalized_file_path, filename) .await } diff --git a/src/storage/memory.rs b/src/storage/memory.rs index a6133e9..cd26478 100644 --- a/src/storage/memory.rs +++ b/src/storage/memory.rs @@ -656,22 +656,28 @@ impl Storage for MemoryStorage { account_hash: &str, file_path: &str, filename: &str, - revision: i64, ) -> crate::storage::Result> { let data = self.data.lock().await; - for file in data.files.values() { - // Check if account, path, filename, and revision match - if file.account_hash == account_hash - && file.file_path == file_path - && file.filename == filename - && (revision == 0 || file.revision == revision) - { - return Ok(Some(file.clone())); - } + // Find all matching files and return the most recently updated one + let mut matching_files: Vec<&FileInfo> = data + .files + .values() + .filter(|file| { + file.account_hash == account_hash + && file.file_path == file_path + && file.filename == filename + }) + .collect(); + + if matching_files.is_empty() { + return Ok(None); } - Ok(None) + // Sort by updated_time descending (most recent first) + matching_files.sort_by(|a, b| b.updated_time.seconds.cmp(&a.updated_time.seconds)); + + Ok(matching_files.first().map(|&f| f.clone())) } /// Find file by path, filename, and group ID diff --git a/src/storage/mod.rs b/src/storage/mod.rs index fe2f642..be0a14d 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -316,7 +316,6 @@ pub trait Storage: Sync + Send { account_hash: &str, file_path: &str, filename: &str, - revision: i64, ) -> Result>; async fn find_file_by_criteria( &self, diff --git a/src/storage/mysql.rs b/src/storage/mysql.rs index 232332d..10a8368 100644 --- a/src/storage/mysql.rs +++ b/src/storage/mysql.rs @@ -1422,10 +1422,8 @@ impl Storage for MySqlStorage { account_hash: &str, file_path: &str, filename: &str, - revision: i64, ) -> Result> { - MySqlFileExt::find_file_by_path_and_name(self, account_hash, file_path, filename, revision) - .await + MySqlFileExt::find_file_by_path_and_name(self, account_hash, file_path, filename).await } /// Find file by criteria diff --git a/src/storage/mysql_file.rs b/src/storage/mysql_file.rs index 95025ec..20251d9 100644 --- a/src/storage/mysql_file.rs +++ b/src/storage/mysql_file.rs @@ -41,7 +41,6 @@ pub trait MySqlFileExt { account_hash: &str, file_path: &str, filename: &str, - revision: i64, ) -> Result>; /// Search file by path, filename and group ID @@ -161,10 +160,10 @@ impl MySqlFileExt for MySqlStorage { return Ok(file_info.file_id); } - debug!("🔍 Querying maximum revision..."); - // Query maximum revision of all files (including deleted files) with the same path and filename - let max_revision: Option = sqlx::query_scalar( - r#"SELECT COALESCE(MAX(revision), 0) FROM files WHERE account_hash = ? AND file_path = ? AND filename = ? AND server_group_id = ?"# + debug!("🔍 Checking active files..."); + // Check if there is a non-deleted file with the same file path and name + let existing_active_file: Option<(u64, i64)> = sqlx::query_as( + r#"SELECT file_id, revision FROM files WHERE account_hash = ? AND file_path = ? AND filename = ? AND server_group_id = ? AND is_deleted = FALSE ORDER BY updated_time DESC, revision DESC LIMIT 1"# ) .bind(&file_info.account_hash) .bind(&file_info.file_path) @@ -172,9 +171,22 @@ impl MySqlFileExt for MySqlStorage { .bind(file_info.group_id) .fetch_optional(&mut *tx) .await - .map_err(|e| { error!("❌ Maximum revision query failed(sqlx): {}", e); StorageError::Database(format!("Maximum revision query failed: {}", e)) })?; + .map_err(|e| { error!("❌ Active file existence check failed(sqlx): {}", e); StorageError::Database(format!("Active file existence check failed: {}", e)) })?; + + let new_revision = if let Some((existing_file_id, existing_revision)) = existing_active_file + { + // Active file exists → this is an update, increment revision + debug!( + "📝 Active file found (file_id={}, revision={}), incrementing revision", + existing_file_id, existing_revision + ); + existing_revision + 1 + } else { + // No active file → this is a new file (or re-upload after deletion), reset to revision 1 + debug!("✨ No active file found, starting with revision 1"); + 1 + }; - let new_revision = max_revision.unwrap_or(0) + 1; debug!( "📄 Preparing to store new file information: file_id={}, revision={} (key_id: {:?})", file_info.file_id, new_revision, file_info.key_id @@ -182,20 +194,7 @@ impl MySqlFileExt for MySqlStorage { // Path encryption and index computation omitted as it's handled elsewhere - debug!("🔍 Checking active files..."); - // Check if there is a non-deleted file with the same file path and name - let existing_active_file: Option = sqlx::query_scalar( - r#"SELECT file_id FROM files WHERE account_hash = ? AND file_path = ? AND filename = ? AND server_group_id = ? AND is_deleted = FALSE LIMIT 1"# - ) - .bind(&file_info.account_hash) - .bind(&file_info.file_path) - .bind(&file_info.filename) - .bind(file_info.group_id) - .fetch_optional(&mut *tx) - .await - .map_err(|e| { error!("❌ Active file existence check failed(sqlx): {}", e); StorageError::Database(format!("Active file existence check failed: {}", e)) })?; - - if let Some(existing_file_id) = existing_active_file { + if let Some((existing_file_id, _)) = existing_active_file { // If there is an existing active file, mark it as deleted debug!( "🗑️ Marking existing active file as deleted: existing_file_id={}", @@ -621,12 +620,11 @@ impl MySqlFileExt for MySqlStorage { account_hash: &str, file_path: &str, filename: &str, - revision: i64, ) -> Result> { use sqlx::Row; debug!( - "Searching file by path and filename: account_hash={}, file_path={}, filename={}, revision={}", - account_hash, file_path, filename, revision + "🔍 Searching file by path and filename (by updated_time): account_hash={}, file_path={}, filename={}", + account_hash, file_path, filename ); // equality by eq_index @@ -642,46 +640,26 @@ impl MySqlFileExt for MySqlStorage { } else { crate::utils::crypto::make_eq_index(account_hash.as_bytes(), file_path) }; - let row_opt = if revision > 0 { - sqlx::query( - r#"SELECT - file_id, account_hash, device_hash, file_path, filename, file_hash, - UNIX_TIMESTAMP(created_time) as created_ts, - UNIX_TIMESTAMP(updated_time) as updated_ts, - group_id, watcher_id, revision, size, key_id, unix_permissions, - is_deleted - FROM files - WHERE account_hash = ? AND eq_index = ? AND is_deleted = FALSE AND revision = ? - ORDER BY revision DESC LIMIT 1"#, - ) - .bind(account_hash) - .bind(&eq_index) - .bind(revision) - .fetch_optional(self.get_sqlx_pool()) - .await - .map_err(|e| { - StorageError::Database(format!("File search failed(exact search, sqlx): {}", e)) - })? - } else { - sqlx::query( - r#"SELECT - file_id, account_hash, device_hash, file_path, filename, file_hash, - UNIX_TIMESTAMP(created_time) as created_ts, - UNIX_TIMESTAMP(updated_time) as updated_ts, - group_id, watcher_id, revision, size, key_id, unix_permissions, - is_deleted - FROM files - WHERE account_hash = ? AND eq_index = ? AND is_deleted = FALSE - ORDER BY revision DESC LIMIT 1"#, - ) - .bind(account_hash) - .bind(&eq_index) - .fetch_optional(self.get_sqlx_pool()) - .await - .map_err(|e| { - StorageError::Database(format!("File search failed(exact search, sqlx): {}", e)) - })? - }; + + let row_opt = sqlx::query( + r#"SELECT + file_id, account_hash, device_hash, file_path, filename, file_hash, + UNIX_TIMESTAMP(created_time) as created_ts, + UNIX_TIMESTAMP(updated_time) as updated_ts, + group_id, watcher_id, revision, size, key_id, unix_permissions, + is_deleted + FROM files + WHERE account_hash = ? AND eq_index = ? AND is_deleted = FALSE + ORDER BY updated_time DESC, revision DESC, id DESC + LIMIT 1"#, + ) + .bind(account_hash) + .bind(&eq_index) + .fetch_optional(self.get_sqlx_pool()) + .await + .map_err(|e| { + StorageError::Database(format!("File search failed(by updated_time, sqlx): {}", e)) + })?; if let Some(row) = row_opt { // Extract is_deleted first for validation @@ -689,8 +667,11 @@ impl MySqlFileExt for MySqlStorage { let file_id: u64 = row.try_get("file_id").unwrap_or(0); let revision: i64 = row.try_get("revision").unwrap_or(0); - debug!("📊 find_file_by_path_and_name query returned: file_id={}, revision={}, is_deleted={}", - file_id, revision, is_deleted); + let updated_ts_value: Option = row.try_get("updated_ts").ok().flatten(); + debug!( + "📊 find_file_by_path_and_name query returned: file_id={}, revision={}, updated_time={:?}, is_deleted={}", + file_id, revision, updated_ts_value, is_deleted + ); // Critical validation: Double-check is_deleted if is_deleted { @@ -767,7 +748,7 @@ impl MySqlFileExt for MySqlStorage { // Check if file exists and belongs to the user // Use Vec for VARBINARY columns (file_path, filename) let row_opt = sqlx::query( - r#"SELECT file_id, revision, file_path, filename, device_hash, group_id, watcher_id + r#"SELECT file_id, revision, file_path, filename, device_hash, group_id, watcher_id, server_group_id, server_watcher_id FROM files WHERE file_id = ? AND account_hash = ?"#, ) .bind(file_id) @@ -800,6 +781,8 @@ impl MySqlFileExt for MySqlStorage { let device_hash: String = row.try_get("device_hash").unwrap_or_default(); let group_id: i32 = row.try_get("group_id").unwrap_or(0); let watcher_id: i32 = row.try_get("watcher_id").unwrap_or(0); + let server_group_id: i32 = row.try_get("server_group_id").unwrap_or(0); + let server_watcher_id: i32 = row.try_get("server_watcher_id").unwrap_or(0); let new_revision = current_revision + 1; @@ -859,8 +842,8 @@ impl MySqlFileExt for MySqlStorage { // Use Vec for VARBINARY columns to avoid type mismatch sqlx::query( r#"INSERT INTO files - (file_id, account_hash, device_hash, file_path, filename, file_hash, size, unix_permissions) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)"#, + (file_id, account_hash, device_hash, file_path, filename, file_hash, size, unix_permissions, server_group_id, server_watcher_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, ) .bind(new_file_id) .bind(account_hash) @@ -870,6 +853,8 @@ impl MySqlFileExt for MySqlStorage { .bind(&file_path_bytes) // file_hash uses file_path_bytes .bind(0i64) .bind(None::) + .bind(server_group_id) + .bind(server_watcher_id) .execute(&mut *tx) .await .map_err(|e| StorageError::Database(format!("Deletion history addition failed (step 1, schema/VARBINARY type mismatch): {}", e)))?; @@ -1221,7 +1206,7 @@ impl MySqlFileExt for MySqlStorage { }; debug!( - "🔍 Final search criteria: path='{}', filename='{}'", + "🔍 Final search criteria (by updated_time): path='{}', filename='{}'", search_path, search_filename ); @@ -1239,7 +1224,7 @@ impl MySqlFileExt for MySqlStorage { (file_path = ? AND filename = ?) OR (file_path = ? AND filename = ?) ) - ORDER BY revision DESC + ORDER BY updated_time DESC, revision DESC, id DESC LIMIT 1"# ) .bind(account_hash) @@ -1259,10 +1244,11 @@ impl MySqlFileExt for MySqlStorage { let file_id: u64 = row.try_get("file_id").unwrap_or(0); let revision: i64 = row.try_get("revision").unwrap_or(0); + let updated_ts_val: Option = row.try_get("updated_ts").ok().flatten(); debug!("✅ File found!"); debug!( - "📊 Query returned: file_id={}, revision={}, is_deleted={}", - file_id, revision, is_deleted + "📊 Query returned: file_id={}, revision={}, updated_time={:?}, is_deleted={}", + file_id, revision, updated_ts_val, is_deleted ); // Critical validation: Double-check is_deleted