Skip to main content

git_policy_cli/subvolume/
subvolume_run.rs

1//! What:
2//!  The state one migration command carries between its steps: the platform, the paths, the journal, the progress
3//!  sink, the list of files this user may not read, and the helper that runs one tree pass and reports it.
4//! Why:
5//!  The migrate, rollback and remove-old commands share it, so each pass is timed, counted and reported the same way
6//!  and the unreadable-file list is written the same way after every pass (a kill between passes loses nothing).
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! class Run { platform; layout; journal; interrupt; say(line); async pass(label, source, destination, options) { ... } }
11//! ```
12
13use crate::subvolume_error::{Interrupt, MigrateError};
14use crate::subvolume_journal::Journal;
15use crate::subvolume_lock::relative_lock_paths;
16use crate::subvolume_platform::Platform;
17use crate::subvolume_preflight::Layout;
18use crate::subvolume_sync::{SyncOptions, SyncStats, sync_tree};
19use std::collections::BTreeSet;
20use std::path::{Path, PathBuf};
21use std::time::Instant;
22
23/// The path, relative to a checkout, of the lock the migration holds during its last pass.
24pub const INDEX_LOCK: &str = ".git/index.lock";
25
26/// What:
27///  One command's working state.
28///  `&'a mut dyn FnMut(&str)` is a borrowed progress callback that receives one line at a time.
29/// Why:
30///   See the module description.
31///
32/// In TS you'd write (pseudocode):
33/// ```ts
34/// type Run = { platform: Platform; layout: Layout; journal: Journal; interrupt: Interrupt; say: (line: string) => void; unreadable: Set<string> };
35/// ```
36pub struct Run<'a> {
37    /// The operating-system operations.
38    pub platform: &'a dyn Platform,
39    /// Every path of the migration.
40    pub layout: &'a Layout,
41    /// The journal.
42    pub journal: Journal,
43    /// The simulated kill of the controls; never in release use.
44    pub interrupt: &'a Interrupt,
45    /// The progress sink.
46    pub say: &'a mut dyn FnMut(&str),
47    /// Relative paths of files this user may not read, which stay in the old tree.
48    pub unreadable: BTreeSet<PathBuf>,
49}
50
51impl Run<'_> {
52    /// What:
53    ///  Start the state: load the journal and the unreadable list left by an earlier run.
54    ///
55    /// In TS you'd write (pseudocode):
56    /// ```ts
57    /// const run = await Run.open(platform, layout, interrupt, say);
58    /// ```
59    pub fn open<'a>(
60        platform: &'a dyn Platform,
61        layout: &'a Layout,
62        interrupt: &'a Interrupt,
63        say: &'a mut dyn FnMut(&str),
64    ) -> Result<Run<'a>, MigrateError> {
65        let journal: Journal = Journal::load(layout.journal_path.as_path())?;
66        let unreadable: BTreeSet<PathBuf> = match std::fs::read_to_string(&layout.unreadable_path) {
67            Ok(text) => text.lines().filter(|line: &&str| { return !line.is_empty(); }).map(PathBuf::from).collect(),
68            // Only a missing list means "no unreadable files were recorded"; a list that exists and cannot be read is
69            // the safeguard failing, and counting it as empty would let the deletion unlink files that exist only there.
70            Err(error) if error.kind() == std::io::ErrorKind::NotFound => BTreeSet::new(),
71            Err(error) => return Err(MigrateError::io("read the list of unreadable files", layout.unreadable_path.as_path(), &error)),
72        };
73        return Ok(Run { platform, layout, journal, interrupt, say, unreadable });
74    }
75
76    /// Send one line to the progress sink.
77    pub fn line(&mut self, text: &str) {
78        (self.say)(text);
79    }
80
81    /// What:
82    ///  Run one tree pass, report its counts and time, and record the unreadable files it found.
83    /// Why:
84    ///   The list is written after every pass, so a kill between passes keeps it; the report names the count and the file.
85    ///
86    /// In TS you'd write (pseudocode):
87    /// ```ts
88    /// const stats = await run.pass('copy', source, destination, { delete: false, updateOnly: false });
89    /// ```
90    pub fn pass(&mut self, label: &str, source: &Path, destination: &Path, delete: bool, update_only: bool) -> Result<SyncStats, MigrateError> {
91        let stats: SyncStats = self.pass_unrecorded(label, source, destination, delete, update_only)?;
92        self.record_unreadable(label, &stats)?;
93        return Ok(stats);
94    }
95
96    /// Record the unreadable files a pass found in the list beside the checkout, and say so.
97    fn record_unreadable(&mut self, label: &str, stats: &SyncStats) -> Result<(), MigrateError> {
98        if !stats.unreadable.is_empty() {
99            self.unreadable.extend(stats.unreadable.iter().cloned());
100            self.write_unreadable()?;
101            self.line(format!("{label}: {} file(s) this user may not read stay in the old tree; list: {}", stats.unreadable.len(), self.layout.unreadable_path.display()).as_str());
102        }
103        return Ok(());
104    }
105
106    /// What:
107    ///  Run one tree pass and report its counts and time, without recording the unreadable files it found.
108    /// Why:
109    ///   The rollback's carry-back pass reads the new root, whose unreadable files are not the old tree's: they must
110    ///  not enter the list that guards the deletion of the old tree, and the rollback decides about them itself.
111    ///
112    /// In TS you'd write (pseudocode):
113    /// ```ts
114    /// const stats = await run.passUnrecorded('carry back', source, destination, { delete: false, updateOnly: true });
115    /// ```
116    pub fn pass_unrecorded(&mut self, label: &str, source: &Path, destination: &Path, delete: bool, update_only: bool) -> Result<SyncStats, MigrateError> {
117        return self.pass_core(label, source, destination, SyncOptions { delete, update_only, excluded: &[], created_since: None });
118    }
119
120    /// What:
121    ///  Run the update-only pass that carries what was written to the old tree after the catch-up: files newer than
122    ///  their copy, and files the new root lacks that are no older than `since` (seconds after the epoch).
123    /// Why:
124    ///   Programs that kept a descriptor in the old tree can write there until the person stops them, and the deletion
125    ///  of the old tree must not be the end of those writes. A file the new root lacks that is older than the catch-up
126    ///  was deleted there on purpose and stays deleted. Unreadable files found are recorded like any other pass.
127    ///
128    /// In TS you'd write (pseudocode):
129    /// ```ts
130    /// const stats = await run.passLate('late writes', oldTree, root, catchupSeconds);
131    /// ```
132    pub fn pass_late(&mut self, label: &str, source: &Path, destination: &Path, since: i64) -> Result<SyncStats, MigrateError> {
133        let stats: SyncStats = self.pass_core(label, source, destination, SyncOptions { delete: false, update_only: true, excluded: &[], created_since: Some(since) })?;
134        self.record_unreadable(label, &stats)?;
135        for path in &stats.copied_sample {
136            self.line(format!("{label}: carried {}", path.display()).as_str());
137        }
138        return Ok(stats);
139    }
140
141    /// The pass itself: the lock files of both trees are added to the excluded paths, and the counts are reported.
142    fn pass_core(&mut self, label: &str, source: &Path, destination: &Path, requested: SyncOptions<'_>) -> Result<SyncStats, MigrateError> {
143        let mut excluded: Vec<PathBuf> = relative_lock_paths(source);
144        excluded.extend(relative_lock_paths(destination));
145        excluded.sort();
146        excluded.dedup();
147        let started: Instant = Instant::now();
148        let stats: SyncStats = sync_tree(self.platform, source, destination, SyncOptions { excluded: excluded.as_slice(), ..requested }, self.interrupt)?;
149        self.line(
150            format!(
151                "{label}: {} entries, {} files copied ({} cloned), {} hard-linked, {} symlinks, {} unchanged, {} deleted, {} vanished, {:.2} s",
152                stats.entries, stats.files_copied, stats.cloned, stats.hard_linked, stats.symlinks, stats.unchanged, stats.deleted, stats.vanished,
153                started.elapsed().as_secs_f64()
154            )
155            .as_str(),
156        );
157        for path in &stats.special_skipped {
158            self.line(format!("{label}: not recreated (a socket or device node): {}", path.display()).as_str());
159        }
160        if stats.losses.owner > 0 || stats.losses.extended_attributes > 0 || stats.losses.mode > 0 || stats.losses.times > 0 {
161            self.line(
162                format!(
163                    "{label}: could not set the owner of {} entries, the permission bits of {}, the times of {}, and {} extended attributes (not running as the owner or as root)",
164                    stats.losses.owner, stats.losses.mode, stats.losses.times, stats.losses.extended_attributes
165                )
166                .as_str(),
167            );
168        }
169        for path in &stats.conflicts {
170            self.line(format!("{label}: kept the newer or differently typed entry {} in the destination; it was not replaced", path.display()).as_str());
171        }
172        if stats.left_out > 0 {
173            self.line(format!("{label}: {} entr(ies) absent from the destination were left out because they predate the catch-up", stats.left_out).as_str());
174        }
175        if stats.recopied > 0 {
176            self.line(format!("{label}: {} file(s) changed while they were copied and were copied again", stats.recopied).as_str());
177        }
178        return Ok(stats);
179    }
180
181    /// Write the unreadable-file list beside the checkout.
182    fn write_unreadable(&self) -> Result<(), MigrateError> {
183        let mut text: String = String::new();
184        for path in &self.unreadable {
185            text.push_str(path.to_string_lossy().as_ref());
186            text.push('\n');
187        }
188        return std::fs::write(&self.layout.unreadable_path, text).map_err(|error: std::io::Error| {
189            return MigrateError::io("write the list of unreadable files", self.layout.unreadable_path.as_path(), &error);
190        });
191    }
192
193    /// Stop here when the control asked for a simulated kill at this point.
194    pub fn checkpoint(&self, point: &str) -> Result<(), MigrateError> {
195        return self.interrupt.check(point);
196    }
197}