docker_wrapper/command/
save.rs

1//! Docker save command implementation.
2//!
3//! This module provides the `docker save` command for saving Docker images to tar archives.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8use std::path::Path;
9
10/// Docker save command builder
11///
12/// Save one or more images to a tar archive (streamed to STDOUT by default).
13///
14/// # Example
15///
16/// ```no_run
17/// use docker_wrapper::SaveCommand;
18/// use std::path::Path;
19///
20/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21/// // Save image to file
22/// SaveCommand::new("alpine:latest")
23///     .output(Path::new("alpine.tar"))
24///     .run()
25///     .await?;
26///
27/// // Save multiple images
28/// SaveCommand::new_multiple(vec!["alpine:latest", "nginx:latest"])
29///     .output(Path::new("images.tar"))
30///     .run()
31///     .await?;
32/// # Ok(())
33/// # }
34/// ```
35#[derive(Debug, Clone)]
36pub struct SaveCommand {
37    /// Images to save
38    images: Vec<String>,
39    /// Output file path
40    output: Option<String>,
41    /// Command executor
42    pub executor: CommandExecutor,
43}
44
45impl SaveCommand {
46    /// Create a new save command for a single image
47    ///
48    /// # Example
49    ///
50    /// ```
51    /// use docker_wrapper::SaveCommand;
52    ///
53    /// let cmd = SaveCommand::new("alpine:latest");
54    /// ```
55    #[must_use]
56    pub fn new(image: impl Into<String>) -> Self {
57        Self {
58            images: vec![image.into()],
59            output: None,
60            executor: CommandExecutor::new(),
61        }
62    }
63
64    /// Create a new save command for multiple images
65    ///
66    /// # Example
67    ///
68    /// ```
69    /// use docker_wrapper::SaveCommand;
70    ///
71    /// let cmd = SaveCommand::new_multiple(vec!["alpine:latest", "nginx:latest"]);
72    /// ```
73    #[must_use]
74    pub fn new_multiple(images: Vec<impl Into<String>>) -> Self {
75        Self {
76            images: images.into_iter().map(Into::into).collect(),
77            output: None,
78            executor: CommandExecutor::new(),
79        }
80    }
81
82    /// Add another image to save
83    #[must_use]
84    pub fn image(mut self, image: impl Into<String>) -> Self {
85        self.images.push(image.into());
86        self
87    }
88
89    /// Set output file path
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// use docker_wrapper::SaveCommand;
95    /// use std::path::Path;
96    ///
97    /// let cmd = SaveCommand::new("alpine:latest")
98    ///     .output(Path::new("alpine.tar"));
99    /// ```
100    #[must_use]
101    pub fn output(mut self, path: &Path) -> Self {
102        self.output = Some(path.to_string_lossy().into_owned());
103        self
104    }
105
106    /// Execute the save command
107    ///
108    /// # Errors
109    /// Returns an error if:
110    /// - The Docker daemon is not running
111    /// - Any of the specified images don't exist
112    /// - Cannot write to the output file
113    ///
114    /// # Example
115    ///
116    /// ```no_run
117    /// use docker_wrapper::SaveCommand;
118    /// use std::path::Path;
119    ///
120    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
121    /// let result = SaveCommand::new("alpine:latest")
122    ///     .output(Path::new("alpine.tar"))
123    ///     .run()
124    ///     .await?;
125    ///
126    /// if result.success() {
127    ///     println!("Images saved successfully");
128    /// }
129    /// # Ok(())
130    /// # }
131    /// ```
132    pub async fn run(&self) -> Result<SaveResult> {
133        let output = self.execute().await?;
134        Ok(SaveResult {
135            output,
136            images: self.images.clone(),
137            output_file: self.output.clone(),
138        })
139    }
140}
141
142#[async_trait]
143impl DockerCommand for SaveCommand {
144    type Output = CommandOutput;
145
146    fn build_command_args(&self) -> Vec<String> {
147        let mut args = vec!["save".to_string()];
148
149        if let Some(ref output_file) = self.output {
150            args.push("--output".to_string());
151            args.push(output_file.clone());
152        }
153
154        // Add image names
155        args.extend(self.images.clone());
156
157        args.extend(self.executor.raw_args.clone());
158        args
159    }
160
161    fn get_executor(&self) -> &CommandExecutor {
162        &self.executor
163    }
164
165    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
166        &mut self.executor
167    }
168
169    async fn execute(&self) -> Result<Self::Output> {
170        let args = self.build_command_args();
171        let command_name = args[0].clone();
172        let command_args = args[1..].to_vec();
173        self.executor
174            .execute_command(&command_name, command_args)
175            .await
176    }
177}
178
179/// Result from the save command
180#[derive(Debug, Clone)]
181pub struct SaveResult {
182    /// Raw command output
183    pub output: CommandOutput,
184    /// Images that were saved
185    pub images: Vec<String>,
186    /// Output file path if specified
187    pub output_file: Option<String>,
188}
189
190impl SaveResult {
191    /// Check if the save was successful
192    #[must_use]
193    pub fn success(&self) -> bool {
194        self.output.success
195    }
196
197    /// Get the saved images
198    #[must_use]
199    pub fn images(&self) -> &[String] {
200        &self.images
201    }
202
203    /// Get the output file path
204    #[must_use]
205    pub fn output_file(&self) -> Option<&str> {
206        self.output_file.as_deref()
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_save_single_image() {
216        let cmd = SaveCommand::new("alpine:latest");
217        let args = cmd.build_command_args();
218        assert_eq!(args, vec!["save", "alpine:latest"]);
219    }
220
221    #[test]
222    fn test_save_multiple_images() {
223        let cmd = SaveCommand::new_multiple(vec!["alpine:latest", "nginx:latest", "redis:latest"]);
224        let args = cmd.build_command_args();
225        assert_eq!(
226            args,
227            vec!["save", "alpine:latest", "nginx:latest", "redis:latest"]
228        );
229    }
230
231    #[test]
232    fn test_save_with_output() {
233        let cmd = SaveCommand::new("alpine:latest").output(Path::new("alpine.tar"));
234        let args = cmd.build_command_args();
235        assert_eq!(
236            args,
237            vec!["save", "--output", "alpine.tar", "alpine:latest"]
238        );
239    }
240
241    #[test]
242    fn test_save_multiple_with_output() {
243        let cmd = SaveCommand::new_multiple(vec!["alpine", "nginx"])
244            .image("redis")
245            .output(Path::new("/tmp/images.tar"));
246        let args = cmd.build_command_args();
247        assert_eq!(
248            args,
249            vec![
250                "save",
251                "--output",
252                "/tmp/images.tar",
253                "alpine",
254                "nginx",
255                "redis"
256            ]
257        );
258    }
259}