mirror of
https://github.com/veeso/termscp.git
synced 2026-07-02 12:44:12 +02:00
feat(config): move config dir to ~/.config/termscp on macOS and %USERPROFILE%\.termscp on Windows
Resolve the config directory through a single per-platform config_dir() function instead of relying on dirs::config_dir everywhere: - macOS: ~/.config/termscp (was ~/Library/Application Support/termscp) - Windows: %USERPROFILE%\.termscp (was roaming %APPDATA%\termscp) - Linux/other: /termscp (unchanged) Existing users are migrated automatically on first run: when the new directory is absent and the legacy location exists, the whole config directory is moved to the new path. The cache directory stays at the platform-native location. Closes #431
This commit is contained in:
@@ -6,24 +6,89 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// termscp's configuration directory, including the `termscp` project subdirectory.
|
||||
///
|
||||
/// See [`config_dir`] for the per-platform locations.
|
||||
#[cfg(not(test))]
|
||||
static CONF_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::config_dir);
|
||||
static CONF_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(config_dir);
|
||||
#[cfg(test)]
|
||||
static CONF_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| Some(std::env::temp_dir()));
|
||||
static CONF_DIR: LazyLock<Option<PathBuf>> =
|
||||
LazyLock::new(|| Some(std::env::temp_dir().join("termscp")));
|
||||
|
||||
/// termscp's cache directory, including the `termscp` project subdirectory.
|
||||
///
|
||||
/// The cache directory stays at the platform-native location (`dirs::cache_dir`)
|
||||
/// on every platform.
|
||||
#[cfg(not(test))]
|
||||
static CACHE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(dirs::cache_dir);
|
||||
static CACHE_DIR: LazyLock<Option<PathBuf>> =
|
||||
LazyLock::new(|| dirs::cache_dir().map(|dir| dir.join("termscp")));
|
||||
#[cfg(test)]
|
||||
static CACHE_DIR: LazyLock<Option<PathBuf>> = LazyLock::new(|| Some(std::env::temp_dir()));
|
||||
static CACHE_DIR: LazyLock<Option<PathBuf>> =
|
||||
LazyLock::new(|| Some(std::env::temp_dir().join("termscp")));
|
||||
|
||||
/// Resolves termscp's configuration directory for the current platform,
|
||||
/// including the `termscp` project subdirectory.
|
||||
///
|
||||
/// The location is platform-specific:
|
||||
///
|
||||
/// - **Linux** (and other Unix): `$XDG_CONFIG_HOME/termscp` (usually `~/.config/termscp`)
|
||||
/// - **macOS**: `~/.config/termscp` (instead of `~/Library/Application Support/termscp`)
|
||||
/// - **Windows**: `%USERPROFILE%\.termscp` (instead of the roaming `%APPDATA%\termscp`)
|
||||
#[cfg(not(test))]
|
||||
fn config_dir() -> Option<PathBuf> {
|
||||
#[cfg(macos)]
|
||||
{
|
||||
// macOS: use ~/.config/termscp for consistency with Linux and easier
|
||||
// CLI access, instead of ~/Library/Application Support/termscp.
|
||||
dirs::home_dir().map(|home| home.join(".config").join("termscp"))
|
||||
}
|
||||
#[cfg(win)]
|
||||
{
|
||||
// Windows: use %USERPROFILE%\.termscp instead of the roaming %APPDATA%.
|
||||
dirs::home_dir().map(|home| home.join(".termscp"))
|
||||
}
|
||||
#[cfg(not(any(macos, win)))]
|
||||
{
|
||||
// Linux and other platforms: keep the XDG-compliant location.
|
||||
dirs::config_dir().map(|dir| dir.join("termscp"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the legacy (pre-1.1.0) configuration directory, if the current
|
||||
/// platform used a different location than the one returned by [`config_dir`].
|
||||
///
|
||||
/// Used to migrate an existing user configuration to the new location.
|
||||
/// Returns `None` on platforms where the location did not change (e.g. Linux).
|
||||
#[cfg(not(test))]
|
||||
fn legacy_config_dir() -> Option<PathBuf> {
|
||||
#[cfg(any(macos, win))]
|
||||
{
|
||||
// Before 1.1.0 the config dir was always `dirs::config_dir()/termscp`.
|
||||
dirs::config_dir().map(|dir| dir.join("termscp"))
|
||||
}
|
||||
#[cfg(not(any(macos, win)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// In tests we never want to touch the developer's real configuration, so there
|
||||
/// is no legacy directory to migrate from.
|
||||
#[cfg(test)]
|
||||
fn legacy_config_dir() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get termscp config directory path and initialize it.
|
||||
/// Returns None if it's not possible to initialize it
|
||||
pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
||||
if let Some(dir) = CONF_DIR.as_deref() {
|
||||
init_dir(dir).map(Option::Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
let Some(dir) = CONF_DIR.as_deref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
// Migrate an existing configuration from the legacy location, if necessary,
|
||||
// before creating (and thus claiming) the new directory.
|
||||
migrate_config_dir(legacy_config_dir().as_deref(), dir)?;
|
||||
init_dir(dir).map(Option::Some)
|
||||
}
|
||||
|
||||
/// Get termscp cache directory path and initialize it.
|
||||
@@ -36,21 +101,36 @@ pub fn init_cache_dir() -> Result<Option<PathBuf>, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Init a termscp env dir
|
||||
/// Moves the legacy configuration directory to the new location when a migration
|
||||
/// is needed.
|
||||
///
|
||||
/// A migration happens only when `new_dir` does not exist yet and `legacy` is
|
||||
/// `Some` and points to an existing directory; otherwise this is a no-op.
|
||||
fn migrate_config_dir(legacy: Option<&Path>, new_dir: &Path) -> Result<(), String> {
|
||||
let Some(legacy) = legacy else {
|
||||
return Ok(());
|
||||
};
|
||||
// Nothing to migrate if the new dir is already there or the legacy one is gone.
|
||||
if new_dir.exists() || !legacy.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
// Ensure the parent of the new dir exists before moving into it.
|
||||
if let Some(parent) = new_dir.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|err| err.to_string())?;
|
||||
}
|
||||
std::fs::rename(legacy, new_dir).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
/// Init a termscp env dir, creating it if it doesn't already exist.
|
||||
fn init_dir(p: &Path) -> Result<PathBuf, String> {
|
||||
// Get path of bookmarks
|
||||
let mut p: PathBuf = p.to_path_buf();
|
||||
// Append termscp dir
|
||||
p.push("termscp/");
|
||||
// If directory doesn't exist, create it
|
||||
if p.exists() {
|
||||
return Ok(p);
|
||||
}
|
||||
// directory doesn't exist; create dir recursively
|
||||
match std::fs::create_dir_all(p.as_path()) {
|
||||
Ok(_) => Ok(p),
|
||||
Err(err) => Err(err.to_string()),
|
||||
// If the directory already exists, there's nothing to do.
|
||||
if p.is_dir() {
|
||||
return Ok(p.to_path_buf());
|
||||
}
|
||||
// Directory doesn't exist; create it recursively. This fails if the path is
|
||||
// already occupied by a non-directory file.
|
||||
std::fs::create_dir_all(p).map_err(|err| err.to_string())?;
|
||||
Ok(p.to_path_buf())
|
||||
}
|
||||
|
||||
/// Get paths for bookmarks client
|
||||
@@ -140,6 +220,58 @@ mod tests {
|
||||
assert!(std::fs::remove_file(conf_dir.as_path()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn should_migrate_legacy_config_dir() {
|
||||
let base = std::env::temp_dir().join("termscp-migrate-test-move");
|
||||
let legacy = base.join("legacy");
|
||||
let new_dir = base.join("new");
|
||||
// Clean up any leftovers from a previous run
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
// Set up a legacy dir holding a config file
|
||||
std::fs::create_dir_all(&legacy).unwrap();
|
||||
let legacy_file = legacy.join("config.toml");
|
||||
std::fs::write(&legacy_file, b"hello").unwrap();
|
||||
// Migrate
|
||||
assert!(migrate_config_dir(Some(legacy.as_path()), new_dir.as_path()).is_ok());
|
||||
// Legacy dir is gone, new dir holds the file
|
||||
assert!(!legacy.exists());
|
||||
assert!(new_dir.join("config.toml").exists());
|
||||
// Cleanup
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn should_not_migrate_when_new_dir_exists() {
|
||||
let base = std::env::temp_dir().join("termscp-migrate-test-keep");
|
||||
let legacy = base.join("legacy");
|
||||
let new_dir = base.join("new");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
std::fs::create_dir_all(&legacy).unwrap();
|
||||
std::fs::create_dir_all(&new_dir).unwrap();
|
||||
// Both exist: must be a no-op, legacy left untouched
|
||||
assert!(migrate_config_dir(Some(legacy.as_path()), new_dir.as_path()).is_ok());
|
||||
assert!(legacy.exists());
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn should_not_migrate_without_legacy_dir() {
|
||||
let base = std::env::temp_dir().join("termscp-migrate-test-none");
|
||||
let new_dir = base.join("new");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
// No legacy dir at all
|
||||
assert!(migrate_config_dir(None, new_dir.as_path()).is_ok());
|
||||
assert!(!new_dir.exists());
|
||||
// Legacy provided but missing
|
||||
let missing = base.join("missing");
|
||||
assert!(migrate_config_dir(Some(missing.as_path()), new_dir.as_path()).is_ok());
|
||||
assert!(!new_dir.exists());
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_system_environment_get_bookmarks_paths() {
|
||||
|
||||
Reference in New Issue
Block a user