mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
refactor
This commit is contained in:
1 parent
242a28bb8f
commit
0a1fa95eb4
10 files changed
+1772
-1750
No files matched your search
@@ -0,0 +1,176 @@
|
||||
//! Pure reconciliation ordering, safety guards, signatures, and path rules.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// The paths a reconcile pass will classify: the union of the local, remote,
|
||||
/// and baseline states, minus anything the ignore rules exclude, shallowest
|
||||
/// first so a parent folder is created before its children are placed in it.
|
||||
///
|
||||
/// The ignore filter belongs *here*, on the union, rather than on the local walk
|
||||
/// alone — filtering the walk alone would be actively destructive. A file synced
|
||||
/// before it became ignored is absent from `local` but present in `remote` and
|
||||
/// `baseline`, which is precisely the "local deleted, remote untouched" shape
|
||||
/// `do_reconcile` responds to by trashing the remote copy. Adding a line to
|
||||
/// `.pdfsignore` must never delete anything from Drive.
|
||||
///
|
||||
/// Ignored baseline rows are deliberately left in the database rather than
|
||||
/// removed: with the row intact, un-ignoring the path later re-adopts the
|
||||
/// existing remote file instead of reading as a brand-new local one.
|
||||
pub(super) fn classification_order<L, R, B>(
|
||||
local: &HashMap<String, L>,
|
||||
remote: &HashMap<String, R>,
|
||||
baseline: &HashMap<String, B>,
|
||||
rules: &IgnoreRules,
|
||||
) -> Vec<String>
|
||||
where
|
||||
L: HasKind,
|
||||
R: HasKind,
|
||||
{
|
||||
let mut paths: HashSet<String> = HashSet::new();
|
||||
paths.extend(local.keys().cloned());
|
||||
paths.extend(remote.keys().cloned());
|
||||
paths.extend(baseline.keys().cloned());
|
||||
let mut order: Vec<String> = paths.into_iter().collect();
|
||||
if !rules.is_empty() {
|
||||
order.retain(|rel| {
|
||||
// A baseline-only path has no kind recorded, so it is tested as
|
||||
// both; see `filter_baseline` for why erring towards "ignored" is
|
||||
// the safe direction here.
|
||||
let kind = local
|
||||
.get(rel)
|
||||
.map(HasKind::is_dir)
|
||||
.or_else(|| remote.get(rel).map(HasKind::is_dir));
|
||||
match kind {
|
||||
Some(is_dir) => !rules.is_ignored(rel, is_dir),
|
||||
None => !rules.is_ignored(rel, false) && !rules.is_ignored(rel, true),
|
||||
}
|
||||
});
|
||||
}
|
||||
order.sort_by_key(|p| p.matches('/').count());
|
||||
order
|
||||
}
|
||||
|
||||
/// Lets [`classification_order`] ask either walk's item whether it is a
|
||||
/// directory without knowing which walk it came from.
|
||||
pub(super) trait HasKind {
|
||||
fn is_dir(&self) -> bool;
|
||||
}
|
||||
|
||||
impl HasKind for LocalItem {
|
||||
fn is_dir(&self) -> bool {
|
||||
self.is_dir
|
||||
}
|
||||
}
|
||||
|
||||
impl HasKind for RemoteItem {
|
||||
fn is_dir(&self) -> bool {
|
||||
self.is_dir
|
||||
}
|
||||
}
|
||||
|
||||
/// The baseline minus its ignored paths, for [`guard_local_wipe`].
|
||||
///
|
||||
/// The guard asks "did every synced path vanish locally?", and an ignored path
|
||||
/// is absent from the local walk by rule rather than by loss. Left in, a rule
|
||||
/// covering the whole tree would trip the guard on every pass and wedge that
|
||||
/// folder's sync for good.
|
||||
///
|
||||
/// A baseline row does not record whether it was a file or a folder, so a path
|
||||
/// counts as ignored if it matches as either. Erring towards "ignored" only ever
|
||||
/// shrinks the set the guard checks, which weakens a safety net rather than
|
||||
/// causing a deletion — the wrong direction to be wrong in is the other one.
|
||||
pub(super) fn filter_baseline<'a, B>(
|
||||
baseline: &'a HashMap<String, B>,
|
||||
rules: &IgnoreRules,
|
||||
) -> HashMap<String, &'a B> {
|
||||
baseline
|
||||
.iter()
|
||||
.filter(|(rel, _)| !rules.is_ignored(rel, false) && !rules.is_ignored(rel, true))
|
||||
.map(|(rel, entry)| (rel.clone(), entry))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Refuse to run a pass whose local side has vanished in its entirety.
|
||||
///
|
||||
/// When every baseline path is absent locally, the likely cause is an unavailable
|
||||
/// mount or unreadable folder rather than a deliberate whole-tree deletion.
|
||||
/// One surviving path is enough to clear the guard.
|
||||
pub(super) fn guard_local_wipe<B, L>(
|
||||
baseline: &HashMap<String, B>,
|
||||
local: &HashMap<String, L>,
|
||||
) -> Result<(), String> {
|
||||
if baseline.len() >= 2 && baseline.keys().all(|rel| !local.contains_key(rel)) {
|
||||
return Err(format!(
|
||||
"every one of the {} synced paths is missing locally; refusing to trash \
|
||||
them on Drive. Check that the folder is mounted and readable.",
|
||||
baseline.len()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The size the baseline recorded for `rel`, if its remote signature's mtime
|
||||
/// still matches `mtime`.
|
||||
pub(super) fn unchanged_remote_size(
|
||||
baseline: &HashMap<String, StoredSyncEntry>,
|
||||
rel: &str,
|
||||
mtime: i64,
|
||||
) -> Option<i64> {
|
||||
match baseline.get(rel).and_then(remote_sig) {
|
||||
Some((recorded, size)) if recorded == mtime => Some(size),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Join a child `name` onto a walk's `prefix`, giving a rel path.
|
||||
pub(super) fn join_rel(prefix: &str, name: &str) -> String {
|
||||
if prefix.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{prefix}/{name}")
|
||||
}
|
||||
}
|
||||
|
||||
/// The stored remote signature of a baseline row, if it has one.
|
||||
pub(super) fn remote_sig(e: &StoredSyncEntry) -> Option<(i64, i64)> {
|
||||
match (&e.remote_rev, &e.remote_hash) {
|
||||
(Some(m), Some(s)) => Some((m.parse().ok()?, s.parse().ok()?)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The parent of a `/`-joined relative path (`""` for a top-level entry).
|
||||
pub(super) fn parent_rel(rel: &str) -> &str {
|
||||
match rel.rfind('/') {
|
||||
Some(i) => &rel[..i],
|
||||
None => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// The final component of a `/`-joined relative path.
|
||||
pub(crate) fn base_name(rel: &str) -> &str {
|
||||
match rel.rfind('/') {
|
||||
Some(i) => &rel[i + 1..],
|
||||
None => rel,
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a `/`-joined relative path into an OS path (`/` is already the separator
|
||||
/// on Linux, this keeps the intent explicit).
|
||||
pub(super) fn rel_to_path(rel: &str) -> PathBuf {
|
||||
rel.split('/').collect()
|
||||
}
|
||||
|
||||
/// The name for a conflict copy of `path`, e.g. `notes (sync-conflict 1700000000).txt`.
|
||||
pub(super) fn conflict_path(path: &Path, stamp: i64) -> PathBuf {
|
||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("file");
|
||||
let ext = path.extension().and_then(|s| s.to_str());
|
||||
let name = match ext {
|
||||
Some(ext) => format!("{stem} (sync-conflict {stamp}).{ext}"),
|
||||
None => format!("{stem} (sync-conflict {stamp})"),
|
||||
};
|
||||
match path.parent() {
|
||||
Some(dir) => dir.join(name),
|
||||
None => PathBuf::from(name),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user