diff --git a/Cargo.toml b/Cargo.toml index 9110227..44b6524 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,12 @@ edition = "2018" serde = "^1.0" serde_derive = "^1.0" reqwest = {version = "0.11.4", features = ["json"]} +log = "0.4.14" + +[dev-dependencies] +env_logger = "0.8.4" +lazy_static = "1.4.0" [dev-dependencies.tokio] version = "1.8.0" -features = ["macros", "rt-multi-thread"] \ No newline at end of file +features = ["macros", "rt-multi-thread"] diff --git a/src/client.rs b/src/client.rs index 84b36bc..c42a44d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,8 +1,14 @@ -use crate::error::Result; +use crate::error::{Error, Result}; use crate::paths::access_management::{ ApiVersionResponse, GetServicesResponse, SessionKeyResponse, VerifyAccessKeyResponse, }; +use crate::paths::adding_files::{ + AddFileRequest, AddFileResponse, ArchiveFilesRequest, ArchiveFilesResponse, DeleteFilesRequest, + DeleteFilesResponse, UnarchiveFilesRequest, UnarchiveFilesResponse, UndeleteFilesRequest, + UndeleteFilesResponse, +}; use crate::paths::Path; +use reqwest::Response; use serde::de::DeserializeOwned; use serde::Serialize; @@ -24,53 +30,139 @@ impl Client { } /// Starts a get request to the path associated with the return type - async fn get( + async fn get_and_parse( &mut self, query: &Q, ) -> Result { - let response: T = self + let response = self .inner .get(format!("{}/{}", self.base_url, T::get_path())) .header(ACCESS_KEY_HEADER, &self.access_key) .query(query) .send() - .await? - .json() .await?; - Ok(response) + let response = Self::extract_error(response).await?; + + Self::extract_content(response).await } /// Stats a post request to the path associated with the return type - async fn post(&mut self, body: B) -> Result { - let response: T = self + async fn post(&mut self, body: B) -> Result { + let response = self .inner .post(format!("{}/{}", self.base_url, T::get_path())) .json(&body) .header(ACCESS_KEY_HEADER, &self.access_key) .send() - .await? - .json() .await?; + let response = Self::extract_error(response).await?; Ok(response) } + /// Stats a post request and parses the body as json + async fn post_and_parse( + &mut self, + body: B, + ) -> Result { + let response = self.post::(body).await?; + + Self::extract_content(response).await + } + + /// Stats a post request to the path associated with the return type + async fn post_binary(&mut self, data: Vec) -> Result { + let response = self + .inner + .post(format!("{}/{}", self.base_url, T::get_path())) + .body(data) + .header(ACCESS_KEY_HEADER, &self.access_key) + .header("Content-Type", "application/octet-stream") + .send() + .await?; + let response = Self::extract_error(response).await?; + + Self::extract_content(response).await + } + + /// Returns an error with the response text content if the status doesn't indicate success + async fn extract_error(response: Response) -> Result { + if !response.status().is_success() { + let msg = response.text().await?; + Err(Error::Hydrus(msg)) + } else { + Ok(response) + } + } + + /// Parses the response as JSOn + async fn extract_content(response: Response) -> Result { + response.json::().await.map_err(Error::from) + } + /// Returns the current API version. It's being incremented every time the API changes. pub async fn api_version(&mut self) -> Result { - self.get(&()).await + self.get_and_parse(&()).await } /// Creates a new session key pub async fn session_key(&mut self) -> Result { - self.get(&()).await + self.get_and_parse(&()).await } /// Verifies if the access key is valid and returns some information about its permissions pub async fn verify_access_key(&mut self) -> Result { - self.get(&()).await + self.get_and_parse(&()).await } /// Returns the list of tag and file services of the client pub async fn get_services(&mut self) -> Result { - self.get(&()).await + self.get_and_parse(&()).await + } + + /// Adds a file to hydrus + pub async fn add_file>(&mut self, path: S) -> Result { + self.post_and_parse(AddFileRequest { + path: path.as_ref().to_string(), + }) + .await + } + + /// Adds a file from binary data to hydrus + pub async fn add_binary_file(&mut self, data: Vec) -> Result { + self.post_binary(data).await + } + + /// Moves files with matching hashes to the trash + pub async fn delete_files(&mut self, hashes: Vec) -> Result<()> { + self.post::(DeleteFilesRequest { hashes }) + .await?; + + Ok(()) + } + + /// Pulls files out of the trash by hash + pub async fn undelete_files(&mut self, hashes: Vec) -> Result<()> { + self.post::(UndeleteFilesRequest { hashes }) + .await?; + + Ok(()) + } + + /// Moves files from the inbox into the archive + pub async fn archive_files(&mut self, hashes: Vec) -> Result<()> { + self.post::(ArchiveFilesRequest { hashes }) + .await?; + + Ok(()) + } + + /// Moves files from the archive into the inbox + pub async fn unarchive_files(&mut self, hashes: Vec) -> Result<()> { + self.post::(UnarchiveFilesRequest { + hashes, + }) + .await?; + + Ok(()) } } diff --git a/src/error.rs b/src/error.rs index 0afba0d..34e7d64 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,12 +6,14 @@ pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { Reqwest(reqwest::Error), + Hydrus(String), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Reqwest(e) => {e.fmt(f)} + Self::Reqwest(e) => e.fmt(f), + Self::Hydrus(msg) => msg.fmt(f), } } } @@ -20,6 +22,7 @@ impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { Self::Reqwest(e) => e.source(), + Self::Hydrus(_) => None, } } } @@ -28,4 +31,4 @@ impl From for Error { fn from(e: reqwest::Error) -> Self { Self::Reqwest(e) } -} \ No newline at end of file +} diff --git a/src/paths/access_management.rs b/src/paths/access_management.rs index 586c9f5..4e182ce 100644 --- a/src/paths/access_management.rs +++ b/src/paths/access_management.rs @@ -2,7 +2,7 @@ use crate::paths::common::BasicServiceInfo; use crate::paths::Path; use std::collections::HashMap; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct ApiVersionResponse { pub version: u32, pub hydrus_version: u32, @@ -14,7 +14,7 @@ impl Path for ApiVersionResponse { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct SessionKeyResponse { pub session_key: String, } @@ -25,7 +25,7 @@ impl Path for SessionKeyResponse { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct VerifyAccessKeyResponse { pub basic_permissions: Vec, pub human_description: String, @@ -37,7 +37,7 @@ impl Path for VerifyAccessKeyResponse { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct GetServicesResponse(pub HashMap>); impl Path for GetServicesResponse { diff --git a/src/paths/adding_files.rs b/src/paths/adding_files.rs new file mode 100644 index 0000000..45544ac --- /dev/null +++ b/src/paths/adding_files.rs @@ -0,0 +1,56 @@ +use crate::paths::common::BasicHashList; +use crate::paths::Path; + +#[derive(Debug, Clone, Serialize)] +pub struct AddFileRequest { + pub path: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AddFileResponse { + pub status: u8, + pub hash: String, + pub note: String, +} + +impl Path for AddFileResponse { + fn get_path() -> String { + String::from("add_files/add_file") + } +} + +pub type DeleteFilesRequest = BasicHashList; +pub struct DeleteFilesResponse; + +impl Path for DeleteFilesResponse { + fn get_path() -> String { + String::from("add_files/delete_files") + } +} + +pub type UndeleteFilesRequest = BasicHashList; +pub struct UndeleteFilesResponse; + +impl Path for UndeleteFilesResponse { + fn get_path() -> String { + String::from("add_files/undelete_files") + } +} + +pub type ArchiveFilesRequest = BasicHashList; +pub struct ArchiveFilesResponse; + +impl Path for ArchiveFilesResponse { + fn get_path() -> String { + String::from("add_files/archive_files") + } +} + +pub type UnarchiveFilesRequest = BasicHashList; +pub struct UnarchiveFilesResponse; + +impl Path for UnarchiveFilesResponse { + fn get_path() -> String { + String::from("add_files/unarchive_files") + } +} diff --git a/src/paths/common.rs b/src/paths/common.rs index 29b37b1..3e35771 100644 --- a/src/paths/common.rs +++ b/src/paths/common.rs @@ -2,4 +2,9 @@ pub struct BasicServiceInfo { pub name: String, pub service_key: String, -} \ No newline at end of file +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BasicHashList { + pub hashes: Vec, +} diff --git a/src/paths/mod.rs b/src/paths/mod.rs index 236809a..978b799 100644 --- a/src/paths/mod.rs +++ b/src/paths/mod.rs @@ -1,6 +1,7 @@ pub mod access_management; +pub mod adding_files; pub mod common; pub trait Path { fn get_path() -> String; -} \ No newline at end of file +} diff --git a/tests/common.rs b/tests/common.rs index 2b376be..d84f128 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,6 +1,23 @@ use hydrus_api::client::Client; +use log::LevelFilter; use std::env; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +pub fn setup() { + lazy_static::lazy_static! { static ref SETUP_DONE: Arc = Arc::new(AtomicBool::new(false)); } + if !SETUP_DONE.swap(true, Ordering::SeqCst) { + env_logger::builder() + .filter_level(LevelFilter::Trace) + .init(); + } +} pub fn get_client() -> Client { - Client::new(env::var("HYDRUS_URL").unwrap(), env::var("HYDRUS_ACCESS_KEY").unwrap()).unwrap() -} \ No newline at end of file + setup(); + Client::new( + env::var("HYDRUS_URL").unwrap(), + env::var("HYDRUS_ACCESS_KEY").unwrap(), + ) + .unwrap() +} diff --git a/tests/test_adding_files.rs b/tests/test_adding_files.rs new file mode 100644 index 0000000..5c4bc2d --- /dev/null +++ b/tests/test_adding_files.rs @@ -0,0 +1,42 @@ +mod common; + +#[tokio::test] +async fn it_adds_files() { + let mut client = common::get_client(); + let result = client.add_file("/does/not/exist").await; + assert!(result.is_err()); // because the path does not exist +} + +#[tokio::test] +async fn it_adds_binary_files() { + let mut client = common::get_client(); + let result = client + .add_binary_file(vec![0u8, 0u8, 0u8, 0u8]) + .await + .unwrap(); + assert_eq!(result.status, 4); // should fail because the filetype is unknown +} + +#[tokio::test] +async fn it_deletes_files() { + let mut client = common::get_client(); + client.delete_files(vec![]).await.unwrap(); +} + +#[tokio::test] +async fn it_undeletes_files() { + let mut client = common::get_client(); + client.undelete_files(vec![]).await.unwrap(); +} + +#[tokio::test] +async fn it_archives_files() { + let mut client = common::get_client(); + client.archive_files(vec![]).await.unwrap(); +} + +#[tokio::test] +async fn it_unarchives_files() { + let mut client = common::get_client(); + client.unarchive_files(vec![]).await.unwrap(); +}