1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use std::collections::HashMap;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};

use directories::ProjectDirs;
use serde::Deserialize;

use crate::ocfl::{DigestAlgorithm, Result, RocflError};

const CONFIG_FILE: &str = "config.toml";
const GLOBAL: &str = "global";

/// Representation of user configuration
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Config {
    pub author_name: Option<String>,
    pub author_address: Option<String>,
    pub root: Option<String>,
    pub staging_root: Option<String>,
    pub region: Option<String>,
    pub bucket: Option<String>,
    pub endpoint: Option<String>,
    pub profile: Option<String>,
}

impl Config {
    pub fn new() -> Self {
        Self {
            author_name: None,
            author_address: None,
            root: None,
            staging_root: None,
            region: None,
            bucket: None,
            endpoint: None,
            profile: None,
        }
    }

    pub fn validate(&self) -> Result<()> {
        if self.bucket.is_some() {
            if self.region.is_none() {
                return Err(RocflError::InvalidConfiguration(
                    "Region must be specified when using S3".to_string(),
                ));
            }
        } else if self.region.is_some() || self.endpoint.is_some() {
            return Err(RocflError::InvalidConfiguration(
                "Region and endpoint should not be set when not using S3. \
                If you intended to use S3, then you must specify a bucket."
                    .to_string(),
            ));
        }

        Ok(())
    }
}

impl Default for Config {
    fn default() -> Self {
        Self::new()
    }
}

/// Parses the user's rocfl config, if it exists
pub fn load_config(name: &Option<String>) -> Result<Config> {
    if let Some(config_file) = config_path() {
        if config_file.exists() {
            let config = parse_config(&config_file)?;
            return Ok(resolve_config(name, config));
        }
    }
    Ok(Config::new())
}

/// The path to the rocfl config file, or None if the config directory cannot be resolved.
/// The file may not exist.
pub fn config_path() -> Option<PathBuf> {
    project_dirs().map(|dirs| dirs.config_dir().join(CONFIG_FILE))
}

/// Reference to the rocfl project directories. These directories do **not** necessarily exist
pub fn project_dirs() -> Option<ProjectDirs> {
    ProjectDirs::from("org", "rocfl", "rocfl")
}

/// Constructs a path to the default S3 staging location. This function should only be called
/// when bucket is set. The path is **not** created
pub fn s3_staging_path(config: &Config) -> Result<String> {
    match project_dirs() {
        Some(dirs) => {
            let mut staging = dirs.data_dir().join("s3").join("staging");
            staging.push(s3_identifier(config)?);
            Ok(staging.to_string_lossy().to_string())
        }
        None => Err(RocflError::General(
            "Failed to locate a suitable directory for staging objects. Please specify a directory using '--staging-root'".to_string()))
    }
}

fn s3_identifier(config: &Config) -> Result<String> {
    let mut name = config.bucket.clone().unwrap();
    if let Some(root) = &config.root {
        name.push('/');
        name.push_str(root);
    }
    let hash = DigestAlgorithm::Sha256.hash_hex(&mut name.as_bytes())?;
    Ok(hash.to_string())
}

fn parse_config(config_file: impl AsRef<Path>) -> Result<HashMap<String, Config>> {
    let mut buffer = Vec::new();
    fs::File::open(config_file.as_ref())?.read_to_end(&mut buffer)?;
    let config: HashMap<String, Config> = toml::from_slice(&buffer)?;
    Ok(config)
}

fn resolve_config(name: &Option<String>, mut config: HashMap<String, Config>) -> Config {
    let global_config = config.remove(GLOBAL);
    let repo_config = match name {
        None => None,
        Some(name) => config.remove(name),
    };

    match (global_config, repo_config) {
        (Some(global), None) => global,
        (None, Some(repo)) => repo,
        (None, None) => Config::new(),
        (Some(global), Some(repo)) => {
            let mut resolved = Config::new();

            resolved.author_name = resolve_field(global.author_name, repo.author_name);
            resolved.author_address = resolve_field(global.author_address, repo.author_address);
            resolved.root = resolve_field(global.root, repo.root);
            resolved.staging_root = resolve_field(global.staging_root, repo.staging_root);
            resolved.region = resolve_field(global.region, repo.region);
            resolved.bucket = resolve_field(global.bucket, repo.bucket);
            resolved.endpoint = resolve_field(global.endpoint, repo.endpoint);
            resolved.profile = resolve_field(global.profile, repo.profile);

            resolved
        }
    }
}

fn resolve_field(global_field: Option<String>, repo_field: Option<String>) -> Option<String> {
    if repo_field.is_some() {
        repo_field
    } else {
        global_field
    }
}