diff --git a/Cargo.lock b/Cargo.lock index fe1478b..755898c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,7 +692,7 @@ dependencies = [ [[package]] name = "git-fs" -version = "0.1.1-alpha.1" +version = "0.1.2-alpha.1" dependencies = [ "async-trait", "base64", @@ -717,6 +717,7 @@ dependencies = [ "semver", "serde", "serde_json", + "shellexpand", "thiserror", "tokio", "toml", @@ -1889,6 +1890,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +dependencies = [ + "dirs", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 164756e..55faf7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ secrecy = { version = "0.10.3", features = ["serde"] } bimap = "0.6.3" self_update = { version = "0.42", default-features = false, features = ["rustls"] } semver = "1.0" +shellexpand = "3.1" inquire = "0.9.2" tracing-indicatif = "0.3.14" diff --git a/src/app_config.rs b/src/app_config.rs index a87b03a..79b43f7 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -16,6 +16,52 @@ use serde::{Deserialize, Serialize}; use crate::onboarding::{self, OnboardingError}; +/// A `PathBuf` that automatically expands `~` to the user's home directory +/// during deserialization. This ensures that any path loaded from configuration +/// is already resolved. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(transparent)] +pub struct ExpandedPathBuf(PathBuf); + +impl<'de> Deserialize<'de> for ExpandedPathBuf { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw = String::deserialize(deserializer)?; + let expanded = shellexpand::tilde(&raw); + Ok(Self(PathBuf::from(expanded.into_owned()))) + } +} + +impl ExpandedPathBuf { + /// Creates a new `ExpandedPathBuf` from any path, without expansion. + /// Use this for programmatically-constructed paths that are already absolute. + pub fn new(path: PathBuf) -> Self { + Self(path) + } +} + +impl std::ops::Deref for ExpandedPathBuf { + type Target = Path; + + fn deref(&self) -> &Path { + &self.0 + } +} + +impl AsRef for ExpandedPathBuf { + fn as_ref(&self) -> &Path { + &self.0 + } +} + +impl std::fmt::Display for ExpandedPathBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.display().fmt(f) + } +} + fn mesa_runtime_dir() -> Option { let runtime_dir = dirs::runtime_dir(); if let Some(path) = runtime_dir { @@ -30,15 +76,17 @@ fn mesa_runtime_dir() -> Option { None } -fn default_pid_file() -> PathBuf { - mesa_runtime_dir().map_or_else( +fn default_pid_file() -> ExpandedPathBuf { + ExpandedPathBuf::new(mesa_runtime_dir().map_or_else( || PathBuf::from("/var/run/git-fs.pid"), |rd| rd.join("git-fs.pid"), - ) + )) } -fn default_mount_point() -> PathBuf { - mesa_runtime_dir().map_or_else(|| PathBuf::from("/tmp/git-fs/mnt"), |rd| rd.join("mnt")) +fn default_mount_point() -> ExpandedPathBuf { + ExpandedPathBuf::new( + mesa_runtime_dir().map_or_else(|| PathBuf::from("/tmp/git-fs/mnt"), |rd| rd.join("mnt")), + ) } fn current_uid() -> u32 { @@ -57,15 +105,17 @@ pub struct CacheConfig { pub max_size: Option, /// The path to the cache directory. - pub path: PathBuf, + pub path: ExpandedPathBuf, } impl Default for CacheConfig { fn default() -> Self { Self { max_size: None, - path: mesa_runtime_dir() - .map_or_else(|| PathBuf::from("/tmp/git-fs/cache"), |rd| rd.join("cache")), + path: ExpandedPathBuf::new( + mesa_runtime_dir() + .map_or_else(|| PathBuf::from("/tmp/git-fs/cache"), |rd| rd.join("cache")), + ), } } } @@ -184,7 +234,7 @@ impl<'a> From<&'a OrganizationConfig> for DangerousOrganizationConfig<'a> { pub struct DaemonConfig { /// The path to the PID file for the daemon. Uses /var/run/git-fs.pid if not specified. #[serde(default = "default_pid_file")] - pub pid_file: PathBuf, + pub pid_file: ExpandedPathBuf, } impl Default for DaemonConfig { @@ -214,7 +264,7 @@ pub struct Config { /// The mount point for the filesystem. #[serde(default = "default_mount_point")] - pub mount_point: PathBuf, + pub mount_point: ExpandedPathBuf, /// The user to mount the filesystem as. If not specified, runs as the current user. #[serde(default = "current_uid")] diff --git a/src/daemon.rs b/src/daemon.rs index e56e708..4e68bea 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -66,7 +66,7 @@ mod managed_fuse { impl ManagedFuse { pub fn new(config: &app_config::Config) -> Self { Self { - mount_point: config.mount_point.clone(), + mount_point: config.mount_point.to_path_buf(), } } diff --git a/src/main.rs b/src/main.rs index e2321c0..5ddb0b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,7 +94,7 @@ fn main() { } let daemonize = daemonize::Daemonize::new() - .pid_file(config.daemon.pid_file.clone()) + .pid_file(&config.daemon.pid_file) .chown_pid_file(true) .user(config.uid) .group(config.gid); diff --git a/src/onboarding.rs b/src/onboarding.rs index 2dbfac3..3fdcb02 100644 --- a/src/onboarding.rs +++ b/src/onboarding.rs @@ -1,11 +1,12 @@ //! Interactive onboarding wizard for first-time configuration. -use std::{io::IsTerminal as _, path::PathBuf}; +use std::io::IsTerminal as _; +use std::path::PathBuf; use inquire::{Confirm, Password, Text, validator::Validation}; use secrecy::SecretString; -use crate::app_config::{Config, OrganizationConfig}; +use crate::app_config::{Config, ExpandedPathBuf, OrganizationConfig}; const WELCOME_MESSAGE: &str = " \x1b[32m@@@@\x1b[0m Welcome to \x1b[1mgit-fs\x1b[0m! Let's get you started! @@ -49,7 +50,9 @@ pub fn run_wizard() -> Result { let mount_point_str = Text::new("Where should git-fs mount the filesystem?") .with_default(&defaults.mount_point.display().to_string()) .prompt()?; - let mount_point = PathBuf::from(mount_point_str); + let mount_point = ExpandedPathBuf::new(PathBuf::from( + shellexpand::tilde(&mount_point_str).into_owned(), + )); let mut org_keys: Vec<(String, SecretString)> = Vec::new(); loop {