From c177ba7a49caf63582719a4086e5032e2c939cfa Mon Sep 17 00:00:00 2001 From: jakka Date: Wed, 1 Oct 2025 00:25:24 +0300 Subject: reconfigured the library, moved async client and trait to their own module. in the future sync and async implementations will be separated --- Cargo.lock | 181 +++++++++++++++++++++ Cargo.toml | 16 +- src/client.rs | 427 ------------------------------------------------- src/client_async.rs | 449 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 8 +- src/tests.rs | 4 +- src/traits.rs | 62 -------- src/traits_async.rs | 71 +++++++++ src/types.rs | 21 +++ 9 files changed, 741 insertions(+), 498 deletions(-) delete mode 100644 src/client.rs create mode 100644 src/client_async.rs delete mode 100644 src/traits.rs create mode 100644 src/traits_async.rs diff --git a/Cargo.lock b/Cargo.lock index c2bf6b2..6b05a94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,35 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -105,6 +134,24 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -116,6 +163,15 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -153,6 +209,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -350,6 +416,7 @@ dependencies = [ "thiserror", "tokio", "tokio-util", + "ureq", "urlencoding", ] @@ -611,6 +678,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "log" version = "0.4.28" @@ -666,6 +739,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "object" version = "0.36.7" @@ -758,6 +837,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.101" @@ -864,13 +949,24 @@ version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -1126,6 +1222,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -1280,6 +1407,39 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +dependencies = [ + "base64", + "cookie_store", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf-8", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.7" @@ -1298,6 +1458,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -1310,6 +1476,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -1438,6 +1610,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-link" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index c3db673..02c7815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,20 @@ version = "0.1.0" edition = "2024" [dependencies] -async-trait = "0.1.89" -reqwest = { version = "0.12.23", features = ["json", "stream"] } +async-trait = { version = "0.1.89", optional = true } +reqwest = { version = "0.12.23", features = [ + "json", + "stream", +], optional = true } serde = { version = "1.0.225", features = ["derive"] } serde_json = "1.0.145" serde_repr = "0.1.20" thiserror = "2.0.16" -tokio = { version = "1.47.1", features = ["rt", "macros"] } -tokio-util = "0.7.16" +tokio = { version = "1.47.1", features = ["rt", "macros"], optional = true } +tokio-util = { version = "0.7.16", optional = true } +ureq = { version = "3.1.2", features = ["json"], optional = true } urlencoding = "2.1.3" + +[features] +async = ["dep:async-trait", "dep:reqwest", "dep:tokio", "dep:tokio-util"] +sync = ["dep:ureq"] diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index c73b962..0000000 --- a/src/client.rs +++ /dev/null @@ -1,427 +0,0 @@ -use std::{collections::HashMap, path::PathBuf}; - -use async_trait::async_trait; -use reqwest::{Body, RequestBuilder}; -use serde::Deserialize; -use tokio_util::codec::{BytesCodec, FramedRead}; - -use crate::{traits::*, types::*}; - -type Result = std::result::Result; - -/// hydrus client -pub struct HydrusClient { - client: reqwest::Client, - apikey: Option, - sessionkey: Option, - url: String, -} - -impl HydrusClient { - /// create a new hydurs client object. requires a url to hydrus API endpoint - pub fn new(url: &str) -> HydrusClient { - HydrusClient { - client: reqwest::Client::new(), - apikey: None, - sessionkey: None, - url: url.to_string(), - } - } - /// set an api key - pub fn set_api_key(&mut self, key: &str) { - self.apikey = Some(key.to_owned()) - } - /// set a session key - pub fn set_session_key(&mut self, key: &str) { - self.sessionkey = Some(key.to_owned()) - } - - fn set_get_request_key(&self, url: &str) -> Result { - let request = self.client.get(url); - if let Some(key) = &self.sessionkey { - Ok(request.header("Hydrus-Client-API-Access-Key", key)) - } else if let Some(key) = &self.apikey { - Ok(request.header("Hydrus-Client-API-Access-Key", key)) - } else { - Err(HydrusError::KeyNotSupplied) - } - } - - fn set_post_request_key(&self, url: &str) -> Result { - let request = self.client.post(url); - if let Some(key) = &self.sessionkey { - Ok(request.header("Hydrus-Client-API-Access-Key", key)) - } else if let Some(key) = &self.apikey { - Ok(request.header("Hydrus-Client-API-Access-Key", key)) - } else { - Err(HydrusError::KeyNotSupplied) - } - } -} - -#[derive(Deserialize, Debug)] -struct HydrusResponse { - #[serde( - alias = "service", - alias = "services", - alias = "access_key", - alias = "session_key" - )] - body: T, -} - -#[async_trait] -impl AccessManagement for HydrusClient { - async fn request_new_permissions( - &self, - name: &str, - permissions: &[HydrusPermissions], - ) -> Result { - let mut req_url = self.url.to_owned(); - req_url.push_str("request_new_permissions"); - - let mut request = self.client.get(req_url); - request = request.query(&[("name", &urlencoding::encode(&name))]); - - if permissions.is_empty() { - request = request.query(&[("permit_everything", "true")]); - } else { - let json_string = serde_json::to_string(&permissions)?; - request = request.query(&[("basic_permissions", &urlencoding::encode(&json_string))]); - }; - - Ok(request - .send() - .await? - .json::>() - .await? - .body) - } - - async fn get_session_key(&self) -> Result { - let mut req_url = self.url.to_owned(); - req_url.push_str("session_key"); - - let mut request = self.client.get(req_url); - - if let Some(key) = &self.apikey { - request = request.header("Hydrus-Client-API-Access-Key", key); - } - - Ok(request - .send() - .await? - .json::>() - .await? - .body) - } - - async fn verify_access_key(&self, key: &str) -> Result { - let mut req_url = self.url.to_owned(); - req_url.push_str("verify_access_key"); - - Ok(self - .client - .get(req_url) - .header("Hydrus-Client-API-Access-Key", key) - .send() - .await? - .json::() - .await?) - } - - async fn get_service_name(&self, name: &str) -> Result { - let mut req_url = self.url.to_owned(); - req_url.push_str("get_service"); - let mut request = self.set_get_request_key(&req_url)?; - request = request.query(&[("service_name", &urlencoding::encode(name))]); - - Ok(request - .send() - .await? - .json::>() - .await? - .body) - } - - async fn get_service_key(&self, key: &str) -> Result { - let mut req_url = self.url.to_owned(); - req_url.push_str("get_service"); - let mut request = self.set_get_request_key(&req_url)?; - request = request.query(&[("service_key", &urlencoding::encode(key))]); - - Ok(request - .send() - .await? - .json::>() - .await? - .body) - } - - async fn get_services(&self) -> Result> { - let mut req_url = self.url.to_owned(); - req_url.push_str("get_services"); - - let mut services = self - .set_get_request_key(&req_url)? - .send() - .await? - .json::>>() - .await? - .body; - - for (key, service) in services.iter_mut() { - service.service_key = key.to_string(); - } - - Ok(services) - } -} - -#[async_trait] -impl ImportingAndDeletingFiles for HydrusClient { - async fn add_file_via_path( - &self, - path: PathBuf, - delete: Option, - domains: Option, - ) -> Result { - let mut form = AddFileRequest { - path: path.to_owned(), - delete_after_successful_import: delete, - ..Default::default() - }; - - if let Some(file_domain) = domains { - match file_domain { - FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()), - FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()), - FileDomain::DeletedFileServiceKey(key) => { - form.deleted_file_service_key = Some(key.to_string()) - } - FileDomain::DeletedFileServiceKeys(keys) => { - form.deleted_file_service_keys = Some(keys.to_owned()) - } - } - } - - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/add_file"); - - Ok(self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/json") - .json(&form) - .send() - .await? - .json::() - .await?) - } - - async fn add_file_via_file(&self, file: PathBuf) -> Result { - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/add_file"); - - let file = tokio::fs::File::open(file).await?; - - let stream = FramedRead::new(file, BytesCodec::new()); - let body = Body::wrap_stream(stream); - - Ok(self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/octet-stream") - .body(body) - .send() - .await? - .json::() - .await?) - } - - async fn delete_files( - &self, - file: HydrusFile, - domain: Option, - reason: Option, - ) -> Result<()> { - let mut form = FileRequest { - file, - reason, - ..Default::default() - }; - - if let Some(file_domain) = domain { - match file_domain { - FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()), - FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()), - FileDomain::DeletedFileServiceKey(key) => { - form.deleted_file_service_key = Some(key.to_string()) - } - FileDomain::DeletedFileServiceKeys(keys) => { - form.deleted_file_service_keys = Some(keys.to_owned()) - } - } - } - - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/delete_files"); - - let _ = self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/json") - .json(&form) - .send() - .await?; - - Ok(()) - } - - async fn undelete_files(&self, file: HydrusFile, domain: Option) -> Result<()> { - let mut form = FileRequest { - file, - ..Default::default() - }; - - if let Some(file_domain) = domain { - match file_domain { - FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()), - FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()), - FileDomain::DeletedFileServiceKey(key) => { - form.deleted_file_service_key = Some(key.to_string()) - } - FileDomain::DeletedFileServiceKeys(keys) => { - form.deleted_file_service_keys = Some(keys.to_owned()) - } - } - } - - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/undelete_files"); - - let _ = self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/json") - .json(&form) - .send() - .await?; - - Ok(()) - } - - async fn clear_file_deletion_records(&self, file: HydrusFile) -> Result<()> { - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/clear_file_deletion_record"); - let _ = self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/json") - .json(&file) - .send() - .await?; - - Ok(()) - } - - async fn migrate_files(&self, file: HydrusFile, domain: FileDomain) -> Result<()> { - let mut form = FileRequest { - file, - ..Default::default() - }; - - match domain { - FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()), - FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()), - FileDomain::DeletedFileServiceKey(key) => { - form.deleted_file_service_key = Some(key.to_string()) - } - FileDomain::DeletedFileServiceKeys(keys) => { - form.deleted_file_service_keys = Some(keys.to_owned()) - } - } - - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/migrate_files"); - let _ = self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/json") - .json(&form) - .send() - .await?; - - Ok(()) - } - - async fn archive_files(&self, file: HydrusFile) -> Result<()> { - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/archive_files"); - - let form = FileRequest { - file, - ..Default::default() - }; - - let _ = self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/json") - .json(&form) - .send() - .await?; - - Ok(()) - } - - async fn unarchive_files(&self, file: HydrusFile) -> Result<()> { - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/unarchive_files"); - - let form = FileRequest { - file, - ..Default::default() - }; - - let _ = self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/json") - .json(&form) - .send() - .await?; - - Ok(()) - } - - async fn generate_hashes_for_path(&self, file: PathBuf) -> Result { - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/generate_hashes"); - - let form = AddFileRequest { - path: file.to_owned(), - ..Default::default() - }; - - Ok(self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/json") - .json(&form) - .send() - .await? - .json::() - .await?) - } - - async fn generate_hashes_for_file(&self, file: PathBuf) -> Result { - let mut req_url = self.url.to_owned(); - req_url.push_str("add_files/generate_hashes"); - - let file = tokio::fs::File::open(file).await?; - - let stream = FramedRead::new(file, BytesCodec::new()); - let body = Body::wrap_stream(stream); - - Ok(self - .set_post_request_key(&req_url)? - .header("Content-Type", "application/octet-stream") - .body(body) - .send() - .await? - .json::() - .await?) - } -} diff --git a/src/client_async.rs b/src/client_async.rs new file mode 100644 index 0000000..c189a06 --- /dev/null +++ b/src/client_async.rs @@ -0,0 +1,449 @@ +use std::{collections::HashMap, path::PathBuf}; + +use async_trait::async_trait; +use reqwest::{Body, RequestBuilder}; +use serde::Deserialize; +use tokio_util::codec::{BytesCodec, FramedRead}; + +use crate::{traits_async::*, types::*}; + +type Result = std::result::Result; + +/// hydrus client +pub struct HydrusClient { + client: reqwest::Client, + apikey: Option, + sessionkey: Option, + url: String, +} + +impl HydrusClient { + /// create a new hydurs client object. requires a url to hydrus API endpoint + pub fn new(url: &str) -> HydrusClient { + HydrusClient { + client: reqwest::Client::new(), + apikey: None, + sessionkey: None, + url: url.to_string(), + } + } + /// set an api key + pub fn set_api_key(&mut self, key: &str) { + self.apikey = Some(key.to_owned()) + } + /// set a session key + pub fn set_session_key(&mut self, key: &str) { + self.sessionkey = Some(key.to_owned()) + } + + fn set_get_request_key(&self, url: &str) -> Result { + let request = self.client.get(url); + if let Some(key) = &self.sessionkey { + Ok(request.header("Hydrus-Client-API-Access-Key", key)) + } else if let Some(key) = &self.apikey { + Ok(request.header("Hydrus-Client-API-Access-Key", key)) + } else { + Err(HydrusError::KeyNotSupplied) + } + } + + fn set_post_request_key(&self, url: &str) -> Result { + let request = self.client.post(url); + if let Some(key) = &self.sessionkey { + Ok(request.header("Hydrus-Client-API-Access-Key", key)) + } else if let Some(key) = &self.apikey { + Ok(request.header("Hydrus-Client-API-Access-Key", key)) + } else { + Err(HydrusError::KeyNotSupplied) + } + } +} + +#[derive(Deserialize, Debug)] +struct HydrusResponse { + #[serde( + alias = "service", + alias = "services", + alias = "access_key", + alias = "session_key" + )] + body: T, +} + +#[async_trait] +impl AccessManagement for HydrusClient { + async fn request_new_permissions( + &self, + name: &str, + permissions: &[HydrusPermissions], + ) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("request_new_permissions"); + + let mut request = self.client.get(req_url); + request = request.query(&[("name", &urlencoding::encode(name))]); + + if permissions.is_empty() { + request = request.query(&[("permit_everything", "true")]); + } else { + let json_string = serde_json::to_string(&permissions)?; + request = request.query(&[("basic_permissions", &urlencoding::encode(&json_string))]); + }; + + Ok(request + .send() + .await? + .json::>() + .await? + .body) + } + + async fn get_session_key(&self) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("session_key"); + + let mut request = self.client.get(req_url); + + if let Some(key) = &self.apikey { + request = request.header("Hydrus-Client-API-Access-Key", key); + } + + Ok(request + .send() + .await? + .json::>() + .await? + .body) + } + + async fn verify_access_key(&self, key: &str) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("verify_access_key"); + + Ok(self + .client + .get(req_url) + .header("Hydrus-Client-API-Access-Key", key) + .send() + .await? + .json::() + .await?) + } + + async fn get_service_name(&self, name: &str) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("get_service"); + let mut request = self.set_get_request_key(&req_url)?; + request = request.query(&[("service_name", &urlencoding::encode(name))]); + + Ok(request + .send() + .await? + .json::>() + .await? + .body) + } + + async fn get_service_key(&self, key: &str) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("get_service"); + let mut request = self.set_get_request_key(&req_url)?; + request = request.query(&[("service_key", &urlencoding::encode(key))]); + + Ok(request + .send() + .await? + .json::>() + .await? + .body) + } + + async fn get_services(&self) -> Result> { + let mut req_url = self.url.to_owned(); + req_url.push_str("get_services"); + + let mut services = self + .set_get_request_key(&req_url)? + .send() + .await? + .json::>>() + .await? + .body; + + for (key, service) in services.iter_mut() { + service.service_key = key.to_string(); + } + + Ok(services) + } +} + +#[async_trait] +impl ImportingAndDeletingFiles for HydrusClient { + async fn add_file_via_path( + &self, + path: PathBuf, + delete: Option, + domains: Option, + ) -> Result { + let mut form = AddFileRequest { + path: path.to_owned(), + delete_after_successful_import: delete, + ..Default::default() + }; + + if let Some(file_domain) = domains { + match file_domain { + FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()), + FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()), + FileDomain::DeletedFileServiceKey(key) => { + form.deleted_file_service_key = Some(key.to_string()) + } + FileDomain::DeletedFileServiceKeys(keys) => { + form.deleted_file_service_keys = Some(keys.to_owned()) + } + } + } + + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/add_file"); + + Ok(self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/json") + .json(&form) + .send() + .await? + .json::() + .await?) + } + + async fn add_file_via_file(&self, file: PathBuf) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/add_file"); + + let file = tokio::fs::File::open(file).await?; + + let stream = FramedRead::new(file, BytesCodec::new()); + let body = Body::wrap_stream(stream); + + Ok(self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/octet-stream") + .body(body) + .send() + .await? + .json::() + .await?) + } + + async fn delete_files( + &self, + file: HydrusFile, + domain: Option, + reason: Option, + ) -> Result<()> { + let mut form = FileRequest { + file, + reason, + ..Default::default() + }; + + if let Some(file_domain) = domain { + match file_domain { + FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()), + FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()), + FileDomain::DeletedFileServiceKey(key) => { + form.deleted_file_service_key = Some(key.to_string()) + } + FileDomain::DeletedFileServiceKeys(keys) => { + form.deleted_file_service_keys = Some(keys.to_owned()) + } + } + } + + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/delete_files"); + + let _ = self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/json") + .json(&form) + .send() + .await?; + + Ok(()) + } + + async fn undelete_files(&self, file: HydrusFile, domain: Option) -> Result<()> { + let mut form = FileRequest { + file, + ..Default::default() + }; + + if let Some(file_domain) = domain { + match file_domain { + FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()), + FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()), + FileDomain::DeletedFileServiceKey(key) => { + form.deleted_file_service_key = Some(key.to_string()) + } + FileDomain::DeletedFileServiceKeys(keys) => { + form.deleted_file_service_keys = Some(keys.to_owned()) + } + } + } + + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/undelete_files"); + + let _ = self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/json") + .json(&form) + .send() + .await?; + + Ok(()) + } + + async fn clear_file_deletion_records(&self, file: HydrusFile) -> Result<()> { + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/clear_file_deletion_record"); + let _ = self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/json") + .json(&file) + .send() + .await?; + + Ok(()) + } + + async fn migrate_files(&self, file: HydrusFile, domain: FileDomain) -> Result<()> { + let mut form = FileRequest { + file, + ..Default::default() + }; + + match domain { + FileDomain::FileServiceKey(key) => form.file_service_key = Some(key.to_string()), + FileDomain::FileServiceKeys(keys) => form.file_service_keys = Some(keys.to_owned()), + FileDomain::DeletedFileServiceKey(key) => { + form.deleted_file_service_key = Some(key.to_string()) + } + FileDomain::DeletedFileServiceKeys(keys) => { + form.deleted_file_service_keys = Some(keys.to_owned()) + } + } + + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/migrate_files"); + let _ = self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/json") + .json(&form) + .send() + .await?; + + Ok(()) + } + + async fn archive_files(&self, file: HydrusFile) -> Result<()> { + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/archive_files"); + + let form = FileRequest { + file, + ..Default::default() + }; + + let _ = self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/json") + .json(&form) + .send() + .await?; + + Ok(()) + } + + async fn unarchive_files(&self, file: HydrusFile) -> Result<()> { + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/unarchive_files"); + + let form = FileRequest { + file, + ..Default::default() + }; + + let _ = self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/json") + .json(&form) + .send() + .await?; + + Ok(()) + } + + async fn generate_hashes_for_path(&self, file: PathBuf) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/generate_hashes"); + + let form = AddFileRequest { + path: file.to_owned(), + ..Default::default() + }; + + Ok(self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/json") + .json(&form) + .send() + .await? + .json::() + .await?) + } + + async fn generate_hashes_for_file(&self, file: PathBuf) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("add_files/generate_hashes"); + + let file = tokio::fs::File::open(file).await?; + + let stream = FramedRead::new(file, BytesCodec::new()); + let body = Body::wrap_stream(stream); + + Ok(self + .set_post_request_key(&req_url)? + .header("Content-Type", "application/octet-stream") + .body(body) + .send() + .await? + .json::() + .await?) + } +} + +#[async_trait] +impl ImportingAndEditingUrls for HydrusClient { + async fn get_url_files( + &self, + url: &str, + doublecheck_file_system: Option, + ) -> Result { + let mut req_url = self.url.to_owned(); + req_url.push_str("add_urls/get_url_files"); + + let mut request = self.set_get_request_key(&req_url)?; + + request = request.query(&[("url", url)]); + + if let Some(doublecheck) = doublecheck_file_system { + request = request.query(&[("doublecheck_file_system", doublecheck)]); + } + + Ok(request.send().await?.json::().await?) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3d70b70..c2a0350 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ //! crate with hydrus client and traits necessary for accesing hydrus API -/// this crates hydrus client implementation -pub mod client; +/// this crate's hydrus client async implementation +#[cfg(feature = "async")] +pub mod client_async; /// traits for accessing hydrus API -pub mod traits; +#[cfg(feature = "async")] +pub mod traits_async; /// various objects for de/serializing requests pub mod types; diff --git a/src/tests.rs b/src/tests.rs index 5e8d9a7..8cb725e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,5 +1,5 @@ -use crate::client::HydrusClient; -use crate::{traits::*, types::*}; +use crate::client_async::HydrusClient; +use crate::{traits_async::*, types::*}; #[tokio::test] async fn test_service_name_info() { diff --git a/src/traits.rs b/src/traits.rs deleted file mode 100644 index 6cdf094..0000000 --- a/src/traits.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::{collections::HashMap, path::PathBuf}; - -use crate::types::*; -use async_trait::async_trait; - -type Result = std::result::Result; - -/// Trait for accessing and managing keys and services. -#[async_trait] -pub trait AccessManagement { - /// Register a new external program with the client. This requires the 'add from api request' mini-dialog under services->review services to be open, otherwise it will 403. - async fn request_new_permissions( - &self, - name: &str, - permissions: &[HydrusPermissions], - ) -> Result; - /// Get a new session key. - async fn get_session_key(&self) -> Result; - /// Check your access key is valid. - async fn verify_access_key(&self, key: &str) -> Result; - /// Ask the client about a specific service by providing its name. - async fn get_service_name(&self, name: &str) -> Result; - /// Ask the client about a specific service by providing its key. - async fn get_service_key(&self, key: &str) -> Result; - /// Ask the client about its services. - async fn get_services(&self) -> Result>; -} - -/// Trait for importing and deleting files. -#[async_trait] -pub trait ImportingAndDeletingFiles { - /// Tell the client to import a file by providing a local (hydrus-local) file path. - async fn add_file_via_path( - &self, - path: PathBuf, - delete: Option, - domains: Option, - ) -> Result; - /// Tell the client to import a file by sending the file. - async fn add_file_via_file(&self, file: PathBuf) -> Result; - /// Tell the client to send files to the trash. - async fn delete_files( - &self, - file: HydrusFile, - domain: Option, - reason: Option, - ) -> Result<()>; - /// Tell the client to restore files that were previously deleted to their old file service(s). - async fn undelete_files(&self, file: HydrusFile, domain: Option) -> Result<()>; - /// Tell the client to forget that it once deleted files. - async fn clear_file_deletion_records(&self, file: HydrusFile) -> Result<()>; - /// Copy files from one local file domain to another. - async fn migrate_files(&self, file: HydrusFile, domain: FileDomain) -> Result<()>; - /// Tell the client to archive inboxed files. - async fn archive_files(&self, file: HydrusFile) -> Result<()>; - /// Tell the client re-inbox archived files. - async fn unarchive_files(&self, file: HydrusFile) -> Result<()>; - /// Generate hashes for an arbitrary file by providing a local path to the file. - async fn generate_hashes_for_path(&self, file: PathBuf) -> Result; - /// Generate hashes for an arbitrary file by sending the file. - async fn generate_hashes_for_file(&self, file: PathBuf) -> Result; -} diff --git a/src/traits_async.rs b/src/traits_async.rs new file mode 100644 index 0000000..9c4de18 --- /dev/null +++ b/src/traits_async.rs @@ -0,0 +1,71 @@ +use std::{collections::HashMap, path::PathBuf}; + +use crate::types::*; +use async_trait::async_trait; + +type Result = std::result::Result; + +/// Trait for accessing and managing keys and services. +#[async_trait] +pub trait AccessManagement { + /// Register a new external program with the client. This requires the 'add from api request' mini-dialog under services->review services to be open, otherwise it will 403. + async fn request_new_permissions( + &self, + name: &str, + permissions: &[HydrusPermissions], + ) -> Result; + /// Get a new session key. + async fn get_session_key(&self) -> Result; + /// Check your access key is valid. + async fn verify_access_key(&self, key: &str) -> Result; + /// Ask the client about a specific service by providing its name. + async fn get_service_name(&self, name: &str) -> Result; + /// Ask the client about a specific service by providing its key. + async fn get_service_key(&self, key: &str) -> Result; + /// Ask the client about its services. + async fn get_services(&self) -> Result>; +} + +/// Trait for importing and deleting files. +#[async_trait] +pub trait ImportingAndDeletingFiles { + /// Tell the client to import a file by providing a local (hydrus-local) file path. + async fn add_file_via_path( + &self, + path: PathBuf, + delete: Option, + domains: Option, + ) -> Result; + /// Tell the client to import a file by sending the file. + async fn add_file_via_file(&self, file: PathBuf) -> Result; + /// Tell the client to send files to the trash. + async fn delete_files( + &self, + file: HydrusFile, + domain: Option, + reason: Option, + ) -> Result<()>; + /// Tell the client to restore files that were previously deleted to their old file service(s). + async fn undelete_files(&self, file: HydrusFile, domain: Option) -> Result<()>; + /// Tell the client to forget that it once deleted files. + async fn clear_file_deletion_records(&self, file: HydrusFile) -> Result<()>; + /// Copy files from one local file domain to another. + async fn migrate_files(&self, file: HydrusFile, domain: FileDomain) -> Result<()>; + /// Tell the client to archive inboxed files. + async fn archive_files(&self, file: HydrusFile) -> Result<()>; + /// Tell the client re-inbox archived files. + async fn unarchive_files(&self, file: HydrusFile) -> Result<()>; + /// Generate hashes for an arbitrary file by providing a local path to the file. + async fn generate_hashes_for_path(&self, file: PathBuf) -> Result; + /// Generate hashes for an arbitrary file by sending the file. + async fn generate_hashes_for_file(&self, file: PathBuf) -> Result; +} + +#[async_trait] +pub trait ImportingAndEditingUrls { + async fn get_url_files( + &self, + url: &str, + doublecheck_file_system: Option, + ) -> Result; +} diff --git a/src/types.rs b/src/types.rs index df2c387..7762065 100644 --- a/src/types.rs +++ b/src/types.rs @@ -226,3 +226,24 @@ pub struct HashResponse { #[serde(default)] pub pixel_hash: Option, } + +#[derive(Debug, Deserialize_repr)] +#[repr(u8)] +pub enum UrlStatus { + NotInDatabase = 0, + AlreadyInDatabase = 2, + PreviouslyDeleted, +} + +#[derive(Debug, Deserialize)] +pub struct UrlFileStatus { + pub status: UrlStatus, + pub hash: String, + pub note: String, +} + +#[derive(Debug, Deserialize)] +pub struct FilesUrlResponse { + pub normalised_url: String, + pub url_file_statuses: Vec, +} -- cgit v1.3.1