Skip to content

Commit 1b5fb66

Browse files
committed
WIP: feat: Add methods for repairing hot/cold repositories
1 parent 749879f commit 1b5fb66

File tree

5 files changed

+335
-20
lines changed

5 files changed

+335
-20
lines changed

crates/core/src/commands/config.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,32 @@ pub(crate) fn save_config<P, S>(
8282
let dbe = DecryptBackend::new(repo.be.clone(), key);
8383
// for hot/cold backend, this only saves the config to the cold repo.
8484
_ = dbe.save_file_uncompressed(&new_config)?;
85+
save_config_hot(repo, new_config, key)
86+
}
8587

88+
/// Save a [`ConfigFile`] only to the hot part of a repository
89+
///
90+
/// # Type Parameters
91+
///
92+
/// * `P` - The progress bar type.
93+
/// * `S` - The state the repository is in.
94+
///
95+
/// # Arguments
96+
///
97+
/// * `repo` - The repository to save the config to
98+
/// * `new_config` - The config to save
99+
/// * `key` - The key to encrypt the config with
100+
///
101+
/// # Errors
102+
///
103+
/// * [`CryptBackendErrorKind::SerializingToJsonByteVectorFailed`] - If the file could not be serialized to json.
104+
///
105+
/// [`CryptBackendErrorKind::SerializingToJsonByteVectorFailed`]: crate::error::CryptBackendErrorKind::SerializingToJsonByteVectorFailed
106+
pub(crate) fn save_config_hot<P, S>(
107+
repo: &Repository<P, S>,
108+
mut new_config: ConfigFile,
109+
key: impl CryptoKey,
110+
) -> RusticResult<()> {
86111
if let Some(hot_be) = repo.be_hot.clone() {
87112
// save config to hot repo
88113
let dbe = DecryptBackend::new(hot_be.clone(), key);

crates/core/src/commands/init.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ pub(crate) fn init<P, S>(
6060
Ok((key, config))
6161
}
6262

63-
/// Initialize a new repository with a given config.
63+
/// Save a [`ConfigFile`] only to the hot part of a repository
6464
///
6565
/// # Type Parameters
6666
///
@@ -69,14 +69,15 @@ pub(crate) fn init<P, S>(
6969
///
7070
/// # Arguments
7171
///
72-
/// * `repo` - The repository to initialize.
73-
/// * `pass` - The password to encrypt the key with.
74-
/// * `key_opts` - The options to create the key with.
75-
/// * `config` - The config to use.
72+
/// * `repo` - The repository to save the config to
73+
/// * `new_config` - The config to save
74+
/// * `key` - The key to encrypt the config with
7675
///
77-
/// # Returns
76+
/// # Errors
77+
///
78+
/// * [`CryptBackendErrorKind::SerializingToJsonByteVectorFailed`] - If the file could not be serialized to json.
7879
///
79-
/// The key used to encrypt the config.
80+
/// [`CryptBackendErrorKind::SerializingToJsonByteVectorFailed`]: crate::error::CryptBackendErrorKind::SerializingToJsonByteVectorFailed
8081
pub(crate) fn init_with_config<P, S>(
8182
repo: &Repository<P, S>,
8283
pass: &str,

crates/core/src/commands/repair.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
pub mod hotcold;
12
pub mod index;
23
pub mod snapshots;
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use std::collections::{BTreeMap, BTreeSet};
2+
3+
use log::{debug, info, warn};
4+
5+
use crate::{
6+
backend::decrypt::DecryptReadBackend,
7+
repofile::{BlobType, IndexFile, PackId},
8+
repository::Open,
9+
ErrorKind, FileType, Id, Progress, ProgressBars, ReadBackend, Repository, RusticError,
10+
RusticResult, WriteBackend, ALL_FILE_TYPES,
11+
};
12+
13+
pub(crate) fn repair_hotcold<P: ProgressBars, S>(
14+
repo: &Repository<P, S>,
15+
dry_run: bool,
16+
) -> RusticResult<()> {
17+
for file_type in ALL_FILE_TYPES {
18+
if file_type != FileType::Pack {
19+
correct_missing_files(repo, file_type, |_| true, dry_run)?;
20+
}
21+
}
22+
Ok(())
23+
}
24+
25+
pub(crate) fn repair_hotcold_packs<P: ProgressBars, S: Open>(
26+
repo: &Repository<P, S>,
27+
dry_run: bool,
28+
) -> RusticResult<()> {
29+
let tree_packs = get_tree_packs(repo)?;
30+
correct_missing_files(
31+
repo,
32+
FileType::Pack,
33+
|id| tree_packs.contains(&PackId::from(*id)),
34+
dry_run,
35+
)
36+
}
37+
38+
pub(crate) fn correct_missing_files<P: ProgressBars, S>(
39+
repo: &Repository<P, S>,
40+
file_type: FileType,
41+
is_relevant: impl Fn(&Id) -> bool,
42+
dry_run: bool,
43+
) -> RusticResult<()> {
44+
let Some(repo_hot) = &repo.be_hot else {
45+
return Err(RusticError::new(
46+
ErrorKind::Repository,
47+
"Repository is no hot/cold repository.",
48+
));
49+
};
50+
51+
let (missing_hot, missing_hot_size, missing_cold, missing_cold_size) =
52+
get_missing_files(repo, file_type, is_relevant)?;
53+
54+
// copy missing files from hot to cold repo
55+
if !missing_cold.is_empty() {
56+
if dry_run {
57+
info!(
58+
"would have copied {} hot {file_type:?} files to cold",
59+
missing_cold.len()
60+
);
61+
debug!("files: {missing_cold:?}");
62+
} else {
63+
let p = repo
64+
.pb
65+
.progress_bytes(format!("copying missing cold {file_type:?} files..."));
66+
p.set_length(missing_cold_size);
67+
copy(missing_cold, file_type, repo_hot, &repo.be_cold)?;
68+
p.finish();
69+
}
70+
}
71+
72+
if !missing_hot.is_empty() {
73+
if dry_run {
74+
info!(
75+
"would have copied {} cold {file_type:?} files to hot",
76+
missing_hot.len()
77+
);
78+
debug!("files: {missing_hot:?}");
79+
} else {
80+
// TODO: warm-up
81+
// copy missing files from cold to hot repo
82+
let p = repo
83+
.pb
84+
.progress_bytes(format!("copying missing hot {file_type:?} files..."));
85+
p.set_length(missing_hot_size);
86+
copy(missing_hot, file_type, &repo.be_cold, repo_hot)?;
87+
p.finish();
88+
}
89+
}
90+
91+
Ok(())
92+
}
93+
94+
fn copy(
95+
files: Vec<Id>,
96+
file_type: FileType,
97+
from: &impl ReadBackend,
98+
to: &impl WriteBackend,
99+
) -> RusticResult<()> {
100+
for id in files {
101+
let file = from.read_full(file_type, &id)?;
102+
to.write_bytes(file_type, &id, false, file)?;
103+
}
104+
Ok(())
105+
}
106+
107+
pub(crate) fn get_tree_packs<P: ProgressBars, S: Open>(
108+
repo: &Repository<P, S>,
109+
) -> RusticResult<BTreeSet<PackId>> {
110+
let p = repo.pb.progress_counter("reading index...");
111+
let mut tree_packs = BTreeSet::new();
112+
for index in repo.dbe().stream_all::<IndexFile>(&p)? {
113+
let index = index?.1;
114+
for (p, _) in index.all_packs() {
115+
let blob_type = p.blob_type();
116+
if blob_type == BlobType::Tree {
117+
_ = tree_packs.insert(p.id);
118+
}
119+
}
120+
}
121+
Ok(tree_packs)
122+
}
123+
124+
pub(crate) fn get_missing_files<P: ProgressBars, S>(
125+
repo: &Repository<P, S>,
126+
file_type: FileType,
127+
is_relevant: impl Fn(&Id) -> bool,
128+
) -> RusticResult<(Vec<Id>, u64, Vec<Id>, u64)> {
129+
let Some(repo_hot) = &repo.be_hot else {
130+
return Err(RusticError::new(
131+
ErrorKind::Repository,
132+
"Repository is no hot/cold repository.",
133+
));
134+
};
135+
136+
let p = repo
137+
.pb
138+
.progress_spinner(format!("listing hot {file_type:?} files..."));
139+
let hot_files: BTreeMap<_, _> = repo_hot.list_with_size(file_type)?.into_iter().collect();
140+
p.finish();
141+
142+
let p = repo
143+
.pb
144+
.progress_spinner(format!("listing cold {file_type:?} files..."));
145+
let cold_files: BTreeMap<_, _> = repo
146+
.be_cold
147+
.list_with_size(file_type)?
148+
.into_iter()
149+
.collect();
150+
p.finish();
151+
152+
let common: BTreeSet<_> = hot_files
153+
.iter()
154+
.filter_map(|(id, size_hot)| match cold_files.get(id) {
155+
Some(size_cold) if size_cold == size_hot => Some(*id),
156+
Some(size_cold) => {
157+
warn!("sizes mismatch: type {file_type:?}, id: {id}, size hot: {size_hot}, size cold: {size_cold}. Ignoring...");
158+
None
159+
}
160+
None => None,
161+
})
162+
.collect();
163+
164+
let retain = |files: BTreeMap<_, _>| {
165+
let mut retain_size: u64 = 0;
166+
let only: Vec<_> = files
167+
.into_iter()
168+
.filter(|(id, _)| !common.contains(id) && is_relevant(id))
169+
.map(|(id, size)| {
170+
retain_size += u64::from(size);
171+
id
172+
})
173+
.collect();
174+
(only, retain_size)
175+
};
176+
177+
let (cold_only, cold_only_size) = retain(cold_files);
178+
let (hot_only, hot_only_size) = retain(hot_files);
179+
Ok((cold_only, cold_only_size, hot_only, hot_only_size))
180+
}

0 commit comments

Comments
 (0)