Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adhere to XDG spec for file locations #385

Merged
merged 1 commit into from
Sep 17, 2024
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Add `default` field to profiles
- When using the CLI, the `--profile` argument can be omitted to use the default profile

### Changed

- Update file locations to adhere to XDG spec on Linux [#371](https://github.com/LucasPickering/slumber/issues/371)
- Move config file to [config dir](https://docs.rs/dirs/latest/dirs/fn.config_dir.html), which remains the same on MacOS/Windows but changes on Linux. For backward compatibility, the previous path ([data dir](https://docs.rs/dirs/latest/dirs/fn.data_dir.html)) will be checked and used if present
- Move log files to [state dir](https://docs.rs/dirs/latest/dirs/fn.state_dir.html) on Linux and [cache dir](https://docs.rs/dirs/latest/dirs/fn.cache_dir.html) on MacOS/Windows
- Database file remains in [data dir](https://docs.rs/dirs/latest/dirs/fn.data_dir.html) on all platforms
- Create config file on startup if it doesn't exist
- If config file fails to load during TUI startup, display an error and fall back to the default, rather than crashing

### Fixed

- Updated the Configuration docs to remove the non-existent `slumber show dir` command (thanks @SVendittelli)
Expand Down
8 changes: 2 additions & 6 deletions crates/cli/src/commands/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ use crate::{GlobalArgs, Subcommand};
use clap::Parser;
use serde::Serialize;
use slumber_config::Config;
use slumber_core::{
collection::CollectionFile, db::Database, util::DataDirectory,
};
use slumber_core::{collection::CollectionFile, db::Database, util::paths};
use std::{borrow::Cow, path::Path, process::ExitCode};

/// Print meta information about Slumber (config, collections, etc.)
Expand All @@ -30,11 +28,9 @@ impl Subcommand for ShowCommand {
ShowTarget::Paths => {
let collection_path =
CollectionFile::try_path(None, global.file);
let data_dir = DataDirectory::get();
println!("Data directory: {}", data_dir);
println!("Log file: {}", data_dir.log_file().display());
println!("Config: {}", Config::path().display());
println!("Database: {}", Database::path().display());
println!("Log file: {}", paths::log_file().display());
println!(
"Collection: {}",
collection_path
Expand Down
61 changes: 40 additions & 21 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use slumber_core::{
http::HttpEngineConfig,
util::{expand_home, parse_yaml, DataDirectory, ResultTraced},
util::{
parse_yaml,
paths::{self, create_parent, expand_home},
ResultTraced,
},
};
use std::{env, fs::File, path::PathBuf};
use std::{env, fs::OpenOptions, path::PathBuf};
use tracing::info;

const PATH_ENV_VAR: &str = "SLUMBER_CONFIG_PATH";
Expand Down Expand Up @@ -51,11 +55,25 @@ pub struct Config {
}

impl Config {
/// Path to the configuration file
/// Path to the configuration file, in this precedence:
/// - Value of `$SLUMBER_CONFIG_PATH`
/// - `$DATA_DIR/slumber/config.yml` **if the file exists**, where
/// `$DATA_DIR` is defined by [dirs::data_dir]. This is a legacy location,
/// supported for backward compatibility only. See this issue for more:
/// https://github.com/LucasPickering/slumber/issues/371
/// - `$CONFIG_DIR/slumber/config.yml`, where `$CONFIG_DIR` is defined by
/// [dirs::config_dir]
pub fn path() -> PathBuf {
env::var(PATH_ENV_VAR)
.map(|path| expand_home(PathBuf::from(path)).into_owned())
.unwrap_or_else(|_| DataDirectory::get().file(FILE))
if let Ok(path) = env::var(PATH_ENV_VAR) {
return expand_home(PathBuf::from(path)).into_owned();
}

let legacy_path = paths::data_directory().join(FILE);
if legacy_path.is_file() {
return legacy_path;
}

paths::config_directory().join(FILE)
}

/// Load configuration from the file, if present. If not, just return a
Expand All @@ -67,23 +85,24 @@ impl Config {
/// can specify their own types
pub fn load() -> anyhow::Result<Self> {
let path = Self::path();
create_parent(&path)?;

info!(?path, "Loading configuration file");

match File::open(&path) {
Ok(file) => parse_yaml::<Self>(&file)
.context(format!("Error loading configuration from {path:?}"))
.traced(),
// An error here is probably just the file missing, so don't make
// a big stink about it
Err(error) => {
info!(
?path,
error = &error as &dyn std::error::Error,
"Error reading configuration file"
);
Ok(Self::default())
}
}
// Open the config file, creating it if it doesn't exist. This will
// never create the legacy file, because the file must already exist in
// order for the legacy location to be used.
(|| {
let file = OpenOptions::new()
.create(true)
.append(true)
.read(true)
.open(&path)?;
let config = parse_yaml::<Self>(&file)?;
Ok::<_, anyhow::Error>(config)
})()
.context(format!("Error loading configuration from {path:?}"))
.traced()
}
}

Expand Down
10 changes: 6 additions & 4 deletions crates/core/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
collection::{ProfileId, RecipeId},
db::convert::{CollectionPath, JsonEncoded, SqlWrap},
http::{Exchange, ExchangeSummary, RequestId},
util::{DataDirectory, ResultTraced},
util::{paths, ResultTraced},
};
use anyhow::{anyhow, Context};
use derive_more::Display;
Expand Down Expand Up @@ -54,6 +54,8 @@ impl Database {
/// not after that.
pub fn load() -> anyhow::Result<Self> {
let path = Self::path();
paths::create_parent(&path)?;

info!(?path, "Loading database");
let mut connection = Connection::open(path)?;
connection.pragma_update(
Expand All @@ -71,7 +73,7 @@ impl Database {

/// Path to the database file
pub fn path() -> PathBuf {
DataDirectory::get().file(Self::FILE)
paths::data_directory().join(Self::FILE)
}

/// Apply database migrations
Expand Down Expand Up @@ -505,7 +507,7 @@ impl crate::test_util::Factory for Database {
#[cfg(any(test, feature = "test"))]
impl crate::test_util::Factory for CollectionDatabase {
fn factory(_: ()) -> Self {
use crate::util::get_repo_root;
use crate::util::paths::get_repo_root;
Database::factory(())
.into_collection(&get_repo_root().join("slumber.yml"))
.expect("Error initializing DB collection")
Expand All @@ -515,7 +517,7 @@ impl crate::test_util::Factory for CollectionDatabase {
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_util::Factory, util::get_repo_root};
use crate::{test_util::Factory, util::paths::get_repo_root};
use itertools::Itertools;
use std::collections::HashMap;

Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/db/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ mod tests {
db::convert::{CollectionPath, JsonEncoded},
http::{RequestRecord, ResponseRecord},
test_util::Factory,
util::get_repo_root,
util::paths::get_repo_root,
};
use itertools::Itertools;
use reqwest::{Method, StatusCode};
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/template/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
Prompt, Select, Template, TemplateChunk, TemplateContext,
TemplateError, TemplateKey,
},
util::{expand_home, FutureCache, FutureCacheOutcome, ResultTraced},
util::{paths::expand_home, FutureCache, FutureCacheOutcome, ResultTraced},
};
use async_trait::async_trait;
use chrono::Utc;
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
collection::{ChainSource, HasId},
http::{HttpEngine, HttpEngineConfig},
template::{Prompt, Prompter, Select},
util::{get_repo_root, ResultTraced},
util::{paths::get_repo_root, ResultTraced},
};
use anyhow::Context;
use derive_more::Deref;
Expand Down
4 changes: 1 addition & 3 deletions crates/core/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
//! Miscellaneous utility constants/types/functions

mod paths;

pub use crate::util::paths::*;
pub mod paths;

use crate::{http::RequestError, template::ChainError};
use chrono::{
Expand Down
112 changes: 55 additions & 57 deletions crates/core/src/util/paths.rs
Original file line number Diff line number Diff line change
@@ -1,76 +1,74 @@
use anyhow::Context;
use derive_more::Display;
use anyhow::{anyhow, Context};
use std::{
borrow::Cow,
fs,
path::{Path, PathBuf},
sync::OnceLock,
};

// Store directories statically so we can create them once at startup and access
// them subsequently anywhere
static DATA_DIRECTORY: OnceLock<DataDirectory> = OnceLock::new();
/// Get the path of the directory to contain the config file (e.g. the
/// database). **Directory may not exist yet**, caller must create it.
pub fn config_directory() -> PathBuf {
// Config dir will be present on all platforms
// https://docs.rs/dirs/5.0.1/dirs/fn.config_dir.html
debug_or(dirs::config_dir().unwrap().join("slumber"))
}

/// The root data directory. All files that Slumber creates on the system should
/// live here.
#[derive(Debug, Display)]
#[display("{}", _0.display())]
pub struct DataDirectory(PathBuf);
/// Get the path of the directory to data files (e.g. the database). **Directory
/// may not exist yet**, caller must create it.
pub fn data_directory() -> PathBuf {
// Data dir will be present on all platforms
// https://docs.rs/dirs/latest/dirs/fn.data_dir.html
debug_or(dirs::data_dir().unwrap().join("slumber"))
}

impl DataDirectory {
/// Initialize directory for all generated files. The path is contextual:
/// - In development, use a directory from the crate root
/// - In release, use a platform-specific directory in the user's home
///
/// This will create the directory, and return an error if that fails
pub fn init() -> anyhow::Result<()> {
let path = Self::get_directory();
/// Get the path of the directory to contain log files. **Directory
/// may not exist yet**, caller must create it.
pub fn log_directory() -> PathBuf {
// State dir is only present on windows, but cache dir will be present on
// all platforms
// https://docs.rs/dirs/latest/dirs/fn.state_dir.html
// https://docs.rs/dirs/latest/dirs/fn.cache_dir.html
debug_or(
dirs::state_dir()
.unwrap_or_else(|| dirs::cache_dir().unwrap())
.join("slumber"),
)
}

// Create the dir
fs::create_dir_all(&path).with_context(|| {
format!("Error creating data directory {path:?}")
})?;
/// Get the path to the primary log file. **Parent direct may not exist yet,**
/// caller must create it.
pub fn log_file() -> PathBuf {
log_directory().join("slumber.log")
}

DATA_DIRECTORY
.set(Self(path))
.expect("Temporary directory is already initialized");
Ok(())
}
/// Get the path to the backup log file **Parent direct may not exist yet,**
/// caller must create it.
pub fn log_file_old() -> PathBuf {
log_directory().join("slumber.log.old")
}

/// In debug mode, use a local directory for all files. In release, use the
/// given path.
fn debug_or(path: PathBuf) -> PathBuf {
#[cfg(debug_assertions)]
fn get_directory() -> PathBuf {
{
let _ = path; // Remove unused warning
get_repo_root().join("data/")
}

#[cfg(not(debug_assertions))]
fn get_directory() -> PathBuf {
// According to the docs, this dir will be present on all platforms
// https://docs.rs/dirs/latest/dirs/fn.data_dir.html
dirs::data_dir().unwrap().join("slumber")
}

/// Get a reference to the global directory for permanent data. See
/// [Self::init] for more info.
pub fn get() -> &'static Self {
DATA_DIRECTORY
.get()
.expect("Temporary directory is not initialized")
}

/// Build a path to a file in this directory
pub fn file(&self, path: impl AsRef<Path>) -> PathBuf {
self.0.join(path)
}

/// Build a path to the log file
pub fn log_file(&self) -> PathBuf {
self.file("slumber.log")
{
path
}
}

/// Build a path to the backup log file
pub fn log_file_old(&self) -> PathBuf {
self.file("slumber.log.old")
}
/// Ensure the parent directory of a file path exists
pub fn create_parent(path: &Path) -> anyhow::Result<()> {
let parent = path.parent().ok_or_else(|| {
anyhow!("Cannot create directory for path {path:?}; it has no parent")
})?;
fs::create_dir_all(parent)
.context("Error creating directory {parent:?}")?;
Ok(())
}

/// Get path to the root of the git repo. This is needed because this crate
Expand All @@ -81,7 +79,7 @@ impl DataDirectory {
#[cfg(any(debug_assertions, test))]
pub(crate) fn get_repo_root() -> &'static Path {
use crate::util::ResultTraced;
use std::process::Command;
use std::{process::Command, sync::OnceLock};

static CACHE: OnceLock<PathBuf> = OnceLock::new();

Expand Down
5 changes: 4 additions & 1 deletion crates/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,13 @@ impl Tui {
// ===== Initialize global state =====
// This stuff only needs to be set up *once per session*

let config = Config::load()?;
// Create a message queue for handling async tasks
let (messages_tx, messages_rx) = mpsc::unbounded_channel();
let messages_tx = MessageSender::new(messages_tx);

// Load config file. Failure shouldn't be fatal since we can fall back
// to default, just show an error to the user
let config = Config::load().reported(&messages_tx).unwrap_or_default();
// Load a database for this particular collection
let database = Database::load()?.into_collection(&collection_path)?;
// Initialize global view context
Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use editor_command::EditorBuilder;
use futures::{future, FutureExt};
use slumber_core::{
template::Prompt,
util::{doc_link, expand_home, ResultTraced},
util::{doc_link, paths::expand_home, ResultTraced},
};
use std::{
io,
Expand Down
11 changes: 2 additions & 9 deletions crates/tui/src/view/component/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use ratatui::{
Frame,
};
use slumber_config::{Action, Config, InputBinding};
use slumber_core::util::DataDirectory;
use slumber_core::util::paths;

const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");

Expand Down Expand Up @@ -98,14 +98,7 @@ impl Draw for HelpModal {
rows: [
("Version", Line::from(CRATE_VERSION)),
("Configuration", Config::path().display().to_string().into()),
(
"Log",
DataDirectory::get()
.log_file()
.display()
.to_string()
.into(),
),
("Log", paths::log_file().display().to_string().into()),
(
"Collection",
ViewContext::with_database(|database| {
Expand Down
Loading
Loading