@@ -73,17 +73,23 @@ pub struct ConflictFileInfo {
7373
7474/// Validates a new filename and checks for conflicts in the same directory.
7575/// Uses inode comparison to detect case-only renames (valid on case-insensitive APFS).
76+ /// When `volume_id` is provided and not `"root"`, uses the Volume trait for conflict detection
77+ /// (needed for MTP and other non-local volumes).
7678#[ tauri:: command]
7779pub async fn check_rename_validity (
7880 dir : String ,
7981 old_name : String ,
8082 new_name : String ,
83+ volume_id : Option < String > ,
8184) -> Result < RenameValidityResult , IpcError > {
8285 let expanded_dir = expand_tilde ( & dir) ;
86+ let volume_id_str = volume_id. unwrap_or_else ( || "root" . to_string ( ) ) ;
8387
8488 tokio:: time:: timeout (
8589 Duration :: from_secs ( 2 ) ,
86- tokio:: task:: spawn_blocking ( move || check_rename_validity_sync ( & expanded_dir, & old_name, & new_name) ) ,
90+ tokio:: task:: spawn_blocking ( move || {
91+ check_rename_validity_sync ( & expanded_dir, & old_name, & new_name, & volume_id_str)
92+ } ) ,
8793 )
8894 . await
8995 . map_err ( |_| IpcError :: timeout ( ) ) ?
@@ -222,7 +228,12 @@ fn check_macos_flags(path: &Path) -> Result<(), String> {
222228}
223229
224230/// Synchronous validity check implementation.
225- fn check_rename_validity_sync ( dir : & str , old_name : & str , new_name : & str ) -> Result < RenameValidityResult , String > {
231+ fn check_rename_validity_sync (
232+ dir : & str ,
233+ old_name : & str ,
234+ new_name : & str ,
235+ volume_id : & str ,
236+ ) -> Result < RenameValidityResult , String > {
226237 use crate :: file_system:: validation:: { validate_filename, validate_path_length} ;
227238
228239 let trimmed = new_name. trim ( ) ;
@@ -252,15 +263,29 @@ fn check_rename_validity_sync(dir: &str, old_name: &str, new_name: &str) -> Resu
252263
253264 // Check for conflict: does a sibling with this name already exist?
254265 let old_path = PathBuf :: from ( dir) . join ( old_name) ;
255- let conflict_info = check_sibling_conflict ( & old_path, & new_path) ;
256-
257- Ok ( RenameValidityResult {
258- valid : true ,
259- error : None ,
260- has_conflict : conflict_info. 0 ,
261- is_case_only_rename : conflict_info. 1 ,
262- conflict : conflict_info. 2 ,
263- } )
266+
267+ if volume_id != "root" {
268+ // Non-local volume: use Volume trait for conflict detection
269+ let conflict_info = check_sibling_conflict_via_volume ( volume_id, & new_path) ;
270+ Ok ( RenameValidityResult {
271+ valid : true ,
272+ error : None ,
273+ has_conflict : conflict_info. 0 ,
274+ // MTP is case-sensitive, no case-only rename ambiguity
275+ is_case_only_rename : false ,
276+ conflict : conflict_info. 1 ,
277+ } )
278+ } else {
279+ // Local filesystem: use symlink_metadata with inode comparison
280+ let conflict_info = check_sibling_conflict ( & old_path, & new_path) ;
281+ Ok ( RenameValidityResult {
282+ valid : true ,
283+ error : None ,
284+ has_conflict : conflict_info. 0 ,
285+ is_case_only_rename : conflict_info. 1 ,
286+ conflict : conflict_info. 2 ,
287+ } )
288+ }
264289}
265290
266291/// Checks if a file with `new_path` exists and whether it's the same inode as `old_path`
@@ -325,6 +350,28 @@ fn check_sibling_conflict(_old_path: &Path, new_path: &Path) -> (bool, bool, Opt
325350 ( true , false , Some ( conflict) )
326351}
327352
353+ /// Checks if a file with `new_path` exists on a non-local volume using the Volume trait's `get_metadata`.
354+ fn check_sibling_conflict_via_volume ( volume_id : & str , new_path : & Path ) -> ( bool , Option < ConflictFileInfo > ) {
355+ let volume = match crate :: file_system:: get_volume_manager ( ) . get ( volume_id) {
356+ Some ( v) => v,
357+ None => return ( false , None ) ,
358+ } ;
359+
360+ let entry = match volume. get_metadata ( new_path) {
361+ Ok ( e) => e,
362+ Err ( _) => return ( false , None ) , // No conflict — file doesn't exist
363+ } ;
364+
365+ let conflict = ConflictFileInfo {
366+ name : entry. name ,
367+ size : entry. size . unwrap_or ( 0 ) ,
368+ modified : entry. modified_at . map ( |t| t as i64 ) ,
369+ is_directory : entry. is_directory ,
370+ } ;
371+
372+ ( true , Some ( conflict) )
373+ }
374+
328375#[ cfg( test) ]
329376mod tests {
330377 use super :: * ;
@@ -371,7 +418,7 @@ mod tests {
371418 let tmp = create_test_dir ( "rename_valid_ok" ) ;
372419 let dir = tmp. to_string_lossy ( ) . to_string ( ) ;
373420 fs:: write ( tmp. join ( "old.txt" ) , "content" ) . unwrap ( ) ;
374- let result = check_rename_validity ( dir, "old.txt" . to_string ( ) , "new.txt" . to_string ( ) ) . await ;
421+ let result = check_rename_validity ( dir, "old.txt" . to_string ( ) , "new.txt" . to_string ( ) , None ) . await ;
375422 assert ! ( result. is_ok( ) ) ;
376423 let check = result. unwrap ( ) ;
377424 assert ! ( check. valid) ;
@@ -385,7 +432,7 @@ mod tests {
385432 async fn test_check_rename_validity_empty_name ( ) {
386433 let tmp = create_test_dir ( "rename_valid_empty" ) ;
387434 let dir = tmp. to_string_lossy ( ) . to_string ( ) ;
388- let result = check_rename_validity ( dir, "old.txt" . to_string ( ) , " " . to_string ( ) ) . await ;
435+ let result = check_rename_validity ( dir, "old.txt" . to_string ( ) , " " . to_string ( ) , None ) . await ;
389436 assert ! ( result. is_ok( ) ) ;
390437 let check = result. unwrap ( ) ;
391438 assert ! ( !check. valid) ;
@@ -397,7 +444,7 @@ mod tests {
397444 async fn test_check_rename_validity_slash_in_name ( ) {
398445 let tmp = create_test_dir ( "rename_valid_slash" ) ;
399446 let dir = tmp. to_string_lossy ( ) . to_string ( ) ;
400- let result = check_rename_validity ( dir, "old.txt" . to_string ( ) , "foo/bar" . to_string ( ) ) . await ;
447+ let result = check_rename_validity ( dir, "old.txt" . to_string ( ) , "foo/bar" . to_string ( ) , None ) . await ;
401448 assert ! ( result. is_ok( ) ) ;
402449 let check = result. unwrap ( ) ;
403450 assert ! ( !check. valid) ;
@@ -410,7 +457,7 @@ mod tests {
410457 let dir = tmp. to_string_lossy ( ) . to_string ( ) ;
411458 fs:: write ( tmp. join ( "old.txt" ) , "old content" ) . unwrap ( ) ;
412459 fs:: write ( tmp. join ( "existing.txt" ) , "existing content" ) . unwrap ( ) ;
413- let result = check_rename_validity ( dir, "old.txt" . to_string ( ) , "existing.txt" . to_string ( ) ) . await ;
460+ let result = check_rename_validity ( dir, "old.txt" . to_string ( ) , "existing.txt" . to_string ( ) , None ) . await ;
414461 assert ! ( result. is_ok( ) ) ;
415462 let check = result. unwrap ( ) ;
416463 assert ! ( check. valid) ;
@@ -430,7 +477,7 @@ mod tests {
430477 let dir = tmp. to_string_lossy ( ) . to_string ( ) ;
431478 fs:: write ( tmp. join ( "MyFile.txt" ) , "content" ) . unwrap ( ) ;
432479 // On case-insensitive APFS, "myfile.txt" resolves to the same inode as "MyFile.txt"
433- let result = check_rename_validity ( dir, "MyFile.txt" . to_string ( ) , "myfile.txt" . to_string ( ) ) . await ;
480+ let result = check_rename_validity ( dir, "MyFile.txt" . to_string ( ) , "myfile.txt" . to_string ( ) , None ) . await ;
434481 assert ! ( result. is_ok( ) ) ;
435482 let check = result. unwrap ( ) ;
436483 assert ! ( check. valid) ;
0 commit comments