summaryrefslogtreecommitdiff
path: root/src/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd')
-rw-r--r--src/cmd/backup.rs178
-rw-r--r--src/cmd/chunk.rs70
-rw-r--r--src/cmd/chunkify.rs110
-rw-r--r--src/cmd/gen_info.rs47
-rw-r--r--src/cmd/get_chunk.rs37
-rw-r--r--src/cmd/init.rs33
-rw-r--r--src/cmd/inspect.rs46
-rw-r--r--src/cmd/list.rs38
-rw-r--r--src/cmd/list_backup_versions.rs31
-rw-r--r--src/cmd/list_files.rs61
-rw-r--r--src/cmd/mod.rs27
-rw-r--r--src/cmd/resolve.rs44
-rw-r--r--src/cmd/restore.rs275
-rw-r--r--src/cmd/show_config.rs17
-rw-r--r--src/cmd/show_gen.rs123
15 files changed, 913 insertions, 224 deletions
diff --git a/src/cmd/backup.rs b/src/cmd/backup.rs
index da7298f..70e9eac 100644
--- a/src/cmd/backup.rs
+++ b/src/cmd/backup.rs
@@ -1,65 +1,137 @@
-use crate::backup_run::BackupRun;
-use crate::client::ClientConfig;
-use crate::fsiter::FsIterator;
-use crate::generation::NascentGeneration;
+//! The `backup` subcommand.
+
+use crate::backup_run::{current_timestamp, BackupRun};
+use crate::chunk::ClientTrust;
+use crate::client::BackupClient;
+use crate::config::ClientConfig;
+use crate::dbgen::{schema_version, FileId, DEFAULT_SCHEMA_MAJOR};
+use crate::error::ObnamError;
+use crate::generation::GenId;
+use crate::performance::{Clock, Performance};
+use crate::schema::VersionComponent;
+
+use clap::Parser;
use log::info;
use std::time::SystemTime;
-use tempfile::NamedTempFile;
-
-pub fn backup(config: &ClientConfig, buffer_size: usize) -> anyhow::Result<()> {
- let runtime = SystemTime::now();
-
- let run = BackupRun::new(config, buffer_size)?;
-
- // Create a named temporary file. We don't meed the open file
- // handle, so we discard that.
- 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 = run.client().list_generations()?;
- let file_count = {
- let iter = FsIterator::new(&config.root);
- let mut new = NascentGeneration::create(&newname)?;
-
- match genlist.resolve("latest") {
- None => {
- info!("fresh backup without a previous generation");
- new.insert_iter(iter.map(|entry| run.backup_file_initially(entry)))?;
+use tempfile::tempdir;
+use tokio::runtime::Runtime;
+
+/// Make a backup.
+#[derive(Debug, Parser)]
+pub struct Backup {
+ /// Force a full backup, instead of an incremental one.
+ #[clap(long)]
+ full: bool,
+
+ /// Backup schema major version to use.
+ #[clap(long)]
+ backup_version: Option<VersionComponent>,
+}
+
+impl Backup {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig, perf: &mut Performance) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config, perf))
+ }
+
+ async fn run_async(
+ &self,
+ config: &ClientConfig,
+ perf: &mut Performance,
+ ) -> Result<(), ObnamError> {
+ let runtime = SystemTime::now();
+
+ let major = self.backup_version.unwrap_or(DEFAULT_SCHEMA_MAJOR);
+ let schema = schema_version(major)?;
+
+ let mut client = BackupClient::new(config)?;
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, current_timestamp(), vec![])))
+ .unwrap();
+ let genlist = client.list_generations(&trust);
+
+ let temp = tempdir()?;
+ let oldtemp = temp.path().join("old.db");
+ let newtemp = temp.path().join("new.db");
+
+ let old_id = if self.full {
+ None
+ } else {
+ match genlist.resolve("latest") {
+ Err(_) => None,
+ Ok(old_id) => Some(old_id),
}
- Some(old) => {
- info!("incremental backup based on {}", old);
- let old = run.client().fetch_generation(&old, &oldname)?;
- run.progress()
- .files_in_previous_generation(old.file_count()? as u64);
- new.insert_iter(iter.map(|entry| run.backup_file_incrementally(entry, &old)))?;
+ };
+
+ let (is_incremental, outcome) = if let Some(old_id) = old_id {
+ info!("incremental backup based on {}", old_id);
+ let mut run = BackupRun::incremental(config, &mut client)?;
+ let old = run.start(Some(&old_id), &oldtemp, perf).await?;
+ (
+ true,
+ run.backup_roots(config, &old, &newtemp, schema, perf)
+ .await?,
+ )
+ } else {
+ info!("fresh backup without a previous generation");
+ let mut run = BackupRun::initial(config, &mut client)?;
+ let old = run.start(None, &oldtemp, perf).await?;
+ (
+ false,
+ run.backup_roots(config, &old, &newtemp, schema, perf)
+ .await?,
+ )
+ };
+
+ perf.start(Clock::GenerationUpload);
+ let mut trust = trust;
+ trust.append_backup(outcome.gen_id.as_chunk_id());
+ trust.finalize(current_timestamp());
+ let trust = trust.to_data_chunk()?;
+ let trust_id = client.upload_chunk(trust).await?;
+ perf.stop(Clock::GenerationUpload);
+ info!("uploaded new client-trust {}", trust_id);
+
+ for w in outcome.warnings.iter() {
+ println!("warning: {}", w);
+ }
+
+ if is_incremental && !outcome.new_cachedir_tags.is_empty() {
+ println!("New CACHEDIR.TAG files since the last backup:");
+ for t in &outcome.new_cachedir_tags {
+ println!("- {:?}", t);
}
+ println!("You can configure Obnam to ignore all such files by setting `exclude_cache_tag_directories` to `false`.");
}
- run.progress().finish();
- new.file_count()
- };
- // Upload the SQLite file, i.e., the named temporary file, which
- // still exists, since we persisted it above.
- let gen_id = run.client().upload_generation(&newname, buffer_size)?;
+ report_stats(
+ &runtime,
+ outcome.files_count,
+ &outcome.gen_id,
+ outcome.warnings.len(),
+ )?;
+
+ if is_incremental && !outcome.new_cachedir_tags.is_empty() {
+ Err(ObnamError::NewCachedirTagsFound)
+ } else {
+ Ok(())
+ }
+ }
+}
+
+fn report_stats(
+ runtime: &SystemTime,
+ file_count: FileId,
+ gen_id: &GenId,
+ num_warnings: usize,
+) -> Result<(), ObnamError> {
println!("status: OK");
+ println!("warnings: {}", num_warnings);
println!("duration: {}", runtime.elapsed()?.as_secs());
println!("file-count: {}", file_count);
println!("generation-id: {}", gen_id);
-
- // Delete the temporary file.q
- std::fs::remove_file(&newname)?;
- std::fs::remove_file(&oldname)?;
-
Ok(())
}
diff --git a/src/cmd/chunk.rs b/src/cmd/chunk.rs
new file mode 100644
index 0000000..293de20
--- /dev/null
+++ b/src/cmd/chunk.rs
@@ -0,0 +1,70 @@
+//! The `encrypt-chunk` and `decrypt-chunk` subcommands.
+
+use crate::chunk::DataChunk;
+use crate::chunkmeta::ChunkMeta;
+use crate::cipher::CipherEngine;
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+use clap::Parser;
+use std::path::PathBuf;
+
+/// Encrypt a chunk.
+#[derive(Debug, Parser)]
+pub struct EncryptChunk {
+ /// The name of the file containing the cleartext chunk.
+ filename: PathBuf,
+
+ /// Name of file where to write the encrypted chunk.
+ output: PathBuf,
+
+ /// Chunk metadata as JSON.
+ json: String,
+}
+
+impl EncryptChunk {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let pass = config.passwords()?;
+ let cipher = CipherEngine::new(&pass);
+
+ let meta = ChunkMeta::from_json(&self.json)?;
+
+ let cleartext = std::fs::read(&self.filename)?;
+ let chunk = DataChunk::new(cleartext, meta);
+ let encrypted = cipher.encrypt_chunk(&chunk)?;
+
+ std::fs::write(&self.output, encrypted.ciphertext())?;
+
+ Ok(())
+ }
+}
+
+/// Decrypt a chunk.
+#[derive(Debug, Parser)]
+pub struct DecryptChunk {
+ /// Name of file containing encrypted chunk.
+ filename: PathBuf,
+
+ /// Name of file where to write the cleartext chunk.
+ output: PathBuf,
+
+ /// Chunk metadata as JSON.
+ json: String,
+}
+
+impl DecryptChunk {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let pass = config.passwords()?;
+ let cipher = CipherEngine::new(&pass);
+
+ let meta = ChunkMeta::from_json(&self.json)?;
+
+ let encrypted = std::fs::read(&self.filename)?;
+ let chunk = cipher.decrypt_chunk(&encrypted, &meta.to_json_vec())?;
+
+ std::fs::write(&self.output, chunk.data())?;
+
+ Ok(())
+ }
+}
diff --git a/src/cmd/chunkify.rs b/src/cmd/chunkify.rs
new file mode 100644
index 0000000..91cb0be
--- /dev/null
+++ b/src/cmd/chunkify.rs
@@ -0,0 +1,110 @@
+//! The `chunkify` subcommand.
+
+use crate::config::ClientConfig;
+use crate::engine::Engine;
+use crate::error::ObnamError;
+use crate::workqueue::WorkQueue;
+use clap::Parser;
+use serde::Serialize;
+use sha2::{Digest, Sha256};
+use std::path::PathBuf;
+use tokio::fs::File;
+use tokio::io::{AsyncReadExt, BufReader};
+use tokio::runtime::Runtime;
+use tokio::sync::mpsc;
+
+// Size of queue with unprocessed chunks, and also queue of computed
+// checksums.
+const Q: usize = 8;
+
+/// Split files into chunks and show their metadata.
+#[derive(Debug, Parser)]
+pub struct Chunkify {
+ /// Names of files to split into chunks.
+ filenames: Vec<PathBuf>,
+}
+
+impl Chunkify {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
+
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let mut q = WorkQueue::new(Q);
+ for filename in self.filenames.iter() {
+ tokio::spawn(split_file(
+ filename.to_path_buf(),
+ config.chunk_size,
+ q.push(),
+ ));
+ }
+ q.close();
+
+ let mut summer = Engine::new(q, just_hash);
+
+ let mut checksums = vec![];
+ while let Some(sum) = summer.next().await {
+ checksums.push(sum);
+ }
+
+ println!("{}", serde_json::to_string_pretty(&checksums)?);
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, Clone)]
+struct Chunk {
+ filename: PathBuf,
+ offset: u64,
+ data: Vec<u8>,
+}
+
+#[derive(Debug, Clone, Serialize)]
+struct Checksum {
+ filename: PathBuf,
+ offset: u64,
+ pub len: u64,
+ checksum: String,
+}
+
+async fn split_file(filename: PathBuf, chunk_size: usize, tx: mpsc::Sender<Chunk>) {
+ // println!("split_file {}", filename.display());
+ let mut file = BufReader::new(File::open(&*filename).await.unwrap());
+
+ let mut offset = 0;
+ loop {
+ let mut data = vec![0; chunk_size];
+ let n = file.read(&mut data).await.unwrap();
+ if n == 0 {
+ break;
+ }
+ let data: Vec<u8> = data[..n].to_vec();
+
+ let chunk = Chunk {
+ filename: filename.clone(),
+ offset,
+ data,
+ };
+ tx.send(chunk).await.unwrap();
+ // println!("split_file sent chunk at offset {}", offset);
+
+ offset += n as u64;
+ }
+ // println!("split_file EOF at {}", offset);
+}
+
+fn just_hash(chunk: Chunk) -> Checksum {
+ let mut hasher = Sha256::new();
+ hasher.update(&chunk.data);
+ let hash = hasher.finalize();
+ let hash = format!("{:x}", hash);
+ Checksum {
+ filename: chunk.filename,
+ offset: chunk.offset,
+ len: chunk.data.len() as u64,
+ checksum: hash,
+ }
+}
diff --git a/src/cmd/gen_info.rs b/src/cmd/gen_info.rs
new file mode 100644
index 0000000..901a0ae
--- /dev/null
+++ b/src/cmd/gen_info.rs
@@ -0,0 +1,47 @@
+//! The `gen-info` subcommand.
+
+use crate::chunk::ClientTrust;
+use crate::client::BackupClient;
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+use clap::Parser;
+use log::info;
+use tempfile::NamedTempFile;
+use tokio::runtime::Runtime;
+
+/// Show metadata for a generation.
+#[derive(Debug, Parser)]
+pub struct GenInfo {
+ /// Reference of the generation.
+ gen_ref: String,
+}
+
+impl GenInfo {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
+
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let temp = NamedTempFile::new()?;
+
+ let client = BackupClient::new(config)?;
+
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, "".to_string(), vec![])))
+ .unwrap();
+
+ let genlist = client.list_generations(&trust);
+ let gen_id = genlist.resolve(&self.gen_ref)?;
+ info!("generation id is {}", gen_id.as_chunk_id());
+
+ let gen = client.fetch_generation(&gen_id, temp.path()).await?;
+ let meta = gen.meta()?;
+ println!("{}", serde_json::to_string_pretty(&meta)?);
+
+ Ok(())
+ }
+}
diff --git a/src/cmd/get_chunk.rs b/src/cmd/get_chunk.rs
index bf653ff..1561492 100644
--- a/src/cmd/get_chunk.rs
+++ b/src/cmd/get_chunk.rs
@@ -1,15 +1,34 @@
+//! The `get-chunk` subcommand.
+
use crate::chunkid::ChunkId;
use crate::client::BackupClient;
-use crate::client::ClientConfig;
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+use clap::Parser;
use std::io::{stdout, Write};
+use tokio::runtime::Runtime;
+
+/// Fetch a chunk from the server.
+#[derive(Debug, Parser)]
+pub struct GetChunk {
+ /// Identifier of chunk to fetch.
+ chunk_id: String,
+}
-pub fn get_chunk(config: &ClientConfig, chunk_id: &str) -> anyhow::Result<()> {
- let client = BackupClient::new(&config.server_url)?;
- let chunk_id: ChunkId = chunk_id.parse().unwrap();
- let chunk = client.fetch_chunk(&chunk_id)?;
+impl GetChunk {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
- let stdout = stdout();
- let mut handle = stdout.lock();
- handle.write_all(chunk.data())?;
- Ok(())
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let client = BackupClient::new(config)?;
+ let chunk_id: ChunkId = self.chunk_id.parse().unwrap();
+ let chunk = client.fetch_chunk(&chunk_id).await?;
+ let stdout = stdout();
+ let mut handle = stdout.lock();
+ handle.write_all(chunk.data())?;
+ Ok(())
+ }
}
diff --git a/src/cmd/init.rs b/src/cmd/init.rs
new file mode 100644
index 0000000..5950fbb
--- /dev/null
+++ b/src/cmd/init.rs
@@ -0,0 +1,33 @@
+//! The `init` subcommand.
+
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+use crate::passwords::{passwords_filename, Passwords};
+use clap::Parser;
+
+const PROMPT: &str = "Obnam passphrase: ";
+
+/// Initialize client by setting passwords.
+#[derive(Debug, Parser)]
+pub struct Init {
+ /// Only for testing.
+ #[clap(long)]
+ insecure_passphrase: Option<String>,
+}
+
+impl Init {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let passphrase = match &self.insecure_passphrase {
+ Some(x) => x.to_string(),
+ None => rpassword::prompt_password(PROMPT).unwrap(),
+ };
+
+ let passwords = Passwords::new(&passphrase);
+ let filename = passwords_filename(&config.filename);
+ passwords
+ .save(&filename)
+ .map_err(|err| ObnamError::PasswordSave(filename, err))?;
+ Ok(())
+ }
+}
diff --git a/src/cmd/inspect.rs b/src/cmd/inspect.rs
new file mode 100644
index 0000000..3b41075
--- /dev/null
+++ b/src/cmd/inspect.rs
@@ -0,0 +1,46 @@
+//! The `inspect` subcommand.
+
+use crate::backup_run::current_timestamp;
+use crate::chunk::ClientTrust;
+use crate::client::BackupClient;
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+
+use clap::Parser;
+use log::info;
+use tempfile::NamedTempFile;
+use tokio::runtime::Runtime;
+
+/// Make a backup.
+#[derive(Debug, Parser)]
+pub struct Inspect {
+ /// Reference to generation to inspect.
+ gen_id: String,
+}
+
+impl Inspect {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
+
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let temp = NamedTempFile::new()?;
+ let client = BackupClient::new(config)?;
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, current_timestamp(), vec![])))
+ .unwrap();
+ let genlist = client.list_generations(&trust);
+ let gen_id = genlist.resolve(&self.gen_id)?;
+ info!("generation id is {}", gen_id.as_chunk_id());
+
+ let gen = client.fetch_generation(&gen_id, temp.path()).await?;
+ let meta = gen.meta()?;
+ println!("schema_version: {}", meta.schema_version());
+
+ Ok(())
+ }
+}
diff --git a/src/cmd/list.rs b/src/cmd/list.rs
index 8766e34..8bc6978 100644
--- a/src/cmd/list.rs
+++ b/src/cmd/list.rs
@@ -1,12 +1,36 @@
-use crate::client::{BackupClient, ClientConfig};
+//! The `list` subcommand.
-pub fn list(config: &ClientConfig) -> anyhow::Result<()> {
- let client = BackupClient::new(&config.server_url)?;
+use crate::chunk::ClientTrust;
+use crate::client::BackupClient;
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+use clap::Parser;
+use tokio::runtime::Runtime;
- let generations = client.list_generations()?;
- for finished in generations.iter() {
- println!("{} {}", finished.id(), finished.ended());
+/// List generations on the server.
+#[derive(Debug, Parser)]
+pub struct List {}
+
+impl List {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
}
- Ok(())
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let client = BackupClient::new(config)?;
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, "".to_string(), vec![])))
+ .unwrap();
+
+ let generations = client.list_generations(&trust);
+ for finished in generations.iter() {
+ println!("{} {}", finished.id(), finished.ended());
+ }
+
+ Ok(())
+ }
}
diff --git a/src/cmd/list_backup_versions.rs b/src/cmd/list_backup_versions.rs
new file mode 100644
index 0000000..c78ccfc
--- /dev/null
+++ b/src/cmd/list_backup_versions.rs
@@ -0,0 +1,31 @@
+//! The `backup` subcommand.
+
+use crate::config::ClientConfig;
+use crate::dbgen::{schema_version, DEFAULT_SCHEMA_MAJOR, SCHEMA_MAJORS};
+use crate::error::ObnamError;
+
+use clap::Parser;
+
+/// List supported backup schema versions.
+#[derive(Debug, Parser)]
+pub struct ListSchemaVersions {
+ /// List only the default version.
+ #[clap(long)]
+ default_only: bool,
+}
+
+impl ListSchemaVersions {
+ /// Run the command.
+ pub fn run(&self, _config: &ClientConfig) -> Result<(), ObnamError> {
+ if self.default_only {
+ let schema = schema_version(DEFAULT_SCHEMA_MAJOR)?;
+ println!("{}", schema);
+ } else {
+ for major in SCHEMA_MAJORS {
+ let schema = schema_version(*major)?;
+ println!("{}", schema);
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/src/cmd/list_files.rs b/src/cmd/list_files.rs
index a69c3df..e8276cd 100644
--- a/src/cmd/list_files.rs
+++ b/src/cmd/list_files.rs
@@ -1,36 +1,51 @@
+//! The `list-files` subcommand.
+
use crate::backup_reason::Reason;
+use crate::chunk::ClientTrust;
use crate::client::BackupClient;
-use crate::client::ClientConfig;
+use crate::config::ClientConfig;
use crate::error::ObnamError;
use crate::fsentry::{FilesystemEntry, FilesystemKind};
+use clap::Parser;
use tempfile::NamedTempFile;
+use tokio::runtime::Runtime;
-pub fn list_files(config: &ClientConfig, gen_ref: &str) -> anyhow::Result<()> {
- // Create a named temporary file. We don't meed the open file
- // handle, so we discard that.
- let dbname = {
- let temp = NamedTempFile::new()?;
- let (_, dbname) = temp.keep()?;
- dbname
- };
+/// List files in a backup.
+#[derive(Debug, Parser)]
+pub struct ListFiles {
+ /// Reference to backup to list files in.
+ #[clap(default_value = "latest")]
+ gen_id: String,
+}
+
+impl ListFiles {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
- let client = BackupClient::new(&config.server_url)?;
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let temp = NamedTempFile::new()?;
- let genlist = client.list_generations()?;
- let gen_id: String = match genlist.resolve(gen_ref) {
- None => return Err(ObnamError::UnknownGeneration(gen_ref.to_string()).into()),
- Some(id) => id,
- };
+ let client = BackupClient::new(config)?;
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, "".to_string(), vec![])))
+ .unwrap();
- let gen = client.fetch_generation(&gen_id, &dbname)?;
- for file in gen.files()? {
- println!("{}", format_entry(&file.entry(), file.reason()));
- }
+ let genlist = client.list_generations(&trust);
+ let gen_id = genlist.resolve(&self.gen_id)?;
- // Delete the temporary file.
- std::fs::remove_file(&dbname)?;
+ let gen = client.fetch_generation(&gen_id, temp.path()).await?;
+ for file in gen.files()?.iter()? {
+ let (_, entry, reason, _) = file?;
+ println!("{}", format_entry(&entry, reason));
+ }
- Ok(())
+ Ok(())
+ }
}
fn format_entry(e: &FilesystemEntry, reason: Reason) -> String {
@@ -38,6 +53,8 @@ fn format_entry(e: &FilesystemEntry, reason: Reason) -> String {
FilesystemKind::Regular => "-",
FilesystemKind::Directory => "d",
FilesystemKind::Symlink => "l",
+ FilesystemKind::Socket => "s",
+ FilesystemKind::Fifo => "p",
};
format!("{} {} ({})", kind, e.pathbuf().display(), reason)
}
diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs
index 8f08668..af7457b 100644
--- a/src/cmd/mod.rs
+++ b/src/cmd/mod.rs
@@ -1,17 +1,16 @@
-mod backup;
-pub use backup::backup;
-
-mod list;
-pub use list::list;
-
-mod list_files;
-pub use list_files::list_files;
-
-pub mod restore;
-pub use restore::restore;
+//! Subcommand implementations.
+pub mod backup;
+pub mod chunk;
+pub mod chunkify;
+pub mod gen_info;
pub mod get_chunk;
-pub use get_chunk::get_chunk;
-
+pub mod init;
+pub mod inspect;
+pub mod list;
+pub mod list_backup_versions;
+pub mod list_files;
+pub mod resolve;
+pub mod restore;
+pub mod show_config;
pub mod show_gen;
-pub use show_gen::show_generation;
diff --git a/src/cmd/resolve.rs b/src/cmd/resolve.rs
new file mode 100644
index 0000000..a7774d7
--- /dev/null
+++ b/src/cmd/resolve.rs
@@ -0,0 +1,44 @@
+//! The `resolve` subcommand.
+
+use crate::chunk::ClientTrust;
+use crate::client::BackupClient;
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+use clap::Parser;
+use tokio::runtime::Runtime;
+
+/// Resolve a generation reference into a generation id.
+#[derive(Debug, Parser)]
+pub struct Resolve {
+ /// The generation reference.
+ generation: String,
+}
+
+impl Resolve {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
+
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let client = BackupClient::new(config)?;
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, "".to_string(), vec![])))
+ .unwrap();
+ let generations = client.list_generations(&trust);
+
+ match generations.resolve(&self.generation) {
+ Err(err) => {
+ return Err(err.into());
+ }
+ Ok(gen_id) => {
+ println!("{}", gen_id.as_chunk_id());
+ }
+ };
+
+ Ok(())
+ }
+}
diff --git a/src/cmd/restore.rs b/src/cmd/restore.rs
index d783a70..58caf61 100644
--- a/src/cmd/restore.rs
+++ b/src/cmd/restore.rs
@@ -1,101 +1,165 @@
-use crate::client::BackupClient;
-use crate::client::ClientConfig;
+//! The `restore` subcommand.
+
+use crate::backup_reason::Reason;
+use crate::chunk::ClientTrust;
+use crate::client::{BackupClient, ClientError};
+use crate::config::ClientConfig;
+use crate::db::DatabaseError;
+use crate::dbgen::FileId;
use crate::error::ObnamError;
use crate::fsentry::{FilesystemEntry, FilesystemKind};
-use crate::generation::LocalGeneration;
+use crate::generation::{LocalGeneration, LocalGenerationError};
+use clap::Parser;
use indicatif::{ProgressBar, ProgressStyle};
-use libc::{fchmod, futimens, timespec};
+use libc::{chmod, mkfifo, timespec, utimensat, AT_FDCWD, AT_SYMLINK_NOFOLLOW};
use log::{debug, error, info};
-use std::fs::File;
+use std::ffi::CString;
use std::io::prelude::*;
use std::io::Error;
+use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::symlink;
-use std::os::unix::io::AsRawFd;
+use std::os::unix::net::UnixListener;
+use std::path::StripPrefixError;
use std::path::{Path, PathBuf};
-use structopt::StructOpt;
use tempfile::NamedTempFile;
+use tokio::runtime::Runtime;
+
+/// Restore a backup.
+#[derive(Debug, Parser)]
+pub struct Restore {
+ /// Reference to generation to restore.
+ gen_id: String,
+
+ /// Path to directory where restored files are written.
+ to: PathBuf,
+}
+
+impl Restore {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
-pub fn restore(config: &ClientConfig, gen_ref: &str, to: &Path) -> anyhow::Result<()> {
- // Create a named temporary file. We don't meed the open file
- // handle, so we discard that.
- let dbname = {
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
let temp = NamedTempFile::new()?;
- let (_, dbname) = temp.keep()?;
- dbname
- };
- let client = BackupClient::new(&config.server_url)?;
+ let client = BackupClient::new(config)?;
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, "".to_string(), vec![])))
+ .unwrap();
- let genlist = client.list_generations()?;
- let gen_id: String = match genlist.resolve(gen_ref) {
- None => return Err(ObnamError::UnknownGeneration(gen_ref.to_string()).into()),
- Some(id) => id,
- };
- info!("generation id is {}", gen_id);
+ let genlist = client.list_generations(&trust);
+ let gen_id = genlist.resolve(&self.gen_id)?;
+ info!("generation id is {}", gen_id.as_chunk_id());
- let gen = client.fetch_generation(&gen_id, &dbname)?;
- info!("restoring {} files", gen.file_count()?);
- let progress = create_progress_bar(gen.file_count()?, true);
- for file in gen.files()? {
- restore_generation(&client, &gen, file.fileno(), file.entry(), &to, &progress)?;
- }
- for file in gen.files()? {
- if file.entry().is_dir() {
- restore_directory_metadata(file.entry(), &to)?;
+ let gen = client.fetch_generation(&gen_id, temp.path()).await?;
+ info!("restoring {} files", gen.file_count()?);
+ let progress = create_progress_bar(gen.file_count()?, true);
+ for file in gen.files()?.iter()? {
+ let (fileno, entry, reason, _) = file?;
+ match reason {
+ Reason::FileError => (),
+ _ => restore_generation(&client, &gen, fileno, &entry, &self.to, &progress).await?,
+ }
+ }
+ for file in gen.files()?.iter()? {
+ let (_, entry, _, _) = file?;
+ if entry.is_dir() {
+ restore_directory_metadata(&entry, &self.to)?;
+ }
}
+ progress.finish();
+
+ Ok(())
}
- progress.finish();
+}
- // Delete the temporary file.
- std::fs::remove_file(&dbname)?;
+/// Possible errors from restoring.
+#[derive(Debug, thiserror::Error)]
+pub enum RestoreError {
+ /// An error using a Database.
+ #[error(transparent)]
+ Database(#[from] DatabaseError),
- Ok(())
-}
+ /// Failed to create a name pipe.
+ #[error("Could not create named pipe (FIFO) {0}")]
+ NamedPipeCreationError(PathBuf),
-#[derive(Debug, StructOpt)]
-#[structopt(name = "obnam-backup", about = "Simplistic backup client")]
-struct Opt {
- #[structopt(parse(from_os_str))]
- config: PathBuf,
+ /// Error from HTTP client.
+ #[error(transparent)]
+ ClientError(#[from] ClientError),
- #[structopt()]
- gen_id: String,
+ /// Error from local generation.
+ #[error(transparent)]
+ LocalGenerationError(#[from] LocalGenerationError),
- #[structopt(parse(from_os_str))]
- dbname: PathBuf,
+ /// Error removing a prefix.
+ #[error(transparent)]
+ StripPrefixError(#[from] StripPrefixError),
- #[structopt(parse(from_os_str))]
- to: PathBuf,
+ /// Error creating a directory.
+ #[error("failed to create directory {0}: {1}")]
+ CreateDirs(PathBuf, std::io::Error),
+
+ /// Error creating a file.
+ #[error("failed to create file {0}: {1}")]
+ CreateFile(PathBuf, std::io::Error),
+
+ /// Error writing a file.
+ #[error("failed to write file {0}: {1}")]
+ WriteFile(PathBuf, std::io::Error),
+
+ /// Error creating a symbolic link.
+ #[error("failed to create symbolic link {0}: {1}")]
+ Symlink(PathBuf, std::io::Error),
+
+ /// Error creating a UNIX domain socket.
+ #[error("failed to create UNIX domain socket {0}: {1}")]
+ UnixBind(PathBuf, std::io::Error),
+
+ /// Error setting permissions.
+ #[error("failed to set permissions for {0}: {1}")]
+ Chmod(PathBuf, std::io::Error),
+
+ /// Error settting timestamp.
+ #[error("failed to set timestamp for {0}: {1}")]
+ SetTimestamp(PathBuf, std::io::Error),
}
-fn restore_generation(
+async fn restore_generation(
client: &BackupClient,
gen: &LocalGeneration,
- fileid: i64,
+ fileid: FileId,
entry: &FilesystemEntry,
to: &Path,
progress: &ProgressBar,
-) -> anyhow::Result<()> {
+) -> Result<(), RestoreError> {
info!("restoring {:?}", entry);
- progress.set_message(&format!("{}", entry.pathbuf().display()));
+ progress.set_message(format!("{}", entry.pathbuf().display()));
progress.inc(1);
let to = restored_path(entry, to)?;
match entry.kind() {
- FilesystemKind::Regular => restore_regular(client, &gen, &to, fileid, &entry)?,
+ FilesystemKind::Regular => restore_regular(client, gen, &to, fileid, entry).await?,
FilesystemKind::Directory => restore_directory(&to)?,
- FilesystemKind::Symlink => restore_symlink(&to, &entry)?,
+ FilesystemKind::Symlink => restore_symlink(&to, entry)?,
+ FilesystemKind::Socket => restore_socket(&to, entry)?,
+ FilesystemKind::Fifo => restore_fifo(&to, entry)?,
}
Ok(())
}
-fn restore_directory(path: &Path) -> anyhow::Result<()> {
+fn restore_directory(path: &Path) -> Result<(), RestoreError> {
debug!("restoring directory {}", path.display());
- std::fs::create_dir_all(path)?;
+ std::fs::create_dir_all(path)
+ .map_err(|err| RestoreError::CreateDirs(path.to_path_buf(), err))?;
Ok(())
}
-fn restore_directory_metadata(entry: &FilesystemEntry, to: &Path) -> anyhow::Result<()> {
+fn restore_directory_metadata(entry: &FilesystemEntry, to: &Path) -> Result<(), RestoreError> {
let to = restored_path(entry, to)?;
match entry.kind() {
FilesystemKind::Directory => restore_metadata(&to, entry)?,
@@ -107,7 +171,7 @@ fn restore_directory_metadata(entry: &FilesystemEntry, to: &Path) -> anyhow::Res
Ok(())
}
-fn restored_path(entry: &FilesystemEntry, to: &Path) -> anyhow::Result<PathBuf> {
+fn restored_path(entry: &FilesystemEntry, to: &Path) -> Result<PathBuf, RestoreError> {
let path = &entry.pathbuf();
let path = if path.is_absolute() {
path.strip_prefix("/")?
@@ -117,22 +181,26 @@ fn restored_path(entry: &FilesystemEntry, to: &Path) -> anyhow::Result<PathBuf>
Ok(to.join(path))
}
-fn restore_regular(
+async fn restore_regular(
client: &BackupClient,
gen: &LocalGeneration,
path: &Path,
- fileid: i64,
+ fileid: FileId,
entry: &FilesystemEntry,
-) -> anyhow::Result<()> {
+) -> Result<(), RestoreError> {
debug!("restoring regular {}", path.display());
let parent = path.parent().unwrap();
debug!(" mkdir {}", parent.display());
- std::fs::create_dir_all(parent)?;
+ std::fs::create_dir_all(parent)
+ .map_err(|err| RestoreError::CreateDirs(parent.to_path_buf(), err))?;
{
- let mut file = std::fs::File::create(path)?;
- for chunkid in gen.chunkids(fileid)? {
- let chunk = client.fetch_chunk(&chunkid)?;
- file.write_all(chunk.data())?;
+ let mut file = std::fs::File::create(path)
+ .map_err(|err| RestoreError::CreateFile(path.to_path_buf(), err))?;
+ for chunkid in gen.chunkids(fileid)?.iter()? {
+ let chunkid = chunkid?;
+ let chunk = client.fetch_chunk(&chunkid).await?;
+ file.write_all(chunk.data())
+ .map_err(|err| RestoreError::WriteFile(path.to_path_buf(), err))?;
}
restore_metadata(path, entry)?;
}
@@ -140,24 +208,44 @@ fn restore_regular(
Ok(())
}
-fn restore_symlink(path: &Path, entry: &FilesystemEntry) -> anyhow::Result<()> {
+fn restore_symlink(path: &Path, entry: &FilesystemEntry) -> Result<(), RestoreError> {
debug!("restoring symlink {}", path.display());
let parent = path.parent().unwrap();
debug!(" mkdir {}", parent.display());
if !parent.exists() {
- std::fs::create_dir_all(parent)?;
- {
- symlink(path, entry.symlink_target().unwrap())?;
+ std::fs::create_dir_all(parent)
+ .map_err(|err| RestoreError::CreateDirs(parent.to_path_buf(), err))?;
+ }
+ symlink(entry.symlink_target().unwrap(), path)
+ .map_err(|err| RestoreError::Symlink(path.to_path_buf(), err))?;
+ restore_metadata(path, entry)?;
+ debug!("restored symlink {}", path.display());
+ Ok(())
+}
+
+fn restore_socket(path: &Path, entry: &FilesystemEntry) -> Result<(), RestoreError> {
+ debug!("creating Unix domain socket {:?}", path);
+ UnixListener::bind(path).map_err(|err| RestoreError::UnixBind(path.to_path_buf(), err))?;
+ restore_metadata(path, entry)?;
+ Ok(())
+}
+
+fn restore_fifo(path: &Path, entry: &FilesystemEntry) -> Result<(), RestoreError> {
+ debug!("creating fifo {:?}", path);
+ let filename = path_to_cstring(path);
+ match unsafe { mkfifo(filename.as_ptr(), 0) } {
+ -1 => {
+ return Err(RestoreError::NamedPipeCreationError(path.to_path_buf()));
}
+ _ => restore_metadata(path, entry)?,
}
- debug!("restored regular {}", path.display());
Ok(())
}
-fn restore_metadata(path: &Path, entry: &FilesystemEntry) -> anyhow::Result<()> {
+fn restore_metadata(path: &Path, entry: &FilesystemEntry) -> Result<(), RestoreError> {
debug!("restoring metadata for {}", entry.pathbuf().display());
- let handle = File::open(path)?;
+ debug!("restoring metadata for {:?}", path);
let atime = timespec {
tv_sec: entry.atime(),
@@ -170,41 +258,58 @@ fn restore_metadata(path: &Path, entry: &FilesystemEntry) -> anyhow::Result<()>
let times = [atime, mtime];
let times: *const timespec = &times[0];
+ let pathbuf = path.to_path_buf();
+ let path = path_to_cstring(path);
+
// We have to use unsafe here to be able call the libc functions
// below.
unsafe {
- let fd = handle.as_raw_fd(); // FIXME: needs to NOT follow symlinks
-
- debug!("fchmod");
- if fchmod(fd, entry.mode()) == -1 {
- let error = Error::last_os_error();
- error!("fchmod failed on {}", path.display());
- return Err(error.into());
+ if entry.kind() != FilesystemKind::Symlink {
+ debug!("chmod {:?}", path);
+ if chmod(path.as_ptr(), entry.mode() as libc::mode_t) == -1 {
+ let error = Error::last_os_error();
+ error!("chmod failed on {:?}", path);
+ return Err(RestoreError::Chmod(pathbuf, error));
+ }
+ } else {
+ debug!(
+ "skipping chmod of a symlink because it'll attempt to change the pointed-at file"
+ );
}
- debug!("futimens");
- if futimens(fd, times) == -1 {
+ debug!("utimens {:?}", path);
+ if utimensat(AT_FDCWD, path.as_ptr(), times, AT_SYMLINK_NOFOLLOW) == -1 {
let error = Error::last_os_error();
- error!("futimens failed on {}", path.display());
- return Err(error.into());
+ error!("utimensat failed on {:?}", path);
+ return Err(RestoreError::SetTimestamp(pathbuf, error));
}
}
Ok(())
}
-fn create_progress_bar(file_count: i64, verbose: bool) -> ProgressBar {
+fn path_to_cstring(path: &Path) -> CString {
+ let path = path.as_os_str();
+ let path = path.as_bytes();
+ CString::new(path).unwrap()
+}
+
+fn create_progress_bar(file_count: FileId, verbose: bool) -> ProgressBar {
let progress = if verbose {
ProgressBar::new(file_count as u64)
} else {
ProgressBar::hidden()
};
- let parts = vec![
+ let parts = [
"{wide_bar}",
"elapsed: {elapsed}",
"files: {pos}/{len}",
"current: {wide_msg}",
"{spinner}",
];
- progress.set_style(ProgressStyle::default_bar().template(&parts.join("\n")));
+ progress.set_style(
+ ProgressStyle::default_bar()
+ .template(&parts.join("\n"))
+ .expect("create indicatif ProgressStyle value"),
+ );
progress
}
diff --git a/src/cmd/show_config.rs b/src/cmd/show_config.rs
new file mode 100644
index 0000000..8e0ce30
--- /dev/null
+++ b/src/cmd/show_config.rs
@@ -0,0 +1,17 @@
+//! The `show-config` subcommand.
+
+use crate::config::ClientConfig;
+use crate::error::ObnamError;
+use clap::Parser;
+
+/// Show actual client configuration.
+#[derive(Debug, Parser)]
+pub struct ShowConfig {}
+
+impl ShowConfig {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ println!("{}", serde_json::to_string_pretty(config)?);
+ Ok(())
+ }
+}
diff --git a/src/cmd/show_gen.rs b/src/cmd/show_gen.rs
index d355389..95d3fd3 100644
--- a/src/cmd/show_gen.rs
+++ b/src/cmd/show_gen.rs
@@ -1,46 +1,101 @@
+//! The `show-generation` subcommand.
+
+use crate::chunk::ClientTrust;
use crate::client::BackupClient;
-use crate::client::ClientConfig;
+use crate::config::ClientConfig;
+use crate::db::DbInt;
use crate::error::ObnamError;
use crate::fsentry::FilesystemKind;
+use crate::generation::GenId;
+use clap::Parser;
use indicatif::HumanBytes;
+use serde::Serialize;
use tempfile::NamedTempFile;
+use tokio::runtime::Runtime;
+
+/// Show information about a generation.
+#[derive(Debug, Parser)]
+pub struct ShowGeneration {
+ /// Reference to the generation. Defaults to latest.
+ #[clap(default_value = "latest")]
+ gen_id: String,
+}
-pub fn show_generation(config: &ClientConfig, gen_ref: &str) -> anyhow::Result<()> {
- // Create a named temporary file. We don't meed the open file
- // handle, so we discard that.
- let dbname = {
+impl ShowGeneration {
+ /// Run the command.
+ pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> {
+ let rt = Runtime::new()?;
+ rt.block_on(self.run_async(config))
+ }
+
+ async fn run_async(&self, config: &ClientConfig) -> Result<(), ObnamError> {
let temp = NamedTempFile::new()?;
- let (_, dbname) = temp.keep()?;
- dbname
- };
-
- let client = BackupClient::new(&config.server_url)?;
-
- let genlist = client.list_generations()?;
- let gen_id: String = match genlist.resolve(gen_ref) {
- None => return Err(ObnamError::UnknownGeneration(gen_ref.to_string()).into()),
- Some(id) => id,
- };
-
- let gen = client.fetch_generation(&gen_id, &dbname)?;
- let files = gen.files()?;
-
- let total_bytes = files.iter().fold(0, |acc, file| {
- let e = file.entry();
- if e.kind() == FilesystemKind::Regular {
- acc + file.entry().len()
- } else {
- acc
+ let client = BackupClient::new(config)?;
+ let trust = client
+ .get_client_trust()
+ .await?
+ .or_else(|| Some(ClientTrust::new("FIXME", None, "".to_string(), vec![])))
+ .unwrap();
+
+ let genlist = client.list_generations(&trust);
+ let gen_id = genlist.resolve(&self.gen_id)?;
+ let gen = client.fetch_generation(&gen_id, temp.path()).await?;
+ let mut files = gen.files()?;
+ let mut files = files.iter()?;
+
+ let total_bytes = files.try_fold(0, |acc, file| {
+ file.map(|(_, e, _, _)| {
+ if e.kind() == FilesystemKind::Regular {
+ acc + e.len()
+ } else {
+ acc
+ }
+ })
+ });
+ let total_bytes = total_bytes?;
+
+ let output = Output::new(gen_id)
+ .db_bytes(temp.path().metadata()?.len())
+ .file_count(gen.file_count()?)
+ .file_bytes(total_bytes);
+ serde_json::to_writer_pretty(std::io::stdout(), &output)?;
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, Default, Serialize)]
+struct Output {
+ generation_id: String,
+ file_count: DbInt,
+ file_bytes: String,
+ file_bytes_raw: u64,
+ db_bytes: String,
+ db_bytes_raw: u64,
+}
+
+impl Output {
+ fn new(gen_id: GenId) -> Self {
+ Self {
+ generation_id: format!("{}", gen_id),
+ ..Self::default()
}
- });
+ }
- println!("generation-id: {}", gen_id);
- println!("file-count: {}", gen.file_count()?);
- println!("file-bytes: {}", HumanBytes(total_bytes));
- println!("file-bytes-raw: {}", total_bytes);
+ fn file_count(mut self, n: DbInt) -> Self {
+ self.file_count = n;
+ self
+ }
- // Delete the temporary file.
- std::fs::remove_file(&dbname)?;
+ fn file_bytes(mut self, n: u64) -> Self {
+ self.file_bytes_raw = n;
+ self.file_bytes = HumanBytes(n).to_string();
+ self
+ }
- Ok(())
+ fn db_bytes(mut self, n: u64) -> Self {
+ self.db_bytes_raw = n;
+ self.db_bytes = HumanBytes(n).to_string();
+ self
+ }
}