diff --git a/services/commtest/src/backup/backup_utils.rs b/services/commtest/src/backup/backup_utils.rs index 29b343445..bc0775736 100644 --- a/services/commtest/src/backup/backup_utils.rs +++ b/services/commtest/src/backup/backup_utils.rs @@ -1,100 +1,100 @@ use crate::identity::device::DeviceInfo; use crate::tools::generate_stable_nbytes; use backup_client::{BackupData, Error as BackupClientError}; use bytesize::ByteSize; use comm_lib::auth::UserIdentity; use comm_lib::backup::UploadLogRequest; use rand::Rng; use reqwest::StatusCode; use uuid::Uuid; pub fn generate_backup_data(predefined_byte_value: Option) -> BackupData { let predefined_byte_value = predefined_byte_value.unwrap_or(rand::thread_rng().gen::()); BackupData { backup_id: Uuid::new_v4().to_string(), user_keys: Some(generate_stable_nbytes( ByteSize::kib(4).as_u64() as usize, Some(predefined_byte_value), )), user_data: Some(generate_stable_nbytes( ByteSize::mib(4).as_u64() as usize, Some(predefined_byte_value), )), attachments: vec![], - siwe_backup_msg: None, + siwe_backup_msg: Some("message".to_string()), } } fn generate_log_data(backup_id: &str, value: u8) -> Vec { const IN_DB_SIZE: usize = ByteSize::kib(4).as_u64() as usize; const IN_BLOB_SIZE: usize = ByteSize::kib(400).as_u64() as usize; (1..30) .map(|log_id| { let size = if log_id % 2 == 0 { IN_DB_SIZE } else { IN_BLOB_SIZE }; let attachments = if log_id % 10 == 0 { Some(vec![Uuid::new_v4().to_string()]) } else { None }; let mut content = generate_stable_nbytes(size, Some(value)); let unique_suffix = log_id.to_string(); content.extend(unique_suffix.as_bytes()); UploadLogRequest { backup_id: backup_id.to_string(), log_id, content, attachments, } }) .collect() } pub fn generate_backup_data_with_logs( predefined_byte_values: Vec, ) -> Vec<(BackupData, Vec)> { predefined_byte_values .into_iter() .map(|byte_value| { let backup_data = generate_backup_data(Some(byte_value)); let log_data = generate_log_data(&backup_data.backup_id, byte_value); (backup_data, log_data) }) .collect() } pub fn assert_reqwest_error( response: Result, expected_status: StatusCode, ) { match response { Err(BackupClientError::ReqwestError(error)) => { assert_eq!( error.status(), Some(expected_status), "Expected status {}", expected_status ); } Err(err) => panic!( "Backup should return ReqwestError, instead got response: {:?}", err ), Ok(_) => panic!("Backup should return BackupClientError"), } } pub fn create_user_identity(device_info: DeviceInfo) -> UserIdentity { UserIdentity { user_id: device_info.user_id.clone(), access_token: device_info.access_token.clone(), device_id: device_info.device_id.clone(), } } diff --git a/services/commtest/tests/backup_integration_test.rs b/services/commtest/tests/backup_integration_test.rs index e5f2d3de6..b4cd50423 100644 --- a/services/commtest/tests/backup_integration_test.rs +++ b/services/commtest/tests/backup_integration_test.rs @@ -1,140 +1,141 @@ use backup_client::{ BackupClient, BackupDescriptor, DownloadedLog, LogUploadConfirmation, RequestedData, SinkExt, StreamExt, TryStreamExt, }; use comm_lib::backup::LatestBackupInfoResponse; use commtest::backup::backup_utils::{ assert_reqwest_error, create_user_identity, generate_backup_data_with_logs, }; use commtest::identity::device::register_user_device; use commtest::{service_addr, tools::Error}; use grpc_clients::identity::DeviceType; use reqwest::StatusCode; use std::collections::HashSet; #[tokio::test] async fn backup_integration_test() -> Result<(), Error> { let backup_client = BackupClient::new(service_addr::BACKUP_SERVICE_HTTP)?; let device_info = register_user_device(None, Some(DeviceType::Ios)).await; let user_identity = create_user_identity(device_info.clone()); let backup_datas = generate_backup_data_with_logs(vec![b'a', b'b']); // Upload backups for (backup_data, log_datas) in &backup_datas { backup_client .upload_backup(&user_identity, backup_data.clone()) .await?; let (mut tx, rx) = backup_client.upload_logs(&user_identity).await?; for log_data in log_datas { tx.send(log_data.clone()).await?; } let result: HashSet = rx.take(log_datas.len()).try_collect().await?; let expected = log_datas .iter() .map(|data| LogUploadConfirmation { backup_id: data.backup_id.clone(), log_id: data.log_id, }) .collect(); assert_eq!(result, expected); } // Test direct lookup let (backup_data, log_datas) = &backup_datas[1]; let second_backup_descriptor = BackupDescriptor::BackupID { backup_id: backup_data.backup_id.clone(), user_identity: user_identity.clone(), }; let user_keys = backup_client .download_backup_data(&second_backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(Some(user_keys), backup_data.user_keys); let user_data = backup_client .download_backup_data(&second_backup_descriptor, RequestedData::UserData) .await?; assert_eq!(Some(user_data), backup_data.user_data); // Test latest backup lookup for nonexistent user let latest_backup_descriptor = BackupDescriptor::Latest { user_identifier: "nonexistent_user".to_string(), }; let nonexistent_user_response = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::BackupInfo) .await; assert_reqwest_error(nonexistent_user_response, StatusCode::BAD_REQUEST); // Test latest backup lookup let latest_backup_descriptor = BackupDescriptor::Latest { user_identifier: device_info.username, }; let backup_info_response = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::BackupInfo) .await?; let response: LatestBackupInfoResponse = serde_json::from_slice(&backup_info_response)?; assert_eq!(response.backup_id, backup_data.backup_id); assert_eq!(response.user_id, device_info.user_id); + assert_eq!(response.siwe_backup_msg, backup_data.siwe_backup_msg); let user_keys = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(Some(user_keys), backup_data.user_keys); // Test log download let log_stream = backup_client .download_logs(&user_identity, &backup_data.backup_id) .await; let downloaded_logs: Vec = log_stream.try_collect().await?; let expected_logs: Vec = log_datas .iter() .map(|data| DownloadedLog { content: data.content.clone(), attachments: data.attachments.clone(), }) .collect(); assert_eq!(downloaded_logs, expected_logs); // Test backup cleanup let (removed_backup, _) = &backup_datas[0]; let removed_backup_descriptor = BackupDescriptor::BackupID { backup_id: removed_backup.backup_id.clone(), user_identity: user_identity.clone(), }; let response = backup_client .download_backup_data(&removed_backup_descriptor, RequestedData::UserKeys) .await; assert_reqwest_error(response, StatusCode::NOT_FOUND); // Test log cleanup let log_stream = backup_client .download_logs(&user_identity, &removed_backup.backup_id) .await; let downloaded_logs: Vec = log_stream.try_collect().await?; if !downloaded_logs.is_empty() { panic!( "Logs for first backup should have been removed, \ instead got: {downloaded_logs:?}" ) } Ok(()) } diff --git a/services/commtest/tests/backup_upload_test.rs b/services/commtest/tests/backup_upload_test.rs index ad0fb0252..b231b777d 100644 --- a/services/commtest/tests/backup_upload_test.rs +++ b/services/commtest/tests/backup_upload_test.rs @@ -1,278 +1,280 @@ use backup_client::{ BackupClient, BackupData, BackupDescriptor, RequestedData, }; use comm_lib::backup::LatestBackupInfoResponse; use commtest::backup::backup_utils::{ assert_reqwest_error, create_user_identity, generate_backup_data, }; use commtest::identity::device::register_user_device; use commtest::{service_addr, tools::Error}; use grpc_clients::identity::DeviceType; use reqwest::StatusCode; #[tokio::test] async fn backup_upload_user_keys() -> Result<(), Error> { let backup_client = BackupClient::new(service_addr::BACKUP_SERVICE_HTTP)?; let device_info = register_user_device(None, Some(DeviceType::Ios)).await; let user_identity = create_user_identity(device_info.clone()); let backup_data = BackupData { user_data: None, ..generate_backup_data(None) }; // Upload backup (User Keys) backup_client .upload_backup(&user_identity, backup_data.clone()) .await?; // Test User Keys download let backup_descriptor = BackupDescriptor::BackupID { backup_id: backup_data.backup_id.clone(), user_identity: user_identity.clone(), }; let user_keys = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(Some(user_keys), backup_data.user_keys); // Test latest backup lookup without User Data let latest_backup_descriptor = BackupDescriptor::Latest { user_identifier: device_info.username, }; let backup_info_response = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::BackupInfo) .await?; let response: LatestBackupInfoResponse = serde_json::from_slice(&backup_info_response)?; assert_eq!(response.backup_id, backup_data.backup_id); assert_eq!(response.user_id, device_info.user_id); + assert_eq!(response.siwe_backup_msg, backup_data.siwe_backup_msg); let user_keys = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(Some(user_keys), backup_data.user_keys); // Test backup cleanup for User Keys only let new_backup_data = BackupData { user_data: None, ..generate_backup_data(None) }; backup_client .upload_backup(&user_identity, new_backup_data.clone()) .await?; // Test Data download for old `backup_id` -> should be not found let user_data_response = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserData) .await; let user_keys_response = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserKeys) .await; assert_reqwest_error(user_data_response, StatusCode::NOT_FOUND); assert_reqwest_error(user_keys_response, StatusCode::NOT_FOUND); Ok(()) } #[tokio::test] async fn backup_upload_the_same_user_keys() -> Result<(), Error> { let backup_client = BackupClient::new(service_addr::BACKUP_SERVICE_HTTP)?; let device_info = register_user_device(None, Some(DeviceType::Ios)).await; let user_identity = create_user_identity(device_info); let backup_data = BackupData { user_data: None, ..generate_backup_data(None) }; // Upload backup twice (User Keys only) backup_client .upload_backup(&user_identity, backup_data.clone()) .await?; backup_client .upload_backup(&user_identity, backup_data.clone()) .await?; // Test User Keys download let backup_descriptor = BackupDescriptor::BackupID { backup_id: backup_data.backup_id.clone(), user_identity: user_identity.clone(), }; let user_keys = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(Some(user_keys), backup_data.user_keys); Ok(()) } #[tokio::test] async fn backup_upload_user_data_without_user_keys() -> Result<(), Error> { let backup_client = BackupClient::new(service_addr::BACKUP_SERVICE_HTTP)?; let device_info = register_user_device(None, Some(DeviceType::Ios)).await; let user_identity = create_user_identity(device_info); let backup_data = BackupData { user_keys: None, ..generate_backup_data(None) }; // Upload backup (User Data) -> should fail, // there is no corresponding User Keys let response = backup_client .upload_backup(&user_identity, backup_data.clone()) .await; assert_reqwest_error(response, StatusCode::NOT_FOUND); Ok(()) } #[tokio::test] async fn backup_upload_user_keys_and_user_data() -> Result<(), Error> { let backup_client = BackupClient::new(service_addr::BACKUP_SERVICE_HTTP)?; let device_info = register_user_device(None, Some(DeviceType::Ios)).await; let user_identity = create_user_identity(device_info.clone()); let backup_data = generate_backup_data(None); let user_keys = BackupData { user_data: None, ..backup_data.clone() }; let user_data = BackupData { user_keys: None, ..backup_data.clone() }; // Upload backups (User Keys and User Data) backup_client .upload_backup(&user_identity, user_keys.clone()) .await?; backup_client .upload_backup(&user_identity, user_data.clone()) .await?; // Test User Keys download let backup_descriptor = BackupDescriptor::BackupID { backup_id: backup_data.backup_id.clone(), user_identity: user_identity.clone(), }; let user_keys = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(Some(user_keys), backup_data.user_keys); // Test User Data download let user_data = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserData) .await?; assert_eq!(Some(user_data), backup_data.user_data); // Upload new User Data let new_backup_data = generate_backup_data(None); let new_user_data = BackupData { // Important we using the same `backup_id` backup_id: backup_data.backup_id.clone(), user_keys: None, user_data: new_backup_data.user_data.clone(), attachments: new_backup_data.attachments, siwe_backup_msg: None, }; backup_client .upload_backup(&user_identity, new_user_data.clone()) .await?; // Test User Keys download again -> should remain unchanged let backup_descriptor = BackupDescriptor::BackupID { backup_id: new_user_data.backup_id.clone(), user_identity: user_identity.clone(), }; let user_keys = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(Some(user_keys), backup_data.user_keys); // Test User Data download, should be updated let user_data = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserData) .await?; assert_eq!(Some(user_data), new_backup_data.user_data); // Upload new User Keys -> should override User Keys and keep User Data unchanged let new_user_keys = BackupData { user_data: None, ..generate_backup_data(None) }; backup_client .upload_backup(&user_identity, new_user_keys.clone()) .await?; // Test latest backup -> should return newest `backup_id` let latest_backup_descriptor = BackupDescriptor::Latest { user_identifier: device_info.username, }; let backup_info_response = backup_client .download_backup_data(&latest_backup_descriptor, RequestedData::BackupInfo) .await?; let response: LatestBackupInfoResponse = serde_json::from_slice(&backup_info_response)?; assert_eq!(response.backup_id, new_user_keys.backup_id); assert_eq!(response.user_id, device_info.user_id); + assert_eq!(response.siwe_backup_msg, backup_data.siwe_backup_msg); // Test User Keys download -> should be updated let backup_descriptor = BackupDescriptor::BackupID { backup_id: new_user_keys.backup_id.clone(), user_identity: user_identity.clone(), }; let user_keys = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserKeys) .await?; assert_eq!(Some(user_keys), new_user_keys.user_keys); // Test User Data download -> should be the old one let backup_descriptor = BackupDescriptor::BackupID { backup_id: new_user_keys.backup_id.clone(), user_identity: user_identity.clone(), }; let user_data = backup_client .download_backup_data(&backup_descriptor, RequestedData::UserData) .await?; assert_eq!(Some(user_data), new_backup_data.user_data); // Test Data download for old `backup_id` -> should be not found let removed_backup_descriptor = BackupDescriptor::BackupID { backup_id: backup_data.backup_id.clone(), user_identity: user_identity.clone(), }; let user_data_response = backup_client .download_backup_data(&removed_backup_descriptor, RequestedData::UserData) .await; let user_keys_response = backup_client .download_backup_data(&removed_backup_descriptor, RequestedData::UserKeys) .await; assert_reqwest_error(user_data_response, StatusCode::NOT_FOUND); assert_reqwest_error(user_keys_response, StatusCode::NOT_FOUND); Ok(()) } diff --git a/shared/backup_client/src/lib.rs b/shared/backup_client/src/lib.rs index 2d971e9d8..40d1c1697 100644 --- a/shared/backup_client/src/lib.rs +++ b/shared/backup_client/src/lib.rs @@ -1,401 +1,400 @@ #[cfg(target_arch = "wasm32")] mod web; use async_stream::{stream, try_stream}; pub use comm_lib::auth::UserIdentity; pub use comm_lib::backup::{ DownloadLogsRequest, LatestBackupInfoResponse, LogWSRequest, LogWSResponse, UploadLogRequest, }; pub use futures_util::{Sink, SinkExt, Stream, StreamExt, TryStreamExt}; use hex::ToHex; use reqwest::{ header::InvalidHeaderValue, multipart::{Form, Part}, Body, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::time::Duration; use tokio_tungstenite_wasm::{ connect, Error as TungsteniteError, Message::Binary, }; const LOG_DOWNLOAD_RETRY_DELAY: Duration = Duration::from_secs(5); const LOG_DOWNLOAD_MAX_RETRY: usize = 3; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::wasm_bindgen; #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Debug, Clone)] pub struct BackupClient { url: reqwest::Url, } impl BackupClient { pub fn new>(url: T) -> Result { Ok(BackupClient { url: url.try_into()?, }) } } /// Backup functions impl BackupClient { pub async fn upload_backup( &self, user_identity: &UserIdentity, backup_data: BackupData, ) -> Result<(), Error> { let BackupData { backup_id, user_keys, user_data, attachments, siwe_backup_msg, } = backup_data; let endpoint = match (user_data.clone(), user_keys.clone()) { (None, None) => return Err(Error::InvalidRequest), (Some(_), Some(_)) => "backups", (Some(_), None) => "backups/user_data", (None, Some(_)) => "backups/user_keys", }; let client = reqwest::Client::new(); let mut form = Form::new().text("backup_id", backup_id); if let Some(user_keys_value) = user_keys.clone() { form = form .text( "user_keys_hash", Sha256::digest(&user_keys_value).encode_hex::(), ) .part("user_keys", Part::stream(Body::from(user_keys_value))); } if let Some(user_data_value) = user_data.clone() { form = form .text( "user_data_hash", Sha256::digest(&user_data_value).encode_hex::(), ) - .part("user_data", Part::stream(Body::from(user_data_value))); + .part("user_data", Part::stream(Body::from(user_data_value))) + .text("attachments", attachments.join("\n")); } - form = form.text("attachments", attachments.join("\n")); - if let Some(siwe_backup_msg_value) = siwe_backup_msg { form = form.text("siwe_backup_msg", siwe_backup_msg_value); } let response = client .post(self.url.join(endpoint)?) .bearer_auth(user_identity.as_authorization_token()?) .multipart(form) .send() .await?; response.error_for_status()?; Ok(()) } pub async fn download_backup_data( &self, backup_descriptor: &BackupDescriptor, requested_data: RequestedData, ) -> Result, Error> { let client = reqwest::Client::new(); let url = self.url.join("backups/")?; let url = match backup_descriptor { BackupDescriptor::BackupID { backup_id, .. } => { url.join(&format!("{backup_id}/"))? } BackupDescriptor::Latest { user_identifier } => { url.join(&format!("latest/{user_identifier}/"))? } }; let url = match &requested_data { RequestedData::BackupInfo => url.join("backup_info")?, RequestedData::UserKeys => url.join("user_keys")?, RequestedData::UserData => url.join("user_data")?, }; let mut request = client.get(url); if let BackupDescriptor::BackupID { user_identity, .. } = backup_descriptor { request = request.bearer_auth(user_identity.as_authorization_token()?) } let response = request.send().await?; let result = response.error_for_status()?.bytes().await?.to_vec(); Ok(result) } } /// Log functions impl BackupClient { pub async fn upload_logs( &self, user_identity: &UserIdentity, ) -> Result< ( impl Sink, impl Stream>, ), Error, > { let (tx, rx) = self.create_log_ws_connection(user_identity).await?; let rx = rx.map(|response| match response? { LogWSResponse::LogUploaded { backup_id, log_id } => { Ok(LogUploadConfirmation { backup_id, log_id }) } LogWSResponse::ServerError => Err(Error::ServerError), msg => Err(Error::InvalidBackupMessage(msg)), }); Ok((tx, rx)) } /// Handles complete log download. /// It will try and retry download a few times, but if the issues persist /// the next item returned will be the last received error and the stream /// will be closed. pub async fn download_logs<'this>( &'this self, user_identity: &'this UserIdentity, backup_id: &'this str, ) -> impl Stream> + 'this { stream! { let mut last_downloaded_log = None; let mut fail_count = 0; 'retry: loop { let stream = self.log_download_stream(user_identity, backup_id, &mut last_downloaded_log).await; let mut stream = Box::pin(stream); while let Some(item) = stream.next().await { match item { Ok(log) => yield Ok(log), Err(err) => { println!("Error when downloading logs: {err:?}"); fail_count += 1; if fail_count >= LOG_DOWNLOAD_MAX_RETRY { yield Err(err); break 'retry; } #[cfg(target_arch = "wasm32")] let _ = web::sleep(LOG_DOWNLOAD_RETRY_DELAY).await; #[cfg(not(target_arch = "wasm32"))] tokio::time::sleep(LOG_DOWNLOAD_RETRY_DELAY).await; continue 'retry; } } } // Everything downloaded return; } println!("Log download failed!"); } } /// Handles singular connection websocket connection. Returns error in case /// anything goes wrong e.g. missing log or connection error. async fn log_download_stream<'stream>( &'stream self, user_identity: &'stream UserIdentity, backup_id: &'stream str, last_downloaded_log: &'stream mut Option, ) -> impl Stream> + 'stream { try_stream! { let (mut tx, mut rx) = self.create_log_ws_connection(user_identity).await?; tx.send(DownloadLogsRequest { backup_id: backup_id.to_string(), from_id: *last_downloaded_log, }) .await?; while let Some(response) = rx.try_next().await? { let expected_log_id = last_downloaded_log.unwrap_or(0); match response { LogWSResponse::LogDownload { content, attachments, log_id, } if log_id == expected_log_id + 1 => { *last_downloaded_log = Some(log_id); yield DownloadedLog { content, attachments, }; } LogWSResponse::LogDownload { .. } => { Err(Error::LogMissing)?; } LogWSResponse::LogDownloadFinished { last_log_id: Some(log_id), } if log_id == expected_log_id => { tx.send(DownloadLogsRequest { backup_id: backup_id.to_string(), from_id: *last_downloaded_log, }) .await? } LogWSResponse::LogDownloadFinished { last_log_id: None } => return, LogWSResponse::LogDownloadFinished { .. } => { Err(Error::LogMissing)?; } msg => Err(Error::InvalidBackupMessage(msg))?, } } Err(Error::WSClosed)?; } } async fn create_log_ws_connection>( &self, user_identity: &UserIdentity, ) -> Result< ( impl Sink, impl Stream>, ), Error, > { let url = self.create_ws_url()?; let stream = connect(url).await?; let (mut tx, rx) = stream.split(); tx.send(Binary(bincode::serialize(&LogWSRequest::Authenticate( user_identity.clone(), ))?)) .await?; let tx = tx.with(|request: Request| async { let request: LogWSRequest = request.into(); let request = bincode::serialize(&request)?; Ok(Binary(request)) }); let rx = rx.filter_map(|msg| async { let bytes = match msg { Ok(Binary(bytes)) => bytes, Ok(_) => return Some(Err(Error::InvalidWSMessage)), Err(err) => return Some(Err(err.into())), }; match bincode::deserialize(&bytes) { Ok(response) => Some(Ok(response)), Err(err) => Some(Err(err.into())), } }); let tx = Box::pin(tx); let mut rx = Box::pin(rx); if let Some(response) = rx.try_next().await? { match response { LogWSResponse::AuthSuccess => {} LogWSResponse::Unauthenticated => Err(Error::Unauthenticated)?, msg => Err(Error::InvalidBackupMessage(msg))?, } } Ok((tx, rx)) } fn create_ws_url(&self) -> Result { let mut url = self.url.clone(); match url.scheme() { "http" => url.set_scheme("ws").map_err(|_| Error::UrlSchemaError)?, "https" => url.set_scheme("wss").map_err(|_| Error::UrlSchemaError)?, _ => (), }; let url = url.join("logs")?; Ok(url) } } #[derive(Debug, Clone)] pub struct BackupData { pub backup_id: String, pub user_keys: Option>, pub user_data: Option>, pub attachments: Vec, pub siwe_backup_msg: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum BackupDescriptor { BackupID { #[serde(rename = "backupID")] backup_id: String, #[serde(rename = "userIdentity")] user_identity: UserIdentity, }, Latest { user_identifier: String, }, } #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] #[derive(Debug, Clone)] pub enum RequestedData { BackupInfo, UserKeys, UserData, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct LogUploadConfirmation { pub backup_id: String, pub log_id: usize, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct DownloadedLog { pub content: Vec, pub attachments: Option>, } #[derive(Debug, derive_more::Display, derive_more::From)] pub enum Error { InvalidAuthorizationHeader, UrlSchemaError, UrlError(url::ParseError), ReqwestError(reqwest::Error), TungsteniteError(TungsteniteError), JsonError(serde_json::Error), BincodeError(bincode::Error), InvalidWSMessage, #[display(fmt = "Error::InvalidBackupMessage({:?})", _0)] InvalidBackupMessage(LogWSResponse), ServerError, LogMissing, WSClosed, Unauthenticated, InvalidRequest, } impl std::error::Error for Error {} impl From for Error { fn from(_: InvalidHeaderValue) -> Self { Self::InvalidAuthorizationHeader } }