1mod commands;
2mod config;
4mod docker;
5mod utils;
6
7#[cfg(test)]
8mod tests;
9
10use anyhow::Result;
11use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
12use std::io;
13use tracing::{Level, info, instrument};
14use tracing_subscriber::{EnvFilter, fmt};
15
16#[macro_use]
17extern crate rust_i18n;
18
19rust_i18n::i18n!(
20 "locales",
21 fallback = ["en", "ja", "ko", "es", "fr", "de", "it"]
22);
23
24#[allow(unused)]
25pub(crate) const DOCKER_CMD: &str = "docker";
26
27#[allow(unused)]
28#[cfg(target_os = "macos")]
29pub(crate) const DOCKER_COMPOSE_CMD: &str = "docker-compose";
30
31#[allow(unused)]
32#[cfg(not(target_os = "macos"))]
33pub(crate) const DOCKER_COMPOSE_CMD: &str = "docker compose";
34
35#[derive(Parser)]
36#[command(author, version, about, long_about = None)]
37struct Cli {
38 #[command(subcommand)]
39 command: Commands,
40
41 #[arg(global = true, short, long, default_value = "true")]
43 interactive: bool,
44
45 #[arg(global = true, short, long, default_value = "false")]
47 restart: bool,
48
49 #[arg(global = true, short, long, default_value = "30")]
51 timeout: u64,
52
53 #[arg(global = true, short, long, default_value = ".git,node_modules,target")]
55 exclude: String,
56
57 #[arg(global = true, short, long, default_value = "false")]
59 yes: bool,
60
61 #[arg(global = true, short, long, default_value = "false")]
63 verbose: bool,
64
65 #[arg(global = true, short, long, default_value = "zh", value_enum)]
67 language: Language,
68}
69
70#[allow(clippy::enum_variant_names)]
71#[derive(Clone, ValueEnum, Debug)]
72enum Shell {
73 Bash,
74 Fish,
75 Zsh,
76 PowerShell,
77}
78
79#[derive(Clone, ValueEnum, Debug)]
80enum Language {
81 Zh,
82 En,
83 Ja,
84 Ko,
85 Es,
86 Fr,
87 De,
88 It,
89}
90
91impl From<Language> for String {
92 fn from(language: Language) -> Self {
93 match language {
94 Language::Zh => "zh-CN".to_string(),
95 Language::En => "en".to_string(),
96 Language::Ja => "ja".to_string(),
97 Language::Ko => "ko".to_string(),
98 Language::Es => "es".to_string(),
99 Language::Fr => "fr".to_string(),
100 Language::De => "de".to_string(),
101 Language::It => "it".to_string(),
102 }
103 }
104}
105
106impl From<Shell> for clap_complete::aot::Shell {
107 fn from(value: Shell) -> Self {
108 match value {
109 Shell::Bash => clap_complete::aot::Shell::Bash,
110 Shell::Fish => clap_complete::aot::Shell::Fish,
111 Shell::Zsh => clap_complete::aot::Shell::Zsh,
112 Shell::PowerShell => clap_complete::aot::Shell::PowerShell,
113 }
114 }
115}
116
117#[derive(Subcommand)]
118enum Commands {
119 Backup {
128 #[arg(short, long)]
130 container: Option<String>,
131
132 #[arg(short, long)]
137 file: Option<String>,
138
139 #[arg(short, long)]
141 #[arg(default_value = "./backup/")]
142 output: Option<String>,
143 },
144
145 Restore {
153 #[arg(short, long)]
155 container: Option<String>,
156
157 #[arg(short, long)]
159 file: Option<String>,
160
161 #[arg(short, long)]
163 output: Option<String>,
164 },
165
166 List,
168
169 Completions {
171 #[arg(value_enum)]
173 shell: Shell,
174 },
175
176 Update,
180
181 Uninstall,
185
186 Link {
187 #[command(subcommand)]
188 action: LinkActions,
189 },
190}
191
192#[derive(Subcommand)]
202enum LinkActions {
203 Install,
205
206 Uninstall,
208}
209
210#[instrument(level = "INFO")]
211fn init_config(
212 timeout_secs: u64,
213 interactive: bool,
214 restart: bool,
215 verbose: bool,
216 yes: bool,
217 exclude: String,
218 language: String,
219) -> Result<()> {
220 let cfg = config::Config {
221 timeout_secs,
222 interactive,
223 restart,
224 verbose,
225 yes,
226 exclude,
227 language,
228 ..config::Config::default()
229 };
230 config::Config::init(cfg)?;
231 Ok(())
232}
233
234#[instrument(level = "INFO")]
235pub fn init_log(log_level: Level) -> Result<()> {
236 let mut log_fmt = fmt()
238 .with_env_filter(
239 EnvFilter::builder()
240 .with_default_directive(log_level.into())
241 .from_env_lossy(),
242 )
243 .with_level(true);
244
245 #[cfg(debug_assertions)]
246 {
247 log_fmt = log_fmt
248 .with_target(true)
249 .with_thread_ids(true)
250 .with_line_number(true)
251 .with_file(true);
252 }
253
254 log_fmt.init();
255 Ok(())
256}
257
258#[instrument(level = "INFO")]
259fn init_docker_client(timeout_secs: u64) -> Result<()> {
260 docker::DockerClient::init(timeout_secs)?;
261 Ok(())
262}
263
264#[instrument(level = "INFO")]
265pub async fn run() -> Result<()> {
266 info!("Starting Docker container backup tool");
267
268 let cli = Cli::parse();
270 let interactive = cli.interactive;
271 let timeout = cli.timeout;
272 let restart = cli.restart;
273 let exclude = cli.exclude;
274 let yes = cli.yes;
275 let verbose = cli.verbose;
276 let language: String = cli.language.into();
277 rust_i18n::set_locale(&language);
278 init_config(
288 timeout,
289 interactive,
290 restart,
291 verbose,
292 yes,
293 exclude,
294 language,
295 )?;
296
297 let log_level = if verbose { Level::DEBUG } else { Level::ERROR };
299 init_log(log_level)?;
300
301 init_docker_client(timeout)?;
303
304 do_action(cli.command).await?;
306
307 info!("Operation completed successfully");
308 Ok(())
309}
310
311async fn do_action(action: Commands) -> Result<()> {
312 match action {
313 Commands::Backup {
314 container,
315 file,
316 output,
317 } => {
318 info!(?container, ?file, ?output, "Executing backup command");
319 commands::backup(container, file, output).await?;
320 }
321 Commands::Restore {
322 container,
323 file,
324 output,
325 } => {
326 info!(?container, ?file, ?output, "Executing restore command");
327 commands::restore(container, file, output).await?;
328 }
329 Commands::List => {
330 info!("Executing list command");
331 commands::list_containers().await?;
332 }
333 Commands::Completions { shell } => {
334 info!(?shell, "Generating shell completions");
335 let mut cmd = Cli::command();
336 let name = cmd.get_name().to_string();
337 let generator: clap_complete::aot::Shell = shell.into();
338 clap_complete::generate(generator, &mut cmd, name, &mut io::stdout());
339 }
340 Commands::Update => {
341 info!("Checking for updates");
342 commands::lifecycle::check_update().await?;
343 }
344 Commands::Uninstall => {
345 info!("Executing uninstall command");
346 commands::lifecycle::uninstall().await?;
347 }
348 Commands::Link { action } => match action {
349 LinkActions::Install => {
350 info!("Executing soft-link install command");
351 commands::symbollink::create_symbollink()?;
352 }
353 LinkActions::Uninstall => {
354 info!("Executing soft-link uninstall command");
355 commands::symbollink::remove_symbollink()?;
356 }
357 },
358 }
359 Ok(())
360}