fix: download path handling

This commit is contained in:
Michel de Graaf
2025-10-13 16:01:30 +02:00
parent 9ccc4fa1e9
commit 27258a09f0
2 changed files with 151 additions and 62 deletions
+150 -62
View File
@@ -60,6 +60,27 @@ impl FileManager {
let _ = fs::remove_file(path); let _ = fs::remove_file(path);
} }
} }
fn extract_filename_from_path(full_path: &str) -> String {
// Split on both forward slashes and backslashes to handle Windows and Unix paths
full_path.split(['/', '\\']).last().unwrap_or(full_path).to_string()
}
fn create_download_path_from_filename(
output_directory: Option<&str>,
remote_username: &str,
token: u32,
filename: Option<&str>
) -> String {
let base_dir = output_directory.unwrap_or("/tmp");
if let Some(filename) = filename {
let clean_filename = Self::extract_filename_from_path(filename);
format!("{}/{}", base_dir, clean_filename)
} else {
format!("{}/{}_{}.mp3", base_dir, remote_username, token)
}
}
} }
#[allow(dead_code)] #[allow(dead_code)]
@@ -68,6 +89,7 @@ struct StreamProcessor {
token: u32, token: u32,
total_bytes: usize, total_bytes: usize,
received: bool, received: bool,
buffer: Vec<u8>,
} }
#[allow(dead_code)] #[allow(dead_code)]
@@ -78,6 +100,7 @@ impl StreamProcessor {
token, token,
total_bytes: 0, total_bytes: 0,
received: false, received: false,
buffer: Vec::new(),
} }
} }
@@ -98,16 +121,8 @@ impl StreamProcessor {
fn process_data_chunk( fn process_data_chunk(
&mut self, &mut self,
data: &[u8], data: &[u8],
writer: &mut Option<BufWriter<File>>,
) -> Result<(), io::Error> { ) -> Result<(), io::Error> {
if let Some(ref mut w) = writer { self.buffer.extend_from_slice(data);
w.write_all(data)?;
if self.total_bytes % (64 * 1024) == 0 {
w.flush()?;
}
}
self.total_bytes += data.len(); self.total_bytes += data.len();
Ok(()) Ok(())
} }
@@ -226,37 +241,13 @@ impl DownloadPeer {
let mut stream = self.establish_connection()?; let mut stream = self.establish_connection()?;
trace!("[download_peer:{}] connected", self.username); trace!("[download_peer:{}] connected", self.username);
// Setup file management if output path is provided
let (paths, mut writer) = if let Some(output_path) = output_path {
let paths = FileManager::create_download_paths(
Some(output_path),
&self.own_username,
self.token,
);
let writer =
Some(FileManager::create_temp_file(&paths.incomplete_path)?);
(paths, writer)
} else {
let paths = FileManager::create_download_paths(
None,
&self.own_username,
self.token,
);
let writer =
Some(FileManager::create_temp_file(&paths.incomplete_path)?);
trace!(
"[download_peer:{}] No output path provided, downloading to temp file: {}",
self.username,
paths.incomplete_path
);
(paths, writer)
};
self.perform_handshake(&mut stream)?; self.perform_handshake(&mut stream)?;
trace!("[download_peer:{}] handshake completed", self.username); trace!("[download_peer:{}] handshake completed", self.username);
let mut processor = StreamProcessor::new(self.no_pierce, self.token); let mut processor = StreamProcessor::new(self.no_pierce, self.token);
let mut read_buffer = [1u8; 8192]; let mut read_buffer = [1u8; 8192];
let mut download_info: Option<crate::types::Download> = None;
trace!( trace!(
"[download_peer:{}] Starting to read data from peer", "[download_peer:{}] Starting to read data from peer",
self.username self.username
@@ -273,11 +264,6 @@ impl DownloadPeer {
} }
Ok(bytes_read) => { Ok(bytes_read) => {
let data = &read_buffer[..bytes_read]; let data = &read_buffer[..bytes_read];
// trace!(
// "[download_peer:{}] received {} bytes",
// self.username,
// bytes_read
// );
if !self.no_pierce if !self.no_pierce
&& !processor.received && !processor.received
@@ -292,18 +278,20 @@ impl DownloadPeer {
token token
); );
match client_context download_info = client_context
.lock() .lock()
.unwrap() .unwrap()
.download_tokens .download_tokens
.get(&token_u32) .get(&token_u32)
.cloned() .cloned();
{
match &download_info {
Some(download) => { Some(download) => {
trace!( trace!(
"[download_peer:{}] got download info for token: {}", "[download_peer:{}] got download info for token: {} - filename: {}",
self.username, self.username,
token_u32 token_u32,
download.filename
); );
expected_size = Some(download.size as usize); expected_size = Some(download.size as usize);
} }
@@ -319,7 +307,7 @@ impl DownloadPeer {
continue; // Skip this data chunk continue; // Skip this data chunk
} }
processor.process_data_chunk(data, &mut writer)?; processor.process_data_chunk(data)?;
if !processor.should_continue(expected_size) { if !processor.should_continue(expected_size) {
break; break;
@@ -329,8 +317,6 @@ impl DownloadPeer {
continue; continue;
} }
Err(e) => { Err(e) => {
drop(writer);
FileManager::cleanup_on_error(Some(&paths.incomplete_path));
return Err(e); return Err(e);
} }
} }
@@ -340,23 +326,58 @@ impl DownloadPeer {
self.username self.username
); );
if let Some(mut w) = writer { // Now determine the final file path using download info
w.flush()?; let output_directory = output_path.as_ref().map(|path| {
drop(w); if Path::new(path).is_dir() {
path.as_str()
} else if let Some(parent) = Path::new(path).parent() {
parent.to_str().unwrap_or("/tmp")
} else {
"/tmp"
}
});
trace!("[download_peer:{}] finalizing download", self.username); let filename = download_info.as_ref().map(|d| d.filename.as_str());
FileManager::finalize_download(
&paths.incomplete_path, let final_path = if let Some(output_path) = &output_path {
&paths.final_path, if Path::new(output_path).is_dir() || output_path.ends_with('/') {
)?; // output_path is a directory, append filename
FileManager::create_download_path_from_filename(
Some(output_path),
&self.username,
self.token,
filename
)
} else {
// output_path is a full file path, use it directly
output_path.clone()
}
} else {
// No output path provided, use default logic
FileManager::create_download_path_from_filename(
output_directory,
&self.username,
self.token,
filename
)
};
// Create directory if needed
if let Some(parent) = Path::new(&final_path).parent() {
fs::create_dir_all(parent)?;
} }
// Write the buffer to the final file
fs::write(&final_path, &processor.buffer)?;
trace!( trace!(
"[download_peer:{}] download completed successfully: {} bytes", "[download_peer:{}] download completed successfully: {} bytes, saved to: {}",
self.username, self.username,
processor.total_bytes processor.total_bytes,
final_path
); );
Ok((processor.total_bytes, paths.final_path.clone())) Ok((processor.total_bytes, final_path))
} }
} }
@@ -520,8 +541,9 @@ mod tests {
); );
assert!(result.is_ok()); assert!(result.is_ok());
let bytes_downloaded = result.unwrap(); let (bytes_downloaded, file_path) = result.unwrap();
assert_eq!(bytes_downloaded, test_data.len()); assert_eq!(bytes_downloaded, test_data.len());
assert_eq!(file_path, "test_download.mp3");
// Verify the file was written correctly // Verify the file was written correctly
let downloaded_data = fs::read("test_download.mp3").unwrap(); let downloaded_data = fs::read("test_download.mp3").unwrap();
@@ -627,11 +649,11 @@ mod tests {
fn test_stream_processor_process_data_chunk() { fn test_stream_processor_process_data_chunk() {
let mut processor = StreamProcessor::new(false, 123); let mut processor = StreamProcessor::new(false, 123);
let data = b"test data"; let data = b"test data";
let mut writer = None;
let result = processor.process_data_chunk(data, &mut writer); let result = processor.process_data_chunk(data);
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(processor.total_bytes, data.len()); assert_eq!(processor.total_bytes, data.len());
assert_eq!(processor.buffer, data);
} }
#[test] #[test]
@@ -648,4 +670,70 @@ mod tests {
let result = download_peer.establish_connection(); let result = download_peer.establish_connection();
assert!(result.is_err()); assert!(result.is_err());
} }
#[test]
fn test_extract_filename_from_path() {
// Test with Unix-style path
assert_eq!(
FileManager::extract_filename_from_path("/path/to/file.mp3"),
"file.mp3"
);
// Test with Windows-style path
assert_eq!(
FileManager::extract_filename_from_path("C:\\path\\to\\file.mp3"),
"file.mp3"
);
// Test with Soulseek Windows path (the actual failing case)
assert_eq!(
FileManager::extract_filename_from_path("@@bhfrv\\Soulseek Downloads\\complete\\Beatport Top Deep House (2021)\\michel test file.mp3"),
"michel test file.mp3"
);
// Test with just filename
assert_eq!(
FileManager::extract_filename_from_path("file.mp3"),
"file.mp3"
);
}
#[test]
fn test_create_download_path_from_filename() {
// Test with filename provided
let path = FileManager::create_download_path_from_filename(
Some("/downloads"),
"remote_user",
123,
Some("song.mp3")
);
assert_eq!(path, "/downloads/song.mp3");
// Test with full path filename
let path = FileManager::create_download_path_from_filename(
Some("/downloads"),
"remote_user",
123,
Some("/remote/path/to/song.mp3")
);
assert_eq!(path, "/downloads/song.mp3");
// Test without filename (fallback)
let path = FileManager::create_download_path_from_filename(
Some("/downloads"),
"remote_user",
123,
None
);
assert_eq!(path, "/downloads/remote_user_123.mp3");
// Test with default directory
let path = FileManager::create_download_path_from_filename(
None,
"remote_user",
123,
Some("song.mp3")
);
assert_eq!(path, "/tmp/song.mp3");
}
} }
+1
View File
@@ -0,0 +1 @@
test file content