summaryrefslogtreecommitdiff
path: root/src/cmd/backup.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/backup.rs')
-rw-r--r--src/cmd/backup.rs138
1 files changed, 111 insertions, 27 deletions
diff --git a/src/cmd/backup.rs b/src/cmd/backup.rs
index c9b7fc5..2a8e086 100644
--- a/src/cmd/backup.rs
+++ b/src/cmd/backup.rs
@@ -1,61 +1,100 @@
use crate::client::{BackupClient, ClientConfig};
+use crate::fsentry::FilesystemEntry;
use crate::fsiter::FsIterator;
-use crate::generation::NascentGeneration;
+use crate::generation::{LocalGeneration, NascentGeneration};
use indicatif::{ProgressBar, ProgressStyle};
-use log::info;
+use log::{debug, info};
use tempfile::NamedTempFile;
-const GUESS_FILE_COUNT: u64 = 0;
-
pub fn backup(config: &ClientConfig, buffer_size: usize) -> anyhow::Result<()> {
let client = BackupClient::new(&config.server_url)?;
// Create a named temporary file. We don't meed the open file
// handle, so we discard that.
- let dbname = {
+ let oldname = {
let temp = NamedTempFile::new()?;
let (_, dbname) = temp.keep()?;
dbname
};
+ // Create a named temporary file. We don't meed the open file
+ // handle, so we discard that.
+ let newname = {
+ let temp = NamedTempFile::new()?;
+ let (_, dbname) = temp.keep()?;
+ dbname
+ };
+
+ let genlist = client.list_generations()?;
{
- // Create the SQLite database using the named temporary file.
- // The fetching is in its own block so that the file handles
- // get closed and data flushed to disk.
- let mut gen = NascentGeneration::create(&dbname)?;
- let progress = create_progress_bar(GUESS_FILE_COUNT, true);
+ let iter = FsIterator::new(&config.root);
+ let mut new = NascentGeneration::create(&newname)?;
+ let progress = create_progress_bar(true);
progress.enable_steady_tick(100);
- gen.insert_iter(FsIterator::new(&config.root).map(|entry| {
- progress.inc(1);
- match entry {
- Err(err) => Err(err),
- Ok(entry) => {
- let path = &entry.pathbuf();
- info!("backup: {}", path.display());
- progress.set_message(&format!("{}", path.display()));
- client.upload_filesystem_entry(entry, buffer_size)
- }
+
+ match genlist.resolve("latest") {
+ None => {
+ info!("fresh backup without a previous generation");
+ new.insert_iter(iter.map(|entry| {
+ progress.inc(1);
+ match entry {
+ Err(err) => Err(err),
+ Ok(entry) => {
+ let path = &entry.pathbuf();
+ info!("backup: {}", path.display());
+ progress.set_message(&format!("{}", path.display()));
+ client.upload_filesystem_entry(entry, buffer_size)
+ }
+ }
+ }))?;
}
- }))?;
- progress.set_length(gen.file_count());
+ Some(old) => {
+ info!("incremental backup based on {}", old);
+ let old = client.fetch_generation(&old, &oldname)?;
+ progress.set_length(old.file_count()?.into());
+ new.insert_iter(iter.map(|entry| {
+ progress.inc(1);
+ match entry {
+ Err(err) => Err(err),
+ Ok(entry) => {
+ let path = &entry.pathbuf();
+ info!("backup: {}", path.display());
+ progress.set_message(&format!("{}", path.display()));
+ if needs_backup(&old, &entry) {
+ client.upload_filesystem_entry(entry, buffer_size)
+ } else {
+ let fileno = old.get_fileno(&entry.pathbuf())?;
+ let ids = if let Some(fileno) = fileno {
+ old.chunkids(fileno)?
+ } else {
+ vec![]
+ };
+ Ok((entry.clone(), ids))
+ }
+ }
+ }
+ }))?;
+ }
+ }
+ progress.set_length(new.file_count());
progress.finish();
- println!("file count: {}", gen.file_count());
}
// Upload the SQLite file, i.e., the named temporary file, which
// still exists, since we persisted it above.
- let gen_id = client.upload_generation(&dbname, buffer_size)?;
+ let gen_id = client.upload_generation(&newname, buffer_size)?;
println!("gen id: {}", gen_id);
// Delete the temporary file.
- std::fs::remove_file(&dbname)?;
+ std::fs::remove_file(&newname)?;
+ std::fs::remove_file(&oldname)?;
Ok(())
}
-fn create_progress_bar(file_count: u64, verbose: bool) -> ProgressBar {
+fn create_progress_bar(verbose: bool) -> ProgressBar {
let progress = if verbose {
- ProgressBar::new(file_count)
+ ProgressBar::new(0)
} else {
ProgressBar::hidden()
};
@@ -69,3 +108,48 @@ fn create_progress_bar(file_count: u64, verbose: bool) -> ProgressBar {
progress.set_style(ProgressStyle::default_bar().template(&parts.join("\n")));
progress
}
+
+fn needs_backup(old: &LocalGeneration, new_entry: &FilesystemEntry) -> bool {
+ let new_name = new_entry.pathbuf();
+ match old.get_file(&new_name) {
+ // File is not in old generation.
+ Ok(None) => {
+ debug!(
+ "needs_backup: file is not in old generation, needs backup: {:?}",
+ new_name
+ );
+ true
+ }
+
+ // File is in old generation. Has its metadata changed?
+ Ok(Some(old_entry)) => {
+ if file_has_changed(&old_entry, new_entry) {
+ debug!("needs_backup: file has changed: {:?}", new_name);
+ true
+ } else {
+ debug!("needs_backup: file has NOT changed: {:?}", new_name);
+ false
+ }
+ }
+
+ // There was an error, which we ignore, but we indicate the
+ // file needs to be backed up now.
+ Err(err) => {
+ debug!(
+ "needs_backup: lookup in old generation returned error, ignored: {:?}: {}",
+ new_name, err
+ );
+ true
+ }
+ }
+}
+
+fn file_has_changed(old: &FilesystemEntry, new: &FilesystemEntry) -> bool {
+ let unchanged = old.kind() == new.kind()
+ && old.len() == new.len()
+ && old.mode() == new.mode()
+ && old.mtime() == new.mtime()
+ && old.mtime_ns() == new.mtime_ns()
+ && old.symlink_target() == new.symlink_target();
+ !unchanged
+}