1use 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
34fn 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
63pub struct StoragePaths {
65 pub registry_url: RegistryUrl,
67 pub registries_dir: PathBuf,
69 pub content_dir: PathBuf,
71 pub namespace_map_path: PathBuf,
73}
74
75#[derive(Default, Clone, Debug, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct Config {
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub home_url: Option<String>,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub registries_dir: Option<PathBuf>,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub content_dir: Option<PathBuf>,
100
101 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub namespace_map_path: Option<PathBuf>,
109
110 #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
112 pub keys: IndexSet<String>,
113
114 #[serde(default)]
116 pub keyring_auth: bool,
117
118 #[serde(default)]
120 pub ignore_federation_hints: bool,
121
122 #[serde(default)]
124 pub disable_auto_accept_federation_hints: bool,
125
126 #[serde(default)]
129 pub disable_auto_package_init: bool,
130
131 #[serde(default)]
133 pub disable_interactive: bool,
134
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub keyring_backend: Option<String>,
138}
139
140impl Config {
141 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 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 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 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 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 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 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 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}