A fork of attic a self-hostable Nix Binary Cache server
1//! Client configurations.
2//!
3//! Configuration files are stored under `$XDG_CONFIG_HOME/attic/config.toml`.
4//! We automatically write modified configurations back for a good end-user
5//! experience (e.g., `attic login`).
6
7use std::collections::HashMap;
8use std::fs::{self, read_to_string, OpenOptions, Permissions};
9use std::io::Write;
10use std::ops::{Deref, DerefMut};
11use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
12use std::path::PathBuf;
13
14use anyhow::{anyhow, Context, Result};
15use serde::{Deserialize, Serialize};
16use xdg::BaseDirectories;
17
18use crate::cache::{CacheName, CacheRef, ServerName};
19
20/// Application prefix in XDG base directories.
21///
22/// This will be concatenated into `$XDG_CONFIG_HOME/attic`.
23const XDG_PREFIX: &str = "attic";
24
25/// The permission the configuration file should have.
26const FILE_MODE: u32 = 0o600;
27
28/// Configuration loader.
29#[derive(Debug)]
30pub struct Config {
31 /// Actual configuration data.
32 data: ConfigData,
33
34 /// Path to write modified configurations back to.
35 path: Option<PathBuf>,
36}
37
38/// Client configurations.
39#[derive(Debug, Clone, Deserialize, Serialize, Default)]
40pub struct ConfigData {
41 /// The default server to connect to.
42 #[serde(rename = "default-server")]
43 pub default_server: Option<ServerName>,
44
45 /// A set of remote servers and access credentials.
46 #[serde(default = "HashMap::new")]
47 #[serde(skip_serializing_if = "HashMap::is_empty")]
48 pub servers: HashMap<ServerName, ServerConfig>,
49}
50
51/// Configuration of a server.
52#[derive(Debug, Clone, Deserialize, Serialize)]
53pub struct ServerConfig {
54 pub endpoint: String,
55 #[serde(flatten)]
56 pub token: Option<ServerTokenConfig>,
57}
58
59impl ServerConfig {
60 pub fn token(&self) -> Result<Option<String>> {
61 self.token.as_ref().map(|token| token.get()).transpose()
62 }
63}
64
65/// Configured server token
66#[derive(Debug, Clone, Deserialize, Serialize)]
67#[serde(untagged)]
68pub enum ServerTokenConfig {
69 Raw {
70 token: String,
71 },
72 File {
73 #[serde(rename = "token-file")]
74 token_file: String,
75 },
76}
77
78impl ServerTokenConfig {
79 /// Get the token either directly from the config or through the token file
80 pub fn get(&self) -> Result<String> {
81 match self {
82 ServerTokenConfig::Raw { token } => Ok(token.clone()),
83 ServerTokenConfig::File { token_file } => Ok(read_to_string(token_file)
84 .map(|t| t.trim().to_string())
85 .with_context(|| format!("Failed to read token from {token_file}"))?),
86 }
87 }
88}
89
90/// Wrapper that automatically saves the config once dropped.
91pub struct ConfigWriteGuard<'a>(&'a mut Config);
92
93impl Config {
94 /// Loads the configuration from the system.
95 pub fn load() -> Result<Self> {
96 let path = get_config_path()
97 .map_err(|e| {
98 tracing::warn!("Could not get config path: {}", e);
99 e
100 })
101 .ok();
102
103 let data = ConfigData::load_from_path(path.as_ref())?;
104
105 Ok(Self { data, path })
106 }
107
108 /// Returns a mutable reference to the configuration.
109 pub fn as_mut(&mut self) -> ConfigWriteGuard {
110 ConfigWriteGuard(self)
111 }
112
113 /// Saves the configuration back to the system, if possible.
114 pub fn save(&self) -> Result<()> {
115 if let Some(path) = &self.path {
116 let serialized = toml::to_string(&self.data)?;
117
118 // This isn't atomic, so some other process might chmod it
119 // to something else before we write. We don't handle this case.
120 if path.exists() {
121 let permissions = Permissions::from_mode(FILE_MODE);
122 fs::set_permissions(path, permissions)?;
123 }
124
125 let mut file = OpenOptions::new()
126 .create(true)
127 .write(true)
128 .truncate(true)
129 .mode(FILE_MODE)
130 .open(path)?;
131
132 file.write_all(serialized.as_bytes())?;
133
134 tracing::debug!("Saved modified configuration to {:?}", path);
135 }
136
137 Ok(())
138 }
139}
140
141impl Deref for Config {
142 type Target = ConfigData;
143
144 fn deref(&self) -> &Self::Target {
145 &self.data
146 }
147}
148
149impl ConfigData {
150 fn load_from_path(path: Option<&PathBuf>) -> Result<Self> {
151 if let Some(path) = path {
152 if path.exists() {
153 let contents = fs::read(path)?;
154 let s = std::str::from_utf8(&contents)?;
155 let data = toml::from_str(s)?;
156 return Ok(data);
157 }
158 }
159
160 Ok(ConfigData::default())
161 }
162
163 pub fn default_server(&self) -> Result<(&ServerName, &ServerConfig)> {
164 if let Some(name) = &self.default_server {
165 let config = self.servers.get(name).ok_or_else(|| {
166 anyhow!(
167 "Configured default server \"{}\" does not exist",
168 name.as_str()
169 )
170 })?;
171 Ok((name, config))
172 } else if let Some((name, config)) = self.servers.iter().next() {
173 Ok((name, config))
174 } else {
175 Err(anyhow!("No servers are available."))
176 }
177 }
178
179 pub fn resolve_cache<'a>(
180 &'a self,
181 r: &'a CacheRef,
182 ) -> Result<(&'a ServerName, &'a ServerConfig, &'a CacheName)> {
183 match r {
184 CacheRef::DefaultServer(cache) => {
185 let (name, config) = self.default_server()?;
186 Ok((name, config, cache))
187 }
188 CacheRef::ServerQualified(server, cache) => {
189 let config = self
190 .servers
191 .get(server)
192 .ok_or_else(|| anyhow!("Server \"{}\" does not exist", server.as_str()))?;
193 Ok((server, config, cache))
194 }
195 }
196 }
197}
198
199impl<'a> Deref for ConfigWriteGuard<'a> {
200 type Target = ConfigData;
201
202 fn deref(&self) -> &Self::Target {
203 &self.0.data
204 }
205}
206
207impl<'a> DerefMut for ConfigWriteGuard<'a> {
208 fn deref_mut(&mut self) -> &mut Self::Target {
209 &mut self.0.data
210 }
211}
212
213impl<'a> Drop for ConfigWriteGuard<'a> {
214 fn drop(&mut self) {
215 if let Err(e) = self.0.save() {
216 tracing::error!("Could not save modified configuration: {}", e);
217 }
218 }
219}
220
221fn get_config_path() -> Result<PathBuf> {
222 let xdg_dirs = BaseDirectories::with_prefix(XDG_PREFIX);
223 let config_path = xdg_dirs.place_config_file("config.toml")?;
224
225 Ok(config_path)
226}