use anyhow::{Context, Result, bail};
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::models::{Note, Project, Resource, Task};
use crate::storage::Storage;
#[derive(Serialize, Deserialize, Default)]
struct Envelope {
#[serde(default)]
tasks: Vec<Task>,
#[serde(default)]
projects: Vec<Project>,
#[serde(default)]
notes: Vec<Note>,
#[serde(default)]
resources: Vec<Resource>,
}
pub fn execute_export(storage: &impl Storage, file: Option<PathBuf>) -> Result<()> {
let (tasks, projects, notes, resources) = storage.load_all_with_resources()?;
let envelope = Envelope {
tasks: tasks.clone(),
projects: projects.clone(),
notes: notes.clone(),
resources: resources.clone(),
};
let json = serde_json::to_string_pretty(&envelope).context("Failed to serialize data")?;
let path = file.unwrap_or_else(|| {
let date = chrono::Local::now().format("%Y-%m-%d");
PathBuf::from(format!("rustodo-export-{}.json", date))
});
std::fs::write(&path, &json)
.context(format!("Failed to write export file: {}", path.display()))?;
println!(
"{} Exported to: {}",
"✓".green(),
path.display().to_string().cyan()
);
println!(
" {} tasks, {} projects, {} notes, {} resources",
tasks.len().to_string().dimmed(),
projects.len().to_string().dimmed(),
notes.len().to_string().dimmed(),
resources.len().to_string().dimmed(),
);
Ok(())
}
pub fn execute_import(
storage: &impl Storage,
file: PathBuf,
replace: bool,
yes: bool,
) -> Result<()> {
if !file.exists() {
bail!("File not found: {}", file.display());
}
let content =
std::fs::read_to_string(&file).context(format!("Failed to read {}", file.display()))?;
let envelope: Envelope = serde_json::from_str(&content)
.context("Failed to parse export file — is it a valid rustodo JSON export?")?;
let task_count = envelope.tasks.len();
let project_count = envelope.projects.len();
let note_count = envelope.notes.len();
let resource_count = envelope.resources.len();
if task_count + project_count + note_count + resource_count == 0 {
println!("{}", "\nNothing to import — file is empty.\n".dimmed());
return Ok(());
}
println!(
"\n{} Importing from: {}\n",
"".blue(),
file.display().to_string().cyan()
);
println!(
" {} tasks, {} projects, {} notes, {} resources",
task_count.to_string().bright_white(),
project_count.to_string().bright_white(),
note_count.to_string().bright_white(),
resource_count.to_string().bright_white(),
);
if replace {
println!(
"\n {} {} All existing data will be replaced.\n",
"!".yellow(),
"Warning:".yellow()
);
} else {
println!(
"\n {} Existing records with matching UUIDs will be updated.\n",
"".dimmed()
);
}
if !yes && !crate::utils::confirm("Proceed with import? [y/N]:")? {
println!("{}", "Import cancelled.".dimmed());
return Ok(());
}
if replace {
storage.save(&envelope.tasks)?;
storage.save_projects(&envelope.projects)?;
storage.save_notes(&envelope.notes)?;
storage.save_resources(&envelope.resources)?;
} else {
if !envelope.tasks.is_empty() {
storage.save(&envelope.tasks)?;
}
if !envelope.projects.is_empty() {
storage.save_projects(&envelope.projects)?;
}
if !envelope.notes.is_empty() {
storage.save_notes(&envelope.notes)?;
}
if !envelope.resources.is_empty() {
storage.save_resources(&envelope.resources)?;
}
}
println!(
"{} Import complete: {} tasks, {} projects, {} notes, {} resources",
"✓".green(),
task_count.to_string().green(),
project_count.to_string().green(),
note_count.to_string().green(),
resource_count.to_string().green(),
);
Ok(())
}