use crate::{ClientError, RegistryUrl};
use anyhow::{anyhow, Context, Result};
use normpath::PathExt;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::{
env::current_dir,
fs::{self, File},
path::{Component, Path, PathBuf},
};
static CACHE_DIR: Lazy<Option<PathBuf>> = Lazy::new(dirs::cache_dir);
static CONFIG_DIR: Lazy<Option<PathBuf>> = Lazy::new(dirs::config_dir);
static CONFIG_FILE_NAME: &str = "warg-config.json";
fn find_warg_config(cwd: &Path) -> Option<PathBuf> {
let mut current = Some(cwd);
while let Some(dir) = current {
let config = dir.join(CONFIG_FILE_NAME);
if config.is_file() {
return Some(config);
}
current = dir.parent();
}
None
}
fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
pub struct StoragePaths {
pub registry_url: RegistryUrl,
pub registries_dir: PathBuf,
pub content_dir: PathBuf,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registries_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_dir: Option<PathBuf>,
}
impl Config {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let config = fs::read_to_string(path).with_context(|| {
format!(
"failed to read configuration file `{path}`",
path = path.display()
)
})?;
let mut config: Self = serde_json::from_str(&config).with_context(|| {
format!("failed to deserialize file `{path}`", path = path.display())
})?;
if let Some(parent) = path.parent() {
config.registries_dir = config.registries_dir.map(|p| parent.join(p));
config.content_dir = config.content_dir.map(|p| parent.join(p));
}
Ok(config)
}
pub fn write_to_file(&self, path: &Path) -> Result<()> {
let current_dir = current_dir().context("failed to get current directory")?;
let path = current_dir.join(path);
let parent = path.parent().ok_or_else(|| {
anyhow!(
"path `{path}` has no parent directory",
path = path.display()
)
})?;
fs::create_dir_all(parent).with_context(|| {
format!(
"failed to create parent directory `{path}`",
path = parent.display()
)
})?;
let parent = parent.normalize().with_context(|| {
format!(
"failed to normalize parent directory `{path}`",
path = parent.display()
)
})?;
assert!(parent.is_absolute());
let config = Config {
default_url: self.default_url.clone(),
registries_dir: self.registries_dir.as_ref().map(|p| {
let p = normalize_path(parent.join(p).as_path());
assert!(p.is_absolute());
pathdiff::diff_paths(&p, &parent).unwrap()
}),
content_dir: self.content_dir.as_ref().map(|p| {
let p = normalize_path(parent.join(p).as_path());
assert!(p.is_absolute());
pathdiff::diff_paths(&p, &parent).unwrap()
}),
};
serde_json::to_writer_pretty(
File::create(&path).with_context(|| {
format!("failed to create file `{path}`", path = path.display())
})?,
&config,
)
.with_context(|| format!("failed to serialize file `{path}`", path = path.display()))
}
pub fn from_default_file() -> Result<Option<Self>> {
if let Some(path) = find_warg_config(&std::env::current_dir()?) {
return Ok(Some(Self::from_file(path)?));
}
let path = Self::default_config_path()?;
if path.is_file() {
return Ok(Some(Self::from_file(path)?));
}
Ok(None)
}
pub fn default_config_path() -> Result<PathBuf> {
CONFIG_DIR
.as_ref()
.map(|p| p.join("warg/config.json"))
.ok_or_else(|| anyhow!("failed to determine operating system configuration directory"))
}
pub fn registries_dir(&self) -> Result<PathBuf> {
self.registries_dir
.as_ref()
.cloned()
.map(Ok)
.unwrap_or_else(|| {
CACHE_DIR
.as_ref()
.map(|p| p.join("warg/registries"))
.ok_or_else(|| anyhow!("failed to determine operating system cache directory"))
})
}
pub fn content_dir(&self) -> Result<PathBuf> {
self.content_dir
.as_ref()
.cloned()
.map(Ok)
.unwrap_or_else(|| {
CACHE_DIR
.as_ref()
.map(|p| p.join("warg/content"))
.ok_or_else(|| anyhow!("failed to determine operating system cache directory"))
})
}
pub(crate) fn storage_paths_for_url(
&self,
url: Option<&str>,
) -> Result<StoragePaths, ClientError> {
let registry_url = RegistryUrl::new(
url.or(self.default_url.as_deref())
.ok_or(ClientError::NoDefaultUrl)?,
)?;
let label = registry_url.safe_label();
let registries_dir = self.registries_dir()?.join(label);
let content_dir = self.content_dir()?;
Ok(StoragePaths {
registry_url,
registries_dir,
content_dir,
})
}
}