use crate::error::{PrefixloadError, Result};
use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(RustEmbed)]
#[folder = "assets/"]
struct Asset;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DirectoryEntry {
pub local_name_prefix: String,
pub remote_path: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub endpoint: String,
pub bucket: String,
pub region: String,
pub force_path_style: bool,
pub part_size: u64,
pub local_directory_path: PathBuf,
pub directory_struct: Vec<DirectoryEntry>,
}
#[cfg(windows)]
fn default_editor() -> String {
"notepad".to_string()
}
#[cfg(not(windows))]
fn default_editor() -> String {
"nano".to_string()
}
impl Config {
fn config_path() -> Result<PathBuf> {
let mut dir = dirs_next::config_dir()
.ok_or_else(|| PrefixloadError::Custom("Failed to get config directory".into()))?;
dir.push("prefixload");
fs::create_dir_all(&dir)?;
dir.push("config.yml");
Ok(dir)
}
fn ensure_config_exists(path: &PathBuf) -> Result<()> {
if !path.exists() {
let bytes = Asset::get("config.yml")
.expect("Embedded config.yml not found")
.data;
std::fs::write(path, bytes)?;
}
Ok(())
}
fn backup_config() -> Result<()> {
let path = Self::config_path()?;
if path.exists() {
let mut backup_path = path.clone();
backup_path.set_extension("yml.bak"); std::fs::copy(path, &backup_path)?;
}
Ok(())
}
pub fn read_to_string() -> Result<String> {
let path = Self::config_path()?;
Self::ensure_config_exists(&path)?;
Ok(fs::read_to_string(&path)?)
}
pub fn load() -> Result<Self> {
let s = Self::read_to_string()?;
Ok(serde_yaml::from_str(&s)?)
}
pub fn save(&self) -> Result<()> {
let path = Self::config_path()?;
Self::backup_config()?;
let s = serde_yaml::to_string(self)?;
fs::write(path, s)?;
Ok(())
}
pub fn edit() -> Result<()> {
let path = Self::config_path()?;
Self::backup_config()?;
let editor = std::env::var("EDITOR").unwrap_or_else(|_| default_editor());
std::process::Command::new(&editor)
.arg(&path)
.status()
.map_err(|e| {
PrefixloadError::Custom(format!("Failed to launch editor '{}': {}", editor, e))
})?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::{env, fs};
use tempfile::TempDir;
#[cfg(windows)]
const CONFIG_ENV: &str = "APPDATA";
#[cfg(not(windows))]
const CONFIG_ENV: &str = "XDG_CONFIG_HOME";
fn temp_config_dir() -> TempDir {
let tmp = TempDir::new().expect("temp dir");
unsafe { env::set_var(CONFIG_ENV, tmp.path()) };
tmp
}
#[test]
#[serial] fn config_path_creates_directory() {
let _guard = temp_config_dir();
let path = Config::config_path().unwrap();
assert!(
path.ends_with("prefixload/config.yml"),
"Expected path .../prefixload/config.yml, got {:?}",
path
);
assert!(
path.parent().unwrap().exists(),
"`prefixload` directory was not created"
);
}
#[test]
#[serial]
fn ensure_config_creates_default_file() {
let _guard = temp_config_dir();
let path = Config::config_path().unwrap();
assert!(!path.exists());
Config::ensure_config_exists(&path).unwrap();
assert!(path.exists(), "`config.yml` was not created");
let disk = fs::read(&path).unwrap();
let embedded: Vec<u8> = Asset::get("config.yml").unwrap().data.into_owned();
assert_eq!(
disk, embedded,
"Disk contents differ from embedded config.yml"
);
}
#[test]
#[serial]
fn load_parses_yaml() {
let _guard = temp_config_dir();
let cfg = Config::load().unwrap();
assert!(
!cfg.endpoint.is_empty(),
"`endpoint` should be populated in default YAML"
);
assert!(cfg.part_size > 0, "`part_size` must be > 0");
assert!(
!cfg.directory_struct.is_empty(),
"`directory_struct` should not be empty"
);
}
#[test]
#[serial]
fn save_creates_backup_and_writes_new_content() {
let _guard = temp_config_dir();
let mut cfg = Config::load().unwrap();
let path = Config::config_path().expect("config path");
let old = fs::read_to_string(&path).unwrap();
cfg.endpoint = "http://example.com".into();
cfg.save().unwrap();
let mut bak = path.clone();
bak.set_extension("yml.bak");
assert!(bak.exists(), "Backup file was not created");
let bak_content = fs::read_to_string(&bak).unwrap();
assert_eq!(bak_content, old, "Backup does not match original file");
let saved: Config = serde_yaml::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
saved.endpoint, "http://example.com",
"`save` did not write new endpoint value"
);
}
#[test]
#[serial]
fn load_fails_on_invalid_yaml() {
let _guard = temp_config_dir();
let path = Config::config_path().unwrap();
fs::write(&path, "endpoint: not-a-real-endpoint\nbucket: :").unwrap();
let result = Config::load();
assert!(result.is_err(), "load() should fail on invalid YAML");
}
}