mirror of
https://github.com/veeso/termscp.git
synced 2026-09-26 14:01:17 -07:00
Deploy docs to GitHub Pages / deploy (push) Has been cancelled
Site / build-site (push) Has been cancelled
Install.sh / build (macos-latest) (push) Has been cancelled
Install.sh / build (ubuntu-latest) (push) Has been cancelled
CI / toolchain (push) Has been cancelled
CI / fmt (push) Has been cancelled
CI / install-scripts (push) Has been cancelled
CI / crates-ubuntu-latest (push) Has been cancelled
CI / crates-windows-latest (push) Has been cancelled
CI / doc (push) Has been cancelled
CI / deny (push) Has been cancelled
CI / crates-macos-latest (push) Has been cancelled
* ci: migrate project automation to Just Centralize build, test, release, dependency, hook, and website commands in Just recipes. Pin workflow tooling, use the repository toolchain, and run the complete validation set in CI. * ci: codex being codex * docs: update CLAUDE.md for just task runner migration Reflect the switch to just recipes for build/test/clippy/fmt, note dprint replacing raw rustfmt, and add a cross-platform code requirement. * fix: resolve clippy warnings breaking CI on ubuntu and windows Use clone() instead of implicit to_string() on already-owned String values, gate the windows-only unused make_file_at import behind cfg(posix), and fix unused mut / manual assign-op in the windows-only localhost test. * fix: fmt
315 lines
11 KiB
Rust
315 lines
11 KiB
Rust
//! ## Environment
|
|
//!
|
|
//! `environment` is the module which provides Path and values for the system environment
|
|
|
|
// Ext
|
|
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(config_dir);
|
|
#[cfg(test)]
|
|
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().map(|dir| dir.join("termscp")));
|
|
#[cfg(test)]
|
|
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> {
|
|
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.
|
|
/// Returns None if it's not possible to initialize it
|
|
pub fn init_cache_dir() -> Result<Option<PathBuf>, String> {
|
|
if let Some(dir) = CACHE_DIR.as_deref() {
|
|
init_dir(dir).map(Option::Some)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// 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> {
|
|
// 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
|
|
/// Returns: path of bookmarks.toml
|
|
pub fn get_bookmarks_paths(config_dir: &Path) -> PathBuf {
|
|
// Prepare paths
|
|
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
|
|
bookmarks_file.push("bookmarks.toml");
|
|
bookmarks_file
|
|
}
|
|
|
|
/// Returns paths for config client
|
|
/// Returns: path of config.toml and path for ssh keys
|
|
pub fn get_config_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
|
|
// Prepare paths
|
|
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
|
|
bookmarks_file.push("config.toml");
|
|
let mut keys_dir: PathBuf = PathBuf::from(config_dir);
|
|
keys_dir.push(".ssh/"); // Path where keys are stored
|
|
(bookmarks_file, keys_dir)
|
|
}
|
|
|
|
/// Returns the path for the supposed log file
|
|
pub fn get_log_paths(cache_dir: &Path) -> PathBuf {
|
|
let mut log_file: PathBuf = PathBuf::from(cache_dir);
|
|
log_file.push("termscp.log");
|
|
log_file
|
|
}
|
|
|
|
/// Get paths for theme provider
|
|
/// Returns: path of theme.toml
|
|
pub fn get_theme_path(config_dir: &Path) -> PathBuf {
|
|
// Prepare paths
|
|
let mut theme_file: PathBuf = PathBuf::from(config_dir);
|
|
theme_file.push("theme.toml");
|
|
theme_file
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
use std::fs::{File, OpenOptions};
|
|
use std::io::Write;
|
|
|
|
use pretty_assertions::assert_eq;
|
|
use serial_test::serial;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_system_environment_get_config_dir() {
|
|
// Create and get conf_dir
|
|
let conf_dir: PathBuf = init_config_dir().ok().unwrap().unwrap();
|
|
// Remove dir
|
|
assert!(std::fs::remove_dir_all(conf_dir.as_path()).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn should_get_cache_dir() {
|
|
// Create and get cache_dir
|
|
let cache_dir: PathBuf = init_cache_dir().ok().unwrap().unwrap();
|
|
// Remove dir
|
|
assert!(std::fs::remove_dir_all(cache_dir.as_path()).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_system_environment_get_config_dir_err() {
|
|
let mut conf_dir: PathBuf = std::env::temp_dir();
|
|
conf_dir.push("termscp");
|
|
// Create file
|
|
let mut f: File = OpenOptions::new()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(true)
|
|
.open(conf_dir.as_path())
|
|
.ok()
|
|
.unwrap();
|
|
// Write
|
|
assert!(writeln!(f, "Hello world!").is_ok());
|
|
// Drop file
|
|
drop(f);
|
|
// Get config dir (will fail)
|
|
assert!(init_config_dir().is_err());
|
|
// Remove file
|
|
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() {
|
|
assert_eq!(
|
|
get_bookmarks_paths(Path::new("/home/omar/.config/termscp/")),
|
|
PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_system_environment_get_config_paths() {
|
|
assert_eq!(
|
|
get_config_paths(Path::new("/home/omar/.config/termscp/")),
|
|
(
|
|
PathBuf::from("/home/omar/.config/termscp/config.toml"),
|
|
PathBuf::from("/home/omar/.config/termscp/.ssh/")
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_system_environment_get_log_paths() {
|
|
assert_eq!(
|
|
get_log_paths(Path::new("/home/omar/.cache/termscp/")),
|
|
PathBuf::from("/home/omar/.cache/termscp/termscp.log"),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn test_system_environment_get_theme_path() {
|
|
assert_eq!(
|
|
get_theme_path(Path::new("/home/omar/.config/termscp/")),
|
|
PathBuf::from("/home/omar/.config/termscp/theme.toml"),
|
|
);
|
|
}
|
|
}
|