Skip to main content

git_policy_cli/
subvolume_platform.rs

1//! What:
2//!  The operating-system operations the subvolume migration needs, behind one trait, and the Linux implementation
3//!  that calls the primitives (`fs_kind`, `fs_btrfs_ioctl`, `fs_clone_exchange`, `fs_mount_options`).
4//! Why:
5//!  The migration's journal, resume, rollback and refusal logic is the same on any filesystem; only five operations
6//!  need btrfs. With the operations behind a trait, the controls run the whole logic on a plain directory tree with a
7//!  stand-in platform where there is no btrfs (the container gate), and run it against the real platform on btrfs
8//!  (the host task). `doc/planning/cli-git-btrfs-worktree.md` is the design the primitives come from.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! interface Platform { filesystemKind(path): Kind; isSubvolumeRoot(path): boolean; createSubvolume(path): void; ... }
13//! ```
14
15use super::fs_btrfs_ioctl::{subvolume_create_at, subvolume_destroy_at};
16use super::fs_clone_exchange::{clone_file, exchange_paths};
17use super::fs_kind::{FilesystemKind, filesystem_facts, is_subvolume_root};
18use super::fs_mount_options::read_mount_facts;
19use super::fs_primitive_error::PrimitiveError;
20use std::fs::File;
21use std::path::Path;
22
23/// What:
24///  The operations of the migration that depend on the kind of filesystem.
25///  `&self` borrows the platform read-only so one platform serves a whole run; `dyn Platform` is how a caller holds
26///  "any implementation" (TS interface).
27/// Why:
28///   See the module description.
29///
30/// In TS you'd write (pseudocode):
31/// ```ts
32/// interface Platform { ... }
33/// ```
34pub trait Platform {
35    /// The filesystem kind holding `path` and the bytes free there.
36    fn filesystem_facts(&self, path: &Path) -> Result<(FilesystemKind, u64), PrimitiveError>;
37    /// Whether `path` is the root of a subvolume.
38    fn is_subvolume_root(&self, path: &Path) -> Result<bool, PrimitiveError>;
39    /// Create an empty subvolume at `path`.
40    fn create_subvolume(&self, path: &Path) -> Result<(), PrimitiveError>;
41    /// Destroy the subvolume at `path` with the kernel call (the caller falls back to emptying and `rmdir`).
42    fn destroy_subvolume(&self, path: &Path) -> Result<(), PrimitiveError>;
43    /// Whether an unprivileged subvolume destroy is allowed on the mount holding `path`.
44    fn removal_allowed(&self, path: &Path) -> bool;
45    /// Whether `path` is itself a mount point (a mount point cannot be exchanged).
46    fn is_mount_point(&self, path: &Path) -> bool;
47    /// Clone a whole file's data blocks.
48    fn clone_file(&self, source: &File, destination: &File, path: &Path) -> Result<(), PrimitiveError>;
49    /// Swap two paths atomically.
50    fn exchange(&self, first: &Path, second: &Path) -> Result<(), PrimitiveError>;
51    /// Make `new` another name of the file `existing`; fails with `EXDEV` where the two sit in different subvolumes.
52    fn hard_link(&self, existing: &Path, new: &Path) -> std::io::Result<()> {
53        return std::fs::hard_link(existing, new);
54    }
55}
56
57/// What:
58///  The real platform: every operation is the primitive of the same name.
59/// Why:
60///   Production code and the host controls use it.
61///
62/// In TS you'd write (pseudocode):
63/// ```ts
64/// const linuxPlatform: Platform = { filesystemKind, isSubvolumeRoot, ... };
65/// ```
66#[derive(Clone, Copy, Debug, Default)]
67pub struct LinuxPlatform;
68
69/// The primitives, called as they are.
70impl Platform for LinuxPlatform {
71    fn filesystem_facts(&self, path: &Path) -> Result<(FilesystemKind, u64), PrimitiveError> {
72        return filesystem_facts(path);
73    }
74
75    fn is_subvolume_root(&self, path: &Path) -> Result<bool, PrimitiveError> {
76        return is_subvolume_root(path);
77    }
78
79    fn create_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
80        return subvolume_create_at(path);
81    }
82
83    fn destroy_subvolume(&self, path: &Path) -> Result<(), PrimitiveError> {
84        return subvolume_destroy_at(path);
85    }
86
87    fn removal_allowed(&self, path: &Path) -> bool {
88        return read_mount_facts(path)
89            .ok()
90            .flatten()
91            .is_some_and(|facts| { return facts.has_option("user_subvol_rm_allowed"); });
92    }
93
94    fn is_mount_point(&self, path: &Path) -> bool {
95        let Ok(real) = std::fs::canonicalize(path) else { return false };
96        return read_mount_facts(real.as_path())
97            .ok()
98            .flatten()
99            .is_some_and(|facts| { return facts.mount_point == real; });
100    }
101
102    fn clone_file(&self, source: &File, destination: &File, path: &Path) -> Result<(), PrimitiveError> {
103        return clone_file(source, destination, path);
104    }
105
106    fn exchange(&self, first: &Path, second: &Path) -> Result<(), PrimitiveError> {
107        return exchange_paths(first, second);
108    }
109}