refactor: store backups in nested vecs instead; introduce concept of
ci/woodpecker/push/lint Pipeline was successful Details
ci/woodpecker/push/clippy Pipeline failed Details
ci/woodpecker/push/build Pipeline was successful Details

chains
incremental-backups
Jef Roosens 2023-06-18 21:15:05 +02:00
parent f7235fb342
commit bb7b57899b
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
1 changed files with 47 additions and 67 deletions

View File

@ -103,8 +103,6 @@ impl BackupDelta {
/// Represents a successful backup
#[derive(Debug, Serialize, Deserialize)]
pub struct Backup {
#[serde(skip)]
previous: Option<Arc<Backup>>,
/// When the backup was started (also corresponds to the name)
start_time: chrono::DateTime<Utc>,
/// Type of the backup
@ -115,42 +113,14 @@ pub struct Backup {
impl Backup {
const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz";
/// Returns a pointer to this backup's previous backup by cloning the Arc pointer.
pub fn previous(&self) -> Option<Arc<Self>> {
self.previous.as_ref().map(Arc::clone)
pub fn state(backups: &Vec<Backup>) -> HashMap<PathBuf, HashSet<PathBuf>> {
let mut state: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
for backup in backups {
backup.delta.apply(&mut state);
}
/// Calculate the full state of the backup by applying all its ancestors' delta's in order,
/// starting from the last full ancestor.
pub fn state(&self) -> BackupResult<HashMap<PathBuf, HashSet<PathBuf>>> {
if self.type_ == BackupType::Full {
let mut state = HashMap::new();
self.delta.apply(&mut state);
Ok(state)
} else if let Some(previous) = &self.previous {
let mut state = previous.state()?;
self.delta.apply(&mut state);
Ok(state)
} else {
return Err(BackupError::NoFullAncestor);
}
}
/// Returns the n'th ancestor of the given backup, if it exists.
pub fn ancestor(&self, n: u64) -> Option<Arc<Self>> {
if n == 0 {
None
} else if let Some(previous) = &self.previous {
if n == 1 {
Some(Arc::clone(previous))
} else {
previous.ancestor(n - 1)
}
} else {
None
}
state
}
/// Return the path to a backup file by properly formatting the data.
@ -200,7 +170,6 @@ impl Backup {
}
Ok(Backup {
previous: None,
type_: BackupType::Full,
start_time,
delta,
@ -209,7 +178,8 @@ impl Backup {
/// Create a new incremental backup from a given previous backup
pub fn create_from<P: AsRef<Path>>(
previous: Arc<Backup>,
previous_state: HashMap<PathBuf, HashSet<PathBuf>>,
previous_start_time: chrono::DateTime<Utc>,
backup_dir: P,
dirs: Vec<(PathBuf, PathBuf)>,
) -> io::Result<Self> {
@ -220,9 +190,6 @@ impl Backup {
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut ar = tar::Builder::new(enc);
let previous_state = previous
.state()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "No Full ancestor"))?;
let mut delta = BackupDelta::new();
for (dir_in_tar, src_dir) in dirs {
@ -233,7 +200,7 @@ impl Backup {
let path = entry?.path();
let stripped = path.strip_prefix(&src_dir).unwrap();
if !path.not_modified_since(previous.start_time) {
if !path.not_modified_since(previous_start_time) {
ar.append_path_with_name(&path, dir_in_tar.join(stripped))?;
added_files.insert(stripped.to_path_buf());
}
@ -252,7 +219,6 @@ impl Backup {
}
Ok(Backup {
previous: Some(previous),
type_: BackupType::Incremental,
start_time,
delta,
@ -264,8 +230,10 @@ pub struct BackupManager {
backup_dir: PathBuf,
config_dir: PathBuf,
world_dir: PathBuf,
max_chain_length: u64,
max_backups: u64,
last_backup: Option<Arc<Backup>>,
chains: Vec<Vec<Backup>>,
}
impl BackupManager {
@ -282,6 +250,8 @@ impl BackupManager {
config_dir,
world_dir,
max_backups,
max_chain_length: 2,
chains: Vec::new(),
last_backup: None,
}
}
@ -304,13 +274,33 @@ impl BackupManager {
(PathBuf::from("worlds"), self.world_dir.clone()),
];
let backup = if let Some(last_backup) = &self.last_backup {
Backup::create_from(Arc::clone(last_backup), &self.backup_dir, dirs)?
// I kinda hate this statement, please just let me combine let statements in if statements
// already
let backup = if let Some(current_chain) = self.chains.last() {
let current_chain_len: u64 = current_chain.len().try_into().unwrap();
if current_chain_len < self.max_chain_length {
if let Some(previous_backup) = current_chain.last() {
let state = Backup::state(current_chain);
Backup::create_from(state, previous_backup.start_time, &self.backup_dir, dirs)?
} else {
Backup::create(&self.backup_dir, dirs)?
}
} else {
self.chains.push(Vec::new());
Backup::create(&self.backup_dir, dirs)?
}
} else {
self.chains.push(Vec::new());
Backup::create(&self.backup_dir, dirs)?
};
self.last_backup = Some(Arc::new(backup));
// The above statement always creates this element, so this unwrap is safe
self.chains.last_mut().unwrap().push(backup);
self.write_json()?;
Ok(())
@ -351,35 +341,25 @@ impl BackupManager {
// }
pub fn write_json(&self) -> std::io::Result<()> {
// Put the backup chain into a list that can be serialized
let mut backups: Vec<Arc<Backup>> = Vec::new();
let mut backup_opt = &self.last_backup;
while let Some(backup) = backup_opt {
backups.insert(0, Arc::clone(backup));
backup_opt = &backup.previous;
}
let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?;
serde_json::to_writer(json_file, &backups)?;
serde_json::to_writer(json_file, &self.chains)?;
Ok(())
}
pub fn load_json(&mut self) -> std::io::Result<()> {
let json_file = File::open(self.backup_dir.join(Self::METADATA_FILE))?;
let mut backups: Vec<Arc<Backup>> = serde_json::from_reader(json_file)?;
if !backups.is_empty() {
for i in 1..backups.len() {
let previous = Arc::clone(&backups[i - 1]);
// We can unwrap here, as this function creates the first instance of each Arc,
// meaning we're definitely the only pointer.
Arc::get_mut(&mut backups[i]).unwrap().previous = Some(previous);
let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) {
Ok(f) => f,
Err(e) => {
// Don't error out if the file isn't there, it will be created when necessary
if e.kind() == io::ErrorKind::NotFound {
return Ok(());
} else {
return Err(e);
}
self.last_backup = Some(Arc::clone(backups.last().unwrap()));
}
};
self.chains = serde_json::from_reader(json_file)?;
Ok(())
}