use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs;
use std::path::Path;
use std::process::Command;
#[derive(Debug, Serialize, Deserialize)]
pub struct SyncConfig {
pub paths: Vec<SyncPath>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SyncPath {
pub source: String,
pub dest: String,
}
#[derive(Debug, Clone, Copy)]
pub enum SyncDirection {
UploadToS3, DownloadFromS3, }
impl SyncPath {
pub fn get_direction(&self) -> SyncDirection {
if self.source.starts_with("s3://") {
SyncDirection::DownloadFromS3
} else {
SyncDirection::UploadToS3
}
}
}
impl SyncConfig {
pub fn read_from_file(path: &str) -> Result<Self, Box<dyn Error>> {
if !Path::new(path).exists() {
return Err(format!("Config file not found: {}", path).into());
}
let yaml_content = fs::read_to_string(path)?;
let config: SyncConfig = serde_yaml::from_str(&yaml_content)
.map_err(|e| format!("Failed to parse YAML: {}", e))?;
Ok(config)
}
pub fn write_to_file(&self, path: &str) -> Result<(), Box<dyn Error>> {
let yaml_content = serde_yaml::to_string(self)
.map_err(|e| format!("Failed to serialize to YAML: {}", e))?;
if let Some(parent) = Path::new(path).parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, yaml_content)?;
Ok(())
}
pub fn new() -> Self {
SyncConfig { paths: Vec::new() }
}
pub fn add_path(&mut self, source: String, dest: String) {
self.paths.push(SyncPath { source, dest });
}
pub fn remove_path(&mut self, index: usize) -> Result<(), Box<dyn Error>> {
if index >= self.paths.len() {
return Err(format!(
"Index {} out of bounds (max: {})",
index,
self.paths.len() - 1
)
.into());
}
self.paths.remove(index);
Ok(())
}
pub fn list_paths(&self) -> Vec<(&String, &String, SyncDirection)> {
self.paths
.iter()
.map(|path| (&path.source, &path.dest, path.get_direction()))
.collect()
}
}
pub async fn execute_continuous_sync(
config_path: String,
interval_seconds: u64,
create_if_missing: bool,
) -> Result<(), Box<dyn Error>> {
println!(
"Starting continuous sync from configuration: {}",
config_path
);
println!("Sync interval: {} seconds", interval_seconds);
loop {
match execute_sync(config_path.clone(), create_if_missing).await {
Ok(_) => println!("Sync completed successfully. Waiting for next sync..."),
Err(e) => println!("Sync error: {}. Will retry on next interval.", e),
}
tokio::time::sleep(tokio::time::Duration::from_secs(interval_seconds)).await;
}
}
pub async fn execute_sync(
config_path: String,
create_if_missing: bool,
) -> Result<(), Box<dyn Error>> {
println!("Reading sync configuration from: {}", config_path);
if !Path::new(&config_path).exists() {
if create_if_missing {
create_empty_config(&config_path)?;
} else {
return Err(format!("Config file not found: {}", config_path).into());
}
}
let config = SyncConfig::read_from_file(&config_path)?;
if config.paths.is_empty() {
println!("No paths to sync found in the configuration file.");
return Ok(());
}
println!("Found {} paths to sync", config.paths.len());
for (index, path) in config.paths.iter().enumerate() {
let direction = path.get_direction();
let direction_str = match direction {
SyncDirection::UploadToS3 => "Upload to S3",
SyncDirection::DownloadFromS3 => "Download from S3",
};
println!(
"[{}/{}] Syncing {} to {} ({})",
index + 1,
config.paths.len(),
path.source,
path.dest,
direction_str
);
let source_exists = if path.source.starts_with("s3://") {
true
} else {
Path::new(&path.source).exists()
};
if !source_exists {
println!("Warning: Source path does not exist: {}", path.source);
continue;
}
let output = Command::new("aws")
.arg("s3")
.arg("sync")
.arg(&path.source)
.arg(&path.dest)
.output()?;
if output.status.success() {
println!("Successfully synced {} to {}", path.source, path.dest);
} else {
let error = String::from_utf8_lossy(&output.stderr);
println!("Failed to sync: {}", error);
}
}
println!("Sync operation completed");
Ok(())
}
pub fn create_empty_config(path: &str) -> Result<(), Box<dyn Error>> {
let config = SyncConfig::new();
config.write_to_file(path)?;
println!("Created empty sync configuration at: {}", path);
Ok(())
}
pub fn create_example_config(path: &str) -> Result<(), Box<dyn Error>> {
let mut config = SyncConfig::new();
config.add_path(
"/path/to/local/directory1".to_string(),
"s3://your-bucket/directory1".to_string(),
);
config.add_path(
"s3://your-bucket/directory2".to_string(),
"/path/to/local/directory2".to_string(),
);
config.add_path(
"/path/to/local/file.txt".to_string(),
"s3://your-bucket/file.txt".to_string(),
);
config.write_to_file(path)?;
println!("Created example sync configuration at: {}", path);
Ok(())
}
pub fn add_sync_path(
config_path: &str,
source: String,
dest: String,
) -> Result<(), Box<dyn Error>> {
let mut config = if Path::new(config_path).exists() {
SyncConfig::read_from_file(config_path)?
} else {
SyncConfig::new()
};
let direction_str = if source.starts_with("s3://") {
"Download from S3"
} else {
"Upload to S3"
};
config.add_path(source.clone(), dest.clone());
config.write_to_file(config_path)?;
println!(
"Added sync path: {} -> {} ({})",
source, dest, direction_str
);
Ok(())
}
pub fn remove_sync_path(config_path: &str, index: usize) -> Result<(), Box<dyn Error>> {
let mut config = SyncConfig::read_from_file(config_path)?;
let path_to_remove = if index < config.paths.len() {
Some((
config.paths[index].source.clone(),
config.paths[index].dest.clone(),
))
} else {
None
};
config.remove_path(index)?;
config.write_to_file(config_path)?;
if let Some((source, destination)) = path_to_remove {
println!("Removed sync path: {} -> {}", source, destination);
}
Ok(())
}
pub fn list_sync_paths(config_path: &str) -> Result<(), Box<dyn Error>> {
let config = SyncConfig::read_from_file(config_path)?;
if config.paths.is_empty() {
println!("No sync paths found in the configuration.");
return Ok(());
}
println!("Sync paths in {}:", config_path);
for (i, (source, destination, direction)) in config.list_paths().iter().enumerate() {
let direction_str = match direction {
SyncDirection::UploadToS3 => "Upload to S3",
SyncDirection::DownloadFromS3 => "Download from S3",
};
println!("[{}] {} -> {} ({})", i, source, destination, direction_str);
}
Ok(())
}