warg_client/
config.rs

1//! Module for client configuration.
2
3use crate::{ClientError, RegistryUrl};
4use anyhow::{anyhow, Context, Result};
5use indexmap::IndexSet;
6use normpath::PathExt;
7use once_cell::sync::Lazy;
8use serde::{Deserialize, Serialize};
9use std::{
10    env::current_dir,
11    fs::{self, File},
12    path::{Component, Path, PathBuf},
13};
14
15static CACHE_DIR: Lazy<Option<PathBuf>> = Lazy::new(dirs::cache_dir);
16static CONFIG_DIR: Lazy<Option<PathBuf>> = Lazy::new(dirs::config_dir);
17static CONFIG_FILE_NAME: &str = "warg-config.json";
18
19fn find_warg_config(cwd: &Path) -> Option<PathBuf> {
20    let mut current = Some(cwd);
21
22    while let Some(dir) = current {
23        let config = dir.join(CONFIG_FILE_NAME);
24        if config.is_file() {
25            return Some(config);
26        }
27
28        current = dir.parent();
29    }
30
31    None
32}
33
34/// Normalize a path, removing things like `.` and `..`.
35/// Sourced from: https://github.com/rust-lang/cargo/blob/15d090969743630bff549a1b068bcaa8174e5ee3/crates/cargo-util/src/paths.rs#L82
36fn normalize_path(path: &Path) -> PathBuf {
37    let mut components = path.components().peekable();
38    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
39        components.next();
40        PathBuf::from(c.as_os_str())
41    } else {
42        PathBuf::new()
43    };
44
45    for component in components {
46        match component {
47            Component::Prefix(..) => unreachable!(),
48            Component::RootDir => {
49                ret.push(component.as_os_str());
50            }
51            Component::CurDir => {}
52            Component::ParentDir => {
53                ret.pop();
54            }
55            Component::Normal(c) => {
56                ret.push(c);
57            }
58        }
59    }
60    ret
61}
62
63/// Paths used for storage
64pub struct StoragePaths {
65    /// The registry URL relating to the storage paths.
66    pub registry_url: RegistryUrl,
67    /// The path to the registry storage directory.
68    pub registries_dir: PathBuf,
69    /// The path to the content storage directory.
70    pub content_dir: PathBuf,
71    /// The path to the namespace map storage directory.
72    pub namespace_map_path: PathBuf,
73}
74
75/// Represents the Warg client configuration.
76#[derive(Default, Clone, Debug, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct Config {
79    /// The home Warg registry server URL.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub home_url: Option<String>,
82
83    /// The path to the top-level directory where per-registry information is stored.
84    ///
85    /// This path is expected to be relative to the configuration file.
86    ///
87    /// If `None`, the default of `$CACHE_DIR/warg/registries` is used, where
88    /// `$CACHE_DIR` is the platform-specific cache directory.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub registries_dir: Option<PathBuf>,
91
92    /// The path to the directory where package content is stored.
93    ///
94    /// This path is expected to be relative to the configuration file.
95    ///
96    /// If `None`, the default of `$CACHE_DIR/warg/content` is used, where
97    /// `$CACHE_DIR` is the platform-specific cache directory.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub content_dir: Option<PathBuf>,
100
101    /// The path to the directory where namespace map is stored.
102    ///
103    /// This path is expected to be relative to the configuration file.
104    ///
105    /// If `None`, the default of `$CACHE_DIR/warg/namespaces` is used, where
106    /// `$CACHE_DIR` is the platform-specific cache directory.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub namespace_map_path: Option<PathBuf>,
109
110    /// List of creds available in keyring
111    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
112    pub keys: IndexSet<String>,
113
114    /// Whether or not an auth key should be retreived from keyring
115    #[serde(default)]
116    pub keyring_auth: bool,
117
118    /// Ignore registry hints provided by a warg server
119    #[serde(default)]
120    pub ignore_federation_hints: bool,
121
122    /// Auto accept registry hint or ask the user to confirm
123    #[serde(default)]
124    pub disable_auto_accept_federation_hints: bool,
125
126    /// Automatically attempt package initialize if does not exist
127    /// or ask the user to confirm first
128    #[serde(default)]
129    pub disable_auto_package_init: bool,
130
131    /// Disable interactive prompts.
132    #[serde(default)]
133    pub disable_interactive: bool,
134
135    /// Use the specified backend for keyring access.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub keyring_backend: Option<String>,
138}
139
140impl Config {
141    /// Reads the client configuration from the given file path.
142    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
143        let path = path.as_ref();
144        let config = fs::read_to_string(path).with_context(|| {
145            format!(
146                "failed to read configuration file `{path}`",
147                path = path.display()
148            )
149        })?;
150
151        let mut config: Self = serde_json::from_str(&config).with_context(|| {
152            format!("failed to deserialize file `{path}`", path = path.display())
153        })?;
154
155        if let Some(parent) = path.parent() {
156            config.registries_dir = config.registries_dir.map(|p| parent.join(p));
157            config.content_dir = config.content_dir.map(|p| parent.join(p));
158        }
159
160        Ok(config)
161    }
162
163    /// Writes the client configuration to the given file path.
164    ///
165    /// This function will normalize the paths in the configuration file to be
166    /// relative to the configuration file's directory.
167    pub fn write_to_file(&self, path: &Path) -> Result<()> {
168        let current_dir = current_dir().context("failed to get current directory")?;
169        let path = current_dir.join(path);
170        let parent = path.parent().ok_or_else(|| {
171            anyhow!(
172                "path `{path}` has no parent directory",
173                path = path.display()
174            )
175        })?;
176
177        fs::create_dir_all(parent).with_context(|| {
178            format!(
179                "failed to create parent directory `{path}`",
180                path = parent.display()
181            )
182        })?;
183
184        // We must normalize the parent directory for forming relative paths
185        // This is used to get the actual path of the configuration file
186        // directory; below we use `normalize_path` as the directories might
187        // not exist.
188        let parent = parent.normalize().with_context(|| {
189            format!(
190                "failed to normalize parent directory `{path}`",
191                path = parent.display()
192            )
193        })?;
194
195        assert!(parent.is_absolute());
196
197        let config = Config {
198            home_url: self.home_url.clone(),
199            registries_dir: self.registries_dir.as_ref().map(|p| {
200                let p = normalize_path(parent.join(p).as_path());
201                assert!(p.is_absolute());
202                pathdiff::diff_paths(&p, &parent).unwrap()
203            }),
204            content_dir: self.content_dir.as_ref().map(|p| {
205                let p = normalize_path(parent.join(p).as_path());
206                assert!(p.is_absolute());
207                pathdiff::diff_paths(&p, &parent).unwrap()
208            }),
209            namespace_map_path: self.namespace_map_path.as_ref().map(|p| {
210                let p = normalize_path(parent.join(p).as_path());
211                assert!(p.is_absolute());
212                pathdiff::diff_paths(&p, &parent).unwrap()
213            }),
214            keys: self.keys.clone(),
215            keyring_auth: self.keyring_auth,
216            ignore_federation_hints: self.ignore_federation_hints,
217            disable_auto_accept_federation_hints: self.disable_auto_accept_federation_hints,
218            disable_auto_package_init: self.disable_auto_package_init,
219            disable_interactive: self.disable_interactive,
220            keyring_backend: self.keyring_backend.clone(),
221        };
222
223        serde_json::to_writer_pretty(
224            File::create(&path).with_context(|| {
225                format!("failed to create file `{path}`", path = path.display())
226            })?,
227            &config,
228        )
229        .with_context(|| format!("failed to serialize file `{path}`", path = path.display()))
230    }
231
232    /// Loads a client configuration from a default file path.
233    ///
234    /// The following paths are checked in order:
235    ///
236    /// * `warg-config.json` at the current directory and its parents
237    /// * `$CONFIG_DIR/warg/config.json`
238    ///
239    /// Where `$CONFIG_DIR` is the platform-specific configuration directory.
240    ///
241    /// Returns `Ok(None)` if no configuration file was found.
242    pub fn from_default_file() -> Result<Option<Self>> {
243        if let Some(path) = find_warg_config(&std::env::current_dir()?) {
244            return Ok(Some(Self::from_file(path)?));
245        }
246
247        let path = Self::default_config_path()?;
248        if path.is_file() {
249            return Ok(Some(Self::from_file(path)?));
250        }
251
252        Ok(None)
253    }
254
255    /// Gets the path to the default configuration file.
256    ///
257    /// The default configuration file is `$CONFIG_DIR/warg/config.json`,
258    pub fn default_config_path() -> Result<PathBuf> {
259        CONFIG_DIR
260            .as_ref()
261            .map(|p| p.join("warg/config.json"))
262            .ok_or_else(|| anyhow!("failed to determine operating system configuration directory"))
263    }
264
265    /// Gets the path to the directory where per-registry packages are stored.
266    pub fn registries_dir(&self) -> Result<PathBuf> {
267        self.registries_dir
268            .as_ref()
269            .cloned()
270            .map(Ok)
271            .unwrap_or_else(|| {
272                CACHE_DIR
273                    .as_ref()
274                    .map(|p| p.join("warg/registries"))
275                    .ok_or_else(|| anyhow!("failed to determine operating system cache directory"))
276            })
277    }
278
279    /// Gets the path to the directory where per-registry packages are stored.
280    pub fn content_dir(&self) -> Result<PathBuf> {
281        self.content_dir
282            .as_ref()
283            .cloned()
284            .map(Ok)
285            .unwrap_or_else(|| {
286                CACHE_DIR
287                    .as_ref()
288                    .map(|p| p.join("warg/content"))
289                    .ok_or_else(|| anyhow!("failed to determine operating system cache directory"))
290            })
291    }
292
293    /// Gets the path to the directory where namespace mapping is stored.
294    pub fn namespace_map_path(&self) -> Result<PathBuf> {
295        self.namespace_map_path
296            .as_ref()
297            .cloned()
298            .map(Ok)
299            .unwrap_or_else(|| {
300                CACHE_DIR
301                    .as_ref()
302                    .map(|p| p.join("warg/namespaces"))
303                    .ok_or_else(|| anyhow!("failed to determine operating system cache directory"))
304            })
305    }
306
307    pub(crate) fn storage_paths_for_url(
308        &self,
309        registry_url: RegistryUrl,
310    ) -> Result<StoragePaths, ClientError> {
311        let label = registry_url.safe_label();
312        let registries_dir = self.registries_dir()?.join(label);
313        let content_dir = self.content_dir()?;
314        let namespace_map_path = self.namespace_map_path()?;
315        Ok(StoragePaths {
316            registry_url,
317            registries_dir,
318            content_dir,
319            namespace_map_path,
320        })
321    }
322}