fix(tui): removed a ton of use_username_dirs references

feat(tui): username is now appended to download_dir before passed as parameter
fix(tui): username is sanitized before appended to path
This commit is contained in:
Maik Steiger
2026-10-09 13:33:22 +02:00
parent 5e979a7425
commit 9805b807e5
28 changed files with 69 additions and 77 deletions
+3 -7
View File
@@ -1148,13 +1148,9 @@ fn main() {
let completed = completed.clone(); let completed = completed.clone();
workers.push(std::thread::spawn(move || { workers.push(std::thread::spawn(move || {
Metrics::bump(&metrics.downloads_issued); Metrics::bump(&metrics.downloads_issued);
let Ok((_download, status_rx)) = client.download( let Ok((_download, status_rx)) =
filename.clone(), client.download(filename.clone(), peer.clone(), size, dir)
peer.clone(), else {
size,
dir,
false,
) else {
return; return;
}; };
let deadline = run_deadline; let deadline = run_deadline;
-4
View File
@@ -99,14 +99,12 @@ impl Client {
username: String, username: String,
size: u64, size: u64,
download_directory: String, download_directory: String,
use_username_dirs: bool,
) -> Result<(Download, Receiver<DownloadStatus>)> { ) -> Result<(Download, Receiver<DownloadStatus>)> {
self.download_with_metadata( self.download_with_metadata(
filename, filename,
username, username,
size, size,
download_directory, download_directory,
use_username_dirs,
DownloadMetadata::default(), DownloadMetadata::default(),
) )
} }
@@ -117,7 +115,6 @@ impl Client {
username: String, username: String,
size: u64, size: u64,
download_directory: String, download_directory: String,
use_username_dirs: bool,
metadata: DownloadMetadata, metadata: DownloadMetadata,
) -> Result<(Download, Receiver<DownloadStatus>)> { ) -> Result<(Download, Receiver<DownloadStatus>)> {
info!("[client] Downloading {} from {}", filename, username); info!("[client] Downloading {} from {}", filename, username);
@@ -135,7 +132,6 @@ impl Client {
token, token,
size, size,
download_directory, download_directory,
use_username_dirs,
status: DownloadStatus::Queued, status: DownloadStatus::Queued,
sender: download_sender, sender: download_sender,
queue_position: None, queue_position: None,
-1
View File
@@ -23,7 +23,6 @@ fn download(
token, token,
size: 100, size: 100,
download_directory: "test".to_string(), download_directory: "test".to_string(),
use_username_dirs: false,
status, status,
sender, sender,
queue_position: None, queue_position: None,
@@ -97,7 +97,6 @@ fn download_without_a_connection_resolves_failed() {
"peer".to_string(), "peer".to_string(),
100, 100,
"test".to_string(), "test".to_string(),
false,
) )
.expect("download() should return a handle"); .expect("download() should return a handle");
assert!(matches!( assert!(matches!(
-1
View File
@@ -242,7 +242,6 @@ mod tests {
token, token,
size: 100, size: 100,
download_directory: "test".to_string(), download_directory: "test".to_string(),
use_username_dirs: false,
status, status,
sender: mpsc::channel().0, sender: mpsc::channel().0,
queue_position: None, queue_position: None,
@@ -110,10 +110,6 @@ fn resolve_download_path(download: &Download) -> Result<String, DownloadError> {
.to_path_buf(); .to_path_buf();
} }
if download.use_username_dirs {
expanded_path = expanded_path.join(download.username.as_str());
}
let final_path = let final_path =
expanded_path.join(extract_filename_from_path(&download.filename)); expanded_path.join(extract_filename_from_path(&download.filename));
@@ -639,7 +635,6 @@ mod tests {
token: 1, token: 1,
size, size,
download_directory: dir.display().to_string(), download_directory: dir.display().to_string(),
use_username_dirs: false,
status: DownloadStatus::Queued, status: DownloadStatus::Queued,
sender: mpsc::channel().0, sender: mpsc::channel().0,
queue_position: None, queue_position: None,
-1
View File
@@ -479,7 +479,6 @@ mod tests {
token, token,
size: 100, size: 100,
download_directory: std::env::temp_dir().display().to_string(), download_directory: std::env::temp_dir().display().to_string(),
use_username_dirs: false,
status: DownloadStatus::Queued, status: DownloadStatus::Queued,
sender: mpsc::channel().0, sender: mpsc::channel().0,
queue_position: None, queue_position: None,
-1
View File
@@ -185,7 +185,6 @@ pub struct Download {
pub token: u32, pub token: u32,
pub size: u64, pub size: u64,
pub download_directory: String, pub download_directory: String,
pub use_username_dirs: bool,
pub status: DownloadStatus, pub status: DownloadStatus,
pub sender: Sender<DownloadStatus>, pub sender: Sender<DownloadStatus>,
pub queue_position: Option<u32>, pub queue_position: Option<u32>,
-4
View File
@@ -240,9 +240,6 @@
"minimum": 0, "minimum": 0,
"type": "integer" "type": "integer"
}, },
"use_username_dirs": {
"type": "boolean"
},
"username": { "username": {
"type": "string" "type": "string"
} }
@@ -253,7 +250,6 @@
"token", "token",
"size", "size",
"download_directory", "download_directory",
"use_username_dirs",
"status" "status"
], ],
"type": "object" "type": "object"
+1 -13
View File
@@ -102,7 +102,6 @@ pub trait SessionApi: Send + Sync {
username: String, username: String,
size: u64, size: u64,
download_directory: String, download_directory: String,
use_username_dirs: bool,
) -> Result<(Download, Receiver<DownloadStatus>)>; ) -> Result<(Download, Receiver<DownloadStatus>)>;
fn download_with_metadata( fn download_with_metadata(
&self, &self,
@@ -110,7 +109,6 @@ pub trait SessionApi: Send + Sync {
username: String, username: String,
size: u64, size: u64,
download_directory: String, download_directory: String,
use_username_dirs: bool,
metadata: DownloadMetadata, metadata: DownloadMetadata,
) -> Result<(Download, Receiver<DownloadStatus>)>; ) -> Result<(Download, Receiver<DownloadStatus>)>;
fn get_all_downloads(&self) -> Vec<Download>; fn get_all_downloads(&self) -> Vec<Download>;
@@ -273,16 +271,8 @@ impl SessionApi for Client {
username: String, username: String,
size: u64, size: u64,
download_directory: String, download_directory: String,
use_username_dirs: bool,
) -> Result<(Download, Receiver<DownloadStatus>)> { ) -> Result<(Download, Receiver<DownloadStatus>)> {
Self::download( Self::download(self, filename, username, size, download_directory)
self,
filename,
username,
size,
download_directory,
use_username_dirs,
)
} }
fn download_with_metadata( fn download_with_metadata(
@@ -291,7 +281,6 @@ impl SessionApi for Client {
username: String, username: String,
size: u64, size: u64,
download_directory: String, download_directory: String,
use_username_dirs: bool,
metadata: DownloadMetadata, metadata: DownloadMetadata,
) -> Result<(Download, Receiver<DownloadStatus>)> { ) -> Result<(Download, Receiver<DownloadStatus>)> {
Self::download_with_metadata( Self::download_with_metadata(
@@ -300,7 +289,6 @@ impl SessionApi for Client {
username, username,
size, size,
download_directory, download_directory,
use_username_dirs,
metadata, metadata,
) )
} }
-1
View File
@@ -518,7 +518,6 @@ fn download_one(
request.user.clone(), request.user.clone(),
request.size, request.size,
download_dir.to_string(), download_dir.to_string(),
false,
) )
.map_err(|e| CliError::transfer(format!("cannot start: {e}")))?; .map_err(|e| CliError::transfer(format!("cannot start: {e}")))?;
-1
View File
@@ -441,7 +441,6 @@ fn restore(
download.username.clone(), download.username.clone(),
download.size, download.size,
download.download_directory.clone(), download.download_directory.clone(),
download.use_username_dirs,
) { ) {
Ok((_, updates)) => { Ok((_, updates)) => {
if let Ok(mut pending) = downloads.lock() { if let Ok(mut pending) = downloads.lock() {
-2
View File
@@ -220,7 +220,6 @@ pub struct DownloadDto {
pub token: u32, pub token: u32,
pub size: u64, pub size: u64,
pub download_directory: String, pub download_directory: String,
pub use_username_dirs: bool,
pub status: DownloadStatusDto, pub status: DownloadStatusDto,
#[serde(default)] #[serde(default)]
pub queue_position: Option<u32>, pub queue_position: Option<u32>,
@@ -236,7 +235,6 @@ impl From<&Download> for DownloadDto {
token: download.token, token: download.token,
size: download.size, size: download.size,
download_directory: download.download_directory.clone(), download_directory: download.download_directory.clone(),
use_username_dirs: download.use_username_dirs,
status: DownloadStatusDto::from(&download.status), status: DownloadStatusDto::from(&download.status),
queue_position: download.queue_position, queue_position: download.queue_position,
metadata: DownloadMetadataDto::from(&download.metadata), metadata: DownloadMetadataDto::from(&download.metadata),
-4
View File
@@ -436,7 +436,6 @@ impl Daemon {
params.username.clone(), params.username.clone(),
params.size, params.size,
directory, directory,
false,
params.metadata.into(), params.metadata.into(),
) )
.map_err(|e| { .map_err(|e| {
@@ -877,7 +876,6 @@ mod tests {
username: String, username: String,
size: u64, size: u64,
directory: String, directory: String,
use_username_dirs: bool,
) -> soulseek_rs::Result<( ) -> soulseek_rs::Result<(
soulseek_rs::types::Download, soulseek_rs::types::Download,
Receiver<soulseek_rs::DownloadStatus>, Receiver<soulseek_rs::DownloadStatus>,
@@ -887,7 +885,6 @@ mod tests {
username, username,
size, size,
directory, directory,
use_username_dirs,
soulseek_rs::types::DownloadMetadata::default(), soulseek_rs::types::DownloadMetadata::default(),
) )
} }
@@ -897,7 +894,6 @@ mod tests {
_username: String, _username: String,
_size: u64, _size: u64,
_directory: String, _directory: String,
_use_username_dirs: bool,
_metadata: soulseek_rs::types::DownloadMetadata, _metadata: soulseek_rs::types::DownloadMetadata,
) -> soulseek_rs::Result<( ) -> soulseek_rs::Result<(
soulseek_rs::types::Download, soulseek_rs::types::Download,
+1 -1
View File
@@ -86,7 +86,7 @@ impl FileConfig {
"daemon" => self.daemon.clone(), "daemon" => self.daemon.clone(),
"daemon_token" => self.daemon_token.clone(), "daemon_token" => self.daemon_token.clone(),
"use_username_dirs" => { "use_username_dirs" => {
self.use_username_dirs.clone().map(|v| v.to_string()) self.use_username_dirs.map(|v| v.to_string())
} }
_ => None, _ => None,
} }
-1
View File
@@ -126,7 +126,6 @@ mod tests {
token: 7, token: 7,
size: 42, size: 42,
download_directory: "/music".into(), download_directory: "/music".into(),
use_username_dirs: false,
status, status,
sender, sender,
queue_position: None, queue_position: None,
-3
View File
@@ -22,7 +22,6 @@ pub struct PersistedDownload {
pub filename: String, pub filename: String,
pub size: u64, pub size: u64,
pub download_directory: String, pub download_directory: String,
pub use_username_dirs: bool,
pub completed: bool, pub completed: bool,
} }
@@ -37,7 +36,6 @@ impl PersistedDownload {
filename: download.filename.clone(), filename: download.filename.clone(),
size: download.size, size: download.size,
download_directory: download.download_directory.clone(), download_directory: download.download_directory.clone(),
use_username_dirs: download.use_username_dirs,
completed: matches!(download.status, DownloadStatus::Completed), completed: matches!(download.status, DownloadStatus::Completed),
}) })
} }
@@ -184,7 +182,6 @@ mod tests {
filename: "@@abc\\music\\song.mp3".into(), filename: "@@abc\\music\\song.mp3".into(),
size: 123, size: 123,
download_directory: "/music".into(), download_directory: "/music".into(),
use_username_dirs: false,
completed: false, completed: false,
} }
} }
-3
View File
@@ -367,7 +367,6 @@ fn rebuild(dto: DownloadDto, sender: Sender<DownloadStatus>) -> Download {
token: dto.token, token: dto.token,
size: dto.size, size: dto.size,
download_directory: dto.download_directory, download_directory: dto.download_directory,
use_username_dirs: dto.use_username_dirs,
status: dto.status.into(), status: dto.status.into(),
sender, sender,
queue_position: dto.queue_position, queue_position: dto.queue_position,
@@ -579,7 +578,6 @@ impl SessionApi for RemoteSession {
username: String, username: String,
size: u64, size: u64,
_download_directory: String, _download_directory: String,
_use_username_dirs: bool,
// the daemon's own download directory wins — a remote caller // the daemon's own download directory wins — a remote caller
// cannot see the filesystem the bytes land on. Plumb a per-transfer // cannot see the filesystem the bytes land on. Plumb a per-transfer
// directory through if a client ever needs to sort into subfolders. // directory through if a client ever needs to sort into subfolders.
@@ -598,7 +596,6 @@ impl SessionApi for RemoteSession {
username: String, username: String,
size: u64, size: u64,
_download_directory: String, _download_directory: String,
_use_username_dirs: bool,
metadata: DownloadMetadata, metadata: DownloadMetadata,
) -> Result<(Download, Receiver<DownloadStatus>)> { ) -> Result<(Download, Receiver<DownloadStatus>)> {
self.start_download(filename, username, size, metadata) self.start_download(filename, username, size, metadata)
-1
View File
@@ -177,7 +177,6 @@ mod tests {
token: 1, token: 1,
size, size,
download_directory: "/tmp".to_string(), download_directory: "/tmp".to_string(),
use_username_dirs: false,
status, status,
sender, sender,
queue_position: None, queue_position: None,
-2
View File
@@ -221,7 +221,6 @@ impl MainTui {
let sender = self.downloads_sender(); let sender = self.downloads_sender();
let client = self.client.clone(); let client = self.client.clone();
let download_dir = self.download_dir.clone(); let download_dir = self.download_dir.clone();
let use_username_dirs = self.use_username_dirs;
thread::spawn(move || { thread::spawn(move || {
for (path, size) in files { for (path, size) in files {
match client.download( match client.download(
@@ -229,7 +228,6 @@ impl MainTui {
username.clone(), username.clone(),
size, size,
download_dir.clone(), download_dir.clone(),
use_username_dirs,
) { ) {
Ok((download, rx)) => { Ok((download, rx)) => {
let _ = sender.send((download, rx)); let _ = sender.send((download, rx));
+14 -10
View File
@@ -1,8 +1,10 @@
use super::MainTui; use super::MainTui;
use crate::models::{DownloadEntry, FileDisplayData}; use crate::models::{DownloadEntry, FileDisplayData};
use crate::ui::panes::{InfoSubject, selected_transfer}; use crate::ui::panes::{InfoSubject, selected_transfer};
use crate::ui::sanitize_path_component;
use soulseek_rs::types::UploadStatus; use soulseek_rs::types::UploadStatus;
use soulseek_rs::{DownloadStatus, types::Download}; use soulseek_rs::{DownloadStatus, types::Download};
use std::path::Path;
use std::{sync::mpsc, thread}; use std::{sync::mpsc, thread};
impl MainTui { impl MainTui {
@@ -48,16 +50,9 @@ impl MainTui {
self.state.downloads.remove(index); self.state.downloads.remove(index);
self.select_download_after_removal(index); self.select_download_after_removal(index);
let _ = self.client.remove_download(&username, &filename); let _ = self.client.remove_download(&username, &filename);
let use_username_dirs = self.use_username_dirs;
thread::spawn(move || { thread::spawn(move || {
match client.download( match client.download(filename.clone(), username, size, directory) {
filename.clone(),
username,
size,
directory,
use_username_dirs,
) {
Ok((download, rx)) => { Ok((download, rx)) => {
let _ = sender.send((download, rx)); let _ = sender.send((download, rx));
} }
@@ -214,12 +209,21 @@ impl MainTui {
peer_upload_speed: Some(file.speed), peer_upload_speed: Some(file.speed),
peer_free_slots: Some(file.slots), peer_free_slots: Some(file.slots),
}; };
let download_dir = if use_username_dirs {
let sanitized_username =
sanitize_path_component(file.username.as_str());
Path::new(download_dir.as_str())
.join(sanitized_username.as_str())
.display()
.to_string()
} else {
download_dir.clone()
};
match client.download_with_metadata( match client.download_with_metadata(
file.filename.clone(), file.filename.clone(),
file.username.clone(), file.username.clone(),
file.size, file.size,
download_dir.clone(), download_dir,
use_username_dirs,
metadata, metadata,
) { ) {
Ok((download, rx)) => { Ok((download, rx)) => {
+14 -3
View File
@@ -12,11 +12,13 @@ use crate::persist::{
snapshot::{Snapshot, restore_messages, restore_searches}, snapshot::{Snapshot, restore_messages, restore_searches},
state::{PersistedMessage, StateStore}, state::{PersistedMessage, StateStore},
}; };
use crate::ui::sanitize_path_component;
use color_eyre::Result; use color_eyre::Result;
use ratatui::{ use ratatui::{
DefaultTerminal, DefaultTerminal,
crossterm::event::{self, Event, KeyEventKind, poll}, crossterm::event::{self, Event, KeyEventKind, poll},
}; };
use std::path::Path;
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
pub struct MainTui { pub struct MainTui {
@@ -121,7 +123,6 @@ impl MainTui {
token: 0, token: 0,
size: entry.size, size: entry.size,
download_directory: entry.download_directory, download_directory: entry.download_directory,
use_username_dirs: entry.use_username_dirs,
status: soulseek_rs::DownloadStatus::Completed, status: soulseek_rs::DownloadStatus::Completed,
sender: std::sync::mpsc::channel().0, sender: std::sync::mpsc::channel().0,
queue_position: None, queue_position: None,
@@ -133,13 +134,23 @@ impl MainTui {
} else { } else {
let client = self.client.clone(); let client = self.client.clone();
let sender = sender.clone(); let sender = sender.clone();
let use_username_dirs = self.use_username_dirs;
std::thread::spawn(move || { std::thread::spawn(move || {
let sanitized_username =
sanitize_path_component(entry.username.as_str());
let download_dir = if use_username_dirs {
Path::new(entry.download_directory.as_str())
.join(sanitized_username.as_str())
.display()
.to_string()
} else {
entry.download_directory
};
match client.download( match client.download(
entry.filename.clone(), entry.filename.clone(),
entry.username, entry.username,
entry.size, entry.size,
entry.download_directory, download_dir,
entry.use_username_dirs,
) { ) {
Ok((download, rx)) => { Ok((download, rx)) => {
let _ = sender.send((download, rx)); let _ = sender.send((download, rx));
+1 -1
View File
@@ -20,7 +20,7 @@ impl MainTui {
account, account,
self.download_dir.clone(), self.download_dir.clone(),
self.client.shared_directories(), self.client.shared_directories(),
self.use_username_dirs.clone(), self.use_username_dirs,
)); ));
} }
-3
View File
@@ -84,7 +84,6 @@ fn at_status(
token: 1, token: 1,
size: 4096, size: 4096,
download_directory: "/tmp".to_string(), download_directory: "/tmp".to_string(),
use_username_dirs: false,
status, status,
sender, sender,
queue_position: None, queue_position: None,
@@ -161,7 +160,6 @@ impl SessionApi for TalkativeSession {
_username: String, _username: String,
_size: u64, _size: u64,
_directory: String, _directory: String,
_use_username_dirs: bool,
) -> soulseek_rs::Result<( ) -> soulseek_rs::Result<(
soulseek_rs::types::Download, soulseek_rs::types::Download,
std::sync::mpsc::Receiver<soulseek_rs::DownloadStatus>, std::sync::mpsc::Receiver<soulseek_rs::DownloadStatus>,
@@ -174,7 +172,6 @@ impl SessionApi for TalkativeSession {
_username: String, _username: String,
_size: u64, _size: u64,
_directory: String, _directory: String,
_use_username_dirs: bool,
_metadata: soulseek_rs::types::DownloadMetadata, _metadata: soulseek_rs::types::DownloadMetadata,
) -> soulseek_rs::Result<( ) -> soulseek_rs::Result<(
soulseek_rs::types::Download, soulseek_rs::types::Download,
-1
View File
@@ -339,7 +339,6 @@ mod tests {
token: 1, token: 1,
size: 4096, size: 4096,
download_directory: "/tmp".to_string(), download_directory: "/tmp".to_string(),
use_username_dirs: false,
status, status,
sender, sender,
queue_position: None, queue_position: None,
@@ -436,7 +436,6 @@ mod tests {
token: 1, token: 1,
size: 100, size: 100,
download_directory: "~/dl".to_string(), download_directory: "~/dl".to_string(),
use_username_dirs: false,
status, status,
sender, sender,
queue_position: None, queue_position: None,
+35
View File
@@ -72,6 +72,41 @@ pub const fn get_spinner_char(state: usize) -> &'static str {
SPINNER_CHARS[state % SPINNER_CHARS.len()] SPINNER_CHARS[state % SPINNER_CHARS.len()]
} }
#[must_use]
pub fn sanitize_path_component(name: &str) -> String {
let mut out = name
.chars()
.map(|c| match c {
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
c if c.is_control() => '_',
c => c,
})
.collect::<String>();
if out.is_empty() || out.ends_with(['.', ' ']) {
out.push('_');
}
if is_reserved(out.as_str()) {
out.insert(0, '_');
}
out
}
#[cfg(target_os = "windows")]
fn is_reserved(name: &str) -> bool {
let stem = name.split('.').next().unwrap_or(name).to_ascii_uppercase();
if matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL") {
true
} else {
matches!(stem.get(..3), Some("COM" | "LPT"))
&& matches!(stem.as_bytes().get(3..), Some([b'1'..=b'9']))
}
}
#[cfg(not(target_os = "windows"))]
fn is_reserved(name: &str) -> bool {
false
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
-1
View File
@@ -17,7 +17,6 @@ fn download_entry(filename: &str, status: DownloadStatus) -> DownloadEntry {
token: 1, token: 1,
size: 1000, size: 1000,
download_directory: "/music".into(), download_directory: "/music".into(),
use_username_dirs: false,
status, status,
sender: std::sync::mpsc::channel().0, sender: std::sync::mpsc::channel().0,
queue_position: None, queue_position: None,