//! Client to the Obnam server HTTP API. use crate::chunk::{DataChunk, GenerationChunk, GenerationChunkError}; use crate::chunkid::ChunkId; use crate::chunkmeta::ChunkMeta; use crate::cipher::{CipherEngine, CipherError}; use crate::config::{ClientConfig, ClientConfigError}; use crate::generation::{FinishedGeneration, GenId, LocalGeneration, LocalGenerationError}; use crate::genlist::GenerationList; use log::{debug, error, info}; use reqwest::header::HeaderMap; use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; use std::path::{Path, PathBuf}; /// Possible errors when using the server API. #[derive(Debug, thiserror::Error)] pub enum ClientError { /// 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(ChunkId), /// Server does not have generation. #[error("Server does not have generation {0}")] GenerationNotFound(ChunkId), /// Server didn't give us a chunk's metadata. #[error("Server response did not have a 'chunk-meta' header for chunk {0}")] NoChunkMeta(ChunkId), /// Chunk has wrong checksum and may be corrupted. #[error("Wrong checksum for chunk {0}, got {1}, expected {2}")] WrongChecksum(ChunkId, String, String), /// Client configuration is wrong. #[error(transparent)] ClientConfigError(#[from] ClientConfigError), /// An error encrypting or decrypting chunks. #[error(transparent)] CipherError(#[from] CipherError), /// An error regarding generation chunks. #[error(transparent)] GenerationChunkError(#[from] GenerationChunkError), /// An error using a backup's local metadata. #[error(transparent)] LocalGenerationError(#[from] LocalGenerationError), /// 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), /// 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), } /// Client for the Obnam server HTTP API. /// /// This is the async version. pub struct AsyncBackupClient { chunk_client: AsyncChunkClient, } impl AsyncBackupClient { /// Create a new backup client. pub fn new(config: &ClientConfig) -> Result { info!("creating backup client with config: {:#?}", config); Ok(Self { chunk_client: AsyncChunkClient::new(config)?, }) } /// Does the server have a chunk? pub async fn has_chunk(&self, meta: &ChunkMeta) -> Result, ClientError> { self.chunk_client.has_chunk(meta).await } /// Upload a data chunk to the srver. pub async fn upload_chunk(&self, chunk: DataChunk) -> Result { self.chunk_client.upload_chunk(chunk).await } /// List backup generations known by the server. pub async fn list_generations(&self) -> Result { self.chunk_client.list_generations().await } /// Fetch a data chunk from the server, given the chunk identifier. pub async fn fetch_chunk(&self, chunk_id: &ChunkId) -> Result { self.chunk_client.fetch_chunk(chunk_id).await } async fn fetch_generation_chunk(&self, gen_id: &GenId) -> Result { let chunk = self.fetch_chunk(gen_id.as_chunk_id()).await?; let gen = GenerationChunk::from_data_chunk(&chunk)?; Ok(gen) } /// Fetch a backup generation's metadata, given it's identifier. pub async fn fetch_generation( &self, gen_id: &GenId, dbname: &Path, ) -> Result { 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) .map_err(|err| ClientError::FileCreate(dbname.to_path_buf(), err))?; for id in gen.chunk_ids() { 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()); let gen = LocalGeneration::open(dbname)?; Ok(gen) } } /// Client for using chunk part of Obnam server HTTP API. pub struct AsyncChunkClient { client: reqwest::Client, base_url: String, cipher: CipherEngine, } impl AsyncChunkClient { /// Create a new chunk client. pub fn new(config: &ClientConfig) -> Result { let pass = config.passwords()?; let client = reqwest::Client::builder() .danger_accept_invalid_certs(!config.verify_tls_cert) .build() .map_err(ClientError::ReqwestError)?; Ok(Self { client, base_url: config.server_url.to_string(), cipher: CipherEngine::new(&pass), }) } fn base_url(&self) -> &str { &self.base_url } fn chunks_url(&self) -> String { format!("{}/chunks", self.base_url()) } /// Does server have a chunk? pub async fn has_chunk(&self, meta: &ChunkMeta) -> Result, ClientError> { let body = match self.get("", &[("sha256", meta.sha256())]).await { Ok((_, body)) => body, Err(err) => return Err(err), }; let hits: HashMap = serde_json::from_slice(&body).map_err(ClientError::JsonParse)?; let mut iter = hits.iter(); let has = if let Some((chunk_id, _)) = iter.next() { Some(chunk_id.into()) } else { None }; Ok(has) } /// Upload a new chunk to the server. pub async fn upload_chunk(&self, chunk: DataChunk) -> Result { let enc = self.cipher.encrypt_chunk(&chunk)?; let res = self .client .post(&self.chunks_url()) .header("chunk-meta", chunk.meta().to_json()) .body(enc.ciphertext().to_vec()) .send() .await .map_err(ClientError::ReqwestError)?; debug!("upload_chunk: res={:?}", res); let res: HashMap = res.json().await.map_err(ClientError::ReqwestError)?; 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); }; info!("uploaded_chunk {}", chunk_id); Ok(chunk_id) } /// List all generation chunks on the server. pub async fn list_generations(&self) -> Result { let (_, body) = self.get("", &[("generation", "true")]).await?; let map: HashMap = serde_yaml::from_slice(&body).map_err(ClientError::YamlParse)?; debug!("list_generations: map={:?}", map); let finished = map .iter() .map(|(id, meta)| FinishedGeneration::new(id, meta.ended().map_or("", |s| s))) .collect(); Ok(GenerationList::new(finished)) } /// Fetch a chunk from the server, given its id. pub async fn fetch_chunk(&self, chunk_id: &ChunkId) -> Result { let (headers, body) = self.get(&format!("/{}", chunk_id), &[]).await?; let meta = self.get_chunk_meta_header(chunk_id, &headers)?; let meta_bytes = meta.to_json_vec(); let chunk = self.cipher.decrypt_chunk(&body, &meta_bytes)?; Ok(chunk) } async fn get( &self, path: &str, query: &[(&str, &str)], ) -> Result<(HeaderMap, Vec), ClientError> { let url = format!("{}{}", &self.chunks_url(), path); info!("GET {}", url); // Build HTTP request structure. let req = self .client .get(&url) .query(query) .build() .map_err(ClientError::ReqwestError)?; // Make HTTP request. let res = self .client .execute(req) .await .map_err(ClientError::ReqwestError)?; // Did it work? if res.status() != 200 { return Err(ClientError::NotFound(path.to_string())); } // Return headers and body. let headers = res.headers().clone(); let body = res.bytes().await.map_err(ClientError::ReqwestError)?; let body = body.to_vec(); Ok((headers, body)) } fn get_chunk_meta_header( &self, chunk_id: &ChunkId, headers: &HeaderMap, ) -> Result { let meta = headers.get("chunk-meta"); if meta.is_none() { let err = ClientError::NoChunkMeta(chunk_id.clone()); error!("fetching chunk {} failed: {}", chunk_id, err); return Err(err); } let meta = meta .unwrap() .to_str() .map_err(ClientError::MetaHeaderToString)?; let meta: ChunkMeta = serde_json::from_str(meta).map_err(ClientError::JsonParse)?; Ok(meta) } }