use std::path::{Path, PathBuf};
use crate::commands::keyring::store::Store;
const RCLONE_CONFIG_FILE_NAME: &str = "rclone.conf";
pub(crate) fn rclone_config_path() -> Result<PathBuf, String> {
let keyring_path = Store::default_path()?;
let dir = keyring_path.parent().ok_or_else(|| {
format!(
"could not determine a parent directory for {}",
keyring_path.display()
)
})?;
Ok(dir.join(RCLONE_CONFIG_FILE_NAME))
}
pub(crate) fn write_sandboxed_config(store: &Store, path: &Path) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|err| format!("failed to create {}: {err}", parent.display()))?;
}
let mut contents = String::new();
for bucket_config in store.bucket_configs() {
contents.push_str(&format!(
"[{}]\ntype = s3\nprovider = Other\naccess_key_id = {}\nsecret_access_key = {}\nendpoint = {}\nacl = private\nno_check_bucket = true\n\n",
bucket_config.alias,
bucket_config.access_key_id,
bucket_config.secret_key,
bucket_config.endpoint,
));
}
std::fs::write(path, contents)
.map_err(|err| format!("failed to write {}: {err}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.map_err(|err| format!("failed to set permissions on {}: {err}", path.display()))?;
}
Ok(())
}
pub(crate) fn remote_address(alias: &str) -> String {
format!("{alias}:")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::keyring::bucket::store::BucketConfig;
use crate::commands::keyring::store::Entry;
fn sample_bucket(alias: &str) -> BucketConfig {
BucketConfig {
alias: alias.to_string(),
endpoint: "https://nyc3.digitaloceanspaces.com".to_string(),
bucket: "my-bucket".to_string(),
access_key_id: "AKID".to_string(),
encryption_key_alias: None,
secret_key: "shh".to_string(),
}
}
#[test]
fn remote_address_appends_a_trailing_colon() {
assert_eq!(remote_address("backup"), "backup:");
}
#[test]
fn write_sandboxed_config_with_no_bucket_configs_writes_an_empty_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rclone.conf");
let store = Store::default();
write_sandboxed_config(&store, &path).unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.is_empty());
}
#[test]
fn write_sandboxed_config_writes_one_section_per_bucket_keyed_by_alias() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rclone.conf");
let mut store = Store::default();
store.push(Entry::Bucket(sample_bucket("backup")));
write_sandboxed_config(&store, &path).unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains("[backup]"));
assert!(contents.contains("type = s3"));
assert!(contents.contains("provider = Other"));
assert!(contents.contains("access_key_id = AKID"));
assert!(contents.contains("secret_access_key = shh"));
assert!(contents.contains("endpoint = https://nyc3.digitaloceanspaces.com"));
assert!(!contents.contains("[my-bucket]"));
}
#[test]
fn write_sandboxed_config_never_reads_an_existing_file_first() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rclone.conf");
std::fs::write(&path, "[unrelated]\ntype = sftp\n").unwrap();
let store = Store::default();
write_sandboxed_config(&store, &path).unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(
!contents.contains("unrelated"),
"write_sandboxed_config must overwrite, never merge with, any pre-existing file"
);
}
#[test]
#[cfg(unix)]
fn write_sandboxed_config_sets_0600_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rclone.conf");
let mut store = Store::default();
store.push(Entry::Bucket(sample_bucket("backup")));
write_sandboxed_config(&store, &path).unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600);
}
}