summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-04-10 07:31:35 +0000
committerLars Wirzenius <liw@liw.fi>2021-04-10 07:31:35 +0000
commit961f3d5373c3185db8ba16ce9af3f35f876772ae (patch)
treef9d54086c43a491b8df9bdcafbe61b70c39d2956
parenta7bbd6d91139093b488c9ac4b543f24ede23a495 (diff)
parent3295950f41342b8b9a312f203b111c7c277500b4 (diff)
downloadobnam2-961f3d5373c3185db8ba16ce9af3f35f876772ae.tar.gz
Merge branch 'cli-handling' into 'main'
refactor: for clarity See merge request larswirzenius/obnam!134
-rw-r--r--src/backup_run.rs3
-rw-r--r--src/bin/obnam.rs108
-rw-r--r--src/client.rs122
-rw-r--r--src/cmd/backup.rs35
-rw-r--r--src/cmd/get_chunk.rs27
-rw-r--r--src/cmd/init.rs42
-rw-r--r--src/cmd/list.rs23
-rw-r--r--src/cmd/list_files.rs31
-rw-r--r--src/cmd/mod.rs27
-rw-r--r--src/cmd/restore.rs62
-rw-r--r--src/cmd/show_config.rs14
-rw-r--r--src/cmd/show_gen.rs51
-rw-r--r--src/config.rs124
-rw-r--r--src/error.rs3
-rw-r--r--src/lib.rs1
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;
diff --git a/src/lib.rs b/src/lib.rs
index fb4c7fe..82dab15 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;