fix(lib): keep a missing download directory as the target, not its parent

resolve_download_path treated "not a directory" as "names a file" and
fell back to the parent, so a directory that simply did not exist yet
redirected the transfer one level up. The CLI and TUI create the folder
before downloading and never hit it; a daemon does not, so its transfers
landed in ~/Downloads while daemon.status - and the paths download and
get report - named ~/Downloads/Soulseek. Green on any machine where the
folder already exists, which is how it survived to CI.

Only an existing file diverts to the parent now. PartFile::open already
creates the directory it streams into.
This commit is contained in:
Michel de Graaf
2026-07-29 17:02:27 +02:00
parent 78a608c4c3
commit f525984a7b
+23 -1
View File
@@ -75,10 +75,15 @@ fn extract_filename_from_path(full_path: &str) -> &str {
/// Where a download's bytes end up: its configured directory (falling back /// Where a download's bytes end up: its configured directory (falling back
/// to the parent if it names a file) joined with the remote basename. /// to the parent if it names a file) joined with the remote basename.
///
/// A directory that does not exist yet stays the target — `PartFile::open`
/// creates it. Treating it as "not a directory" and dodging to the parent
/// made a daemon whose folder was never created download into `~/Downloads`
/// while reporting `~/Downloads/Soulseek`.
fn resolve_download_path(download: &Download) -> Result<String, DownloadError> { fn resolve_download_path(download: &Download) -> Result<String, DownloadError> {
let mut expanded_path = expand_tilde(&download.download_directory); let mut expanded_path = expand_tilde(&download.download_directory);
if !expanded_path.is_dir() { if expanded_path.is_file() {
expanded_path = expanded_path expanded_path = expanded_path
.parent() .parent()
.ok_or_else(|| { .ok_or_else(|| {
@@ -527,6 +532,23 @@ mod tests {
} }
} }
#[test]
fn a_directory_that_does_not_exist_yet_is_created_not_dodged() {
let dir = scratch_dir("fresh").join("Soulseek");
let mut part = PartFile::open(&download_into(&dir, 4)).unwrap();
part.write(b"data").unwrap();
let final_path = part.finish().unwrap();
assert_eq!(
std::path::Path::new(&final_path).parent(),
Some(dir.as_path()),
"the bytes belong in the configured directory, not its parent"
);
assert_eq!(std::fs::read(&final_path).unwrap(), b"data");
let _ = std::fs::remove_dir_all(dir.parent().unwrap());
}
#[test] #[test]
fn a_partial_file_sets_the_resume_offset() { fn a_partial_file_sets_the_resume_offset() {
let dir = scratch_dir("resume"); let dir = scratch_dir("resume");