diff options
Diffstat (limited to 'src/client.rs')
-rw-r--r-- | src/client.rs | 375 |
1 files changed, 153 insertions, 222 deletions
diff --git a/src/client.rs b/src/client.rs index 515b8c9..a924052 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,271 +1,207 @@ -use crate::checksummer::sha256; -use crate::chunk::DataChunk; -use crate::chunk::GenerationChunk; -use crate::chunker::Chunker; +//! Client to the Obnam server HTTP API. + +use crate::chunk::{ + ClientTrust, ClientTrustError, DataChunk, GenerationChunk, GenerationChunkError, +}; use crate::chunkid::ChunkId; use crate::chunkmeta::ChunkMeta; -use crate::error::ObnamError; -use crate::fsentry::{FilesystemEntry, FilesystemKind}; -use crate::generation::{FinishedGeneration, LocalGeneration}; +use crate::chunkstore::{ChunkStore, StoreError}; +use crate::cipher::{CipherEngine, CipherError}; +use crate::config::{ClientConfig, ClientConfigError}; +use crate::generation::{FinishedGeneration, GenId, LocalGeneration, LocalGenerationError}; use crate::genlist::GenerationList; +use crate::label::Label; -use anyhow::Context; -use chrono::{DateTime, Local}; -use log::{debug, error, info, trace}; -use reqwest::blocking::Client; -use serde::Deserialize; -use std::collections::HashMap; +use log::{error, info}; use std::fs::File; use std::io::prelude::*; use std::path::{Path, PathBuf}; -#[derive(Debug, Deserialize, Clone)] -pub struct ClientConfig { - pub server_url: String, - pub root: PathBuf, - pub log: Option<PathBuf>, -} - -impl ClientConfig { - pub fn read_config(filename: &Path) -> anyhow::Result<Self> { - trace!("read_config: filename={:?}", filename); - let config = std::fs::read_to_string(filename) - .with_context(|| format!("reading configuration file {}", filename.display()))?; - let config = serde_yaml::from_str(&config)?; - Ok(config) - } -} - +/// Possible errors when using the server API. #[derive(Debug, thiserror::Error)] pub enum ClientError { - #[error("Server successful response to creating chunk lacked chunk id")] + /// No chunk id for uploaded chunk. + #[error("Server response claimed it had created a chunk, but lacked chunk id")] NoCreatedChunkId, + /// Server claims to not have an entity. + #[error("Server does not have {0}")] + NotFound(String), + + /// Server does not have a chunk. #[error("Server does not have chunk {0}")] - ChunkNotFound(String), + ChunkNotFound(ChunkId), + /// Server does not have generation. #[error("Server does not have generation {0}")] - GenerationNotFound(String), -} + GenerationNotFound(ChunkId), -pub struct BackupClient { - client: Client, - base_url: String, -} + /// Server didn't give us a chunk's metadata. + #[error("Server response did not have a 'chunk-meta' header for chunk {0}")] + NoChunkMeta(ChunkId), -impl BackupClient { - pub fn new(base_url: &str) -> anyhow::Result<Self> { - let client = Client::builder() - .danger_accept_invalid_certs(true) - .build()?; - Ok(Self { - client, - base_url: base_url.to_string(), - }) - } + /// Chunk has wrong checksum and may be corrupted. + #[error("Wrong checksum for chunk {0}, got {1}, expected {2}")] + WrongChecksum(ChunkId, String, String), - pub fn upload_filesystem_entry( - &self, - e: &FilesystemEntry, - size: usize, - ) -> anyhow::Result<Vec<ChunkId>> { - info!("upload entry: {:?}", e); - let ids = match e.kind() { - FilesystemKind::Regular => self.read_file(e.pathbuf(), size)?, - FilesystemKind::Directory => vec![], - FilesystemKind::Symlink => vec![], - }; - Ok(ids) - } + /// Client configuration is wrong. + #[error(transparent)] + ClientConfigError(#[from] ClientConfigError), - pub fn upload_generation(&self, filename: &Path, size: usize) -> anyhow::Result<ChunkId> { - info!("upload SQLite {}", filename.display()); - let ids = self.read_file(filename.to_path_buf(), size)?; - let gen = GenerationChunk::new(ids); - let data = gen.to_data_chunk()?; - let meta = ChunkMeta::new_generation(&sha256(data.data()), ¤t_timestamp()); - let gen_id = self.upload_gen_chunk(meta.clone(), gen)?; - info!("uploaded generation {}, meta {:?}", gen_id, meta); - Ok(gen_id) - } + /// An error encrypting or decrypting chunks. + #[error(transparent)] + CipherError(#[from] CipherError), - fn read_file(&self, filename: PathBuf, size: usize) -> anyhow::Result<Vec<ChunkId>> { - info!("upload file {}", filename.display()); - let file = std::fs::File::open(filename)?; - let chunker = Chunker::new(size, file); - let chunk_ids = self.upload_new_file_chunks(chunker)?; - Ok(chunk_ids) - } + /// An error regarding generation chunks. + #[error(transparent)] + GenerationChunkError(#[from] GenerationChunkError), - fn base_url(&self) -> &str { - &self.base_url - } + /// An error regarding client trust. + #[error(transparent)] + ClientTrust(#[from] ClientTrustError), - fn chunks_url(&self) -> String { - format!("{}/chunks", self.base_url()) - } + /// An error using a backup's local metadata. + #[error(transparent)] + LocalGenerationError(#[from] LocalGenerationError), - pub fn has_chunk(&self, meta: &ChunkMeta) -> anyhow::Result<Option<ChunkId>> { - trace!("has_chunk: url={:?}", self.base_url()); - let req = self - .client - .get(&self.chunks_url()) - .query(&[("sha256", meta.sha256())]) - .build()?; - - let res = self.client.execute(req)?; - debug!("has_chunk: status={}", res.status()); - let has = if res.status() != 200 { - debug!("has_chunk: error from server"); - None - } else { - let text = res.text()?; - debug!("has_chunk: text={:?}", text); - let hits: HashMap<String, ChunkMeta> = serde_json::from_str(&text)?; - debug!("has_chunk: hits={:?}", hits); - let mut iter = hits.iter(); - if let Some((chunk_id, _)) = iter.next() { - debug!("has_chunk: chunk_id={:?}", chunk_id); - Some(chunk_id.into()) - } else { - None - } - }; + /// An error with the `chunk-meta` header. + #[error("couldn't convert response chunk-meta header to string: {0}")] + MetaHeaderToString(reqwest::header::ToStrError), + + /// An error from the HTTP library. + #[error("error from reqwest library: {0}")] + ReqwestError(reqwest::Error), + + /// Couldn't look up a chunk via checksum. + #[error("lookup by chunk checksum failed: {0}")] + ChunkExists(reqwest::Error), + + /// Error parsing JSON. + #[error("failed to parse JSON: {0}")] + JsonParse(serde_json::Error), - info!("has_chunk result: {:?}", has); - Ok(has) + /// Error generating JSON. + #[error("failed to generate JSON: {0}")] + JsonGenerate(serde_json::Error), + + /// Error parsing YAML. + #[error("failed to parse YAML: {0}")] + YamlParse(serde_yaml::Error), + + /// Failed to open a file. + #[error("failed to open file {0}: {1}")] + FileOpen(PathBuf, std::io::Error), + + /// Failed to create a file. + #[error("failed to create file {0}: {1}")] + FileCreate(PathBuf, std::io::Error), + + /// Failed to write a file. + #[error("failed to write to file {0}: {1}")] + FileWrite(PathBuf, std::io::Error), + + /// Error from a chunk store. + #[error(transparent)] + ChunkStore(#[from] StoreError), +} + +/// Client for the Obnam server HTTP API. +pub struct BackupClient { + store: ChunkStore, + cipher: CipherEngine, +} + +impl BackupClient { + /// Create a new backup client. + pub fn new(config: &ClientConfig) -> Result<Self, ClientError> { + info!("creating backup client with config: {:#?}", config); + let pass = config.passwords()?; + Ok(Self { + store: ChunkStore::remote(config)?, + cipher: CipherEngine::new(&pass), + }) } - pub fn upload_chunk(&self, meta: ChunkMeta, chunk: DataChunk) -> anyhow::Result<ChunkId> { - let res = self - .client - .post(&self.chunks_url()) - .header("chunk-meta", meta.to_json()) - .body(chunk.data().to_vec()) - .send()?; - debug!("upload_chunk: res={:?}", res); - let res: HashMap<String, String> = res.json()?; - let chunk_id = if let Some(chunk_id) = res.get("chunk_id") { - debug!("upload_chunk: id={}", chunk_id); - chunk_id.parse().unwrap() - } else { - return Err(ClientError::NoCreatedChunkId.into()); - }; - info!("uploaded_chunk {} meta {:?}", chunk_id, meta); - Ok(chunk_id) + /// Does the server have a chunk? + pub async fn has_chunk(&self, meta: &ChunkMeta) -> Result<Option<ChunkId>, ClientError> { + let mut ids = self.store.find_by_label(meta).await?; + Ok(ids.pop()) } - pub fn upload_gen_chunk( - &self, - meta: ChunkMeta, - gen: GenerationChunk, - ) -> anyhow::Result<ChunkId> { - let res = self - .client - .post(&self.chunks_url()) - .header("chunk-meta", meta.to_json()) - .body(serde_json::to_string(&gen)?) - .send()?; - debug!("upload_chunk: res={:?}", res); - let res: HashMap<String, String> = res.json()?; - let chunk_id = if let Some(chunk_id) = res.get("chunk_id") { - debug!("upload_chunk: id={}", chunk_id); - chunk_id.parse().unwrap() - } else { - return Err(ClientError::NoCreatedChunkId.into()); - }; - info!("uploaded_generation chunk {}", chunk_id); - Ok(chunk_id) + /// Upload a data chunk to the server. + pub async fn upload_chunk(&mut self, chunk: DataChunk) -> Result<ChunkId, ClientError> { + let enc = self.cipher.encrypt_chunk(&chunk)?; + let data = enc.ciphertext().to_vec(); + let id = self.store.put(data, chunk.meta()).await?; + Ok(id) } - pub fn upload_new_file_chunks(&self, chunker: Chunker) -> anyhow::Result<Vec<ChunkId>> { - let mut chunk_ids = vec![]; - for item in chunker { - let (meta, chunk) = item?; - if let Some(chunk_id) = self.has_chunk(&meta)? { - chunk_ids.push(chunk_id.clone()); - info!("reusing existing chunk {}", chunk_id); + /// Get current client trust chunk from repository, if there is one. + pub async fn get_client_trust(&self) -> Result<Option<ClientTrust>, ClientError> { + let ids = self.find_client_trusts().await?; + let mut latest: Option<ClientTrust> = None; + for id in ids { + let chunk = self.fetch_chunk(&id).await?; + let new = ClientTrust::from_data_chunk(&chunk)?; + if let Some(t) = &latest { + if new.timestamp() > t.timestamp() { + latest = Some(new); + } } else { - let chunk_id = self.upload_chunk(meta, chunk)?; - chunk_ids.push(chunk_id.clone()); - info!("created new chunk {}", chunk_id); + latest = Some(new); } } + Ok(latest) + } - Ok(chunk_ids) + async fn find_client_trusts(&self) -> Result<Vec<ChunkId>, ClientError> { + let label = Label::literal("client-trust"); + let meta = ChunkMeta::new(&label); + let ids = self.store.find_by_label(&meta).await?; + Ok(ids) } - pub fn list_generations(&self) -> anyhow::Result<GenerationList> { - let url = format!("{}?generation=true", &self.chunks_url()); - trace!("list_generations: url={:?}", url); - let req = self.client.get(&url).build()?; - let res = self.client.execute(req)?; - debug!("list_generations: status={}", res.status()); - let body = res.bytes()?; - debug!("list_generations: body={:?}", body); - let map: HashMap<String, ChunkMeta> = serde_yaml::from_slice(&body)?; - debug!("list_generations: map={:?}", map); - let finished = map + /// List backup generations known by the server. + pub fn list_generations(&self, trust: &ClientTrust) -> GenerationList { + let finished = trust + .backups() .iter() - .map(|(id, meta)| FinishedGeneration::new(id, meta.ended().map_or("", |s| s))) + .map(|id| FinishedGeneration::new(&format!("{}", id), "")) .collect(); - Ok(GenerationList::new(finished)) + GenerationList::new(finished) } - pub fn fetch_chunk(&self, chunk_id: &ChunkId) -> anyhow::Result<DataChunk> { - info!("fetch chunk {}", chunk_id); - - let url = format!("{}/{}", &self.chunks_url(), chunk_id); - let req = self.client.get(&url).build()?; - let res = self.client.execute(req)?; - if res.status() != 200 { - let err = ClientError::ChunkNotFound(chunk_id.to_string()); - error!("fetching chunk {} failed: {}", chunk_id, err); - return Err(err.into()); - } - - let headers = res.headers(); - let meta = headers.get("chunk-meta"); - if meta.is_none() { - let err = ObnamError::NoChunkMeta(chunk_id.clone()); - error!("fetching chunk {} failed: {}", chunk_id, err); - return Err(err.into()); - } - let meta = meta.unwrap().to_str()?; - debug!("fetching chunk {}: meta={:?}", chunk_id, meta); - let meta: ChunkMeta = serde_json::from_str(meta)?; - debug!("fetching chunk {}: meta={:?}", chunk_id, meta); - - let body = res.bytes()?; - let body = body.to_vec(); - let actual = sha256(&body); - if actual != meta.sha256() { - let err = - ObnamError::WrongChecksum(chunk_id.clone(), actual, meta.sha256().to_string()); - error!("fetching chunk {} failed: {}", chunk_id, err); - return Err(err.into()); - } - - let chunk: DataChunk = DataChunk::new(body); + /// Fetch a data chunk from the server, given the chunk identifier. + pub async fn fetch_chunk(&self, chunk_id: &ChunkId) -> Result<DataChunk, ClientError> { + let (body, meta) = self.store.get(chunk_id).await?; + let meta_bytes = meta.to_json_vec(); + let chunk = self.cipher.decrypt_chunk(&body, &meta_bytes)?; Ok(chunk) } - fn fetch_generation_chunk(&self, gen_id: &str) -> anyhow::Result<GenerationChunk> { - let chunk_id = ChunkId::from_str(gen_id); - let chunk = self.fetch_chunk(&chunk_id)?; + async fn fetch_generation_chunk(&self, gen_id: &GenId) -> Result<GenerationChunk, ClientError> { + let chunk = self.fetch_chunk(gen_id.as_chunk_id()).await?; let gen = GenerationChunk::from_data_chunk(&chunk)?; Ok(gen) } - pub fn fetch_generation(&self, gen_id: &str, dbname: &Path) -> anyhow::Result<LocalGeneration> { - let gen = self.fetch_generation_chunk(gen_id)?; + /// Fetch a backup generation's metadata, given it's identifier. + pub async fn fetch_generation( + &self, + gen_id: &GenId, + dbname: &Path, + ) -> Result<LocalGeneration, ClientError> { + let gen = self.fetch_generation_chunk(gen_id).await?; // Fetch the SQLite file, storing it in the named file. - let mut dbfile = File::create(&dbname)?; + let mut dbfile = File::create(dbname) + .map_err(|err| ClientError::FileCreate(dbname.to_path_buf(), err))?; for id in gen.chunk_ids() { - let chunk = self.fetch_chunk(id)?; - dbfile.write_all(chunk.data())?; + let chunk = self.fetch_chunk(id).await?; + dbfile + .write_all(chunk.data()) + .map_err(|err| ClientError::FileWrite(dbname.to_path_buf(), err))?; } info!("downloaded generation to {}", dbname.display()); @@ -273,8 +209,3 @@ impl BackupClient { Ok(gen) } } - -fn current_timestamp() -> String { - let now: DateTime<Local> = Local::now(); - format!("{}", now.format("%Y-%m-%d %H:%M:%S.%f %z")) -} |