diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-04-10 07:31:35 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-04-10 07:31:35 +0000 |
commit | 961f3d5373c3185db8ba16ce9af3f35f876772ae (patch) | |
tree | f9d54086c43a491b8df9bdcafbe61b70c39d2956 | |
parent | a7bbd6d91139093b488c9ac4b543f24ede23a495 (diff) | |
parent | 3295950f41342b8b9a312f203b111c7c277500b4 (diff) | |
download | obnam2-961f3d5373c3185db8ba16ce9af3f35f876772ae.tar.gz |
Merge branch 'cli-handling' into 'main'
refactor: for clarity
See merge request larswirzenius/obnam!134
-rw-r--r-- | src/backup_run.rs | 3 | ||||
-rw-r--r-- | src/bin/obnam.rs | 108 | ||||
-rw-r--r-- | src/client.rs | 122 | ||||
-rw-r--r-- | src/cmd/backup.rs | 35 | ||||
-rw-r--r-- | src/cmd/get_chunk.rs | 27 | ||||
-rw-r--r-- | src/cmd/init.rs | 42 | ||||
-rw-r--r-- | src/cmd/list.rs | 23 | ||||
-rw-r--r-- | src/cmd/list_files.rs | 31 | ||||
-rw-r--r-- | src/cmd/mod.rs | 27 | ||||
-rw-r--r-- | src/cmd/restore.rs | 62 | ||||
-rw-r--r-- | src/cmd/show_config.rs | 14 | ||||
-rw-r--r-- | src/cmd/show_gen.rs | 51 | ||||
-rw-r--r-- | src/config.rs | 124 | ||||
-rw-r--r-- | src/error.rs | 3 | ||||
-rw-r--r-- | src/lib.rs | 1 |
15 files changed, 359 insertions, 314 deletions
diff --git a/src/backup_run.rs b/src/backup_run.rs index e9d094d..e966855 100644 --- a/src/backup_run.rs +++ b/src/backup_run.rs @@ -1,7 +1,8 @@ use crate::backup_progress::BackupProgress; use crate::backup_reason::Reason; use crate::chunkid::ChunkId; -use crate::client::{BackupClient, ClientConfig, ClientError}; +use crate::client::{BackupClient, ClientError}; +use crate::config::ClientConfig; use crate::error::ObnamError; use crate::fsentry::FilesystemEntry; use crate::fsiter::{FsIterError, FsIterResult}; diff --git a/src/bin/obnam.rs b/src/bin/obnam.rs index 2dbbaa2..cdb5179 100644 --- a/src/bin/obnam.rs +++ b/src/bin/obnam.rs @@ -1,11 +1,16 @@ use directories_next::ProjectDirs; use log::{debug, error, info, LevelFilter}; use log4rs::append::file::FileAppender; -use log4rs::config::{Appender, Config, Logger, Root}; -use obnam::client::ClientConfig; -use obnam::cmd::{ - backup, get_chunk, init, list, list_files, restore, show_config, show_generation, -}; +use log4rs::config::{Appender, Logger, Root}; +use obnam::cmd::backup::Backup; +use obnam::cmd::get_chunk::GetChunk; +use obnam::cmd::init::Init; +use obnam::cmd::list::List; +use obnam::cmd::list_files::ListFiles; +use obnam::cmd::restore::Restore; +use obnam::cmd::show_config::ShowConfig; +use obnam::cmd::show_gen::ShowGeneration; +use obnam::config::ClientConfig; use std::path::{Path, PathBuf}; use structopt::StructOpt; @@ -22,25 +27,20 @@ fn main() -> anyhow::Result<()> { debug!("{:?}", opt); debug!("configuration: {:#?}", config); - let cfgname = config_filename(&opt); - let result = if let Command::Init { - insecure_passphrase, - } = opt.cmd - { - init(config.config(), &cfgname, insecure_passphrase) - } else { - let config = load_config_with_passwords(&opt)?; - match opt.cmd { - Command::Init { - insecure_passphrase: _, - } => panic!("this cannot happen"), - Command::Backup => backup(&config), - Command::List => list(&config), - Command::ShowGeneration { gen_id } => show_generation(&config, &gen_id), - Command::ListFiles { gen_id } => list_files(&config, &gen_id), - Command::Restore { gen_id, to } => restore(&config, &gen_id, &to), - Command::GetChunk { chunk_id } => get_chunk(&config, &chunk_id), - Command::Config => show_config(&config), + let result = match opt.cmd { + Command::Init(x) => x.run(config.config()), + _ => { + let config = load_config_with_passwords(&opt)?; + match opt.cmd { + Command::Init(_) => panic!("this can't happen"), + Command::Backup(x) => x.run(&config), + Command::List(x) => x.run(&config), + Command::ShowGeneration(x) => x.run(&config), + Command::ListFiles(x) => x.run(&config), + Command::Restore(x) => x.run(&config), + Command::GetChunk(x) => x.run(&config), + Command::Config(x) => x.run(&config), + } } }; @@ -53,6 +53,19 @@ fn main() -> anyhow::Result<()> { Ok(()) } +fn setup_logging(filename: &Path) -> anyhow::Result<()> { + let logfile = FileAppender::builder().build(filename)?; + + let config = log4rs::Config::builder() + .appender(Appender::builder().build("obnam", Box::new(logfile))) + .logger(Logger::builder().build("obnam", LevelFilter::Debug)) + .build(Root::builder().appender("obnam").build(LevelFilter::Debug))?; + + log4rs::init_config(config)?; + + Ok(()) +} + fn load_config_with_passwords(opt: &Opt) -> Result<ClientConfig, anyhow::Error> { Ok(ClientConfig::read_with_passwords(&config_filename(opt))?) } @@ -88,43 +101,12 @@ struct Opt { #[derive(Debug, StructOpt)] enum Command { - Init { - #[structopt(long)] - insecure_passphrase: Option<String>, - }, - Backup, - List, - ListFiles { - #[structopt(default_value = "latest")] - gen_id: String, - }, - Restore { - #[structopt()] - gen_id: String, - - #[structopt(parse(from_os_str))] - to: PathBuf, - }, - ShowGeneration { - #[structopt(default_value = "latest")] - gen_id: String, - }, - GetChunk { - #[structopt()] - chunk_id: String, - }, - Config, -} - -fn setup_logging(filename: &Path) -> anyhow::Result<()> { - let logfile = FileAppender::builder().build(filename)?; - - let config = Config::builder() - .appender(Appender::builder().build("obnam", Box::new(logfile))) - .logger(Logger::builder().build("obnam", LevelFilter::Debug)) - .build(Root::builder().appender("obnam").build(LevelFilter::Debug))?; - - log4rs::init_config(config)?; - - Ok(()) + Init(Init), + Backup(Backup), + List(List), + ListFiles(ListFiles), + Restore(Restore), + ShowGeneration(ShowGeneration), + GetChunk(GetChunk), + Config(ShowConfig), } diff --git a/src/client.rs b/src/client.rs index 37bfe91..1b33372 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,136 +4,18 @@ use crate::chunk::{GenerationChunk, GenerationChunkError}; use crate::chunker::{Chunker, ChunkerError}; use crate::chunkid::ChunkId; use crate::chunkmeta::ChunkMeta; +use crate::config::ClientConfig; use crate::fsentry::{FilesystemEntry, FilesystemKind}; use crate::generation::{FinishedGeneration, LocalGeneration, LocalGenerationError}; use crate::genlist::GenerationList; -use crate::passwords::{passwords_filename, PasswordError, Passwords}; -use bytesize::MIB; use chrono::{DateTime, Local}; use log::{debug, error, info, trace}; use reqwest::blocking::Client; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; -use std::path::{Path, PathBuf}; - -const DEFAULT_CHUNK_SIZE: usize = MIB as usize; -const DEVNULL: &str = "/dev/null"; - -#[derive(Debug, Deserialize, Clone)] -#[serde(deny_unknown_fields)] -struct TentativeClientConfig { - server_url: String, - verify_tls_cert: Option<bool>, - chunk_size: Option<usize>, - roots: Vec<PathBuf>, - log: Option<PathBuf>, - encrypt: Option<bool>, -} - -#[derive(Debug, Serialize, Clone)] -pub enum ClientConfig { - Plain(ClientConfigWithoutPasswords), - WithPasswords(ClientConfigWithoutPasswords, Passwords), -} - -impl ClientConfig { - pub fn read_without_passwords(filename: &Path) -> Result<Self, ClientConfigError> { - let config = ClientConfigWithoutPasswords::read_config(filename)?; - Ok(ClientConfig::Plain(config)) - } - - pub fn read_with_passwords(filename: &Path) -> Result<Self, ClientConfigError> { - let config = ClientConfigWithoutPasswords::read_config(filename)?; - if config.encrypt { - let passwords = Passwords::load(&passwords_filename(filename)) - .map_err(ClientConfigError::PasswordsMissing)?; - Ok(ClientConfig::WithPasswords(config, passwords)) - } else { - Ok(ClientConfig::Plain(config)) - } - } - - pub fn config(&self) -> &ClientConfigWithoutPasswords { - match self { - Self::Plain(config) => &config, - Self::WithPasswords(config, _) => &config, - } - } -} - -#[derive(Debug, Serialize, Clone)] -pub struct ClientConfigWithoutPasswords { - pub server_url: String, - pub verify_tls_cert: bool, - pub chunk_size: usize, - pub roots: Vec<PathBuf>, - pub log: PathBuf, - pub encrypt: bool, -} - -#[derive(Debug, thiserror::Error)] -pub enum ClientConfigError { - #[error("server_url is empty")] - ServerUrlIsEmpty, - - #[error("No backup roots in config; at least one is needed")] - NoBackupRoot, - - #[error("server URL doesn't use https: {0}")] - NotHttps(String), - - #[error("No passwords are set: you may need to run 'obnam init': {0}")] - PasswordsMissing(PasswordError), - - #[error(transparent)] - IoError(#[from] std::io::Error), - - #[error(transparent)] - SerdeYamlError(#[from] serde_yaml::Error), -} - -pub type ClientConfigResult<T> = Result<T, ClientConfigError>; - -impl ClientConfigWithoutPasswords { - pub fn read_config(filename: &Path) -> ClientConfigResult<Self> { - trace!("read_config: filename={:?}", filename); - let config = std::fs::read_to_string(filename)?; - let tentative: TentativeClientConfig = serde_yaml::from_str(&config)?; - - let encrypt = tentative.encrypt.or(Some(false)).unwrap(); - - let config = Self { - server_url: tentative.server_url, - roots: tentative.roots, - verify_tls_cert: tentative.verify_tls_cert.or(Some(false)).unwrap(), - chunk_size: tentative.chunk_size.or(Some(DEFAULT_CHUNK_SIZE)).unwrap(), - log: tentative - .log - .or_else(|| Some(PathBuf::from(DEVNULL))) - .unwrap(), - encrypt, - }; - - config.check()?; - Ok(config) - } - - fn check(&self) -> Result<(), ClientConfigError> { - if self.server_url.is_empty() { - return Err(ClientConfigError::ServerUrlIsEmpty); - } - if !self.server_url.starts_with("https://") { - return Err(ClientConfigError::NotHttps(self.server_url.to_string())); - } - if self.roots.is_empty() { - return Err(ClientConfigError::NoBackupRoot); - } - Ok(()) - } -} +use std::path::Path; #[derive(Debug, thiserror::Error)] pub enum ClientError { diff --git a/src/cmd/backup.rs b/src/cmd/backup.rs index bd36a3a..a0e0599 100644 --- a/src/cmd/backup.rs +++ b/src/cmd/backup.rs @@ -1,7 +1,8 @@ use crate::backup_progress::BackupProgress; use crate::backup_run::{BackupError, IncrementalBackup, InitialBackup}; use crate::chunkid::ChunkId; -use crate::client::{BackupClient, ClientConfig}; +use crate::client::BackupClient; +use crate::config::ClientConfig; use crate::error::ObnamError; use crate::fsiter::FsIterator; use crate::generation::NascentGeneration; @@ -9,27 +10,33 @@ use bytesize::MIB; use log::info; use std::path::Path; use std::time::SystemTime; +use structopt::StructOpt; use tempfile::NamedTempFile; const SQLITE_CHUNK_SIZE: usize = MIB as usize; -pub fn backup(config: &ClientConfig) -> Result<(), ObnamError> { - let runtime = SystemTime::now(); +#[derive(Debug, StructOpt)] +pub struct Backup {} - let client = BackupClient::new(config)?; - let genlist = client.list_generations()?; - let (gen_id, file_count, warnings) = match genlist.resolve("latest") { - Err(_) => initial_backup(&config, &client)?, - Ok(old_ref) => incremental_backup(&old_ref, &config, &client)?, - }; +impl Backup { + pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> { + let runtime = SystemTime::now(); - for w in warnings.iter() { - println!("warning: {}", w); - } + let client = BackupClient::new(config)?; + let genlist = client.list_generations()?; + let (gen_id, file_count, warnings) = match genlist.resolve("latest") { + Err(_) => initial_backup(&config, &client)?, + Ok(old_ref) => incremental_backup(&old_ref, &config, &client)?, + }; + + for w in warnings.iter() { + println!("warning: {}", w); + } - report_stats(&runtime, file_count, &gen_id, warnings.len())?; + report_stats(&runtime, file_count, &gen_id, warnings.len())?; - Ok(()) + Ok(()) + } } fn report_stats( diff --git a/src/cmd/get_chunk.rs b/src/cmd/get_chunk.rs index 385c4d5..4ee70fe 100644 --- a/src/cmd/get_chunk.rs +++ b/src/cmd/get_chunk.rs @@ -1,16 +1,25 @@ use crate::chunkid::ChunkId; use crate::client::BackupClient; -use crate::client::ClientConfig; +use crate::config::ClientConfig; use crate::error::ObnamError; use std::io::{stdout, Write}; +use structopt::StructOpt; -pub fn get_chunk(config: &ClientConfig, chunk_id: &str) -> Result<(), ObnamError> { - let client = BackupClient::new(config)?; - let chunk_id: ChunkId = chunk_id.parse().unwrap(); - let chunk = client.fetch_chunk(&chunk_id)?; +#[derive(Debug, StructOpt)] +pub struct GetChunk { + #[structopt()] + chunk_id: String, +} + +impl GetChunk { + pub fn run(&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)?; - let stdout = stdout(); - let mut handle = stdout.lock(); - handle.write_all(chunk.data())?; - Ok(()) + 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 index f0ddb69..cb61fba 100644 --- a/src/cmd/init.rs +++ b/src/cmd/init.rs @@ -1,28 +1,32 @@ -use crate::client::ClientConfigWithoutPasswords; +use crate::config::ClientConfigWithoutPasswords; use crate::error::ObnamError; use crate::passwords::{passwords_filename, Passwords}; -use std::path::Path; +use structopt::StructOpt; const PROMPT: &str = "Obnam passphrase: "; -pub fn init( - config: &ClientConfigWithoutPasswords, - config_filename: &Path, +#[derive(Debug, StructOpt)] +pub struct Init { + #[structopt(long)] insecure_passphrase: Option<String>, -) -> Result<(), ObnamError> { - if !config.encrypt { - panic!("no encryption specified"); - } +} - let passphrase = match insecure_passphrase { - Some(x) => x, - None => rpassword::read_password_from_tty(Some(PROMPT)).unwrap(), - }; +impl Init { + pub fn run(&self, config: &ClientConfigWithoutPasswords) -> Result<(), ObnamError> { + if !config.encrypt { + panic!("no encryption specified"); + } - let passwords = Passwords::new(&passphrase); - let filename = passwords_filename(config_filename); - passwords - .save(&filename) - .map_err(|err| ObnamError::PasswordSave(filename, err))?; - Ok(()) + let passphrase = match &self.insecure_passphrase { + Some(x) => x.to_string(), + None => rpassword::read_password_from_tty(Some(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/list.rs b/src/cmd/list.rs index a3f059b..66036b9 100644 --- a/src/cmd/list.rs +++ b/src/cmd/list.rs @@ -1,13 +1,20 @@ -use crate::client::{BackupClient, ClientConfig}; +use crate::client::BackupClient; +use crate::config::ClientConfig; use crate::error::ObnamError; +use structopt::StructOpt; -pub fn list(config: &ClientConfig) -> Result<(), ObnamError> { - let client = BackupClient::new(config)?; +#[derive(Debug, StructOpt)] +pub struct List {} - let generations = client.list_generations()?; - for finished in generations.iter() { - println!("{} {}", finished.id(), finished.ended()); - } +impl List { + pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> { + let client = BackupClient::new(config)?; + + let generations = client.list_generations()?; + for finished in generations.iter() { + println!("{} {}", finished.id(), finished.ended()); + } - Ok(()) + Ok(()) + } } diff --git a/src/cmd/list_files.rs b/src/cmd/list_files.rs index 71b0d68..22b102e 100644 --- a/src/cmd/list_files.rs +++ b/src/cmd/list_files.rs @@ -1,24 +1,33 @@ use crate::backup_reason::Reason; use crate::client::BackupClient; -use crate::client::ClientConfig; +use crate::config::ClientConfig; use crate::error::ObnamError; use crate::fsentry::{FilesystemEntry, FilesystemKind}; +use structopt::StructOpt; use tempfile::NamedTempFile; -pub fn list_files(config: &ClientConfig, gen_ref: &str) -> Result<(), ObnamError> { - let temp = NamedTempFile::new()?; +#[derive(Debug, StructOpt)] +pub struct ListFiles { + #[structopt(default_value = "latest")] + gen_id: String, +} - let client = BackupClient::new(config)?; +impl ListFiles { + pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> { + let temp = NamedTempFile::new()?; - let genlist = client.list_generations()?; - let gen_id: String = genlist.resolve(gen_ref)?; + let client = BackupClient::new(config)?; - let gen = client.fetch_generation(&gen_id, temp.path())?; - for file in gen.files()? { - println!("{}", format_entry(&file.entry(), file.reason())); - } + let genlist = client.list_generations()?; + let gen_id: String = genlist.resolve(&self.gen_id)?; - Ok(()) + let gen = client.fetch_generation(&gen_id, temp.path())?; + for file in gen.files()? { + println!("{}", format_entry(&file.entry(), file.reason())); + } + + Ok(()) + } } fn format_entry(e: &FilesystemEntry, reason: Reason) -> String { diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 70dde59..890e176 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,23 +1,8 @@ -mod init; -pub use init::init; - -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; - +pub mod backup; pub mod get_chunk; -pub use get_chunk::get_chunk; - -pub mod show_gen; -pub use show_gen::show_generation; - +pub mod init; +pub mod list; +pub mod list_files; +pub mod restore; pub mod show_config; -pub use show_config::show_config; +pub mod show_gen; diff --git a/src/cmd/restore.rs b/src/cmd/restore.rs index 183f207..a321e80 100644 --- a/src/cmd/restore.rs +++ b/src/cmd/restore.rs @@ -1,6 +1,6 @@ use crate::backup_reason::Reason; -use crate::client::ClientConfig; use crate::client::{BackupClient, ClientError}; +use crate::config::ClientConfig; use crate::error::ObnamError; use crate::fsentry::{FilesystemEntry, FilesystemKind}; use crate::generation::{LocalGeneration, LocalGenerationError}; @@ -18,32 +18,50 @@ use std::path::{Path, PathBuf}; use structopt::StructOpt; use tempfile::NamedTempFile; -pub fn restore(config: &ClientConfig, gen_ref: &str, to: &Path) -> Result<(), ObnamError> { - let temp = NamedTempFile::new()?; - - let client = BackupClient::new(config)?; +#[derive(Debug, StructOpt)] +pub struct Restore { + #[structopt()] + gen_id: String, - let genlist = client.list_generations()?; - let gen_id: String = genlist.resolve(gen_ref)?; - info!("generation id is {}", gen_id); + #[structopt(parse(from_os_str))] + to: PathBuf, +} - let gen = client.fetch_generation(&gen_id, temp.path())?; - info!("restoring {} files", gen.file_count()?); - let progress = create_progress_bar(gen.file_count()?, true); - for file in gen.files()? { - match file.reason() { - Reason::FileError => (), - _ => restore_generation(&client, &gen, file.fileno(), file.entry(), &to, &progress)?, +impl Restore { + pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> { + let temp = NamedTempFile::new()?; + + let client = BackupClient::new(config)?; + + let genlist = client.list_generations()?; + let gen_id: String = genlist.resolve(&self.gen_id)?; + info!("generation id is {}", gen_id); + + let gen = client.fetch_generation(&gen_id, temp.path())?; + info!("restoring {} files", gen.file_count()?); + let progress = create_progress_bar(gen.file_count()?, true); + for file in gen.files()? { + match file.reason() { + Reason::FileError => (), + _ => restore_generation( + &client, + &gen, + file.fileno(), + file.entry(), + &self.to, + &progress, + )?, + } } - } - for file in gen.files()? { - if file.entry().is_dir() { - restore_directory_metadata(file.entry(), &to)?; + for file in gen.files()? { + if file.entry().is_dir() { + restore_directory_metadata(file.entry(), &self.to)?; + } } - } - progress.finish(); + progress.finish(); - Ok(()) + Ok(()) + } } #[derive(Debug, StructOpt)] diff --git a/src/cmd/show_config.rs b/src/cmd/show_config.rs index 8acd1c8..424e2ed 100644 --- a/src/cmd/show_config.rs +++ b/src/cmd/show_config.rs @@ -1,7 +1,13 @@ -use crate::client::ClientConfig; +use crate::config::ClientConfig; use crate::error::ObnamError; +use structopt::StructOpt; -pub fn show_config(config: &ClientConfig) -> Result<(), ObnamError> { - println!("{}", serde_json::to_string_pretty(&config.config())?); - Ok(()) +#[derive(Debug, StructOpt)] +pub struct ShowConfig {} + +impl ShowConfig { + pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> { + println!("{}", serde_json::to_string_pretty(&config.config())?); + Ok(()) + } } diff --git a/src/cmd/show_gen.rs b/src/cmd/show_gen.rs index 143aed6..ba39809 100644 --- a/src/cmd/show_gen.rs +++ b/src/cmd/show_gen.rs @@ -1,33 +1,42 @@ use crate::client::BackupClient; -use crate::client::ClientConfig; +use crate::config::ClientConfig; use crate::error::ObnamError; use crate::fsentry::FilesystemKind; use indicatif::HumanBytes; +use structopt::StructOpt; use tempfile::NamedTempFile; -pub fn show_generation(config: &ClientConfig, gen_ref: &str) -> Result<(), ObnamError> { - let temp = NamedTempFile::new()?; +#[derive(Debug, StructOpt)] +pub struct ShowGeneration { + #[structopt(default_value = "latest")] + gen_id: String, +} + +impl ShowGeneration { + pub fn run(&self, config: &ClientConfig) -> Result<(), ObnamError> { + let temp = NamedTempFile::new()?; - let client = BackupClient::new(config)?; + let client = BackupClient::new(config)?; - let genlist = client.list_generations()?; - let gen_id: String = genlist.resolve(gen_ref)?; - let gen = client.fetch_generation(&gen_id, temp.path())?; - let files = gen.files()?; + let genlist = client.list_generations()?; + let gen_id: String = genlist.resolve(&self.gen_id)?; + let gen = client.fetch_generation(&gen_id, temp.path())?; + 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 total_bytes = files.iter().fold(0, |acc, file| { + let e = file.entry(); + if e.kind() == FilesystemKind::Regular { + acc + file.entry().len() + } else { + acc + } + }); - println!("generation-id: {}", gen_id); - println!("file-count: {}", gen.file_count()?); - println!("file-bytes: {}", HumanBytes(total_bytes)); - println!("file-bytes-raw: {}", total_bytes); + println!("generation-id: {}", gen_id); + println!("file-count: {}", gen.file_count()?); + println!("file-bytes: {}", HumanBytes(total_bytes)); + println!("file-bytes-raw: {}", total_bytes); - Ok(()) + Ok(()) + } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d6ffbc5 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,124 @@ +use crate::passwords::{passwords_filename, PasswordError, Passwords}; + +use bytesize::MIB; +use log::{error, trace}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +const DEFAULT_CHUNK_SIZE: usize = MIB as usize; +const DEVNULL: &str = "/dev/null"; + +#[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +struct TentativeClientConfig { + server_url: String, + verify_tls_cert: Option<bool>, + chunk_size: Option<usize>, + roots: Vec<PathBuf>, + log: Option<PathBuf>, + encrypt: Option<bool>, +} + +#[derive(Debug, Serialize, Clone)] +pub enum ClientConfig { + Plain(ClientConfigWithoutPasswords), + WithPasswords(ClientConfigWithoutPasswords, Passwords), +} + +impl ClientConfig { + pub fn read_without_passwords(filename: &Path) -> Result<Self, ClientConfigError> { + let config = ClientConfigWithoutPasswords::read_config(filename)?; + Ok(ClientConfig::Plain(config)) + } + + pub fn read_with_passwords(filename: &Path) -> Result<Self, ClientConfigError> { + let config = ClientConfigWithoutPasswords::read_config(filename)?; + if config.encrypt { + let passwords = Passwords::load(&passwords_filename(filename)) + .map_err(ClientConfigError::PasswordsMissing)?; + Ok(ClientConfig::WithPasswords(config, passwords)) + } else { + Ok(ClientConfig::Plain(config)) + } + } + + pub fn config(&self) -> &ClientConfigWithoutPasswords { + match self { + Self::Plain(config) => &config, + Self::WithPasswords(config, _) => &config, + } + } +} + +#[derive(Debug, Serialize, Clone)] +pub struct ClientConfigWithoutPasswords { + pub filename: PathBuf, + pub server_url: String, + pub verify_tls_cert: bool, + pub chunk_size: usize, + pub roots: Vec<PathBuf>, + pub log: PathBuf, + pub encrypt: bool, +} + +#[derive(Debug, thiserror::Error)] +pub enum ClientConfigError { + #[error("server_url is empty")] + ServerUrlIsEmpty, + + #[error("No backup roots in config; at least one is needed")] + NoBackupRoot, + + #[error("server URL doesn't use https: {0}")] + NotHttps(String), + + #[error("No passwords are set: you may need to run 'obnam init': {0}")] + PasswordsMissing(PasswordError), + + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error(transparent)] + SerdeYamlError(#[from] serde_yaml::Error), +} + +pub type ClientConfigResult<T> = Result<T, ClientConfigError>; + +impl ClientConfigWithoutPasswords { + pub fn read_config(filename: &Path) -> ClientConfigResult<Self> { + trace!("read_config: filename={:?}", filename); + let config = std::fs::read_to_string(filename)?; + let tentative: TentativeClientConfig = serde_yaml::from_str(&config)?; + + let encrypt = tentative.encrypt.or(Some(false)).unwrap(); + + let config = Self { + filename: filename.to_path_buf(), + server_url: tentative.server_url, + roots: tentative.roots, + verify_tls_cert: tentative.verify_tls_cert.or(Some(false)).unwrap(), + chunk_size: tentative.chunk_size.or(Some(DEFAULT_CHUNK_SIZE)).unwrap(), + log: tentative + .log + .or_else(|| Some(PathBuf::from(DEVNULL))) + .unwrap(), + encrypt, + }; + + config.check()?; + Ok(config) + } + + fn check(&self) -> Result<(), ClientConfigError> { + if self.server_url.is_empty() { + return Err(ClientConfigError::ServerUrlIsEmpty); + } + if !self.server_url.starts_with("https://") { + return Err(ClientConfigError::NotHttps(self.server_url.to_string())); + } + if self.roots.is_empty() { + return Err(ClientConfigError::NoBackupRoot); + } + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs index 454bba6..8241d5d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,7 @@ use crate::backup_run::BackupError; -use crate::client::{ClientConfigError, ClientError}; +use crate::client::ClientError; use crate::cmd::restore::RestoreError; +use crate::config::ClientConfigError; use crate::generation::{LocalGenerationError, NascentError}; use crate::genlist::GenerationListError; use crate::passwords::PasswordError; @@ -9,6 +9,7 @@ pub mod chunkid; pub mod chunkmeta; pub mod client; pub mod cmd; +pub mod config; pub mod error; pub mod fsentry; pub mod fsiter; |