1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Config {
10 pub format: String,
12 pub output_name: Option<String>,
14 pub ignore: Option<Vec<String>>,
16 pub ignore_file: Option<String>,
18 #[serde(skip)]
20 pub resolved_ignore_file_content: Option<String>,
21}
22
23impl Default for Config {
24 fn default() -> Self {
25 Self {
26 format: "tar.gz".to_string(),
27 output_name: None,
28 ignore: Some(vec![
29 "target/".to_string(),
30 "*.tmp".to_string(),
31 "*.log".to_string(),
32 ".DS_Store".to_string(),
33 "Thumbs.db".to_string(),
34 "*.swp".to_string(),
35 "*.swo".to_string(),
36 "*~".to_string(),
37 ".git/".to_string(),
38 ".svn/".to_string(),
39 ".hg/".to_string(),
40 "node_modules/".to_string(),
41 "__pycache__/".to_string(),
42 ".pytest_cache/".to_string(),
43 ".venv/".to_string(),
44 "venv/".to_string(),
45 "env/".to_string(),
46 "*.pyc".to_string(),
47 "*.pyo".to_string(),
48 "*.pyd".to_string(),
49 ".idea/".to_string(),
50 ".vscode/".to_string(),
51 "*.iml".to_string(),
52 ]),
53 ignore_file: None,
54 resolved_ignore_file_content: None, }
56 }
57}
58
59impl Config {
60 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
70 let content = std::fs::read_to_string(&path)
71 .with_context(|| format!("无法读取配置文件: {}", path.as_ref().display()))?;
72
73 let mut config: Config = toml::from_str(&content).with_context(|| "解析配置文件失败")?;
74
75 if !["zip", "tar.gz", "7z"].contains(&config.format.as_str()) {
77 anyhow::bail!(
78 "不支持的压缩格式: {},支持的格式: zip, tar.gz, 7z",
79 config.format
80 );
81 }
82
83 if let Some(ignore_file_path) = &config.ignore_file {
85 if let Ok(file_content) = std::fs::read_to_string(ignore_file_path) {
86 config.resolved_ignore_file_content = Some(file_content);
87 }
88 }
89
90 Ok(config)
91 }
92
93 pub fn create_default_config_file(output_path: Option<&Path>) -> Result<()> {
101 let config = Config::default();
102 let toml_content = toml::to_string_pretty(&config).context("无法序列化默认配置")?;
103
104 let path = output_path.unwrap_or(&Path::new("ztr.toml"));
105 std::fs::write(path, toml_content)
106 .with_context(|| format!("无法写入配置文件: {}", path.display()))?;
107
108 Ok(())
109 }
110
111 pub fn get_output_name(&self) -> String {
117 if let Some(name) = &self.output_name {
118 name.clone()
119 } else {
120 match std::env::current_dir() {
122 Ok(path) => path
123 .file_name()
124 .and_then(|n| n.to_str())
125 .unwrap_or("archive")
126 .to_string(),
127 Err(_) => "archive".to_string(),
128 }
129 }
130 }
131
132 pub fn get_ignore_rules(&self) -> Vec<String> {
134 let mut all_rules: HashSet<String> = HashSet::new();
135
136 if let Some(ignore_list) = &self.ignore {
137 for rule in ignore_list {
138 all_rules.insert(rule.clone());
139 }
140 }
141
142 if let Some(content) = &self.resolved_ignore_file_content {
143 for line in content.lines() {
144 let trimmed_line = line.trim();
145 if !trimmed_line.is_empty() && !trimmed_line.starts_with('#') {
146 all_rules.insert(trimmed_line.to_string());
147 }
148 }
149 }
150
151 all_rules.into_iter().collect()
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use std::io::Write;
159 use tempfile::NamedTempFile;
160
161 #[test]
162 fn test_config_load() -> Result<()> {
163 let toml_content = r#"
164 format = "zip"
165 output_name = "test_archive"
166 ignore = [".test_ignore"]
167 ignore_file = "./test_ignore_file.txt"
168 "#;
169 let mut file = NamedTempFile::new()?;
170 write!(file, "{}", toml_content)?;
171 let config = Config::load(file.path())?;
172
173 assert_eq!(config.format, "zip");
174 assert_eq!(config.output_name, Some("test_archive".to_string()));
175 assert_eq!(config.ignore, Some(vec![".test_ignore".to_string()]));
176 assert_eq!(
177 config.ignore_file,
178 Some("./test_ignore_file.txt".to_string())
179 );
180 assert_eq!(config.resolved_ignore_file_content, None); Ok(())
182 }
183
184 #[test]
185 fn test_config_load_with_ignore_file_content() -> Result<()> {
186 let mut ignore_file = NamedTempFile::new()?;
187 writeln!(ignore_file, "file_from_ignore.txt")?;
188 let ignore_file_path = ignore_file.path().to_string_lossy().to_string();
189
190 let toml_content = format!(
191 r#"
192 format = "zip"
193 ignore_file = "{}"
194 "#,
195 ignore_file_path
196 );
197 let mut config_file = NamedTempFile::new()?;
198 write!(config_file, "{}", toml_content)?;
199 let config = Config::load(config_file.path())?;
200
201 assert_eq!(config.format, "zip");
202 assert_eq!(config.ignore_file, Some(ignore_file_path.clone()));
203 assert_eq!(
204 config.resolved_ignore_file_content,
205 Some("file_from_ignore.txt\n".to_string())
206 );
207
208 let rules = config.get_ignore_rules();
209 assert!(rules.contains(&"file_from_ignore.txt".to_string()));
210 assert_eq!(rules.len(), 1);
211 Ok(())
212 }
213
214 #[test]
215 fn test_config_load_invalid_format() -> Result<()> {
216 let toml_content = r#"
217 format = "rar"
218 "#;
219 let mut file = NamedTempFile::new()?;
220 write!(file, "{}", toml_content)?;
221 let err = Config::load(file.path()).unwrap_err();
222 assert!(err.to_string().contains("不支持的压缩格式"));
223 Ok(())
224 }
225
226 #[test]
227 fn test_get_output_name_from_config() {
228 let config = Config {
229 format: "zip".to_string(),
230 output_name: Some("my_custom_name".to_string()),
231 ignore: None,
232 ignore_file: None,
233 resolved_ignore_file_content: None,
234 };
235 assert_eq!(config.get_output_name(), "my_custom_name");
236 }
237
238 #[test]
239 fn test_get_output_name_default() {
240 let config = Config::default();
241 let output_name = config.get_output_name();
244 assert!(!output_name.is_empty());
245 assert_ne!(output_name, "archive"); }
247
248 #[test]
249 fn test_get_ignore_rules_from_config() {
250 let config = Config {
251 format: "zip".to_string(),
252 output_name: None,
253 ignore: Some(vec!["rule1".to_string(), "rule2".to_string()]),
254 ignore_file: None,
255 resolved_ignore_file_content: None,
256 };
257 let rules = config.get_ignore_rules();
258 assert!(rules.contains(&"rule1".to_string()));
259 assert!(rules.contains(&"rule2".to_string()));
260 assert_eq!(rules.len(), 2);
261 }
262
263 #[test]
264 fn test_get_ignore_rules_from_resolved_file_content() {
265 let mut config_with_file_content = Config::default();
266 config_with_file_content.resolved_ignore_file_content =
267 Some("# 注释\nrule_from_file1\n\nrule_from_file2".to_string());
268 let rules = config_with_file_content.get_ignore_rules();
269 assert!(rules.contains(&"rule_from_file1".to_string()));
270 assert!(rules.contains(&"rule_from_file2".to_string()));
271 assert_eq!(rules.len(), 2);
272 }
273
274 #[test]
275 fn test_get_ignore_rules_priority() {
276 let mut config = Config {
277 format: "zip".to_string(),
278 output_name: None,
279 ignore: Some(vec![
280 "rule_from_config".to_string(),
281 "common_rule".to_string(),
282 ]),
283 ignore_file: None,
284 resolved_ignore_file_content: None,
285 };
286 config.resolved_ignore_file_content = Some("rule_from_file\ncommon_rule".to_string());
287 let rules = config.get_ignore_rules();
288 assert!(rules.contains(&"rule_from_config".to_string()));
289 assert!(rules.contains(&"rule_from_file".to_string()));
290 assert!(rules.contains(&"common_rule".to_string()));
291 assert_eq!(rules.len(), 3); }
293}