Skip to content

Commit a5315cf

Browse files
authored
feat!: Add support for multiple parents (#427)
To solve rustic-rs/rustic#493 it will be necessary to add support for multiple parents. This PR adds this support: - The `parent` option can be now given multiple times - In the `SnapshotFile` also mutliple parent ids can now be saved - All trees of all parent snapshots are processed and a suitable entry of any of the parents is taken
1 parent ac60ad0 commit a5315cf

15 files changed

+171
-135
lines changed

crates/core/src/archiver/parent.rs

Lines changed: 82 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ pub(crate) type ItemWithParent<O> = TreeType<(O, ParentResult<()>), ParentResult
2323
#[derive(Debug)]
2424
pub struct Parent {
2525
/// The tree id of the parent tree.
26-
tree_id: Option<TreeId>,
26+
tree_ids: Vec<TreeId>,
2727
/// The parent tree.
28-
tree: Option<Tree>,
29-
/// The current node index.
30-
node_idx: usize,
28+
trees: Vec<(Tree, usize)>,
3129
/// The stack of parent trees.
32-
stack: Vec<(Option<Tree>, usize)>,
30+
stack: Vec<Vec<(Tree, usize)>>,
3331
/// Ignore ctime when comparing nodes.
3432
ignore_ctime: bool,
3533
/// Ignore inode number when comparing nodes.
@@ -90,25 +88,28 @@ impl Parent {
9088
pub(crate) fn new(
9189
be: &impl DecryptReadBackend,
9290
index: &impl ReadGlobalIndex,
93-
tree_id: Option<TreeId>,
91+
tree_id: impl IntoIterator<Item = TreeId>,
9492
ignore_ctime: bool,
9593
ignore_inode: bool,
9694
) -> Self {
9795
// if tree_id is given, try to load tree from backend.
98-
let tree = tree_id.and_then(|tree_id| match Tree::from_backend(be, index, tree_id) {
99-
Ok(tree) => Some(tree),
100-
Err(err) => {
101-
warn!(
102-
"ignoring error when loading parent tree {tree_id}: {}",
103-
err.display_log()
104-
);
105-
None
106-
}
107-
});
96+
let (trees, tree_ids) = tree_id
97+
.into_iter()
98+
.filter_map(|tree_id| match Tree::from_backend(be, index, tree_id) {
99+
Ok(tree) => Some(((tree, 0), tree_id)),
100+
Err(err) => {
101+
warn!(
102+
"ignoring error when loading parent tree {tree_id:?}: {}",
103+
err.display_log()
104+
);
105+
None
106+
}
107+
})
108+
.unzip();
109+
108110
Self {
109-
tree_id,
110-
tree,
111-
node_idx: 0,
111+
tree_ids,
112+
trees,
112113
stack: Vec::new(),
113114
ignore_ctime,
114115
ignore_inode,
@@ -124,27 +125,24 @@ impl Parent {
124125
/// # Returns
125126
///
126127
/// The parent node with the given name, or `None` if the parent node is not found.
127-
fn p_node(&mut self, name: &OsStr) -> Option<&Node> {
128-
match &self.tree {
129-
None => None,
130-
Some(tree) => {
131-
let p_nodes = &tree.nodes;
132-
loop {
133-
match p_nodes.get(self.node_idx) {
134-
None => break None,
135-
Some(p_node) => match p_node.name().as_os_str().cmp(name) {
136-
Ordering::Less => self.node_idx += 1,
137-
Ordering::Equal => {
138-
break Some(p_node);
139-
}
140-
Ordering::Greater => {
141-
break None;
142-
}
143-
},
144-
}
128+
fn p_node(&mut self, name: &OsStr) -> impl Iterator<Item = &Node> {
129+
self.trees.iter_mut().filter_map(|(tree, idx)| {
130+
let p_nodes = &tree.nodes;
131+
loop {
132+
match p_nodes.get(*idx) {
133+
None => break None,
134+
Some(p_node) => match p_node.name().as_os_str().cmp(name) {
135+
Ordering::Less => *idx += 1,
136+
Ordering::Equal => {
137+
break Some(p_node);
138+
}
139+
Ordering::Greater => {
140+
break None;
141+
}
142+
},
145143
}
146144
}
147-
}
145+
})
148146
}
149147

150148
/// Returns whether the given node is the parent of the given tree.
@@ -166,18 +164,22 @@ impl Parent {
166164
let ignore_ctime = self.ignore_ctime;
167165
let ignore_inode = self.ignore_inode;
168166

169-
self.p_node(name).map_or(ParentResult::NotFound, |p_node| {
170-
if p_node.node_type == node.node_type
171-
&& p_node.meta.size == node.meta.size
172-
&& p_node.meta.mtime == node.meta.mtime
173-
&& (ignore_ctime || p_node.meta.ctime == node.meta.ctime)
174-
&& (ignore_inode || p_node.meta.inode == 0 || p_node.meta.inode == node.meta.inode)
175-
{
176-
ParentResult::Matched(p_node)
177-
} else {
178-
ParentResult::NotMatched
179-
}
180-
})
167+
let mut p_node = self.p_node(name).peekable();
168+
if p_node.peek().is_none() {
169+
return ParentResult::NotFound;
170+
}
171+
172+
p_node
173+
.find(|p_node| {
174+
p_node.node_type == node.node_type
175+
&& p_node.meta.size == node.meta.size
176+
&& p_node.meta.mtime == node.meta.mtime
177+
&& (ignore_ctime || p_node.meta.ctime == node.meta.ctime)
178+
&& (ignore_inode
179+
|| p_node.meta.inode == 0
180+
|| p_node.meta.inode == node.meta.inode)
181+
})
182+
.map_or(ParentResult::NotMatched, ParentResult::Matched)
181183
}
182184

183185
// TODO: add documentation!
@@ -196,27 +198,35 @@ impl Parent {
196198
index: &impl ReadGlobalIndex,
197199
name: &OsStr,
198200
) {
199-
let tree = self.p_node(name).and_then(|p_node| {
200-
p_node.subtree.map_or_else(
201-
|| {
201+
let mut new_ids: Vec<_> = self
202+
.p_node(name)
203+
.filter_map(|p_node| {
204+
p_node.subtree.or_else(|| {
202205
warn!("ignoring parent node {}: is no tree!", p_node.name);
203206
None
204-
},
205-
|tree_id| match Tree::from_backend(be, index, tree_id) {
206-
Ok(tree) => Some(tree),
207-
Err(err) => {
208-
warn!(
209-
"ignoring error when loading parent tree {tree_id}: {}",
210-
err.display_log()
211-
);
212-
None
213-
}
214-
},
215-
)
216-
});
217-
self.stack.push((self.tree.take(), self.node_idx));
218-
self.tree = tree;
219-
self.node_idx = 0;
207+
})
208+
})
209+
.collect();
210+
211+
// remove potentially identical trees
212+
new_ids.sort();
213+
new_ids.dedup();
214+
215+
let new_tree = new_ids
216+
.into_iter()
217+
.filter_map(|tree_id| match Tree::from_backend(be, index, tree_id) {
218+
Ok(tree) => Some((tree, 0)),
219+
Err(err) => {
220+
warn!(
221+
"ignoring error when loading parent tree {tree_id}: {}",
222+
err.display_log()
223+
);
224+
None
225+
}
226+
})
227+
.collect();
228+
let old_tree = std::mem::replace(&mut self.trees, new_tree);
229+
self.stack.push(old_tree);
220230
}
221231

222232
// TODO: add documentation!
@@ -225,17 +235,15 @@ impl Parent {
225235
///
226236
/// * If the tree stack is empty.
227237
fn finish_dir(&mut self) -> Result<(), TreeStackEmptyError> {
228-
let (tree, node_idx) = self.stack.pop().ok_or(TreeStackEmptyError)?;
229-
230-
self.tree = tree;
231-
self.node_idx = node_idx;
238+
let tree = self.stack.pop().ok_or(TreeStackEmptyError)?;
239+
self.trees = tree;
232240

233241
Ok(())
234242
}
235243

236244
// TODO: add documentation!
237245
pub(crate) fn tree_id(&self) -> Option<TreeId> {
238-
self.tree_id
246+
self.tree_ids.first().copied()
239247
}
240248

241249
// TODO: add documentation!

crates/core/src/commands/backup.rs

Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! `backup` subcommand
22
use derive_setters::Setters;
3+
use itertools::Itertools;
34
use log::info;
45

56
use std::path::PathBuf;
@@ -51,8 +52,8 @@ pub struct ParentOptions {
5152
feature = "clap",
5253
clap(long, value_name = "SNAPSHOT", conflicts_with = "force",)
5354
)]
54-
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
55-
pub parent: Option<String>,
55+
#[cfg_attr(feature = "merge", merge(strategy = conflate::vec::append))]
56+
pub parent: Vec<String>,
5657

5758
/// Skip writing of snapshot if nothing changed w.r.t. the parent snapshot.
5859
#[cfg_attr(feature = "clap", clap(long))]
@@ -97,30 +98,38 @@ impl ParentOptions {
9798
repo: &Repository<P, S>,
9899
snap: &SnapshotFile,
99100
backup_stdin: bool,
100-
) -> (Option<SnapshotId>, Parent) {
101-
let parent = match (backup_stdin, self.force, &self.parent) {
102-
(true, _, _) | (false, true, _) => None,
103-
(false, false, None) => {
104-
// get suitable snapshot group from snapshot and opts.group_by. This is used to filter snapshots for the parent detection
105-
let group = SnapshotGroup::from_snapshot(snap, self.group_by.unwrap_or_default());
106-
SnapshotFile::latest(
107-
repo.dbe(),
108-
|snap| snap.has_group(&group),
109-
&repo.pb.progress_counter(""),
110-
)
111-
.ok()
112-
}
113-
(false, false, Some(parent)) => SnapshotFile::from_id(repo.dbe(), parent).ok(),
101+
) -> (Vec<SnapshotId>, Parent) {
102+
let parent = if backup_stdin || self.force {
103+
Vec::new()
104+
} else if self.parent.is_empty() {
105+
// get suitable snapshot group from snapshot and opts.group_by. This is used to filter snapshots for the parent detection
106+
let group = SnapshotGroup::from_snapshot(snap, self.group_by.unwrap_or_default());
107+
SnapshotFile::latest(
108+
repo.dbe(),
109+
|snap| snap.has_group(&group),
110+
&repo.pb.progress_counter(""),
111+
)
112+
.ok()
113+
.into_iter()
114+
.collect()
115+
} else {
116+
self.parent
117+
.iter()
118+
.filter_map(|parent| SnapshotFile::from_id(repo.dbe(), parent).ok())
119+
.collect()
114120
};
115121

116-
let (parent_tree, parent_id) = parent.map(|parent| (parent.tree, parent.id)).unzip();
122+
let (parent_trees, parent_ids): (Vec<_>, _) = parent
123+
.into_iter()
124+
.map(|parent| (parent.tree, parent.id))
125+
.unzip();
117126

118127
(
119-
parent_id,
128+
parent_ids,
120129
Parent::new(
121130
repo.dbe(),
122131
repo.index(),
123-
parent_tree,
132+
parent_trees,
124133
self.ignore_ctime,
125134
self.ignore_inode,
126135
),
@@ -238,44 +247,33 @@ pub(crate) fn backup<P: ProgressBars, S: IndexedIds>(
238247
})
239248
.transpose()?;
240249

241-
match &as_path {
242-
Some(p) => snap
243-
.paths
244-
.set_paths(std::slice::from_ref(p))
245-
.map_err(|err| {
246-
RusticError::with_source(
247-
ErrorKind::Internal,
248-
"Failed to set paths `{paths}` in snapshot.",
249-
err,
250-
)
251-
.attach_context("paths", p.display().to_string())
252-
})?,
253-
None => snap.paths.set_paths(&backup_path).map_err(|err| {
254-
RusticError::with_source(
255-
ErrorKind::Internal,
256-
"Failed to set paths `{paths}` in snapshot.",
257-
err,
258-
)
259-
.attach_context(
260-
"paths",
261-
backup_path
262-
.iter()
263-
.map(|p| p.display().to_string())
264-
.collect::<Vec<_>>()
265-
.join(","),
266-
)
267-
})?,
268-
}
250+
let paths = match &as_path {
251+
Some(p) => std::slice::from_ref(p),
252+
None => &backup_path,
253+
};
269254

270-
let (parent_id, parent) = opts.parent_opts.get_parent(repo, &snap, backup_stdin);
271-
match parent_id {
272-
Some(id) => {
273-
info!("using parent {id}");
274-
snap.parent = Some(id);
275-
}
276-
None => {
277-
info!("using no parent");
278-
}
255+
snap.paths.set_paths(paths).map_err(|err| {
256+
RusticError::with_source(
257+
ErrorKind::Internal,
258+
"Failed to set paths `{paths}` in snapshot.",
259+
err,
260+
)
261+
.attach_context(
262+
"paths",
263+
backup_path
264+
.iter()
265+
.map(|p| p.display().to_string())
266+
.join(","),
267+
)
268+
})?;
269+
270+
let (parent_ids, parent) = opts.parent_opts.get_parent(repo, &snap, backup_stdin);
271+
if parent_ids.is_empty() {
272+
info!("using no parent");
273+
} else {
274+
info!("using parents {}", parent_ids.iter().join(", "));
275+
snap.parent = Some(parent_ids[0]);
276+
snap.parents = parent_ids;
279277
}
280278

281279
let be = DryRunBackend::new(repo.dbe().clone(), opts.dry_run);

0 commit comments

Comments
 (0)