docker_wrapper/command/compose/
cp.rs

1//! Docker Compose cp command implementation using unified trait pattern.
2
3use crate::command::{CommandExecutor, ComposeCommand, ComposeConfig, DockerCommand};
4use crate::error::Result;
5use async_trait::async_trait;
6
7/// Docker Compose cp command builder
8#[derive(Debug, Clone)]
9pub struct ComposeCpCommand {
10    /// Base command executor
11    pub executor: CommandExecutor,
12    /// Base compose configuration
13    pub config: ComposeConfig,
14    /// Source path (can be service:path or local path)
15    pub source: String,
16    /// Destination path (can be service:path or local path)  
17    pub destination: String,
18    /// Archive mode (preserve permissions)
19    pub archive: bool,
20    /// Follow symbolic links
21    pub follow_link: bool,
22    /// Index of the container (if service has multiple instances)
23    pub index: Option<u32>,
24}
25
26/// Result from compose cp command
27#[derive(Debug, Clone)]
28pub struct ComposeCpResult {
29    /// Raw stdout output
30    pub stdout: String,
31    /// Raw stderr output
32    pub stderr: String,
33    /// Success status
34    pub success: bool,
35    /// Source path used
36    pub source: String,
37    /// Destination path used
38    pub destination: String,
39}
40
41impl ComposeCpCommand {
42    /// Create a new compose cp command
43    #[must_use]
44    pub fn new(source: impl Into<String>, destination: impl Into<String>) -> Self {
45        Self {
46            executor: CommandExecutor::new(),
47            config: ComposeConfig::new(),
48            source: source.into(),
49            destination: destination.into(),
50            archive: false,
51            follow_link: false,
52            index: None,
53        }
54    }
55
56    /// Enable archive mode (preserve permissions and ownership)
57    #[must_use]
58    pub fn archive(mut self) -> Self {
59        self.archive = true;
60        self
61    }
62
63    /// Follow symbolic links in source path
64    #[must_use]
65    pub fn follow_link(mut self) -> Self {
66        self.follow_link = true;
67        self
68    }
69
70    /// Set container index if service has multiple instances
71    #[must_use]
72    pub fn index(mut self, index: u32) -> Self {
73        self.index = Some(index);
74        self
75    }
76}
77
78#[async_trait]
79impl DockerCommand for ComposeCpCommand {
80    type Output = ComposeCpResult;
81
82    fn get_executor(&self) -> &CommandExecutor {
83        &self.executor
84    }
85
86    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
87        &mut self.executor
88    }
89
90    fn build_command_args(&self) -> Vec<String> {
91        <Self as ComposeCommand>::build_command_args(self)
92    }
93
94    async fn execute(&self) -> Result<Self::Output> {
95        let args = <Self as ComposeCommand>::build_command_args(self);
96        let output = self.execute_command(args).await?;
97
98        Ok(ComposeCpResult {
99            stdout: output.stdout,
100            stderr: output.stderr,
101            success: output.success,
102            source: self.source.clone(),
103            destination: self.destination.clone(),
104        })
105    }
106}
107
108impl ComposeCommand for ComposeCpCommand {
109    fn get_config(&self) -> &ComposeConfig {
110        &self.config
111    }
112
113    fn get_config_mut(&mut self) -> &mut ComposeConfig {
114        &mut self.config
115    }
116
117    fn subcommand(&self) -> &'static str {
118        "cp"
119    }
120
121    fn build_subcommand_args(&self) -> Vec<String> {
122        let mut args = Vec::new();
123
124        if self.archive {
125            args.push("--archive".to_string());
126        }
127
128        if self.follow_link {
129            args.push("--follow-link".to_string());
130        }
131
132        if let Some(index) = self.index {
133            args.push("--index".to_string());
134            args.push(index.to_string());
135        }
136
137        args.push(self.source.clone());
138        args.push(self.destination.clone());
139
140        args
141    }
142}
143
144impl ComposeCpResult {
145    /// Check if the command was successful
146    #[must_use]
147    pub fn success(&self) -> bool {
148        self.success
149    }
150
151    /// Get the source path used
152    #[must_use]
153    pub fn source(&self) -> &str {
154        &self.source
155    }
156
157    /// Get the destination path used
158    #[must_use]
159    pub fn destination(&self) -> &str {
160        &self.destination
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_compose_cp_basic() {
170        let cmd = ComposeCpCommand::new("web:/app/config.json", "./config.json");
171        let args = cmd.build_subcommand_args();
172        assert!(args.contains(&"web:/app/config.json".to_string()));
173        assert!(args.contains(&"./config.json".to_string()));
174
175        let full_args = ComposeCommand::build_command_args(&cmd);
176        assert_eq!(full_args[0], "compose");
177        assert!(full_args.contains(&"cp".to_string()));
178    }
179
180    #[test]
181    fn test_compose_cp_with_options() {
182        let cmd = ComposeCpCommand::new("./data", "db:/var/lib/data")
183            .archive()
184            .follow_link()
185            .index(2);
186
187        let args = cmd.build_subcommand_args();
188        assert!(args.contains(&"--archive".to_string()));
189        assert!(args.contains(&"--follow-link".to_string()));
190        assert!(args.contains(&"--index".to_string()));
191        assert!(args.contains(&"2".to_string()));
192    }
193}