1use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9#[derive(Debug, Clone)]
32pub struct ImportCommand {
33 source: String,
35 repository: Option<String>,
37 message: Option<String>,
39 changes: Vec<String>,
41 pub executor: CommandExecutor,
43}
44
45impl ImportCommand {
46 #[must_use]
63 pub fn new(source: impl Into<String>) -> Self {
64 Self {
65 source: source.into(),
66 repository: None,
67 message: None,
68 changes: Vec::new(),
69 executor: CommandExecutor::new(),
70 }
71 }
72
73 #[must_use]
84 pub fn repository(mut self, repository: impl Into<String>) -> Self {
85 self.repository = Some(repository.into());
86 self
87 }
88
89 #[must_use]
100 pub fn message(mut self, message: impl Into<String>) -> Self {
101 self.message = Some(message.into());
102 self
103 }
104
105 #[must_use]
117 pub fn change(mut self, change: impl Into<String>) -> Self {
118 self.changes.push(change.into());
119 self
120 }
121
122 #[must_use]
137 pub fn changes<I, S>(mut self, changes: I) -> Self
138 where
139 I: IntoIterator<Item = S>,
140 S: Into<String>,
141 {
142 self.changes.extend(changes.into_iter().map(Into::into));
143 self
144 }
145
146 pub async fn run(&self) -> Result<ImportResult> {
178 let output = self.execute().await?;
179
180 let image_id = Self::parse_image_id(&output.stdout);
182
183 Ok(ImportResult {
184 output,
185 source: self.source.clone(),
186 repository: self.repository.clone(),
187 image_id,
188 })
189 }
190
191 fn parse_image_id(stdout: &str) -> Option<String> {
193 let trimmed = stdout.trim();
194 if trimmed.is_empty() {
195 return None;
196 }
197
198 Some(trimmed.to_string())
200 }
201}
202
203#[async_trait]
204impl DockerCommand for ImportCommand {
205 type Output = CommandOutput;
206
207 fn build_command_args(&self) -> Vec<String> {
208 let mut args = vec!["import".to_string()];
209
210 if let Some(ref message) = self.message {
212 args.push("--message".to_string());
213 args.push(message.clone());
214 }
215
216 for change in &self.changes {
218 args.push("--change".to_string());
219 args.push(change.clone());
220 }
221
222 args.push(self.source.clone());
224
225 if let Some(ref repository) = self.repository {
227 args.push(repository.clone());
228 }
229
230 args.extend(self.executor.raw_args.clone());
231 args
232 }
233
234 async fn execute(&self) -> Result<Self::Output> {
235 let args = self.build_command_args();
236 let command_name = args[0].clone();
237 let command_args = args[1..].to_vec();
238 self.executor
239 .execute_command(&command_name, command_args)
240 .await
241 }
242
243 fn get_executor(&self) -> &CommandExecutor {
244 &self.executor
245 }
246
247 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
248 &mut self.executor
249 }
250}
251
252#[derive(Debug, Clone)]
254pub struct ImportResult {
255 pub output: CommandOutput,
257 pub source: String,
259 pub repository: Option<String>,
261 pub image_id: Option<String>,
263}
264
265impl ImportResult {
266 #[must_use]
268 pub fn success(&self) -> bool {
269 self.output.success
270 }
271
272 #[must_use]
274 pub fn source(&self) -> &str {
275 &self.source
276 }
277
278 #[must_use]
280 pub fn repository(&self) -> Option<&str> {
281 self.repository.as_deref()
282 }
283
284 #[must_use]
286 pub fn image_id(&self) -> Option<&str> {
287 self.image_id.as_deref()
288 }
289
290 #[must_use]
292 pub fn output(&self) -> &CommandOutput {
293 &self.output
294 }
295
296 #[must_use]
298 pub fn has_repository(&self) -> bool {
299 self.repository.is_some()
300 }
301
302 #[must_use]
304 pub fn imported_from_stdin(&self) -> bool {
305 self.source == "-"
306 }
307
308 #[must_use]
310 pub fn imported_from_url(&self) -> bool {
311 self.source.starts_with("http://") || self.source.starts_with("https://")
312 }
313
314 #[must_use]
316 pub fn imported_from_file(&self) -> bool {
317 !self.imported_from_stdin() && !self.imported_from_url()
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn test_import_basic() {
327 let cmd = ImportCommand::new("backup.tar");
328 let args = cmd.build_command_args();
329 assert_eq!(args, vec!["import", "backup.tar"]);
330 }
331
332 #[test]
333 fn test_import_with_repository() {
334 let cmd = ImportCommand::new("backup.tar").repository("my-app:v1.0");
335 let args = cmd.build_command_args();
336 assert_eq!(args, vec!["import", "backup.tar", "my-app:v1.0"]);
337 }
338
339 #[test]
340 fn test_import_all_options() {
341 let cmd = ImportCommand::new("backup.tar")
342 .repository("my-app:v1.0")
343 .message("Imported from backup")
344 .change("ENV NODE_ENV=production")
345 .change("EXPOSE 3000");
346 let args = cmd.build_command_args();
347 assert_eq!(
348 args,
349 vec![
350 "import",
351 "--message",
352 "Imported from backup",
353 "--change",
354 "ENV NODE_ENV=production",
355 "--change",
356 "EXPOSE 3000",
357 "backup.tar",
358 "my-app:v1.0"
359 ]
360 );
361 }
362
363 #[test]
364 fn test_import_with_changes() {
365 let cmd = ImportCommand::new("app.tar")
366 .changes(vec!["ENV PATH=/usr/local/bin:$PATH", "WORKDIR /app"]);
367 let args = cmd.build_command_args();
368 assert_eq!(
369 args,
370 vec![
371 "import",
372 "--change",
373 "ENV PATH=/usr/local/bin:$PATH",
374 "--change",
375 "WORKDIR /app",
376 "app.tar"
377 ]
378 );
379 }
380
381 #[test]
382 fn test_import_from_stdin() {
383 let cmd = ImportCommand::new("-").repository("stdin-image");
384 let args = cmd.build_command_args();
385 assert_eq!(args, vec!["import", "-", "stdin-image"]);
386 }
387
388 #[test]
389 fn test_import_from_url() {
390 let cmd = ImportCommand::new("http://example.com/image.tar.gz").repository("remote-image");
391 let args = cmd.build_command_args();
392 assert_eq!(
393 args,
394 vec!["import", "http://example.com/image.tar.gz", "remote-image"]
395 );
396 }
397
398 #[test]
399 fn test_parse_image_id() {
400 assert_eq!(
401 ImportCommand::parse_image_id("sha256:abcd1234"),
402 Some("sha256:abcd1234".to_string())
403 );
404 assert_eq!(ImportCommand::parse_image_id(""), None);
405 assert_eq!(ImportCommand::parse_image_id(" \n "), None);
406 }
407
408 #[test]
409 fn test_import_result() {
410 let result = ImportResult {
411 output: CommandOutput {
412 stdout: "sha256:abcd1234".to_string(),
413 stderr: String::new(),
414 exit_code: 0,
415 success: true,
416 },
417 source: "backup.tar".to_string(),
418 repository: Some("my-app:v1.0".to_string()),
419 image_id: Some("sha256:abcd1234".to_string()),
420 };
421
422 assert!(result.success());
423 assert_eq!(result.source(), "backup.tar");
424 assert_eq!(result.repository(), Some("my-app:v1.0"));
425 assert_eq!(result.image_id(), Some("sha256:abcd1234"));
426 assert!(result.has_repository());
427 assert!(!result.imported_from_stdin());
428 assert!(!result.imported_from_url());
429 assert!(result.imported_from_file());
430 }
431
432 #[test]
433 fn test_import_result_source_types() {
434 let stdin_result = ImportResult {
435 output: CommandOutput {
436 stdout: String::new(),
437 stderr: String::new(),
438 exit_code: 0,
439 success: true,
440 },
441 source: "-".to_string(),
442 repository: None,
443 image_id: None,
444 };
445 assert!(stdin_result.imported_from_stdin());
446 assert!(!stdin_result.imported_from_url());
447 assert!(!stdin_result.imported_from_file());
448
449 let url_result = ImportResult {
450 output: CommandOutput {
451 stdout: String::new(),
452 stderr: String::new(),
453 exit_code: 0,
454 success: true,
455 },
456 source: "https://example.com/image.tar".to_string(),
457 repository: None,
458 image_id: None,
459 };
460 assert!(!url_result.imported_from_stdin());
461 assert!(url_result.imported_from_url());
462 assert!(!url_result.imported_from_file());
463 }
464}