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