1use crate::error::handlers::ValidationErrorHandler;
7use crate::error::{PusherError, Result};
8use clap::Parser;
9
10#[derive(Parser, Debug, Clone)]
11#[command(
12 name = "docker-image-pusher",
13 version = "0.1.3",
14 about = "Push Docker images to registries with optimized large layer handling",
15 long_about = None
16)]
17pub struct Args {
18 #[arg(short, long, value_name = "FILE")]
20 pub file: String,
21
22 #[arg(short, long, value_name = "URL")]
24 pub repository_url: String,
25
26 #[arg(short, long)]
28 pub username: Option<String>,
29
30 #[arg(short, long)]
32 pub password: Option<String>,
33
34 #[arg(short = 't', long, default_value = "7200")]
36 pub timeout: u64,
37
38 #[arg(long)]
40 pub skip_tls: bool,
41
42 #[arg(short, long)]
44 pub verbose: bool,
45
46 #[arg(short, long)]
48 pub quiet: bool,
49
50 #[arg(long)]
52 pub dry_run: bool,
53
54 #[arg(long, default_value = "1073741824")]
56 pub large_layer_threshold: u64,
57
58 #[arg(long, default_value = "1")]
60 pub max_concurrent: usize,
61
62 #[arg(long, default_value = "3")]
64 pub retry_attempts: usize,
65
66 #[arg(long, default_value = "true")]
68 pub storage_error_backoff: bool,
69
70 #[arg(long)]
72 pub skip_existing: bool,
73
74 #[arg(long)]
76 pub force_upload: bool,
77}
78
79impl Args {
80 pub fn parse() -> Self {
83 <Self as Parser>::parse()
84 }
85
86 pub fn try_parse() -> Result<Self> {
89 <Self as Parser>::try_parse()
90 .map_err(|e| PusherError::Validation(format!("Failed to parse arguments: {}", e)))
91 }
92 pub fn validate(&self) -> Result<()> {
93 ValidationErrorHandler::validate_file_path(&self.file)?;
95
96 ValidationErrorHandler::validate_repository_url(&self.repository_url)?;
98
99 ValidationErrorHandler::validate_timeout(self.timeout)?;
101
102 ValidationErrorHandler::validate_credentials(&self.username, &self.password)?;
104
105 if self.large_layer_threshold == 0 {
107 return Err(PusherError::Validation(
108 "Large layer threshold must be greater than 0".to_string(),
109 ));
110 }
111
112 if self.max_concurrent == 0 {
114 return Err(PusherError::Validation(
115 "Max concurrent uploads must be at least 1".to_string(),
116 ));
117 }
118
119 if self.max_concurrent > 10 {
120 return Err(PusherError::Validation(
121 "Max concurrent uploads cannot exceed 10".to_string(),
122 ));
123 }
124
125 if self.retry_attempts > 10 {
127 return Err(PusherError::Validation(
128 "Retry attempts cannot exceed 10".to_string(),
129 ));
130 }
131
132 match (&self.username, &self.password) {
134 (Some(_), None) => {
135 return Err(PusherError::Validation(
136 "Password is required when username is provided".to_string(),
137 ));
138 }
139 (None, Some(_)) => {
140 return Err(PusherError::Validation(
141 "Username is required when password is provided".to_string(),
142 ));
143 }
144 _ => {} }
146
147 if self.verbose && self.quiet {
149 return Err(PusherError::Validation(
150 "Cannot specify both --verbose and --quiet flags".to_string(),
151 ));
152 }
153
154 if self.skip_existing && self.force_upload {
155 return Err(PusherError::Validation(
156 "Cannot specify both --skip-existing and --force-upload flags".to_string(),
157 ));
158 }
159
160 Ok(())
161 }
162
163 pub fn get_file_size(&self) -> Result<u64> {
164 let metadata = std::fs::metadata(&self.file)
165 .map_err(|e| PusherError::Io(format!("Failed to read file metadata: {}", e)))?;
166 Ok(metadata.len())
167 }
168
169 pub fn has_credentials(&self) -> bool {
170 self.username.is_some() && self.password.is_some()
171 }
172
173 pub fn get_credentials(&self) -> Option<(String, String)> {
174 match (&self.username, &self.password) {
175 (Some(u), Some(p)) => Some((u.clone(), p.clone())),
176 _ => None,
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_validation_missing_file() {
187 let args = Args {
188 file: "nonexistent.tar".to_string(),
189 repository_url: "https://registry.example.com/test:latest".to_string(),
190 username: None,
191 password: None,
192 timeout: 7200,
193 skip_tls: false,
194 verbose: false,
195 quiet: false,
196 dry_run: false,
197 large_layer_threshold: 1073741824,
198 max_concurrent: 1,
199 retry_attempts: 3,
200 skip_existing: false,
201 force_upload: false,
202 storage_error_backoff: true,
203 };
204
205 assert!(args.validate().is_err());
206 }
207
208 #[test]
209 fn test_validation_invalid_url() {
210 let args = Args {
211 file: "test.tar".to_string(),
212 repository_url: "invalid-url".to_string(),
213 username: None,
214 password: None,
215 timeout: 7200,
216 skip_tls: false,
217 verbose: false,
218 quiet: false,
219 dry_run: false,
220 large_layer_threshold: 1073741824,
221 max_concurrent: 1,
222 retry_attempts: 3,
223 skip_existing: false,
224 force_upload: false,
225 storage_error_backoff: true,
226 };
227
228 assert!(args.validate().is_err());
229 }
230
231 #[test]
232 fn test_validation_credentials_mismatch() {
233 let args = Args {
234 file: "test.tar".to_string(),
235 repository_url: "https://registry.example.com/test:latest".to_string(),
236 username: Some("user".to_string()),
237 password: None, timeout: 7200,
239 skip_tls: false,
240 verbose: false,
241 quiet: false,
242 dry_run: false,
243 large_layer_threshold: 1073741824,
244 max_concurrent: 1,
245 retry_attempts: 3,
246 skip_existing: false,
247 force_upload: false,
248 storage_error_backoff: true,
249 };
250
251 assert!(args.validate().is_err());
252 }
253}