common/transfer/
fs.rs

1use fs_extra::error::{Error, ErrorKind, Result};
2use fs_extra::{dir, file};
3use lazy_static::lazy_static;
4use same_file::is_same_file;
5use std::fs;
6use std::path::Path;
7
8#[derive(PartialEq, Debug)]
9pub enum FileType {
10    File,
11    Dir,
12    Unknown,
13}
14
15impl From<&Path> for FileType {
16    fn from(path: &Path) -> Self {
17        match path.metadata() {
18            Ok(metadata) => {
19                if metadata.is_dir() {
20                    FileType::Dir
21                } else {
22                    FileType::File
23                }
24            }
25            Err(_) => FileType::Unknown,
26        }
27    }
28}
29
30#[derive(Clone, Copy)]
31pub enum TransferMode {
32    Move,
33    Copy,
34}
35
36pub fn transfer_path(src_path: &Path, dst_path: &Path, mode: TransferMode) -> Result<()> {
37    match (FileType::from(src_path), FileType::from(dst_path)) {
38        (FileType::Unknown, _) => Err(Error::new(
39            ErrorKind::NotFound,
40            &format!(
41                "Path '{}' not found or user lacks permission",
42                src_path.to_string_lossy()
43            ),
44        )),
45
46        (FileType::File, FileType::Dir) => Err(Error::new(
47            ErrorKind::Other,
48            &format!(
49                "Cannot to overwrite directory '{}' with file '{}'",
50                dst_path.to_string_lossy(),
51                src_path.to_string_lossy()
52            ),
53        )),
54
55        (FileType::Dir, FileType::File) => Err(Error::new(
56            ErrorKind::Other,
57            &format!(
58                "Cannot to overwrite file '{}' with directory '{}'",
59                dst_path.to_string_lossy(),
60                src_path.to_string_lossy()
61            ),
62        )),
63
64        (FileType::File, dst_type) => {
65            if let Some(dst_parent) = dst_path.parent() {
66                dir::create_all(dst_parent, false)?;
67            }
68            match mode {
69                TransferMode::Move => {
70                    if fs::rename(src_path, dst_path).is_err() {
71                        file::move_file(src_path, dst_path, &FILE_COPY_OPTIONS)?;
72                    }
73                }
74                TransferMode::Copy => {
75                    if dst_type == FileType::Unknown || !is_same_file(src_path, dst_path)? {
76                        file::copy(src_path, dst_path, &FILE_COPY_OPTIONS)?;
77                    }
78                }
79            }
80            Ok(())
81        }
82
83        (FileType::Dir, dst_type) => {
84            dir::create_all(dst_path, false)?;
85            match mode {
86                TransferMode::Move => {
87                    if fs::rename(src_path, dst_path).is_err() {
88                        dir::move_dir(src_path, dst_path, &DIR_COPY_OPTIONS)?;
89                    }
90                }
91                TransferMode::Copy => {
92                    if dst_type == FileType::Unknown || !is_same_file(src_path, dst_path)? {
93                        dir::copy(src_path, dst_path, &DIR_COPY_OPTIONS)?;
94                    }
95                }
96            }
97            Ok(())
98        }
99    }
100}
101
102lazy_static! {
103    pub static ref FILE_COPY_OPTIONS: file::CopyOptions = get_file_copy_options();
104    pub static ref DIR_COPY_OPTIONS: dir::CopyOptions = get_dir_copy_options();
105}
106
107fn get_file_copy_options() -> file::CopyOptions {
108    let mut options = file::CopyOptions::new();
109    options.overwrite = true;
110    options.skip_exist = false;
111    options
112}
113
114fn get_dir_copy_options() -> dir::CopyOptions {
115    let mut options = dir::CopyOptions::new();
116    options.overwrite = true;
117    options.skip_exist = false;
118    options.copy_inside = true;
119    options.content_only = true;
120    options
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::transfer::testing::{debug_fse_error_kind, unpack_fse_error};
127    use assert_fs::prelude::*;
128    use assert_fs::{NamedTempFile, TempDir};
129    use fs_extra::error::ErrorKind;
130    use ntest::*;
131    use test_case::test_case;
132
133    #[test_case(temp_dir().path(),            FileType::Dir     ; "dir")]
134    #[test_case(touch(temp_file("a")).path(), FileType::File    ; "file")]
135    #[test_case(temp_file("b").path(),        FileType::Unknown ; "unknown")]
136    fn file_type(path: &Path, file_type: FileType) {
137        assert_eq!(FileType::from(path), file_type);
138    }
139
140    mod transfer_path {
141        use super::*;
142
143        #[test]
144        fn path_not_found() {
145            let src_file = temp_file("a");
146
147            assert_eq!(
148                transfer_path(src_file.path(), &Path::new("b"), TransferMode::Move) // Mode is irrelevant
149                    .map_err(unpack_fse_error),
150                Err((
151                    debug_fse_error_kind(ErrorKind::NotFound),
152                    format!(
153                        "Path '{}' not found or user lacks permission",
154                        src_file.path().to_string_lossy()
155                    )
156                ))
157            );
158
159            src_file.assert(predicates::path::missing());
160        }
161
162        #[test]
163        fn overwrite_dir_with_file() {
164            let src_file = touch(temp_file("a"));
165            let dst_dir = temp_dir();
166
167            assert_eq!(
168                transfer_path(src_file.path(), dst_dir.path(), TransferMode::Move) // Mode is irrelevant
169                    .map_err(unpack_fse_error),
170                Err((
171                    debug_fse_error_kind(ErrorKind::Other),
172                    format!(
173                        "Cannot to overwrite directory '{}' with file '{}'",
174                        dst_dir.path().to_string_lossy(),
175                        src_file.path().to_string_lossy()
176                    ),
177                ))
178            );
179
180            src_file.assert(predicates::path::is_file());
181            dst_dir.assert(predicates::path::is_dir());
182        }
183
184        #[test]
185        fn overwrite_file_with_dir() {
186            let src_dir = temp_dir();
187            let dst_file = touch(temp_file("a"));
188
189            assert_eq!(
190                transfer_path(src_dir.path(), dst_file.path(), TransferMode::Move) // Mode is irrelevant
191                    .map_err(unpack_fse_error),
192                Err((
193                    debug_fse_error_kind(ErrorKind::Other),
194                    format!(
195                        "Cannot to overwrite file '{}' with directory '{}'",
196                        dst_file.path().to_string_lossy(),
197                        src_dir.path().to_string_lossy()
198                    ),
199                ))
200            );
201
202            src_dir.assert(predicates::path::is_dir());
203            dst_file.assert(predicates::path::is_file());
204        }
205
206        #[test]
207        fn rename_file() {
208            let src_file = write(temp_file("a"), "1");
209            let dst_file = temp_file("b");
210
211            assert_eq!(
212                transfer_path(src_file.path(), dst_file.path(), TransferMode::Move)
213                    .map_err(unpack_fse_error),
214                Ok(())
215            );
216
217            src_file.assert(predicates::path::missing());
218            dst_file.assert("1");
219        }
220
221        #[test]
222        fn rename_file_to_itself() {
223            let src_file = write(temp_file("a"), "1");
224
225            assert_eq!(
226                transfer_path(src_file.path(), src_file.path(), TransferMode::Move)
227                    .map_err(unpack_fse_error),
228                Ok(())
229            );
230
231            src_file.assert("1");
232        }
233
234        #[test]
235        fn move_file_to_other() {
236            let src_file = write(temp_file("a"), "1");
237            let dst_file = write(temp_file("b"), "2");
238
239            assert_eq!(
240                transfer_path(src_file.path(), dst_file.path(), TransferMode::Move)
241                    .map_err(unpack_fse_error),
242                Ok(())
243            );
244
245            src_file.assert(predicates::path::missing());
246            dst_file.assert("1");
247        }
248
249        #[test]
250        fn copy_file() {
251            let src_file = write(temp_file("a"), "1");
252            let dst_file = temp_file("b");
253
254            assert_eq!(
255                transfer_path(src_file.path(), dst_file.path(), TransferMode::Copy)
256                    .map_err(unpack_fse_error),
257                Ok(())
258            );
259
260            src_file.assert("1");
261            dst_file.assert("1");
262        }
263
264        #[test]
265        #[timeout(5000)] // fs_extra::file::copy freezes for same src/dst path
266        fn copy_file_to_itself() {
267            let src_file = write(temp_file("a"), "1");
268
269            assert_eq!(
270                transfer_path(src_file.path(), src_file.path(), TransferMode::Copy)
271                    .map_err(unpack_fse_error),
272                Ok(())
273            );
274
275            src_file.assert("1");
276        }
277
278        #[test]
279        fn copy_file_to_other() {
280            let src_file = write(temp_file("a"), "1");
281            let dst_file = write(temp_file("b"), "2");
282
283            assert_eq!(
284                transfer_path(src_file.path(), dst_file.path(), TransferMode::Copy)
285                    .map_err(unpack_fse_error),
286                Ok(())
287            );
288
289            src_file.assert("1");
290            dst_file.assert("1");
291        }
292
293        #[test]
294        fn rename_dir() {
295            let root_dir = temp_dir();
296
297            let src_dir = mkdir(root_dir.child("a"));
298            let src_file = write(src_dir.child("c"), "1");
299
300            let dst_dir = root_dir.child("b");
301            let dst_file = dst_dir.child("c");
302
303            assert_eq!(
304                transfer_path(src_dir.path(), dst_dir.path(), TransferMode::Move)
305                    .map_err(unpack_fse_error),
306                Ok(())
307            );
308
309            src_dir.assert(predicates::path::missing());
310            src_file.assert(predicates::path::missing());
311
312            dst_dir.assert(predicates::path::is_dir());
313            dst_file.assert("1");
314        }
315
316        #[test]
317        fn rename_dir_to_itself() {
318            let src_dir = temp_dir();
319            let src_file = write(src_dir.child("a"), "1");
320
321            assert_eq!(
322                transfer_path(src_dir.path(), src_dir.path(), TransferMode::Move)
323                    .map_err(unpack_fse_error),
324                Ok(())
325            );
326
327            src_dir.assert(predicates::path::is_dir());
328            src_file.assert("1");
329        }
330
331        #[test]
332        fn move_dir_to_other() {
333            let root_dir = temp_dir();
334
335            let src_dir = mkdir(root_dir.child("a"));
336            let src_file = write(src_dir.child("c"), "1");
337
338            let dst_dir = mkdir(root_dir.child("b"));
339            let dst_file = write(dst_dir.child("c"), "2");
340
341            assert_eq!(
342                transfer_path(src_dir.path(), dst_dir.path(), TransferMode::Move)
343                    .map_err(unpack_fse_error),
344                Ok(())
345            );
346
347            src_dir.assert(predicates::path::missing());
348            src_file.assert(predicates::path::missing());
349
350            dst_dir.assert(predicates::path::is_dir());
351            dst_file.assert("1");
352        }
353
354        #[test]
355        fn copy_dir() {
356            let root_dir = temp_dir();
357
358            let src_dir = mkdir(root_dir.child("a"));
359            let src_file = write(src_dir.child("c"), "1");
360
361            let dst_dir = root_dir.child("b");
362            let dst_file = dst_dir.child("c");
363
364            assert_eq!(
365                transfer_path(src_dir.path(), dst_dir.path(), TransferMode::Copy)
366                    .map_err(unpack_fse_error),
367                Ok(())
368            );
369
370            src_dir.assert(predicates::path::is_dir());
371            src_file.assert("1");
372
373            dst_dir.assert(predicates::path::is_dir());
374            dst_file.assert("1");
375        }
376
377        #[test]
378        #[timeout(5000)] // fs_extra::dir::copy freezes for same src/dst path
379        fn copy_dir_to_itself() {
380            let src_dir = temp_dir();
381            let src_file = write(src_dir.child("a"), "1");
382
383            assert_eq!(
384                transfer_path(src_dir.path(), src_dir.path(), TransferMode::Copy)
385                    .map_err(unpack_fse_error),
386                Ok(())
387            );
388
389            src_dir.assert(predicates::path::is_dir());
390            src_file.assert("1");
391        }
392
393        #[test]
394        fn copy_dir_to_other() {
395            let root_dir = temp_dir();
396
397            let src_dir = mkdir(root_dir.child("a"));
398            let src_file = write(src_dir.child("c"), "1");
399
400            let dst_dir = mkdir(root_dir.child("b"));
401            let dst_file = write(dst_dir.child("c"), "2");
402
403            assert_eq!(
404                transfer_path(src_dir.path(), dst_dir.path(), TransferMode::Copy)
405                    .map_err(unpack_fse_error),
406                Ok(())
407            );
408
409            src_dir.assert(predicates::path::is_dir());
410            src_file.assert("1");
411
412            dst_dir.assert(predicates::path::is_dir());
413            dst_file.assert("1");
414        }
415    }
416
417    #[test]
418    fn same_dir_and_file_copy_options() {
419        assert_eq!(DIR_COPY_OPTIONS.overwrite, FILE_COPY_OPTIONS.overwrite);
420        assert_eq!(DIR_COPY_OPTIONS.skip_exist, FILE_COPY_OPTIONS.skip_exist);
421        assert_eq!(DIR_COPY_OPTIONS.buffer_size, FILE_COPY_OPTIONS.buffer_size);
422    }
423
424    fn temp_dir() -> TempDir {
425        TempDir::new().unwrap()
426    }
427
428    fn temp_file(name: &str) -> NamedTempFile {
429        NamedTempFile::new(name).unwrap()
430    }
431
432    fn mkdir<P: PathCreateDir>(path: P) -> P {
433        path.create_dir_all().unwrap();
434        path
435    }
436
437    fn touch<F: FileTouch>(file: F) -> F {
438        file.touch().unwrap();
439        file
440    }
441
442    fn write<F: FileWriteStr>(file: F, data: &str) -> F {
443        file.write_str(data).unwrap();
444        file
445    }
446}