1use std::path::Path;
13use std::time::Duration;
14
15use serde::{Deserialize, Serialize};
16
17use crate::error::Result;
18use crate::runner::{RunOptions, run_expect_success, which};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CopyOptions {
23 #[serde(default)]
25 pub delete: bool,
26 #[serde(default = "default_true")]
28 pub preserve: bool,
29 #[serde(default = "default_true")]
31 pub progress: bool,
32 #[serde(default)]
34 pub dry_run: bool,
35}
36
37impl Default for CopyOptions {
38 fn default() -> Self {
39 Self {
40 delete: false,
41 preserve: true,
42 progress: true,
43 dry_run: false,
44 }
45 }
46}
47
48fn default_true() -> bool {
49 true
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct CopyResult {
55 pub bytes_transferred: Option<u64>,
56 pub files_transferred: Option<u64>,
57 pub duration_ms: u64,
58 pub tool_used: String,
59 pub dry_run: bool,
60}
61
62pub fn copy(src: &Path, dst: &Path, opts: &CopyOptions) -> Result<CopyResult> {
64 let started = std::time::Instant::now();
65 let result = if which("rsync").is_ok() {
66 copy_with_rsync(src, dst, opts)?
67 } else {
68 copy_with_cp(src, dst, opts)?
69 };
70 Ok(CopyResult {
71 bytes_transferred: result.bytes_transferred,
72 files_transferred: result.files_transferred,
73 duration_ms: started.elapsed().as_millis() as u64,
74 tool_used: result.tool_used,
75 dry_run: opts.dry_run,
76 })
77}
78
79struct CopyOutcome {
80 bytes_transferred: Option<u64>,
81 files_transferred: Option<u64>,
82 tool_used: String,
83}
84
85fn copy_with_rsync(src: &Path, dst: &Path, opts: &CopyOptions) -> Result<CopyOutcome> {
86 let mut args: Vec<String> = Vec::new();
87 if opts.delete {
88 args.push("--delete".into());
89 }
90 if opts.preserve {
91 args.push("-a".into());
92 } else {
93 args.push("-rt".into());
94 }
95 if opts.progress {
96 args.push("--progress".into());
99 }
100 if opts.dry_run {
101 args.push("--dry-run".into());
102 }
103 args.push(src.to_string_lossy().to_string());
104 args.push(dst.to_string_lossy().to_string());
105
106 let refs: Vec<&str> = args.iter().map(String::as_str).collect();
107 let capture = !(opts.progress && !opts.dry_run);
112 run_expect_success(
113 "rsync",
114 &refs,
115 &RunOptions {
116 timeout: Some(Duration::from_secs(3600)),
117 capture,
118 ..Default::default()
119 },
120 )?;
121 Ok(CopyOutcome {
122 bytes_transferred: None,
123 files_transferred: None,
124 tool_used: "rsync".into(),
125 })
126}
127
128fn copy_with_cp(src: &Path, dst: &Path, opts: &CopyOptions) -> Result<CopyOutcome> {
129 let mut args: Vec<String> = Vec::new();
130 args.push("-a".into());
131 if opts.dry_run {
132 args.push("-n".into());
133 }
134 args.push(src.to_string_lossy().to_string());
135 args.push(dst.to_string_lossy().to_string());
136
137 let refs: Vec<&str> = args.iter().map(String::as_str).collect();
138 run_expect_success(
139 "cp",
140 &refs,
141 &RunOptions {
142 timeout: Some(Duration::from_secs(3600)),
143 ..Default::default()
144 },
145 )?;
146 let _ = opts;
147 Ok(CopyOutcome {
148 bytes_transferred: None,
149 files_transferred: None,
150 tool_used: "cp".into(),
151 })
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn copy_options_defaults() {
160 let opts = CopyOptions::default();
161 assert!(!opts.delete);
162 assert!(opts.preserve);
163 assert!(opts.progress);
164 assert!(!opts.dry_run);
165 }
166
167 #[test]
168 fn dry_run_does_not_panic() {
169 let tmp = tempfile::tempdir().unwrap();
170 let src = tmp.path().join("src");
171 let dst = tmp.path().join("dst");
172 std::fs::create_dir_all(&src).unwrap();
173 std::fs::write(src.join("a.txt"), "hello").unwrap();
174
175 let opts = CopyOptions {
176 dry_run: true,
177 ..Default::default()
178 };
179 let _r = copy(&src, &dst, &opts);
180 }
181}