1use std::path::{Path, PathBuf};
2
3use tokio::io::{AsyncReadExt, AsyncWriteExt};
4use tokio::sync::mpsc::UnboundedSender;
5use tokio_stream::wrappers::UnboundedReceiverStream;
6use tokio_util::sync::CancellationToken;
7
8use crate::error::{from_io, FileEngineError, Result};
9use crate::handle::Handle;
10use crate::progress::Progress;
11
12pub struct CopyBuilder {
13 src: PathBuf,
14 dst: PathBuf,
15 overwrite: bool,
16 buffer_size: usize,
17 follow_symlinks: bool,
18 #[cfg(feature = "permissions")]
19 pub(crate) preserve_permissions: bool,
20 cancel_token: Option<CancellationToken>,
21}
22
23impl CopyBuilder {
24 pub(crate) fn new(
25 src: PathBuf,
26 dst: PathBuf,
27 buffer_size: usize,
28 follow_symlinks: bool,
29 ) -> Self {
30 Self {
31 src,
32 dst,
33 overwrite: false,
34 buffer_size,
35 follow_symlinks,
36 #[cfg(feature = "permissions")]
37 preserve_permissions: false,
38 cancel_token: None,
39 }
40 }
41
42 pub fn overwrite(mut self, enabled: bool) -> Self {
43 self.overwrite = enabled;
44 self
45 }
46
47 pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
48 self.cancel_token = Some(token);
49 self
50 }
51
52 pub fn start(self) -> Result<Handle<()>> {
53 let cancel_token = self.cancel_token.unwrap_or_default();
54 let (progress_tx, progress_rx) = tokio::sync::mpsc::unbounded_channel();
55
56 let task_cancel_token = cancel_token.clone();
57 let buffer_size = self.buffer_size;
58 let follow_symlinks = self.follow_symlinks;
59 let overwrite = self.overwrite;
60 let src = self.src;
61 let dst = self.dst;
62 #[cfg(feature = "permissions")]
63 let preserve_permissions = self.preserve_permissions;
64
65 let join = tokio::spawn(async move {
66 if !overwrite && tokio::fs::try_exists(&dst).await.unwrap_or(false) {
67 return Err(FileEngineError::DestinationExists(dst));
68 }
69
70 let src_metadata = tokio::fs::symlink_metadata(&src)
71 .await
72 .map_err(|e| from_io(src.clone(), e))?;
73
74 if src_metadata.is_dir() {
75 copy_dir(
76 &src,
77 &dst,
78 buffer_size,
79 follow_symlinks,
80 &progress_tx,
81 &task_cancel_token,
82 )
83 .await?;
84 } else if src_metadata.is_symlink() && !follow_symlinks {
85 copy_symlink(&src, &dst).await?;
86 } else {
87 copy_file(
88 &src,
89 &dst,
90 buffer_size,
91 0,
92 1,
93 &progress_tx,
94 &task_cancel_token,
95 )
96 .await?;
97 }
98
99 #[cfg(feature = "permissions")]
100 if preserve_permissions {
101 preserve_permissions_recursive(&src, &dst).await?;
102 }
103
104 Ok(())
105 });
106
107 Ok(Handle {
108 join,
109 progress_rx: UnboundedReceiverStream::new(progress_rx),
110 cancel_token,
111 })
112 }
113}
114
115pub(crate) async fn copy_file(
132 src: &Path,
133 dst: &Path,
134 buffer_size: usize,
135 files_done: u64,
136 files_total: u64,
137 progress_tx: &UnboundedSender<Progress>,
138 cancel_token: &CancellationToken,
139) -> Result<()> {
140 let src_metadata = tokio::fs::metadata(src)
141 .await
142 .map_err(|e| from_io(src.to_path_buf(), e))?;
143 let bytes_total = src_metadata.len();
144
145 if let Some(parent) = dst.parent() {
146 tokio::fs::create_dir_all(parent)
147 .await
148 .map_err(|e| from_io(parent.to_path_buf(), e))?;
149 }
150
151 let mut reader = tokio::fs::File::open(src)
152 .await
153 .map_err(|e| from_io(src.to_path_buf(), e))?;
154 let mut writer = tokio::fs::File::create(dst)
155 .await
156 .map_err(|e| from_io(dst.to_path_buf(), e))?;
157
158 let mut buf = vec![0u8; buffer_size.max(1)];
159 let mut bytes_done = 0u64;
160
161 loop {
162 if cancel_token.is_cancelled() {
163 return Err(FileEngineError::Cancelled);
164 }
165
166 let n = reader
167 .read(&mut buf)
168 .await
169 .map_err(|e| from_io(src.to_path_buf(), e))?;
170 if n == 0 {
171 break;
172 }
173
174 writer
175 .write_all(&buf[..n])
176 .await
177 .map_err(|e| from_io(dst.to_path_buf(), e))?;
178
179 bytes_done += n as u64;
180 let _ = progress_tx.send(Progress {
181 bytes_done,
182 bytes_total,
183 files_done,
184 files_total,
185 current_file: Some(src.to_path_buf()),
186 });
187 }
188
189 writer
190 .flush()
191 .await
192 .map_err(|e| from_io(dst.to_path_buf(), e))?;
193 drop(writer);
194
195 if let Ok(modified) = src_metadata.modified() {
196 let dst_owned = dst.to_path_buf();
197 let _ = tokio::task::spawn_blocking(move || {
198 std::fs::OpenOptions::new()
199 .write(true)
200 .open(&dst_owned)
201 .and_then(|f| f.set_modified(modified))
202 })
203 .await;
204 }
205
206 Ok(())
207}
208
209pub(crate) async fn copy_symlink(src: &Path, dst: &Path) -> Result<()> {
213 let target = tokio::fs::read_link(src)
214 .await
215 .map_err(|e| from_io(src.to_path_buf(), e))?;
216
217 #[cfg(unix)]
218 {
219 tokio::fs::symlink(&target, dst)
220 .await
221 .map_err(|e| from_io(dst.to_path_buf(), e))?;
222 }
223 #[cfg(windows)]
224 {
225 let target_abs = src.parent().unwrap_or_else(|| Path::new(".")).join(&target);
226 let target_is_dir = tokio::fs::metadata(&target_abs)
227 .await
228 .map(|m| m.is_dir())
229 .unwrap_or(false);
230 let result = if target_is_dir {
231 tokio::fs::symlink_dir(&target, dst).await
232 } else {
233 tokio::fs::symlink_file(&target, dst).await
234 };
235 result.map_err(|e| from_io(dst.to_path_buf(), e))?;
236 }
237
238 Ok(())
239}
240
241pub(crate) async fn copy_dir(
246 src: &Path,
247 dst: &Path,
248 buffer_size: usize,
249 follow_symlinks: bool,
250 progress_tx: &UnboundedSender<Progress>,
251 cancel_token: &CancellationToken,
252) -> Result<()> {
253 let mut entries: Vec<(PathBuf, bool)> = Vec::new();
254 let mut dirs = vec![PathBuf::new()];
255
256 while let Some(rel_dir) = dirs.pop() {
257 let abs_dir = src.join(&rel_dir);
258 let mut read_dir = tokio::fs::read_dir(&abs_dir)
259 .await
260 .map_err(|e| from_io(abs_dir.clone(), e))?;
261
262 while let Some(entry) = read_dir
263 .next_entry()
264 .await
265 .map_err(|e| from_io(abs_dir.clone(), e))?
266 {
267 let rel_path = rel_dir.join(entry.file_name());
268 let file_type = entry
269 .file_type()
270 .await
271 .map_err(|e| from_io(entry.path(), e))?;
272
273 if file_type.is_dir() {
274 dirs.push(rel_path);
275 } else if file_type.is_symlink() && !follow_symlinks {
276 entries.push((rel_path, true));
277 } else {
278 entries.push((rel_path, false));
279 }
280 }
281 }
282
283 let files_total = entries.len() as u64;
284 tokio::fs::create_dir_all(dst)
285 .await
286 .map_err(|e| from_io(dst.to_path_buf(), e))?;
287
288 for (files_done, (rel_path, is_symlink)) in entries.into_iter().enumerate() {
289 if cancel_token.is_cancelled() {
290 return Err(FileEngineError::Cancelled);
291 }
292
293 let entry_src = src.join(&rel_path);
294 let entry_dst = dst.join(&rel_path);
295
296 if is_symlink {
297 if let Some(parent) = entry_dst.parent() {
298 tokio::fs::create_dir_all(parent)
299 .await
300 .map_err(|e| from_io(parent.to_path_buf(), e))?;
301 }
302 copy_symlink(&entry_src, &entry_dst).await?;
303 } else {
304 copy_file(
305 &entry_src,
306 &entry_dst,
307 buffer_size,
308 files_done as u64,
309 files_total,
310 progress_tx,
311 cancel_token,
312 )
313 .await?;
314 }
315 }
316
317 Ok(())
318}
319
320#[cfg(feature = "permissions")]
321pub(crate) async fn preserve_permissions_recursive(src: &Path, dst: &Path) -> Result<()> {
322 let src_metadata = tokio::fs::symlink_metadata(src)
323 .await
324 .map_err(|e| from_io(src.to_path_buf(), e))?;
325
326 if !src_metadata.is_dir() {
327 let perms = tokio::fs::metadata(src)
328 .await
329 .map_err(|e| from_io(src.to_path_buf(), e))?
330 .permissions();
331 return tokio::fs::set_permissions(dst, perms)
332 .await
333 .map_err(|e| from_io(dst.to_path_buf(), e));
334 }
335
336 let mut dirs = vec![PathBuf::new()];
337 while let Some(rel_dir) = dirs.pop() {
338 let abs_src_dir = src.join(&rel_dir);
339 let abs_dst_dir = dst.join(&rel_dir);
340
341 let perms = tokio::fs::metadata(&abs_src_dir)
342 .await
343 .map_err(|e| from_io(abs_src_dir.clone(), e))?
344 .permissions();
345 tokio::fs::set_permissions(&abs_dst_dir, perms)
346 .await
347 .map_err(|e| from_io(abs_dst_dir.clone(), e))?;
348
349 let mut read_dir = tokio::fs::read_dir(&abs_src_dir)
350 .await
351 .map_err(|e| from_io(abs_src_dir.clone(), e))?;
352 while let Some(entry) = read_dir
353 .next_entry()
354 .await
355 .map_err(|e| from_io(abs_src_dir.clone(), e))?
356 {
357 let rel_path = rel_dir.join(entry.file_name());
358 let file_type = entry
359 .file_type()
360 .await
361 .map_err(|e| from_io(entry.path(), e))?;
362
363 if file_type.is_dir() {
364 dirs.push(rel_path);
365 } else if file_type.is_file() {
366 let entry_src = src.join(&rel_path);
367 let entry_dst = dst.join(&rel_path);
368 let perms = tokio::fs::metadata(&entry_src)
369 .await
370 .map_err(|e| from_io(entry_src.clone(), e))?
371 .permissions();
372 tokio::fs::set_permissions(&entry_dst, perms)
373 .await
374 .map_err(|e| from_io(entry_dst.clone(), e))?;
375 }
376 }
377 }
378
379 Ok(())
380}