diff options
Diffstat (limited to 'src/cmd')
-rw-r--r-- | src/cmd/backup.rs | 178 | ||||
-rw-r--r-- | src/cmd/chunk.rs | 70 | ||||
-rw-r--r-- | src/cmd/chunkify.rs | 110 | ||||
-rw-r--r-- | src/cmd/gen_info.rs | 47 | ||||
-rw-r--r-- | src/cmd/get_chunk.rs | 37 | ||||
-rw-r--r-- | src/cmd/init.rs | 33 | ||||
-rw-r--r-- | src/cmd/inspect.rs | 46 | ||||
-rw-r--r-- | src/cmd/list.rs | 38 | ||||
-rw-r--r-- | src/cmd/list_backup_versions.rs | 31 | ||||
-rw-r--r-- | src/cmd/list_files.rs | 61 | ||||
-rw-r--r-- | src/cmd/mod.rs | 27 | ||||
-rw-r--r-- | src/cmd/resolve.rs | 44 | ||||
-rw-r--r-- | src/cmd/restore.rs | 275 | ||||
-rw-r--r-- | src/cmd/show_config.rs | 17 | ||||
-rw-r--r-- | src/cmd/show_gen.rs | 123 |
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 = ×[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 + } } |