Skip to main content

mitosis/clone_backend/
mod.rs

1use crate::config::Config;
2use crate::error::{ErrorCode, MitosisError, Result};
3use crate::paths::Home;
4use std::ffi::CString;
5use std::fs::{Metadata, OpenOptions, Permissions};
6use std::os::unix::ffi::OsStrExt;
7use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11mod copy;
12pub use copy::{CopyBackend, SLOW_WARNING};
13#[cfg(target_os = "linux")]
14mod reflink;
15#[cfg(target_os = "linux")]
16pub use reflink::ReflinkBackend;
17#[cfg(target_os = "linux")]
18pub mod zfs;
19#[cfg(target_os = "linux")]
20pub use zfs::ZfsBackend;
21
22// Not exported by libc; value from <sys/clonefile.h>.
23#[cfg(target_os = "macos")]
24const CLONE_NOFOLLOW: u32 = 0x0001;
25
26pub trait CloneBackend: Send + Sync {
27    /// `"apfs"`, `"reflink"`, `"zfs"` or `"copy"`.
28    fn name(&self) -> &'static str;
29    fn check_supported(&self, path: &Path) -> Result<()>;
30    /// Makes `dst` a clone of `src`; `dst` must not exist.
31    fn clone_dir(&self, src: &Path, dst: &Path) -> Result<()>;
32    /// Makes `dst` (which must not exist) a template generation copied from `src`, a generation
33    /// or a clone. Unlike `clone_dir`, the result must not depend on `src` staying around.
34    fn copy_dir(&self, src: &Path, dst: &Path) -> Result<()> {
35        self.clone_dir(src, dst)
36    }
37    /// Makes an empty directory a build writes into and later clones are made from.
38    fn create_dir(&self, path: &Path) -> Result<()>;
39    /// Removes a clone or generation directory and everything in it; a missing path is `Ok(())`.
40    fn remove_dir(&self, path: &Path) -> Result<()>;
41    /// Renames a clone or generation directory within the same parent tree.
42    fn rename_dir(&self, from: &Path, to: &Path) -> Result<()>;
43    /// Bytes under `path` not shared with any clone, or `None` when it cannot be told.
44    fn private_size(&self, path: &Path) -> Option<u64>;
45    /// Folders `remove_dir` left in place (and returned `Ok`) because clones still depend on
46    /// them, since the last call; only ZFS keeps any. gc reports them and tries again later.
47    fn take_kept(&self) -> Vec<PathBuf> {
48        Vec::new()
49    }
50    /// ZFS only: where `dataset`, a dataset Mitosis does not own (a running source), is mounted.
51    fn external_mountpoint(&self, _dataset: &str) -> Result<PathBuf> {
52        Err(no_running_source(self.name()))
53    }
54    /// ZFS only: takes the snapshot `<dataset>@<name>` and returns the folder it is read through.
55    fn snapshot_external(&self, _dataset: &str, _name: &str) -> Result<PathBuf> {
56        Err(no_running_source(self.name()))
57    }
58    /// ZFS only: destroys the snapshot `snapshot_external` took.
59    fn destroy_external(&self, _dataset: &str, _name: &str) -> Result<()> {
60        Err(no_running_source(self.name()))
61    }
62}
63
64fn no_running_source(backend: &str) -> MitosisError {
65    unsupported(
66        format!("--from-running needs the zfs back end; this home uses {backend}"),
67        "set backend = zfs and zfs_dataset, with MITOSIS_HOME on that dataset",
68    )
69}
70
71/// Fills `dst`, an empty folder that exists, with a full copy of the tree at `src` (owner, modes
72/// and times kept, symlinks recreated, never followed).
73pub(crate) fn fill_copy(src: &Path, dst: &Path) -> Result<()> {
74    fill_tree(src, dst, &copy::copy_file)
75}
76
77/// `create_dir`, `remove_dir` and `rename_dir` as plain file system calls (APFS, reflink, copy).
78/// Mode 0700: Postgres refuses a data folder others can read, and `pg_basebackup` keeps the
79/// mode of a folder that already exists (`initdb` sets 0700 itself).
80fn fs_create_dir(path: &Path) -> Result<()> {
81    std::fs::DirBuilder::new()
82        .recursive(true)
83        .mode(0o700)
84        .create(path)
85        .map_err(|e| {
86            MitosisError::new(
87                ErrorCode::Internal,
88                format!("create {}: {e}", path.display()),
89            )
90        })
91}
92
93fn fs_remove_dir(path: &Path) -> Result<()> {
94    match std::fs::remove_dir_all(path) {
95        Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(MitosisError::new(
96            ErrorCode::Internal,
97            format!("remove {}: {e}", path.display()),
98        )),
99        _ => Ok(()),
100    }
101}
102
103/// The error message is the bare OS error, so callers add which move failed.
104fn fs_rename_dir(from: &Path, to: &Path) -> Result<()> {
105    Ok(std::fs::rename(from, to)?)
106}
107
108#[cfg(target_os = "macos")]
109pub struct ApfsBackend;
110
111/// The `backend` setting: which way clones are made.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum BackendKind {
114    Auto,
115    Apfs,
116    Reflink,
117    Zfs,
118    Copy,
119}
120
121impl BackendKind {
122    pub fn as_str(self) -> &'static str {
123        match self {
124            BackendKind::Auto => "auto",
125            BackendKind::Apfs => "apfs",
126            BackendKind::Reflink => "reflink",
127            BackendKind::Zfs => "zfs",
128            BackendKind::Copy => "copy",
129        }
130    }
131}
132
133impl std::str::FromStr for BackendKind {
134    type Err = MitosisError;
135
136    fn from_str(s: &str) -> Result<Self> {
137        Ok(match s {
138            "auto" => BackendKind::Auto,
139            "apfs" => BackendKind::Apfs,
140            "reflink" => BackendKind::Reflink,
141            "zfs" => BackendKind::Zfs,
142            "copy" => BackendKind::Copy,
143            _ => {
144                return Err(MitosisError::new(
145                    ErrorCode::InvalidRequest,
146                    format!("unknown back end {s:?}"),
147                )
148                .with_hint("backends are: auto, apfs, reflink, zfs, copy"))
149            }
150        })
151    }
152}
153
154fn unsupported(msg: impl Into<String>, hint: impl Into<String>) -> MitosisError {
155    MitosisError::new(ErrorCode::BackendUnsupported, msg).with_hint(hint)
156}
157
158/// The file system type of `path`, for error messages (`btrfs`, `ext4`, or the hex magic).
159#[cfg(not(target_os = "macos"))]
160fn fs_type_name(path: &Path) -> String {
161    let Ok(c) = cpath(path) else {
162        return "unknown".into();
163    };
164    let mut st: libc::statfs = unsafe { std::mem::zeroed() };
165    if unsafe { libc::statfs(c.as_ptr(), &mut st) } != 0 {
166        return "unknown".into();
167    }
168    let magic = st.f_type as u64 & 0xFFFF_FFFF;
169    match magic {
170        0x9123683E => "btrfs".into(),
171        0x5846_5342 => "xfs".into(),
172        0xEF53 => "ext4".into(),
173        0x2FC1_2FC1 => "zfs".into(),
174        0x0102_1994 => "tmpfs".into(),
175        0x794C_7630 => "overlayfs".into(),
176        m => format!("filesystem 0x{m:x}"),
177    }
178}
179
180/// Picks the clone back end for `home` from the `backend` setting. `Copy` is never picked by
181/// `Auto`; ZFS is Linux only.
182pub fn select_backend(home: &Home, cfg: &Config) -> Result<Arc<dyn CloneBackend>> {
183    match cfg.backend {
184        #[cfg(target_os = "macos")]
185        BackendKind::Auto | BackendKind::Apfs => Ok(Arc::new(ApfsBackend)),
186        #[cfg(not(target_os = "macos"))]
187        BackendKind::Apfs => Err(unsupported(
188            "the apfs back end only works on macOS",
189            "set backend = \"auto\", or put MITOSIS_HOME on Btrfs, XFS (mkfs.xfs -m reflink=1) or ZFS",
190        )),
191        #[cfg(not(target_os = "macos"))]
192        BackendKind::Auto => {
193            // Order: a configured ZFS dataset whose mountpoint holds the home, then a FICLONE
194            // probe (OpenZFS block cloning answers it too).
195            // A zfs_dataset that does not work only matters when no other back end does.
196            #[cfg(target_os = "linux")]
197            let zfs_err = match cfg.zfs_dataset.as_deref().map(ZfsBackend::new) {
198                Some(Ok(zfs)) if zfs.contains(&home.root) => return Ok(Arc::new(zfs)),
199                Some(Err(e)) => Some(e),
200                _ => None,
201            };
202            let fs = fs_type_name(&home.root);
203            let no_backend = || {
204                unsupported(
205                    format!(
206                        "no clone back end works on the '{fs}' file system at {}",
207                        home.root.display()
208                    ),
209                    "put MITOSIS_HOME on Btrfs, XFS (mkfs.xfs -m reflink=1) or ZFS, or set backend = \"copy\"",
210                )
211            };
212            #[cfg(target_os = "linux")]
213            match ReflinkBackend.check_supported(&home.root) {
214                Ok(()) => return Ok(Arc::new(ReflinkBackend)),
215                Err(e) if e.code != ErrorCode::BackendUnsupported => return Err(e),
216                Err(_) => {}
217            }
218            #[cfg(target_os = "linux")]
219            if let (Some(e), "zfs") = (zfs_err, fs.as_str()) {
220                return Err(e);
221            }
222            if fs == "zfs" {
223                return Err(unsupported(
224                    format!(
225                        "{} is on ZFS without block cloning, and no zfs_dataset holds it",
226                        home.root.display()
227                    ),
228                    "set zfs_dataset to the dataset whose mountpoint holds MITOSIS_HOME, for example: mitosis config set zfs_dataset tank/mitosis",
229                ));
230            }
231            Err(no_backend())
232        }
233        #[cfg(target_os = "linux")]
234        BackendKind::Reflink => Ok(Arc::new(ReflinkBackend)),
235        #[cfg(not(target_os = "linux"))]
236        BackendKind::Reflink => Err(unsupported(
237            "the reflink back end only works on Linux",
238            "set backend = \"auto\"",
239        )),
240        BackendKind::Copy => Ok(Arc::new(CopyBackend)),
241        #[cfg(target_os = "linux")]
242        BackendKind::Zfs => {
243            let ds = cfg.zfs_dataset.as_deref().ok_or_else(|| {
244                unsupported(
245                    "backend = \"zfs\" needs zfs_dataset",
246                    "set zfs_dataset to the dataset Mitosis owns, for example: mitosis config set zfs_dataset tank/mitosis",
247                )
248            })?;
249            Ok(Arc::new(ZfsBackend::new(ds)?))
250        }
251        #[cfg(not(target_os = "linux"))]
252        BackendKind::Zfs => {
253            let _ = home;
254            Err(unsupported("the zfs back end only works on Linux", "set backend = \"auto\""))
255        }
256    }
257}
258
259fn cpath(p: &Path) -> Result<CString> {
260    CString::new(p.as_os_str().as_bytes()).map_err(|_| {
261        MitosisError::new(
262            ErrorCode::Internal,
263            format!("path contains a NUL byte: {}", p.display()),
264        )
265    })
266}
267
268#[cfg(target_os = "macos")]
269impl CloneBackend for ApfsBackend {
270    fn name(&self) -> &'static str {
271        "apfs"
272    }
273
274    fn check_supported(&self, path: &Path) -> Result<()> {
275        let c = cpath(path)?;
276        let mut st: libc::statfs = unsafe { std::mem::zeroed() };
277        if unsafe { libc::statfs(c.as_ptr(), &mut st) } != 0 {
278            let e = std::io::Error::last_os_error();
279            return Err(MitosisError::new(
280                ErrorCode::Internal,
281                format!("statfs {}: {e}", path.display()),
282            ));
283        }
284        let name = unsafe { std::ffi::CStr::from_ptr(st.f_fstypename.as_ptr()) }.to_string_lossy();
285        if name != "apfs" {
286            return Err(MitosisError::new(
287                ErrorCode::NotApfs,
288                format!("{} is on a '{name}' file system, not APFS", path.display()),
289            )
290            .with_hint("set MITOSIS_HOME to a folder on an APFS volume"));
291        }
292        Ok(())
293    }
294
295    fn clone_dir(&self, src: &Path, dst: &Path) -> Result<()> {
296        let (s, d) = (cpath(src)?, cpath(dst)?);
297        if unsafe { libc::clonefile(s.as_ptr(), d.as_ptr(), CLONE_NOFOLLOW) } == 0 {
298            return Ok(());
299        }
300        let e = std::io::Error::last_os_error();
301        let msg = format!("clonefile {} -> {}: {e}", src.display(), dst.display());
302        Err(match e.raw_os_error() {
303            Some(libc::EXDEV) => MitosisError::new(ErrorCode::CrossVolume, msg)
304                .with_hint("source and destination must be on the same volume"),
305            _ => MitosisError::new(ErrorCode::Internal, msg),
306        })
307    }
308
309    fn create_dir(&self, path: &Path) -> Result<()> {
310        fs_create_dir(path)
311    }
312
313    fn remove_dir(&self, path: &Path) -> Result<()> {
314        fs_remove_dir(path)
315    }
316
317    fn rename_dir(&self, from: &Path, to: &Path) -> Result<()> {
318        fs_rename_dir(from, to)
319    }
320
321    fn private_size(&self, path: &Path) -> Option<u64> {
322        apfs_private_size(path)
323    }
324}
325
326fn stat(path: &Path) -> Result<Metadata> {
327    std::fs::metadata(path).map_err(|e| {
328        MitosisError::new(ErrorCode::Internal, format!("stat {}: {e}", path.display()))
329    })
330}
331
332pub fn same_volume(a: &Path, b: &Path) -> Result<bool> {
333    Ok(stat(a)?.dev() == stat(b)?.dev())
334}
335
336pub fn free_space(path: &Path) -> Result<u64> {
337    let c = cpath(path)?;
338    let mut st: libc::statvfs = unsafe { std::mem::zeroed() };
339    if unsafe { libc::statvfs(c.as_ptr(), &mut st) } != 0 {
340        let e = std::io::Error::last_os_error();
341        return Err(MitosisError::new(
342            ErrorCode::Internal,
343            format!("statvfs {}: {e}", path.display()),
344        ));
345    }
346    Ok(st.f_bavail as u64 * st.f_frsize as u64)
347}
348
349/// Calls `f` for every regular file under `path` (or `path` itself), never following symlinks.
350fn walk_files(
351    path: &Path,
352    f: &mut dyn FnMut(&Path, &Metadata) -> Option<()>,
353) -> Result<Option<()>> {
354    let md = std::fs::symlink_metadata(path).map_err(|e| {
355        MitosisError::new(
356            ErrorCode::Internal,
357            format!("lstat {}: {e}", path.display()),
358        )
359    })?;
360    if md.is_dir() {
361        let rd = std::fs::read_dir(path).map_err(|e| {
362            MitosisError::new(
363                ErrorCode::Internal,
364                format!("read_dir {}: {e}", path.display()),
365            )
366        })?;
367        for entry in rd {
368            let entry = entry?;
369            if walk_files(&entry.path(), f)?.is_none() {
370                return Ok(None);
371            }
372        }
373        Ok(Some(()))
374    } else if md.is_file() {
375        Ok(f(path, &md))
376    } else {
377        Ok(Some(())) // symlinks and special files are skipped
378    }
379}
380
381fn io_error(what: &str, path: &Path, e: std::io::Error) -> MitosisError {
382    MitosisError::new(
383        ErrorCode::Internal,
384        format!("{what} {}: {e}", path.display()),
385    )
386}
387
388/// Makes `dst` (which must not exist) a copy of the tree at `src`; `file` copies or clones one
389/// regular file. A half-made tree is removed so a retry starts clean.
390fn clone_tree(
391    src: &Path,
392    dst: &Path,
393    file: &dyn Fn(&Path, &Path, &Metadata) -> Result<()>,
394) -> Result<()> {
395    if std::fs::symlink_metadata(dst).is_ok() {
396        return Err(MitosisError::new(
397            ErrorCode::Internal,
398            format!(
399                "clone {} -> {}: destination exists",
400                src.display(),
401                dst.display()
402            ),
403        ));
404    }
405    clone_entry(src, dst, file).inspect_err(|_| {
406        let _ = std::fs::remove_dir_all(dst).or_else(|_| std::fs::remove_file(dst));
407    })
408}
409
410/// `clone_tree` into `dst`, an empty folder that already exists (a fresh ZFS dataset): the
411/// entries of `src` are recreated in it, and it takes `src`'s owner, mode and times.
412fn fill_tree(
413    src: &Path,
414    dst: &Path,
415    file: &dyn Fn(&Path, &Path, &Metadata) -> Result<()>,
416) -> Result<()> {
417    let md = std::fs::symlink_metadata(src).map_err(|e| io_error("lstat", src, e))?;
418    for entry in std::fs::read_dir(src).map_err(|e| io_error("read_dir", src, e))? {
419        let entry = entry.map_err(|e| io_error("read_dir", src, e))?;
420        clone_entry(&entry.path(), &dst.join(entry.file_name()), file)?;
421    }
422    keep_owner(dst, &md)?;
423    std::fs::set_permissions(dst, Permissions::from_mode(md.mode() & 0o1777))
424        .map_err(|e| io_error("chmod", dst, e))?;
425    keep_times(dst, &md)
426}
427
428/// Creates `dst` for a regular file with the source's permission bits (O_CREAT | O_EXCL).
429fn create_dst(dst: &Path, md: &Metadata) -> Result<std::fs::File> {
430    OpenOptions::new()
431        .write(true)
432        .create_new(true)
433        .mode(md.mode() & 0o777)
434        .open(dst)
435        .map_err(|e| io_error("create", dst, e))
436}
437
438/// Recreates `src` at `dst`: directories made, symlinks recreated (never followed), regular
439/// files handed to `file`; owner, mode and times kept. Sockets, fifos and devices are skipped.
440fn clone_entry(
441    src: &Path,
442    dst: &Path,
443    file: &dyn Fn(&Path, &Path, &Metadata) -> Result<()>,
444) -> Result<()> {
445    let md = std::fs::symlink_metadata(src).map_err(|e| io_error("lstat", src, e))?;
446    let ft = md.file_type();
447    if ft.is_symlink() {
448        let target = std::fs::read_link(src).map_err(|e| io_error("readlink", src, e))?;
449        std::os::unix::fs::symlink(&target, dst).map_err(|e| io_error("symlink", dst, e))?;
450    } else if ft.is_dir() {
451        // Owner-writable while it is filled; the source mode is set after the children.
452        std::fs::DirBuilder::new()
453            .mode(0o700)
454            .create(dst)
455            .map_err(|e| io_error("mkdir", dst, e))?;
456        for entry in std::fs::read_dir(src).map_err(|e| io_error("read_dir", src, e))? {
457            let entry = entry.map_err(|e| io_error("read_dir", src, e))?;
458            clone_entry(&entry.path(), &dst.join(entry.file_name()), file)?;
459        }
460    } else if ft.is_file() {
461        file(src, dst, &md)?;
462    } else {
463        return Ok(());
464    }
465    keep_owner(dst, &md)?;
466    if !ft.is_symlink() {
467        // After the chown; setuid/setgid are dropped, a data folder never needs them.
468        std::fs::set_permissions(dst, Permissions::from_mode(md.mode() & 0o1777))
469            .map_err(|e| io_error("chmod", dst, e))?;
470    }
471    keep_times(dst, &md)
472}
473
474/// Gives `path` the source's owner when it differs from ours and we are allowed to (root).
475fn keep_owner(path: &Path, md: &Metadata) -> Result<()> {
476    let (uid, gid) = unsafe { (libc::geteuid(), libc::getegid()) };
477    if md.uid() == uid && md.gid() == gid {
478        return Ok(());
479    }
480    let c = cpath(path)?;
481    if unsafe { libc::lchown(c.as_ptr(), md.uid(), md.gid()) } == 0 {
482        return Ok(());
483    }
484    let e = std::io::Error::last_os_error();
485    match e.raw_os_error() {
486        Some(libc::EPERM) => Ok(()), // not allowed: the clone stays ours
487        _ => Err(io_error("chown", path, e)),
488    }
489}
490
491/// Copies access and modify times onto `path`, without following a symlink.
492fn keep_times(path: &Path, md: &Metadata) -> Result<()> {
493    let c = cpath(path)?;
494    let mut times: [libc::timespec; 2] = unsafe { std::mem::zeroed() };
495    times[0].tv_sec = md.atime() as libc::time_t;
496    times[0].tv_nsec = md.atime_nsec() as _;
497    times[1].tv_sec = md.mtime() as libc::time_t;
498    times[1].tv_nsec = md.mtime_nsec() as _;
499    let rc = unsafe {
500        libc::utimensat(
501            libc::AT_FDCWD,
502            c.as_ptr(),
503            times.as_ptr(),
504            libc::AT_SYMLINK_NOFOLLOW,
505        )
506    };
507    if rc != 0 {
508        return Err(io_error("utimensat", path, std::io::Error::last_os_error()));
509    }
510    Ok(())
511}
512
513pub fn apparent_size(path: &Path) -> Result<u64> {
514    let mut total = 0;
515    walk_files(path, &mut |_, md| {
516        total += md.len();
517        Some(())
518    })?;
519    Ok(total)
520}
521
522/// Bytes of one file not shared with any clone, via ATTR_CMNEXT_PRIVATESIZE.
523#[cfg(target_os = "macos")]
524fn file_private_size(path: &Path) -> Option<u64> {
525    #[repr(C, packed)]
526    struct Reply {
527        length: u32,
528        private_size: i64, // off_t
529    }
530    let c = cpath(path).ok()?;
531    let mut al: libc::attrlist = unsafe { std::mem::zeroed() };
532    al.bitmapcount = libc::ATTR_BIT_MAP_COUNT;
533    al.forkattr = libc::ATTR_CMNEXT_PRIVATESIZE; // extended common attrs travel in forkattr
534    let mut reply = Reply {
535        length: 0,
536        private_size: 0,
537    };
538    let rc = unsafe {
539        libc::getattrlist(
540            c.as_ptr(),
541            &mut al as *mut _ as *mut libc::c_void,
542            &mut reply as *mut _ as *mut libc::c_void,
543            std::mem::size_of::<Reply>(),
544            libc::FSOPT_NOFOLLOW | libc::FSOPT_ATTR_CMN_EXTENDED,
545        )
546    };
547    let size = reply.private_size;
548    (rc == 0 && size >= 0).then_some(size as u64)
549}
550
551// shortcut: any failure on any file makes the whole sum None rather than a partial number;
552// upgrade if callers need a best-effort partial total.
553#[cfg(target_os = "macos")]
554fn apfs_private_size(path: &Path) -> Option<u64> {
555    let mut total = 0u64;
556    let done = walk_files(path, &mut |p, _| {
557        total += file_private_size(p)?;
558        Some(())
559    });
560    matches!(done, Ok(Some(()))).then_some(total)
561}
562
563#[cfg(all(test, target_os = "macos"))]
564mod tests {
565    use super::*;
566    use std::fs::{create_dir, read_to_string, write};
567
568    #[test]
569    fn auto_picks_apfs_on_apfs() {
570        let tmp = tempfile::tempdir().unwrap();
571        let home = Home::at(tmp.path().to_path_buf());
572        let b = select_backend(&home, &Config::default()).unwrap();
573        assert_eq!(b.name(), "apfs");
574        b.check_supported(tmp.path()).unwrap();
575    }
576
577    #[test]
578    fn unbuilt_backends_are_unsupported() {
579        let tmp = tempfile::tempdir().unwrap();
580        let home = Home::at(tmp.path().to_path_buf());
581        for kind in [BackendKind::Reflink, BackendKind::Zfs] {
582            let cfg = Config {
583                backend: kind,
584                ..Config::default()
585            };
586            let e = select_backend(&home, &cfg).err().unwrap();
587            assert_eq!(e.code, ErrorCode::BackendUnsupported);
588        }
589    }
590
591    #[test]
592    fn clone_is_isolated_and_shares_blocks() {
593        let tmp = tempfile::tempdir().unwrap();
594        ApfsBackend.check_supported(tmp.path()).unwrap();
595        let (src, dst) = (tmp.path().join("src"), tmp.path().join("dst"));
596        create_dir(&src).unwrap();
597        write(src.join("a.txt"), "hello").unwrap();
598        create_dir(src.join("sub")).unwrap();
599        write(src.join("sub/b.txt"), "x".repeat(1_000_000)).unwrap();
600
601        ApfsBackend.clone_dir(&src, &dst).unwrap();
602        assert_eq!(read_to_string(dst.join("a.txt")).unwrap(), "hello");
603        write(dst.join("a.txt"), "changed").unwrap();
604        assert_eq!(read_to_string(src.join("a.txt")).unwrap(), "hello");
605
606        assert!(same_volume(&src, &dst).unwrap());
607        assert!(free_space(tmp.path()).unwrap() > 0);
608        assert!(ApfsBackend.clone_dir(&src, &dst).is_err());
609        assert_eq!(apparent_size(&src).unwrap(), 1_000_005);
610        if let Some(p) = ApfsBackend.private_size(&dst) {
611            assert!(p < 1_000_000);
612        }
613    }
614}
615
616#[cfg(test)]
617mod shared_tests {
618    use super::*;
619    use std::fs::{create_dir, write};
620
621    #[test]
622    fn symlinks_are_not_followed() {
623        let tmp = tempfile::tempdir().unwrap();
624        let big = tmp.path().join("big");
625        write(&big, vec![0u8; 4096]).unwrap();
626        let dir = tmp.path().join("d");
627        create_dir(&dir).unwrap();
628        std::os::unix::fs::symlink(&big, dir.join("link")).unwrap();
629        assert_eq!(apparent_size(&dir).unwrap(), 0);
630    }
631}
632
633#[cfg(all(test, not(target_os = "macos")))]
634mod auto_tests {
635    use super::*;
636
637    #[test]
638    fn auto_never_picks_copy() {
639        let tmp = tempfile::tempdir_in(std::env::var("MITOSIS_TEST_ROOT").unwrap_or("/tmp".into()))
640            .unwrap();
641        let home = Home::at(tmp.path().to_path_buf());
642        // Skipped on a reflink file system, where auto picks reflink.
643        if let Ok(b) = select_backend(&home, &Config::default()) {
644            assert_ne!(b.name(), "copy");
645            return;
646        }
647        let e = select_backend(&home, &Config::default()).err().unwrap();
648        assert_eq!(e.code, ErrorCode::BackendUnsupported);
649    }
650}