docker_wrapper/compose/
cp.rs

1//! Docker Compose cp command implementation.
2
3use crate::compose::{ComposeCommandV2 as ComposeCommand, ComposeConfig};
4use crate::error::Result;
5use async_trait::async_trait;
6use std::path::PathBuf;
7
8/// Docker Compose cp command
9///
10/// Copy files/folders between a service container and the local filesystem.
11#[derive(Debug, Clone)]
12pub struct ComposeCpCommand {
13    /// Base configuration
14    pub config: ComposeConfig,
15    /// Source path (can be container:path or local path)
16    pub source: String,
17    /// Destination path (can be container:path or local path)
18    pub destination: String,
19    /// Archive mode (preserve permissions)
20    pub archive: bool,
21    /// Follow symbolic links
22    pub follow_link: bool,
23    /// Index of the container (if service has multiple instances)
24    pub index: Option<u32>,
25}
26
27/// Result from cp command
28#[derive(Debug, Clone)]
29pub struct CpResult {
30    /// Output from the command
31    pub output: String,
32    /// Whether the operation succeeded
33    pub success: bool,
34}
35
36impl ComposeCpCommand {
37    /// Create a new cp command
38    #[must_use]
39    pub fn new(source: impl Into<String>, destination: impl Into<String>) -> Self {
40        Self {
41            config: ComposeConfig::default(),
42            source: source.into(),
43            destination: destination.into(),
44            archive: false,
45            follow_link: false,
46            index: None,
47        }
48    }
49
50    /// Copy from container to local
51    #[must_use]
52    pub fn from_container(
53        service: impl Into<String>,
54        container_path: impl Into<String>,
55        local_path: impl Into<PathBuf>,
56    ) -> Self {
57        let source = format!("{}:{}", service.into(), container_path.into());
58        let destination = local_path.into().display().to_string();
59        Self::new(source, destination)
60    }
61
62    /// Copy from local to container
63    #[must_use]
64    pub fn to_container(
65        local_path: impl Into<PathBuf>,
66        service: impl Into<String>,
67        container_path: impl Into<String>,
68    ) -> Self {
69        let source = local_path.into().display().to_string();
70        let destination = format!("{}:{}", service.into(), container_path.into());
71        Self::new(source, destination)
72    }
73
74    /// Add a compose file
75    #[must_use]
76    pub fn file<P: Into<std::path::PathBuf>>(mut self, file: P) -> Self {
77        self.config.files.push(file.into());
78        self
79    }
80
81    /// Set project name
82    #[must_use]
83    pub fn project_name(mut self, name: impl Into<String>) -> Self {
84        self.config.project_name = Some(name.into());
85        self
86    }
87
88    /// Enable archive mode
89    #[must_use]
90    pub fn archive(mut self) -> Self {
91        self.archive = true;
92        self
93    }
94
95    /// Follow symbolic links
96    #[must_use]
97    pub fn follow_link(mut self) -> Self {
98        self.follow_link = true;
99        self
100    }
101
102    /// Set container index
103    #[must_use]
104    pub fn index(mut self, index: u32) -> Self {
105        self.index = Some(index);
106        self
107    }
108
109    fn build_args(&self) -> Vec<String> {
110        let mut args = vec!["cp".to_string()];
111
112        // Add flags
113        if self.archive {
114            args.push("--archive".to_string());
115        }
116        if self.follow_link {
117            args.push("--follow-link".to_string());
118        }
119
120        // Add index if specified
121        if let Some(index) = self.index {
122            args.push("--index".to_string());
123            args.push(index.to_string());
124        }
125
126        // Add source and destination
127        args.push(self.source.clone());
128        args.push(self.destination.clone());
129
130        args
131    }
132}
133
134#[async_trait]
135impl ComposeCommand for ComposeCpCommand {
136    type Output = CpResult;
137
138    fn get_config(&self) -> &ComposeConfig {
139        &self.config
140    }
141
142    fn get_config_mut(&mut self) -> &mut ComposeConfig {
143        &mut self.config
144    }
145
146    async fn execute_compose(&self, args: Vec<String>) -> Result<Self::Output> {
147        let output = self.execute_compose_command(args).await?;
148
149        Ok(CpResult {
150            output: output.stdout,
151            success: output.success,
152        })
153    }
154
155    async fn execute(&self) -> Result<Self::Output> {
156        let args = self.build_args();
157        self.execute_compose(args).await
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_cp_command_basic() {
167        let cmd = ComposeCpCommand::new("web:/app/config.yml", "./config.yml");
168        let args = cmd.build_args();
169        assert_eq!(args[0], "cp");
170        assert!(args.contains(&"web:/app/config.yml".to_string()));
171        assert!(args.contains(&"./config.yml".to_string()));
172    }
173
174    #[test]
175    fn test_cp_from_container() {
176        let cmd = ComposeCpCommand::from_container("web", "/app/logs", "./logs");
177        let args = cmd.build_args();
178        assert!(args.contains(&"web:/app/logs".to_string()));
179        assert!(args.contains(&"./logs".to_string()));
180    }
181
182    #[test]
183    fn test_cp_to_container() {
184        let cmd = ComposeCpCommand::to_container("./config.yml", "web", "/app/config.yml");
185        let args = cmd.build_args();
186        assert!(args.contains(&"./config.yml".to_string()));
187        assert!(args.contains(&"web:/app/config.yml".to_string()));
188    }
189
190    #[test]
191    fn test_cp_command_with_flags() {
192        let cmd = ComposeCpCommand::new("web:/data", "./data")
193            .archive()
194            .follow_link()
195            .index(1);
196        let args = cmd.build_args();
197        assert!(args.contains(&"--archive".to_string()));
198        assert!(args.contains(&"--follow-link".to_string()));
199        assert!(args.contains(&"--index".to_string()));
200        assert!(args.contains(&"1".to_string()));
201    }
202}