@@ -484,7 +484,7 @@ async fn link_torrent(
484484 debug ! ( "Skiping \" {}\" " , file. name) ;
485485 continue ;
486486 }
487- let torrent_path = PathBuf :: from ( & file. name ) ;
487+ let torrent_path = qbit_file_path ( & file. name ) ;
488488 let mut path_components = torrent_path. components ( ) ;
489489 let file_name = path_components. next_back ( ) . unwrap ( ) ;
490490 let dir_name = path_components. next_back ( ) . and_then ( |dir_name| {
@@ -505,8 +505,8 @@ async fn link_torrent(
505505 } ;
506506 let library_path = dir. join ( & file_path) ;
507507 library_files. push ( file_path. clone ( ) ) ;
508- let download_path =
509- map_path ( & qbit_config . path_mapping , & torrent . save_path ) . join ( & file . name ) ;
508+ let download_path = map_path ( & qbit_config . path_mapping , & torrent . save_path )
509+ . join ( & torrent_path ) ;
510510 match library. method ( ) {
511511 LibraryLinkMethod :: Hardlink => {
512512 hard_link ( & download_path, & library_path, & file_path) ?
@@ -700,14 +700,16 @@ fn select_format(
700700#[ instrument( skip_all) ]
701701fn hard_link ( download_path : & Path , library_path : & Path , file_path : & Path ) -> Result < ( ) > {
702702 debug ! ( "linking: {:?} -> {:?}" , download_path, library_path) ;
703- fs:: hard_link ( download_path, library_path)
703+ let download_fs_path = windows_fs_path ( download_path) ;
704+ let library_fs_path = windows_fs_path ( library_path) ;
705+ fs:: hard_link ( & download_fs_path, & library_fs_path)
704706 . map_err ( |err| link_not_found_diagnostics ( err, "hardlink" , download_path, library_path) )
705707 . or_else ( |err| {
706708 if err. kind ( ) == ErrorKind :: AlreadyExists {
707709 trace ! ( "AlreadyExists: {}" , err) ;
708- let download_id = get_file_id ( download_path ) ;
710+ let download_id = get_file_id ( & download_fs_path ) ;
709711 trace ! ( "got 1: {download_id:?}" ) ;
710- let library_id = get_file_id ( library_path ) ;
712+ let library_id = get_file_id ( & library_fs_path ) ;
711713 trace ! ( "got 2: {library_id:?}" ) ;
712714 if let ( Ok ( download_id) , Ok ( library_id) ) = ( download_id, library_id) {
713715 trace ! ( "got both" ) ;
@@ -719,8 +721,8 @@ fn hard_link(download_path: &Path, library_path: &Path, file_path: &Path) -> Res
719721 bail ! (
720722 "File \" {:?}\" already exists, torrent file size: {}, library file size: {}" ,
721723 file_path,
722- fs:: metadata( download_path ) . map_or( "?" . to_string( ) , |s| Size :: from_bytes( file_size( & s) ) . to_string( ) ) ,
723- fs:: metadata( library_path ) . map_or( "?" . to_string( ) , |s| Size :: from_bytes( file_size( & s) ) . to_string( ) )
724+ fs:: metadata( & download_fs_path ) . map_or( "?" . to_string( ) , |s| Size :: from_bytes( file_size( & s) ) . to_string( ) ) ,
725+ fs:: metadata( & library_fs_path ) . map_or( "?" . to_string( ) , |s| Size :: from_bytes( file_size( & s) ) . to_string( ) )
724726 ) ;
725727 }
726728 }
@@ -733,7 +735,9 @@ fn hard_link(download_path: &Path, library_path: &Path, file_path: &Path) -> Res
733735#[ instrument( skip_all) ]
734736fn copy ( download_path : & Path , library_path : & Path ) -> Result < ( ) > {
735737 debug ! ( "copying: {:?} -> {:?}" , download_path, library_path) ;
736- fs:: copy ( download_path, library_path)
738+ let download_fs_path = windows_fs_path ( download_path) ;
739+ let library_fs_path = windows_fs_path ( library_path) ;
740+ fs:: copy ( & download_fs_path, & library_fs_path)
737741 . map_err ( |err| link_not_found_diagnostics ( err, "copy" , download_path, library_path) ) ?;
738742 Ok ( ( ) )
739743}
@@ -812,6 +816,37 @@ fn first_missing_component(path: &Path) -> Option<PathBuf> {
812816 None
813817}
814818
819+ fn qbit_file_path ( file_name : & str ) -> PathBuf {
820+ let mut path = PathBuf :: new ( ) ;
821+ for part in file_name. split ( [ '/' , '\\' ] ) {
822+ if part. is_empty ( ) || part == "." {
823+ continue ;
824+ }
825+ path. push ( part) ;
826+ }
827+ path
828+ }
829+
830+ #[ cfg( target_family = "windows" ) ]
831+ fn windows_fs_path ( path : & Path ) -> PathBuf {
832+ if !path. is_absolute ( ) {
833+ return path. to_path_buf ( ) ;
834+ }
835+ let path_str = path. as_os_str ( ) . to_string_lossy ( ) ;
836+ if path_str. starts_with ( r"\\?\" ) {
837+ return path. to_path_buf ( ) ;
838+ }
839+ if let Some ( without_unc) = path_str. strip_prefix ( r"\\" ) {
840+ return PathBuf :: from ( format ! ( r"\\?\UNC\{without_unc}" ) ) ;
841+ }
842+ PathBuf :: from ( format ! ( r"\\?\{path_str}" ) )
843+ }
844+
845+ #[ cfg( not( target_family = "windows" ) ) ]
846+ fn windows_fs_path ( path : & Path ) -> PathBuf {
847+ path. to_path_buf ( )
848+ }
849+
815850pub fn file_size ( m : & Metadata ) -> u64 {
816851 #[ cfg( target_family = "unix" ) ]
817852 return m. size ( ) ;
@@ -854,4 +889,59 @@ mod tests {
854889 PathBuf :: from( "/ebooks/torrent" )
855890 ) ;
856891 }
892+
893+ #[ test]
894+ fn test_qbit_file_path_mixed_separators ( ) {
895+ assert_eq ! (
896+ qbit_file_path( r"Isaac Asimov AudioBook Collection/Book 01\CD 07/Track01.mp3" ) ,
897+ PathBuf :: from( "Isaac Asimov AudioBook Collection" )
898+ . join( "Book 01" )
899+ . join( "CD 07" )
900+ . join( "Track01.mp3" )
901+ ) ;
902+ }
903+
904+ #[ test]
905+ fn test_qbit_file_path_ignores_empty_and_dot_segments ( ) {
906+ assert_eq ! (
907+ qbit_file_path( r"/Book 01//./CD 07\.\Track01.mp3" ) ,
908+ PathBuf :: from( "Book 01" )
909+ . join( "CD 07" )
910+ . join( "Track01.mp3" )
911+ ) ;
912+ }
913+
914+ #[ cfg( target_family = "windows" ) ]
915+ #[ test]
916+ fn test_windows_fs_path_drive_prefix ( ) {
917+ let path = PathBuf :: from ( r"F:\Sharon\Media\Audio\Hardlinks\file.m4b" ) ;
918+ assert_eq ! (
919+ windows_fs_path( & path) ,
920+ PathBuf :: from( r"\\?\F:\Sharon\Media\Audio\Hardlinks\file.m4b" )
921+ ) ;
922+ }
923+
924+ #[ cfg( target_family = "windows" ) ]
925+ #[ test]
926+ fn test_windows_fs_path_unc_prefix ( ) {
927+ let path = PathBuf :: from ( r"\\server\share\Audio\file.m4b" ) ;
928+ assert_eq ! (
929+ windows_fs_path( & path) ,
930+ PathBuf :: from( r"\\?\UNC\server\share\Audio\file.m4b" )
931+ ) ;
932+ }
933+
934+ #[ cfg( target_family = "windows" ) ]
935+ #[ test]
936+ fn test_windows_fs_path_preserves_existing_extended_prefix ( ) {
937+ let path = PathBuf :: from ( r"\\?\F:\Sharon\Media\Audio\Hardlinks\file.m4b" ) ;
938+ assert_eq ! ( windows_fs_path( & path) , path) ;
939+ }
940+
941+ #[ cfg( not( target_family = "windows" ) ) ]
942+ #[ test]
943+ fn test_windows_fs_path_noop_on_non_windows ( ) {
944+ let path = PathBuf :: from ( "/tmp/test/file.m4b" ) ;
945+ assert_eq ! ( windows_fs_path( & path) , path) ;
946+ }
857947}
0 commit comments