docker_wrapper/command/
load.rs

1//! Docker load command implementation.
2//!
3//! This module provides the `docker load` command for loading Docker images from tar archives.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8use std::path::Path;
9
10/// Docker load command builder
11///
12/// Load an image from a tar archive or STDIN.
13///
14/// # Example
15///
16/// ```no_run
17/// use docker_wrapper::LoadCommand;
18/// use std::path::Path;
19///
20/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21/// // Load image from file
22/// let result = LoadCommand::new()
23///     .input(Path::new("alpine.tar"))
24///     .run()
25///     .await?;
26///
27/// println!("Loaded images: {:?}", result.loaded_images());
28/// # Ok(())
29/// # }
30/// ```
31#[derive(Debug, Clone)]
32pub struct LoadCommand {
33    /// Input file path
34    input: Option<String>,
35    /// Suppress progress output during load
36    quiet: bool,
37    /// Command executor
38    pub executor: CommandExecutor,
39}
40
41impl LoadCommand {
42    /// Create a new load command
43    ///
44    /// # Example
45    ///
46    /// ```
47    /// use docker_wrapper::LoadCommand;
48    ///
49    /// let cmd = LoadCommand::new();
50    /// ```
51    #[must_use]
52    pub fn new() -> Self {
53        Self {
54            input: None,
55            quiet: false,
56            executor: CommandExecutor::new(),
57        }
58    }
59
60    /// Set input file path
61    ///
62    /// # Example
63    ///
64    /// ```
65    /// use docker_wrapper::LoadCommand;
66    /// use std::path::Path;
67    ///
68    /// let cmd = LoadCommand::new()
69    ///     .input(Path::new("images.tar"));
70    /// ```
71    #[must_use]
72    pub fn input(mut self, path: &Path) -> Self {
73        self.input = Some(path.to_string_lossy().into_owned());
74        self
75    }
76
77    /// Suppress progress output during load
78    ///
79    /// # Example
80    ///
81    /// ```
82    /// use docker_wrapper::LoadCommand;
83    ///
84    /// let cmd = LoadCommand::new().quiet();
85    /// ```
86    #[must_use]
87    pub fn quiet(mut self) -> Self {
88        self.quiet = true;
89        self
90    }
91
92    /// Execute the load command
93    ///
94    /// # Errors
95    /// Returns an error if:
96    /// - The Docker daemon is not running
97    /// - The input file doesn't exist or is not readable
98    /// - The tar archive is corrupted or invalid
99    ///
100    /// # Example
101    ///
102    /// ```no_run
103    /// use docker_wrapper::LoadCommand;
104    /// use std::path::Path;
105    ///
106    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
107    /// let result = LoadCommand::new()
108    ///     .input(Path::new("alpine.tar"))
109    ///     .run()
110    ///     .await?;
111    ///
112    /// if result.success() {
113    ///     println!("Images loaded successfully");
114    ///     for image in result.loaded_images() {
115    ///         println!("  - {}", image);
116    ///     }
117    /// }
118    /// # Ok(())
119    /// # }
120    /// ```
121    pub async fn run(&self) -> Result<LoadResult> {
122        let output = self.execute().await?;
123
124        // Parse loaded images from output
125        let loaded_images = Self::parse_loaded_images(&output.stdout);
126
127        Ok(LoadResult {
128            output,
129            loaded_images,
130        })
131    }
132
133    /// Parse loaded image names from the command output
134    fn parse_loaded_images(stdout: &str) -> Vec<String> {
135        let mut images = Vec::new();
136        for line in stdout.lines() {
137            if line.starts_with("Loaded image:") {
138                if let Some(image) = line.strip_prefix("Loaded image:") {
139                    images.push(image.trim().to_string());
140                }
141            } else if line.starts_with("Loaded image ID:") {
142                if let Some(id) = line.strip_prefix("Loaded image ID:") {
143                    images.push(id.trim().to_string());
144                }
145            }
146        }
147        images
148    }
149}
150
151impl Default for LoadCommand {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157#[async_trait]
158impl DockerCommand for LoadCommand {
159    type Output = CommandOutput;
160
161    fn build_command_args(&self) -> Vec<String> {
162        let mut args = vec!["load".to_string()];
163
164        if let Some(ref input_file) = self.input {
165            args.push("--input".to_string());
166            args.push(input_file.clone());
167        }
168
169        if self.quiet {
170            args.push("--quiet".to_string());
171        }
172
173        args.extend(self.executor.raw_args.clone());
174        args
175    }
176
177    fn get_executor(&self) -> &CommandExecutor {
178        &self.executor
179    }
180
181    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
182        &mut self.executor
183    }
184
185    async fn execute(&self) -> Result<Self::Output> {
186        let args = self.build_command_args();
187        let command_name = args[0].clone();
188        let command_args = args[1..].to_vec();
189        self.executor
190            .execute_command(&command_name, command_args)
191            .await
192    }
193}
194
195/// Result from the load command
196#[derive(Debug, Clone)]
197pub struct LoadResult {
198    /// Raw command output
199    pub output: CommandOutput,
200    /// List of loaded image names/IDs
201    pub loaded_images: Vec<String>,
202}
203
204impl LoadResult {
205    /// Check if the load was successful
206    #[must_use]
207    pub fn success(&self) -> bool {
208        self.output.success
209    }
210
211    /// Get the list of loaded images
212    #[must_use]
213    pub fn loaded_images(&self) -> &[String] {
214        &self.loaded_images
215    }
216
217    /// Get the count of loaded images
218    #[must_use]
219    pub fn image_count(&self) -> usize {
220        self.loaded_images.len()
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_load_basic() {
230        let cmd = LoadCommand::new();
231        let args = cmd.build_command_args();
232        assert_eq!(args, vec!["load"]);
233    }
234
235    #[test]
236    fn test_load_with_input() {
237        let cmd = LoadCommand::new().input(Path::new("images.tar"));
238        let args = cmd.build_command_args();
239        assert_eq!(args, vec!["load", "--input", "images.tar"]);
240    }
241
242    #[test]
243    fn test_load_with_quiet() {
244        let cmd = LoadCommand::new().quiet();
245        let args = cmd.build_command_args();
246        assert_eq!(args, vec!["load", "--quiet"]);
247    }
248
249    #[test]
250    fn test_load_with_all_options() {
251        let cmd = LoadCommand::new()
252            .input(Path::new("/tmp/alpine.tar"))
253            .quiet();
254        let args = cmd.build_command_args();
255        assert_eq!(args, vec!["load", "--input", "/tmp/alpine.tar", "--quiet"]);
256    }
257
258    #[test]
259    fn test_parse_loaded_images() {
260        let output =
261            "Loaded image: alpine:latest\nLoaded image: nginx:1.21\nLoaded image ID: sha256:abc123";
262        let images = LoadCommand::parse_loaded_images(output);
263        assert_eq!(images, vec!["alpine:latest", "nginx:1.21", "sha256:abc123"]);
264    }
265
266    #[test]
267    fn test_parse_loaded_images_empty() {
268        let output = "";
269        let images = LoadCommand::parse_loaded_images(output);
270        assert!(images.is_empty());
271    }
272}