rdbkp2/
lib.rs

1mod commands;
2// #[deprecated(since = "1.0.0", note = "no need to load config file")]
3mod 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    /// 是否使用交互模式 [default: true]
42    #[arg(global = true, short, long, default_value = "true")]
43    interactive: bool,
44
45    /// 是否在操作 (备份/恢复) 后重启容器 [default: false]
46    #[arg(global = true, short, long, default_value = "false")]
47    restart: bool,
48
49    /// 停止容器超时时间 (秒)
50    #[arg(global = true, short, long, default_value = "30")]
51    timeout: u64,
52
53    /// 排除模式:备份时将排除包含这些模式的文件/目录
54    #[arg(global = true, short, long, default_value = ".git,node_modules,target")]
55    exclude: String,
56
57    /// 是否自动确认 [default: false]
58    #[arg(global = true, short, long, default_value = "false")]
59    yes: bool,
60
61    /// 是否显示详细日志 [default: false]
62    #[arg(global = true, short, long, default_value = "false")]
63    verbose: bool,
64
65    /// 设置语言
66    #[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    /// 备份 Docker 容器数据
120    ///
121    /// 备份时将会进行的操作:
122    /// 1. 检查容器是否存在
123    /// 2. 检查容器是否正在运行,如果正在运行,则先停止容器
124    /// 3. 检查容器是否存在挂载卷
125    /// 4. 压缩备份挂载卷到输出目录
126    /// 5. 如果设置了 --restart 选项,则重启容器
127    Backup {
128        /// 容器名称或 ID
129        #[arg(short, long)]
130        container: Option<String>,
131
132        /// 需要备份的路径 (file/dir)
133        ///
134        /// 如果设置了该选项,则将只备份该路径下的数据
135        /// 如果未设置该选项,则将备份容器内的所有 Volumes
136        #[arg(short, long)]
137        file: Option<String>,
138
139        /// 备份文件输出路径
140        #[arg(short, long)]
141        #[arg(default_value = "./backup/")]
142        output: Option<String>,
143    },
144
145    /// 恢复 Docker 容器数据
146    ///
147    /// 恢复时将会进行的操作:
148    /// 1. 检查容器是否存在
149    /// 2. 检查容器是否正在运行,如果正在运行,则先停止容器
150    /// 3. 恢复挂载卷到指定路径 (如果未指定,则恢复到容器工作目录)
151    /// 4. 如果设置了 --restart 选项,则重启容器
152    Restore {
153        /// 容器名称或 ID
154        #[arg(short, long)]
155        container: Option<String>,
156
157        /// 备份文件路径
158        #[arg(short, long)]
159        file: Option<String>,
160
161        /// 备份文件恢复输出路径
162        #[arg(short, long)]
163        output: Option<String>,
164    },
165
166    /// 列出可用的 Docker 容器
167    List,
168
169    /// 生成命令行补全脚本
170    Completions {
171        /// Shell 类型
172        #[arg(value_enum)]
173        shell: Shell,
174    },
175
176    /// 检查更新
177    ///
178    /// 检查是否有新版本可用,如果有则提示更新方法
179    Update,
180
181    /// 完全卸载
182    ///
183    /// 删除符号链接并提示如何完成卸载
184    Uninstall,
185
186    Link {
187        #[command(subcommand)]
188        action: LinkActions,
189    },
190}
191
192/// 链接操作
193///
194/// 安装/卸载软连接链接
195///
196/// 示例:
197/// ```bash
198/// rdbkp2 link install
199/// rdbkp2 link uninstall
200/// ```
201#[derive(Subcommand)]
202enum LinkActions {
203    /// 安装软连接链接 sudo ln -s $(where rdbkp2) /usr/local/bin/rdbkp2
204    Install,
205
206    /// 卸载软连接链接 sudo rm /usr/local/bin/rdbkp2
207    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    // 初始化日志
237    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    // 解析命令行参数
269    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    // #[cfg(debug_assertions)]
279    // {
280    //     println!("1. langugage:{}", t!("language"));
281    //     println!("2. langugage:{}", t!("language.en"));
282    //     println!("3. langugage:{}", t!("language.ja"));
283    //     println!("3. langugage:{}", t!("language"));
284    // }
285
286    // 初始化全局 runtime 配置
287    init_config(
288        timeout,
289        interactive,
290        restart,
291        verbose,
292        yes,
293        exclude,
294        language,
295    )?;
296
297    // 设置日志级别,初始化全局日志
298    let log_level = if verbose { Level::DEBUG } else { Level::ERROR };
299    init_log(log_level)?;
300
301    // 初始化全局 docker client
302    init_docker_client(timeout)?;
303
304    // 根据子命令执行相应的操作
305    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}