docker_wrapper/command/
cp.rs

1//! Docker cp command implementation.
2//!
3//! This module provides the `docker cp` command for copying files/folders between
4//! a container and the local filesystem.
5
6use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use std::path::Path;
10
11/// Docker cp command builder
12///
13/// Copy files/folders between a container and the local filesystem.
14/// Use `-` as the source to read from stdin or as the destination to write to stdout.
15///
16/// # Example
17///
18/// ```no_run
19/// use docker_wrapper::CpCommand;
20/// use std::path::Path;
21///
22/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
23/// // Copy from container to host
24/// CpCommand::from_container("my-container", "/app/config.yml")
25///     .to_host(Path::new("./config.yml"))
26///     .run()
27///     .await?;
28///
29/// // Copy from host to container
30/// CpCommand::from_host(Path::new("./data.txt"))
31///     .to_container("my-container", "/data/data.txt")
32///     .run()
33///     .await?;
34/// # Ok(())
35/// # }
36/// ```
37#[derive(Debug, Clone)]
38pub struct CpCommand {
39    /// Source path (container:path or local path)
40    source: String,
41    /// Destination path (container:path or local path)
42    destination: String,
43    /// Archive mode (preserve permissions)
44    archive: bool,
45    /// Follow symbolic links
46    follow_link: bool,
47    /// Suppress progress output
48    quiet: bool,
49    /// Command executor
50    pub executor: CommandExecutor,
51}
52
53impl CpCommand {
54    /// Create a cp command copying from container to host
55    ///
56    /// # Example
57    ///
58    /// ```
59    /// use docker_wrapper::CpCommand;
60    /// use std::path::Path;
61    ///
62    /// let cmd = CpCommand::from_container("my-container", "/etc/config")
63    ///     .to_host(Path::new("./config"));
64    /// ```
65    #[must_use]
66    pub fn from_container(container: impl Into<String>, path: impl Into<String>) -> Self {
67        let source = format!("{}:{}", container.into(), path.into());
68        Self {
69            source,
70            destination: String::new(),
71            archive: false,
72            follow_link: false,
73            quiet: false,
74            executor: CommandExecutor::new(),
75        }
76    }
77
78    /// Create a cp command copying from host to container
79    ///
80    /// # Example
81    ///
82    /// ```
83    /// use docker_wrapper::CpCommand;
84    /// use std::path::Path;
85    ///
86    /// let cmd = CpCommand::from_host(Path::new("./file.txt"))
87    ///     .to_container("my-container", "/app/file.txt");
88    /// ```
89    #[must_use]
90    pub fn from_host(path: &Path) -> Self {
91        Self {
92            source: path.to_string_lossy().into_owned(),
93            destination: String::new(),
94            archive: false,
95            follow_link: false,
96            quiet: false,
97            executor: CommandExecutor::new(),
98        }
99    }
100
101    /// Set destination on host filesystem
102    #[must_use]
103    pub fn to_host(mut self, path: &Path) -> Self {
104        self.destination = path.to_string_lossy().into_owned();
105        self
106    }
107
108    /// Set destination in container
109    #[must_use]
110    pub fn to_container(mut self, container: impl Into<String>, path: impl Into<String>) -> Self {
111        self.destination = format!("{}:{}", container.into(), path.into());
112        self
113    }
114
115    /// Archive mode - preserve UIDs/GIDs and permissions
116    ///
117    /// # Example
118    ///
119    /// ```
120    /// use docker_wrapper::CpCommand;
121    /// use std::path::Path;
122    ///
123    /// let cmd = CpCommand::from_container("my-container", "/app")
124    ///     .to_host(Path::new("./app-backup"))
125    ///     .archive();
126    /// ```
127    #[must_use]
128    pub fn archive(mut self) -> Self {
129        self.archive = true;
130        self
131    }
132
133    /// Follow symbolic links in source
134    #[must_use]
135    pub fn follow_link(mut self) -> Self {
136        self.follow_link = true;
137        self
138    }
139
140    /// Suppress progress output during copy
141    #[must_use]
142    pub fn quiet(mut self) -> Self {
143        self.quiet = true;
144        self
145    }
146
147    /// Execute the cp command
148    ///
149    /// # Errors
150    /// Returns an error if:
151    /// - The Docker daemon is not running
152    /// - The container doesn't exist
153    /// - The source path doesn't exist
154    /// - Permission denied for destination
155    pub async fn run(&self) -> Result<CpResult> {
156        let output = self.execute().await?;
157        Ok(CpResult {
158            output,
159            source: self.source.clone(),
160            destination: self.destination.clone(),
161        })
162    }
163}
164
165#[async_trait]
166impl DockerCommand for CpCommand {
167    type Output = CommandOutput;
168
169    fn build_command_args(&self) -> Vec<String> {
170        let mut args = vec!["cp".to_string()];
171
172        if self.archive {
173            args.push("--archive".to_string());
174        }
175
176        if self.follow_link {
177            args.push("--follow-link".to_string());
178        }
179
180        if self.quiet {
181            args.push("--quiet".to_string());
182        }
183
184        // Add source and destination
185        args.push(self.source.clone());
186        args.push(self.destination.clone());
187
188        args.extend(self.executor.raw_args.clone());
189        args
190    }
191
192    fn get_executor(&self) -> &CommandExecutor {
193        &self.executor
194    }
195
196    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
197        &mut self.executor
198    }
199
200    async fn execute(&self) -> Result<Self::Output> {
201        if self.destination.is_empty() {
202            return Err(crate::error::Error::invalid_config(
203                "Destination not specified",
204            ));
205        }
206
207        let args = self.build_command_args();
208        let command_name = args[0].clone();
209        let command_args = args[1..].to_vec();
210        self.executor
211            .execute_command(&command_name, command_args)
212            .await
213    }
214}
215
216/// Result from the cp command
217#[derive(Debug, Clone)]
218pub struct CpResult {
219    /// Raw command output
220    pub output: CommandOutput,
221    /// Source path that was copied
222    pub source: String,
223    /// Destination path
224    pub destination: String,
225}
226
227impl CpResult {
228    /// Check if the copy was successful
229    #[must_use]
230    pub fn success(&self) -> bool {
231        self.output.success
232    }
233
234    /// Get the source path
235    #[must_use]
236    pub fn source(&self) -> &str {
237        &self.source
238    }
239
240    /// Get the destination path
241    #[must_use]
242    pub fn destination(&self) -> &str {
243        &self.destination
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_cp_from_container_to_host() {
253        let cmd = CpCommand::from_container("test-container", "/app/file.txt")
254            .to_host(Path::new("./file.txt"));
255        let args = cmd.build_command_args();
256        assert_eq!(
257            args,
258            vec!["cp", "test-container:/app/file.txt", "./file.txt"]
259        );
260    }
261
262    #[test]
263    fn test_cp_from_host_to_container() {
264        let cmd = CpCommand::from_host(Path::new("./data.txt"))
265            .to_container("test-container", "/data/data.txt");
266        let args = cmd.build_command_args();
267        assert_eq!(
268            args,
269            vec!["cp", "./data.txt", "test-container:/data/data.txt"]
270        );
271    }
272
273    #[test]
274    fn test_cp_with_archive() {
275        let cmd = CpCommand::from_container("test-container", "/app")
276            .to_host(Path::new("./backup"))
277            .archive();
278        let args = cmd.build_command_args();
279        assert_eq!(
280            args,
281            vec!["cp", "--archive", "test-container:/app", "./backup"]
282        );
283    }
284
285    #[test]
286    fn test_cp_with_follow_link() {
287        let cmd = CpCommand::from_container("test-container", "/link")
288            .to_host(Path::new("./file"))
289            .follow_link();
290        let args = cmd.build_command_args();
291        assert_eq!(
292            args,
293            vec!["cp", "--follow-link", "test-container:/link", "./file"]
294        );
295    }
296
297    #[test]
298    fn test_cp_with_all_options() {
299        let cmd = CpCommand::from_host(Path::new("./src"))
300            .to_container("test-container", "/dest")
301            .archive()
302            .follow_link()
303            .quiet();
304        let args = cmd.build_command_args();
305        assert_eq!(
306            args,
307            vec![
308                "cp",
309                "--archive",
310                "--follow-link",
311                "--quiet",
312                "./src",
313                "test-container:/dest"
314            ]
315        );
316    }
317}