use crate::core::shell_escape::sh_squote;
use crate::core::types::Resource;
pub(super) fn conf_path(home: &str) -> String {
format!("{}/.config/rclone/rclone.conf", home.trim_end_matches('/'))
}
pub(super) fn is_unresolved(token: &str) -> bool {
token.contains("{{") && token.contains("}}")
}
pub(super) fn stanza(resource: &Resource, remote_name: &str, token: Option<&str>) -> String {
let backend = resource.backup.remote_type.as_deref().unwrap_or("drive");
let mut keys: Vec<(&String, &String)> = resource.backup.remote_config.iter().collect();
keys.sort_by(|a, b| a.0.cmp(b.0));
let mut out = format!("[{remote_name}]\ntype = {backend}\n");
for (k, v) in keys {
if k == "token" {
continue;
}
out.push_str(&format!("{k} = {v}\n"));
}
if let Some(t) = token {
out.push_str(&format!("token = {t}\n"));
}
out
}
pub(super) fn install(home: &str, body: &str) -> String {
let path = conf_path(home);
let path_q = sh_squote(&path);
let dir_q = sh_squote(&format!("{}/.config/rclone", home.trim_end_matches('/')));
format!(
r#"mkdir -p {dir_q}
chmod 0700 {dir_q}
BS_CONF_TMP={path_q}.tmp
# umask before creation: the token must never exist world-readable, not even
# for the instant between creat() and chmod().
( umask 077; cat > "$BS_CONF_TMP" <<'FORJAR_RCLONE_CONF'
{body}
FORJAR_RCLONE_CONF
)
chmod 0600 "$BS_CONF_TMP"
mv -f "$BS_CONF_TMP" {path_q}
"#
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::{BackupSpec, ResourceType};
use std::collections::HashMap;
fn res() -> Resource {
let mut cfg = HashMap::new();
cfg.insert("scope".to_string(), "drive.file".to_string());
cfg.insert("root_folder_id".to_string(), "ABC123".to_string());
Resource {
resource_type: ResourceType::BackupSync,
backup: BackupSpec {
remote_config: cfg,
..Default::default()
},
..Default::default()
}
}
#[test]
fn stanza_declares_the_backend_and_config_keys() {
let s = stanza(&res(), "gdrive", None);
assert!(s.starts_with("[gdrive]\ntype = drive\n"));
assert!(s.contains("scope = drive.file"));
assert!(s.contains("root_folder_id = ABC123"));
}
#[test]
fn backend_is_overridable() {
let r = Resource {
backup: BackupSpec {
remote_type: Some("s3".into()),
..res().backup
},
..res()
};
assert!(stanza(&r, "x", None).contains("type = s3"));
}
#[test]
fn keys_are_sorted_so_the_hash_is_stable() {
let a = stanza(&res(), "gdrive", None);
for _ in 0..8 {
assert_eq!(stanza(&res(), "gdrive", None), a);
}
let scope = a.find("scope").unwrap();
let root = a.find("root_folder_id").unwrap();
assert!(root < scope, "keys must be sorted");
}
#[test]
fn a_config_key_named_token_cannot_shadow_the_secret() {
let mut r = res();
r.backup
.remote_config
.insert("token".into(), "NOT-THE-REAL-TOKEN".into());
let s = stanza(&r, "gdrive", Some("REAL"));
assert!(!s.contains("NOT-THE-REAL-TOKEN"));
assert_eq!(s.matches("token = ").count(), 1);
assert!(s.contains("token = REAL"));
}
#[test]
fn token_is_omitted_when_absent() {
assert!(!stanza(&res(), "gdrive", None).contains("token ="));
}
#[test]
fn detects_an_unresolved_secret_template() {
assert!(is_unresolved("{{secrets.rclone_token}}"));
assert!(!is_unresolved("ya29.a0Af..."));
}
#[test]
fn install_is_atomic_and_never_world_readable() {
let s = install("/home/noah", "[gdrive]\ntype = drive\n");
assert!(
s.contains("umask 077"),
"token must not exist world-readable"
);
assert!(s.contains("chmod 0600"));
assert!(s.contains("chmod 0700"));
assert!(s.contains("mv -f"), "install must be atomic");
}
#[test]
fn install_uses_a_quoted_heredoc() {
assert!(install("/home/noah", "x").contains("<<'FORJAR_RCLONE_CONF'"));
}
#[test]
fn conf_path_is_the_standard_location() {
assert_eq!(
conf_path("/home/noah"),
"/home/noah/.config/rclone/rclone.conf"
);
assert_eq!(
conf_path("/home/noah/"),
"/home/noah/.config/rclone/rclone.conf"
);
}
}