A fork of attic a self-hostable Nix Binary Cache server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 226 lines 6.5 kB view raw
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}