task_graph_mcp/cli/
export.rs1use clap::Args;
7use std::path::PathBuf;
8
9#[derive(Args, Debug)]
11pub struct ExportArgs {
12 #[arg(short, long, value_name = "FILE")]
14 pub output: Option<PathBuf>,
15
16 #[arg(long)]
18 pub gzip: bool,
19
20 #[arg(long, value_name = "LIST", value_delimiter = ',')]
25 pub tables: Option<Vec<String>>,
26
27 #[arg(long)]
29 pub no_history: bool,
30
31 #[arg(long)]
33 pub exclude_deleted: bool,
34
35 #[arg(long, value_name = "SIZE")]
41 pub compress_threshold: Option<String>,
42}
43
44impl ExportArgs {
45 pub fn tables_to_export(&self) -> Option<Vec<String>> {
47 if self.no_history {
48 if let Some(ref tables) = self.tables {
50 Some(
51 tables
52 .iter()
53 .filter(|t| *t != "task_sequence")
54 .cloned()
55 .collect(),
56 )
57 } else {
58 Some(vec![
60 "tasks".to_string(),
61 "dependencies".to_string(),
62 "attachments".to_string(),
63 "task_tags".to_string(),
64 "task_needed_tags".to_string(),
65 "task_wanted_tags".to_string(),
66 ])
67 }
68 } else {
69 self.tables.clone()
70 }
71 }
72
73 pub fn compress_threshold_bytes(&self) -> Option<u64> {
75 self.compress_threshold.as_ref().and_then(|s| parse_size(s))
76 }
77
78 pub fn should_compress(&self, output_size: Option<u64>) -> bool {
80 if self.gzip {
82 return true;
83 }
84
85 if let Some(ref path) = self.output
87 && path.extension().is_some_and(|ext| ext == "gz")
88 {
89 return true;
90 }
91
92 if let (Some(threshold), Some(size)) = (self.compress_threshold_bytes(), output_size) {
94 return size > threshold;
95 }
96
97 false
98 }
99}
100
101fn parse_size(s: &str) -> Option<u64> {
105 let s = s.trim().to_uppercase();
106
107 if let Some(num) = s.strip_suffix("GB") {
108 num.trim()
109 .parse::<u64>()
110 .ok()
111 .map(|n| n * 1024 * 1024 * 1024)
112 } else if let Some(num) = s.strip_suffix("MB") {
113 num.trim().parse::<u64>().ok().map(|n| n * 1024 * 1024)
114 } else if let Some(num) = s.strip_suffix("KB") {
115 num.trim().parse::<u64>().ok().map(|n| n * 1024)
116 } else if let Some(num) = s.strip_suffix('B') {
117 num.trim().parse::<u64>().ok()
118 } else {
119 s.parse::<u64>().ok()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_parse_size() {
130 assert_eq!(parse_size("100"), Some(100));
131 assert_eq!(parse_size("100B"), Some(100));
132 assert_eq!(parse_size("100KB"), Some(100 * 1024));
133 assert_eq!(parse_size("100kb"), Some(100 * 1024));
134 assert_eq!(parse_size("1MB"), Some(1024 * 1024));
135 assert_eq!(parse_size("1GB"), Some(1024 * 1024 * 1024));
136 assert_eq!(parse_size("invalid"), None);
137 }
138
139 #[test]
140 fn test_tables_to_export_no_history() {
141 let args = ExportArgs {
142 output: None,
143 gzip: false,
144 tables: None,
145 no_history: true,
146 exclude_deleted: false,
147 compress_threshold: None,
148 };
149
150 let tables = args.tables_to_export().unwrap();
151 assert!(!tables.contains(&"task_sequence".to_string()));
152 assert!(tables.contains(&"tasks".to_string()));
153 }
154
155 #[test]
156 fn test_should_compress() {
157 let args = ExportArgs {
159 output: None,
160 gzip: true,
161 tables: None,
162 no_history: false,
163 exclude_deleted: false,
164 compress_threshold: None,
165 };
166 assert!(args.should_compress(None));
167
168 let args = ExportArgs {
170 output: Some(PathBuf::from("snapshot.json.gz")),
171 gzip: false,
172 tables: None,
173 no_history: false,
174 exclude_deleted: false,
175 compress_threshold: None,
176 };
177 assert!(args.should_compress(None));
178
179 let args = ExportArgs {
181 output: None,
182 gzip: false,
183 tables: None,
184 no_history: false,
185 exclude_deleted: false,
186 compress_threshold: Some("100KB".to_string()),
187 };
188 assert!(!args.should_compress(Some(50 * 1024))); assert!(args.should_compress(Some(150 * 1024))); }
191}