feat: conventional default download and share folders
The default download directory is now the platform Downloads folder plus a Soulseek subfolder (via the directories crate — XDG on Linux, known folder on Windows) instead of a tilde path that only worked with a HOME variable. Sharing follows the Soulseek convention of sharing what you download: shared_dir defaults to the download folder, and an explicitly empty shared_dir opts out. The folder is created at startup so first-run share validation succeeds.
This commit is contained in:
@@ -40,7 +40,8 @@ pub struct Cli {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Option<Commands>,
|
pub command: Option<Commands>,
|
||||||
|
|
||||||
/// Directory downloads are saved to (default: ~/Downloads)
|
/// Directory downloads are saved to (default: your Downloads/Soulseek
|
||||||
|
/// folder, which is also shared unless shared_dir says otherwise)
|
||||||
#[arg(short, long, env = "SOULSEEK_DOWNLOAD_DIR")]
|
#[arg(short, long, env = "SOULSEEK_DOWNLOAD_DIR")]
|
||||||
pub download_dir: Option<String>,
|
pub download_dir: Option<String>,
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,16 @@ fn run_default_tui(
|
|||||||
|
|
||||||
let (server_host, server_port) = parse_server_address(&resolved.server)?;
|
let (server_host, server_port) = parse_server_address(&resolved.server)?;
|
||||||
|
|
||||||
|
// Make sure the download folder exists up front: first-run defaults point
|
||||||
|
// download and shared at the same fresh Downloads/Soulseek folder, and
|
||||||
|
// shared-directory validation requires an existing path.
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&resolved.download_dir) {
|
||||||
|
eprintln!(
|
||||||
|
"⚠️ Could not create download directory {}: {e}",
|
||||||
|
resolved.download_dir
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let shared_directory = match directories::resolve_shared_directory(
|
let shared_directory = match directories::resolve_shared_directory(
|
||||||
resolved.shared_dir.as_deref(),
|
resolved.shared_dir.as_deref(),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ pub struct Resolved {
|
|||||||
|
|
||||||
pub const DEFAULT_SERVER: &str = "server.slsknet.org:2416";
|
pub const DEFAULT_SERVER: &str = "server.slsknet.org:2416";
|
||||||
pub const DEFAULT_LISTENER_PORT: u16 = 2234;
|
pub const DEFAULT_LISTENER_PORT: u16 = 2234;
|
||||||
pub const DEFAULT_DOWNLOAD_DIR: &str = "~/Downloads";
|
|
||||||
pub const DEFAULT_MAX_CONCURRENT_DOWNLOADS: usize = 5;
|
pub const DEFAULT_MAX_CONCURRENT_DOWNLOADS: usize = 5;
|
||||||
pub const DEFAULT_SEARCH_TIMEOUT: u64 = 10;
|
pub const DEFAULT_SEARCH_TIMEOUT: u64 = 10;
|
||||||
|
|
||||||
@@ -80,6 +79,11 @@ pub const DEFAULT_SEARCH_TIMEOUT: u64 = 10;
|
|||||||
/// unless the flag is passed.
|
/// unless the flag is passed.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn resolve(cli: &crate::cli::Cli, file: &FileConfig) -> Resolved {
|
pub fn resolve(cli: &crate::cli::Cli, file: &FileConfig) -> Resolved {
|
||||||
|
let download_dir = cli
|
||||||
|
.download_dir
|
||||||
|
.clone()
|
||||||
|
.or_else(|| file.download_dir.clone())
|
||||||
|
.unwrap_or_else(super::paths::default_download_dir);
|
||||||
Resolved {
|
Resolved {
|
||||||
username: cli.username.clone().or_else(|| file.username.clone()),
|
username: cli.username.clone().or_else(|| file.username.clone()),
|
||||||
server: cli
|
server: cli
|
||||||
@@ -93,12 +97,15 @@ pub fn resolve(cli: &crate::cli::Cli, file: &FileConfig) -> Resolved {
|
|||||||
.unwrap_or(DEFAULT_LISTENER_PORT),
|
.unwrap_or(DEFAULT_LISTENER_PORT),
|
||||||
disable_listener: cli.disable_listener
|
disable_listener: cli.disable_listener
|
||||||
|| file.disable_listener.unwrap_or(false),
|
|| file.disable_listener.unwrap_or(false),
|
||||||
download_dir: cli
|
download_dir: download_dir.clone(),
|
||||||
.download_dir
|
// Soulseek convention: share what you download. An explicitly empty
|
||||||
|
// shared_dir ("") opts out of sharing entirely.
|
||||||
|
shared_dir: cli
|
||||||
|
.shared_dir
|
||||||
.clone()
|
.clone()
|
||||||
.or_else(|| file.download_dir.clone())
|
.or_else(|| file.shared_dir.clone())
|
||||||
.unwrap_or_else(|| DEFAULT_DOWNLOAD_DIR.to_string()),
|
.or(Some(download_dir))
|
||||||
shared_dir: cli.shared_dir.clone().or_else(|| file.shared_dir.clone()),
|
.filter(|dir| !dir.trim().is_empty()),
|
||||||
max_concurrent_downloads: cli
|
max_concurrent_downloads: cli
|
||||||
.max_concurrent_downloads
|
.max_concurrent_downloads
|
||||||
.or(file.max_concurrent_downloads)
|
.or(file.max_concurrent_downloads)
|
||||||
@@ -138,7 +145,6 @@ mod tests {
|
|||||||
let resolved = resolve(&bare_cli(), &FileConfig::default());
|
let resolved = resolve(&bare_cli(), &FileConfig::default());
|
||||||
assert_eq!(resolved.server, DEFAULT_SERVER);
|
assert_eq!(resolved.server, DEFAULT_SERVER);
|
||||||
assert_eq!(resolved.listener_port, DEFAULT_LISTENER_PORT);
|
assert_eq!(resolved.listener_port, DEFAULT_LISTENER_PORT);
|
||||||
assert_eq!(resolved.download_dir, DEFAULT_DOWNLOAD_DIR);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolved.max_concurrent_downloads,
|
resolved.max_concurrent_downloads,
|
||||||
DEFAULT_MAX_CONCURRENT_DOWNLOADS
|
DEFAULT_MAX_CONCURRENT_DOWNLOADS
|
||||||
@@ -148,6 +154,49 @@ mod tests {
|
|||||||
assert_eq!(resolved.username, None);
|
assert_eq!(resolved.username, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_download_dir_is_a_soulseek_folder_under_downloads() {
|
||||||
|
let resolved = resolve(&bare_cli(), &FileConfig::default());
|
||||||
|
let path = std::path::Path::new(&resolved.download_dir);
|
||||||
|
assert!(path.is_absolute(), "must not rely on ~ expansion");
|
||||||
|
assert_eq!(path.file_name().and_then(|n| n.to_str()), Some("Soulseek"));
|
||||||
|
assert_eq!(
|
||||||
|
path.parent()
|
||||||
|
.and_then(|p| p.file_name())
|
||||||
|
.and_then(|n| n.to_str()),
|
||||||
|
Some("Downloads")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shared_dir_defaults_to_the_download_dir() {
|
||||||
|
let resolved = resolve(&bare_cli(), &FileConfig::default());
|
||||||
|
assert_eq!(
|
||||||
|
resolved.shared_dir.as_deref(),
|
||||||
|
Some(resolved.download_dir.as_str())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shared_dir_follows_a_customized_download_dir() {
|
||||||
|
let file = FileConfig {
|
||||||
|
download_dir: Some("/music".into()),
|
||||||
|
..FileConfig::default()
|
||||||
|
};
|
||||||
|
let resolved = resolve(&bare_cli(), &file);
|
||||||
|
assert_eq!(resolved.shared_dir.as_deref(), Some("/music"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_shared_dir_disables_sharing() {
|
||||||
|
let file = FileConfig {
|
||||||
|
shared_dir: Some(String::new()),
|
||||||
|
..FileConfig::default()
|
||||||
|
};
|
||||||
|
let resolved = resolve(&bare_cli(), &file);
|
||||||
|
assert_eq!(resolved.shared_dir, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn file_values_override_defaults() {
|
fn file_values_override_defaults() {
|
||||||
let file = FileConfig {
|
let file = FileConfig {
|
||||||
|
|||||||
@@ -23,3 +23,21 @@ pub fn state_dir() -> Option<PathBuf> {
|
|||||||
fn project_dirs() -> Option<ProjectDirs> {
|
fn project_dirs() -> Option<ProjectDirs> {
|
||||||
ProjectDirs::from("", "", "soulseek-rs")
|
ProjectDirs::from("", "", "soulseek-rs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default download (and shared) folder: a `Soulseek` subfolder of the
|
||||||
|
/// platform Downloads directory (XDG on Linux, known-folder on Windows),
|
||||||
|
/// so Soulseek files don't clutter the Downloads root.
|
||||||
|
#[must_use]
|
||||||
|
pub fn default_download_dir() -> String {
|
||||||
|
let user_dirs = directories::UserDirs::new();
|
||||||
|
let downloads = user_dirs
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|dirs| dirs.download_dir().map(std::path::Path::to_path_buf))
|
||||||
|
.or_else(|| {
|
||||||
|
user_dirs
|
||||||
|
.as_ref()
|
||||||
|
.map(|dirs| dirs.home_dir().join("Downloads"))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| std::path::PathBuf::from("Downloads"));
|
||||||
|
downloads.join("Soulseek").display().to_string()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user