use crate::{properties::Properties, Error, Result};
use fs::File;
use lazy_static::lazy_static;
use regex::Regex;
use std::{cmp::Ordering, collections::HashMap, fs, io::BufReader, path::PathBuf};
lazy_static! {
static ref NAME_REGEX: Regex = Regex::new("^[a-z][-a-z0-9]*$").unwrap();
}
#[derive(Debug, Clone)]
pub struct Configuration {
name: String,
path: PathBuf,
}
impl Configuration {
pub fn name(&self) -> &str {
&self.name
}
pub fn is_valid_name(name: &str) -> bool {
NAME_REGEX.is_match(name)
}
}
impl Ord for Configuration {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name.cmp(&other.name)
}
}
impl PartialOrd for Configuration {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Configuration {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for Configuration {}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ConflictAction {
Abort,
Overwrite,
}
impl From<bool> for ConflictAction {
fn from(value: bool) -> Self {
if value {
ConflictAction::Overwrite
} else {
ConflictAction::Abort
}
}
}
#[derive(Debug)]
pub struct ConfigurationStore {
location: PathBuf,
configurations_path: PathBuf,
configurations: HashMap<String, Configuration>,
active: String,
}
impl ConfigurationStore {
pub fn with_default_location() -> Result<Self> {
let gcloud_path: PathBuf = if let Ok(value) = std::env::var("CLOUDSDK_CONFIG") {
value.into()
} else {
let gcloud_path = if cfg!(target_os = "macos") {
dirs::home_dir()
.ok_or(Error::ConfigurationDirectoryNotFound)?
.join(".config")
} else {
dirs::config_dir().ok_or(Error::ConfigurationDirectoryNotFound)?
};
gcloud_path.join("gcloud")
};
Self::with_location(gcloud_path)
}
pub fn with_location(gcloud_path: PathBuf) -> Result<Self> {
if !gcloud_path.is_dir() {
return Err(Error::ConfigurationStoreNotFound(gcloud_path));
}
let configurations_path = gcloud_path.join("configurations");
if !configurations_path.is_dir() {
return Err(Error::ConfigurationStoreNotFound(configurations_path));
}
let mut configurations: HashMap<String, Configuration> = HashMap::new();
for file in fs::read_dir(&configurations_path)? {
if file.is_err() {
continue;
}
let file = file.unwrap();
let name = file.file_name();
let name = match name.to_str() {
Some(name) => name,
None => continue, };
let name = name.trim_start_matches("config_");
if !Configuration::is_valid_name(name) {
continue;
}
configurations.insert(
name.to_owned(),
Configuration {
name: name.to_owned(),
path: file.path(),
},
);
}
if configurations.is_empty() {
return Err(Error::NoConfigurationsFound(configurations_path));
}
let active = gcloud_path.join("active_config");
let active = fs::read_to_string(active)?;
Ok(ConfigurationStore {
location: gcloud_path,
configurations_path,
configurations,
active,
})
}
pub fn active(&self) -> &str {
&self.active
}
pub fn configurations(&self) -> Vec<&Configuration> {
let mut value: Vec<&Configuration> = self.configurations.values().collect();
value.sort();
value
}
pub fn is_active(&self, configuration: &Configuration) -> bool {
configuration.name == self.active
}
pub fn activate(&mut self, name: &str) -> Result<()> {
let configuration = self
.find_by_name(name)
.ok_or_else(|| Error::UnknownConfiguration(name.to_owned()))?;
let path = self.location.join("active_config");
std::fs::write(path, &configuration.name)?;
self.active = configuration.name.to_owned();
Ok(())
}
pub fn copy(&mut self, src_name: &str, dest_name: &str, conflict: ConflictAction) -> Result<()> {
let src = self
.configurations
.get(src_name)
.ok_or_else(|| Error::UnknownConfiguration(src_name.to_owned()))?;
if !Configuration::is_valid_name(dest_name) {
return Err(Error::InvalidName(dest_name.to_owned()));
}
if conflict == ConflictAction::Abort && self.configurations.contains_key(dest_name) {
return Err(Error::ExistingConfiguration(dest_name.to_owned()));
}
let filename = self.configurations_path.join(format!("config_{}", dest_name));
fs::copy(&src.path, &filename)?;
let dest = Configuration {
name: dest_name.to_owned(),
path: filename,
};
self.configurations.insert(dest_name.to_owned(), dest);
Ok(())
}
pub fn create(&mut self, name: &str, properties: &Properties, conflict: ConflictAction) -> Result<()> {
if !Configuration::is_valid_name(name) {
return Err(Error::InvalidName(name.to_owned()));
}
if conflict == ConflictAction::Abort && self.configurations.contains_key(name) {
return Err(Error::ExistingConfiguration(name.to_owned()));
}
let filename = self.configurations_path.join(format!("config_{}", name));
let file = File::create(&filename)?;
properties.to_writer(file)?;
self.configurations.insert(
name.to_owned(),
Configuration {
name: name.to_owned(),
path: filename,
},
);
Ok(())
}
pub fn delete(&mut self, name: &str) -> Result<()> {
let configuration = self
.find_by_name(name)
.ok_or_else(|| Error::UnknownConfiguration(name.to_owned()))?;
if self.is_active(configuration) {
return Err(Error::DeleteActiveConfiguration);
}
let path = &configuration.path;
fs::remove_file(&path)?;
self.configurations.remove(name);
Ok(())
}
pub fn describe(&self, name: &str) -> Result<Properties> {
let configuration = self
.find_by_name(name)
.ok_or_else(|| Error::UnknownConfiguration(name.to_owned()))?;
let path = &configuration.path;
let handle = File::open(path)?;
let reader = BufReader::new(handle);
let properties = Properties::from_reader(reader)?;
Ok(properties)
}
pub fn rename(&mut self, old_name: &str, new_name: &str, conflict: ConflictAction) -> Result<()> {
let src = self
.configurations
.get(old_name)
.ok_or_else(|| Error::UnknownConfiguration(old_name.to_owned()))?;
let active = self.is_active(src);
if !Configuration::is_valid_name(new_name) {
return Err(Error::InvalidName(new_name.to_owned()));
}
if conflict == ConflictAction::Abort && self.configurations.contains_key(new_name) {
return Err(Error::ExistingConfiguration(new_name.to_owned()));
}
let new_value = Configuration {
name: new_name.to_owned(),
path: src.path.with_file_name(format!("config_{}", new_name)),
};
std::fs::rename(&src.path, &new_value.path)?;
self.configurations.remove(old_name);
self.configurations.insert(new_name.to_owned(), new_value);
if active {
self.activate(new_name)?;
}
Ok(())
}
pub fn find_by_name(&self, name: &str) -> Option<&Configuration> {
self.configurations.get(&name.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
pub fn test_is_valid_name_with_valid_name() {
assert!(Configuration::is_valid_name("foo"));
assert!(Configuration::is_valid_name("f"));
assert!(Configuration::is_valid_name("f123"));
assert!(Configuration::is_valid_name("foo-bar"));
assert!(Configuration::is_valid_name("foo-123"));
assert!(Configuration::is_valid_name("foo-a1b2c3"));
}
#[test]
pub fn test_is_valid_name_with_invalid_name() {
assert!(!Configuration::is_valid_name(""));
assert!(!Configuration::is_valid_name("F"));
assert!(!Configuration::is_valid_name("1"));
assert!(!Configuration::is_valid_name("-"));
assert!(!Configuration::is_valid_name("foo_bar"));
assert!(!Configuration::is_valid_name("foo.bar"));
assert!(!Configuration::is_valid_name("foo|bar"));
assert!(!Configuration::is_valid_name("foo$bar"));
assert!(!Configuration::is_valid_name("foo#bar"));
assert!(!Configuration::is_valid_name("foo@bar"));
assert!(!Configuration::is_valid_name("foo;bar"));
assert!(!Configuration::is_valid_name("foo?bar"));
assert!(!Configuration::is_valid_name("camelCase"));
}
}