Skip to content

Commit 7eb90b5

Browse files
committed
Improve path handling on windows
1 parent bb8cf9a commit 7eb90b5

2 files changed

Lines changed: 111 additions & 18 deletions

File tree

.github/workflows/windows-publish.yml

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,18 @@ jobs:
3232
with:
3333
targets: ${{ matrix.target }}
3434

35-
- uses: shogo82148/actions-setup-perl@v1
36-
with:
37-
perl-version: "5.32"
38-
distribution: strawberry
39-
40-
- name: Build
41-
run: |
42-
cargo install cargo-packager
43-
cargo packager --release
35+
- uses: shogo82148/actions-setup-perl@v1
36+
with:
37+
perl-version: "5.32"
38+
distribution: strawberry
39+
40+
- name: Test
41+
run: cargo test -p mlm
42+
43+
- name: Build
44+
run: |
45+
cargo install cargo-packager
46+
cargo packager --release
4447
4548
- name: Prepare files
4649
shell: bash

server/src/linker.rs

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
701701
fn 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)]
734736
fn 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+
815850
pub 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

Comments
 (0)