summaryrefslogtreecommitdiff
path: root/src/client.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client.rs')
-rw-r--r--src/client.rs375
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()), &current_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"))
-}