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