Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,19 @@ options. Therefore `commandline arguments` have the highest precedence.
Configuration files can be placed in the user's local config directory, e.g.
`~/.config/rustic/` or in the global config dir, e.g. `/etc/rustic/`. You can
use different config files, e.g. `myconfig.toml` and use the `-P` option to
specify the profile name, e.g. `rustic -P myconfig`. env variables can be
substituted in config files using the `profile-substitute-env` option. This
allows extra customization possibilities.
specify the profile name, e.g. `rustic -P myconfig`.

On Windows, rustic also searches `%USERPROFILE%\.config\rustic\` after the
platform user config directory and before the global config directory.

To override these locations, set `RUSTIC_HOME` to an absolute rustic
installation home. rustic searches `RUSTIC_HOME/config/` first, then
`XDG_CONFIG_HOME/rustic/` and each `XDG_CONFIG_DIRS/rustic/` directory in
order, before its platform user and global configuration directories. Relative
environment directory entries are ignored.

Environment variables can be substituted in config files using the
`profile-substitute-env` option. This allows extra customization possibilities.

Examples for different configuration files can be found here in the
[/config/](/config) directory.
Expand Down
95 changes: 81 additions & 14 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,20 +314,77 @@ impl GlobalOptions {
///
/// A vector of [`PathBuf`]s to the config files
fn get_config_paths(filename: &str) -> Vec<PathBuf> {
[
ProjectDirs::from("", "", "rustic")
.map(|project_dirs| project_dirs.config_dir().to_path_buf()),
get_global_config_path(),
Some(PathBuf::from(".")),
]
.into_iter()
.filter_map(|path| {
path.map(|mut p| {
p.push(filename);
p
})
})
.collect()
get_rustic_home_config_dir()
.into_iter()
.chain(get_xdg_config_dirs())
.chain(
[
ProjectDirs::from("", "", "rustic")
.map(|project_dirs| project_dirs.config_dir().to_path_buf()),
get_user_config_path(),
get_global_config_path(),
Some(PathBuf::from(".")),
]
.into_iter()
.flatten(),
)
.unique()
.map(|path| path.join(filename))
.collect()
}

/// Get the profile directory configured through `RUSTIC_HOME`.
///
/// An absolute `RUSTIC_HOME` points to a rustic installation home, whose
/// profiles are stored in its `config` directory. Relative values are ignored.
fn get_rustic_home_config_dir() -> Option<PathBuf> {
std::env::var_os("RUSTIC_HOME")
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.map(|path| path.join("config"))
}

/// Get profile directories configured through the XDG environment variables.
///
/// `XDG_CONFIG_HOME` is searched first, followed by the directories in
/// `XDG_CONFIG_DIRS`. Relative paths are ignored as required by the XDG Base
/// Directory Specification.
fn get_xdg_config_dirs() -> Vec<PathBuf> {
std::env::var_os("XDG_CONFIG_HOME")
.into_iter()
.map(PathBuf::from)
.chain(
std::env::var_os("XDG_CONFIG_DIRS")
.as_deref()
.into_iter()
.flat_map(std::env::split_paths),
)
.filter(|path| path.is_absolute())
.map(|path| path.join("rustic"))
.unique()
.collect()
}

/// Get the user-managed config directory on Windows.
///
/// Besides the platform configuration directory returned by [`ProjectDirs`],
/// also look in the XDG-style location commonly used by command-line tools on
/// Windows.
#[cfg(target_os = "windows")]
fn get_user_config_path() -> Option<PathBuf> {
std::env::var_os("USERPROFILE").map(user_config_path)
}

#[cfg(any(test, target_os = "windows"))]
fn user_config_path(user_profile: impl Into<PathBuf>) -> PathBuf {
user_profile.into().join(".config").join("rustic")
}

/// There is no additional user-managed config directory on non-Windows
/// platforms: [`ProjectDirs`] already supplies the conventional location.
#[cfg(not(target_os = "windows"))]
fn get_user_config_path() -> Option<PathBuf> {
None
}

/// Get the path to the global config directory on Windows.
Expand Down Expand Up @@ -408,4 +465,14 @@ mod tests {
// Check Debug
assert_debug_snapshot!(deserialized);
}

#[test]
fn windows_user_config_path_uses_dot_config_directory() {
assert_eq!(
user_config_path(r"C:\Users\rustic"),
PathBuf::from(r"C:\Users\rustic")
.join(".config")
.join("rustic")
);
}
}
139 changes: 139 additions & 0 deletions tests/config_paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! Profile lookup tests using isolated child-process environments.

use std::{env, fs};

use assert_cmd::Command;
use predicates::prelude::*;
use rustic_testing::TestResult;
use tempfile::tempdir;

fn write_profile(directory: &std::path::Path, name: &str, repository: &str) -> TestResult<()> {
let profile_dir = directory.join("rustic");
fs::create_dir_all(&profile_dir)?;
fs::write(
profile_dir.join(format!("{name}.toml")),
format!("[repository]\nrepository = \"{repository}\"\n"),
)?;
Ok(())
}

fn write_rustic_home_profile(
rustic_home: &std::path::Path,
name: &str,
repository: &str,
) -> TestResult<()> {
let config_dir = rustic_home.join("config");
fs::create_dir_all(&config_dir)?;
fs::write(
config_dir.join(format!("{name}.toml")),
format!("[repository]\nrepository = \"{repository}\"\n"),
)?;
Ok(())
}

fn show_config(profile: &str) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_rustic"));
command
.env_remove("RUSTIC_HOME")
.env_remove("RUSTIC_REPOSITORY")
.env_remove("RUSTIC_USE_PROFILE")
.args(["--use-profile", profile, "show-config"]);
command
}

#[test]
fn xdg_config_home_precedes_xdg_config_dirs() -> TestResult<()> {
let temp_dir = tempdir()?;
let config_home = temp_dir.path().join("config-home");
let config_dir = temp_dir.path().join("config-dir");
let profile = "xdg-home-precedence";
write_profile(&config_home, profile, "home-marker")?;
write_profile(&config_dir, profile, "directory-marker")?;

show_config(profile)
.current_dir(temp_dir.path())
.env("XDG_CONFIG_HOME", &config_home)
.env("XDG_CONFIG_DIRS", env::join_paths([&config_dir])?)
.assert()
.success()
.stdout(predicate::str::contains("repository = \"home-marker\""))
.stdout(predicate::str::contains("directory-marker").not());

Ok(())
}

#[test]
fn xdg_config_dirs_are_searched_in_order() -> TestResult<()> {
let temp_dir = tempdir()?;
let config_home = temp_dir.path().join("config-home-without-profile");
let first_config_dir = temp_dir.path().join("first-config-dir");
let second_config_dir = temp_dir.path().join("second-config-dir");
let profile = "xdg-directory-order";
write_profile(&first_config_dir, profile, "first-directory-marker")?;
write_profile(&second_config_dir, profile, "second-directory-marker")?;

show_config(profile)
.current_dir(temp_dir.path())
.env("XDG_CONFIG_HOME", &config_home)
.env(
"XDG_CONFIG_DIRS",
env::join_paths([&first_config_dir, &second_config_dir])?,
)
.assert()
.success()
.stdout(predicate::str::contains(
"repository = \"first-directory-marker\"",
))
.stdout(predicate::str::contains("second-directory-marker").not());

Ok(())
}

#[test]
fn rustic_home_precedes_xdg_config_directories() -> TestResult<()> {
let temp_dir = tempdir()?;
let rustic_home = temp_dir.path().join("rustic-home");
let config_home = temp_dir.path().join("config-home");
let config_dir = temp_dir.path().join("config-dir");
let profile = "rustic-home-precedence";
write_rustic_home_profile(&rustic_home, profile, "rustic-home-marker")?;
write_profile(&config_home, profile, "xdg-home-marker")?;
write_profile(&config_dir, profile, "xdg-directory-marker")?;

show_config(profile)
.current_dir(temp_dir.path())
.env("RUSTIC_HOME", &rustic_home)
.env("XDG_CONFIG_HOME", &config_home)
.env("XDG_CONFIG_DIRS", env::join_paths([&config_dir])?)
.assert()
.success()
.stdout(predicate::str::contains(
"repository = \"rustic-home-marker\"",
))
.stdout(predicate::str::contains("xdg-home-marker").not())
.stdout(predicate::str::contains("xdg-directory-marker").not());

Ok(())
}

#[test]
fn relative_rustic_home_is_ignored() -> TestResult<()> {
let temp_dir = tempdir()?;
let config_home = temp_dir.path().join("config-home");
let relative_rustic_home = temp_dir.path().join("relative-rustic-home");
let profile = "relative-rustic-home";
write_rustic_home_profile(&relative_rustic_home, profile, "relative-marker")?;
write_profile(&config_home, profile, "xdg-home-marker")?;

show_config(profile)
.current_dir(temp_dir.path())
.env("RUSTIC_HOME", "relative-rustic-home")
.env("XDG_CONFIG_HOME", &config_home)
.env_remove("XDG_CONFIG_DIRS")
.assert()
.success()
.stdout(predicate::str::contains("repository = \"xdg-home-marker\""))
.stdout(predicate::str::contains("relative-marker").not());

Ok(())
}