1use super::subvolume_error::{Interrupt, MigrateError};
14use super::subvolume_journal::Journal;
15use super::subvolume_lock::relative_lock_paths;
16use super::subvolume_platform::Platform;
17use super::subvolume_preflight::Layout;
18use super::subvolume_sync::{SyncOptions, SyncStats, sync_tree};
19use std::collections::BTreeSet;
20use std::path::{Path, PathBuf};
21use std::time::Instant;
22
23pub const INDEX_LOCK: &str = ".git/index.lock";
25
26pub struct Run<'a> {
37 pub platform: &'a dyn Platform,
39 pub layout: &'a Layout,
41 pub journal: Journal,
43 pub interrupt: &'a Interrupt,
45 pub say: &'a mut dyn FnMut(&str),
47 pub unreadable: BTreeSet<PathBuf>,
49}
50
51impl Run<'_> {
52 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 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 pub fn line(&mut self, text: &str) {
78 (self.say)(text);
79 }
80
81 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 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 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 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 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 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 pub fn checkpoint(&self, point: &str) -> Result<(), MigrateError> {
195 return self.interrupt.check(point);
196 }
197}