diff --git a/src/api_core/adding_files.rs b/src/api_core/adding_files.rs index fce22b4..52bf489 100644 --- a/src/api_core/adding_files.rs +++ b/src/api_core/adding_files.rs @@ -1,5 +1,6 @@ -use crate::api_core::common::BasicHashList; +use crate::api_core::common::{BasicHashList, ServiceIdentifier}; use crate::api_core::Endpoint; +use serde::Serialize; pub static STATUS_IMPORT_SUCCESS: u8 = 1; pub static STATUS_IMPORT_ALREADY_EXISTS: u8 = 2; @@ -30,7 +31,42 @@ impl Endpoint for AddFile { } } -pub type DeleteFilesRequest = BasicHashList; +#[derive(Clone, Debug, Serialize)] +pub struct DeleteFilesRequest { + /// The files by hashes to delete + pub hashes: Vec, + /// The files by file ids to delete + pub file_ids: Vec, + pub file_service_name: Option, + pub file_service_key: Option, + pub reason: Option, +} + +impl DeleteFilesRequest { + pub fn new(hashes: Vec, file_ids: Vec) -> Self { + Self { + hashes, + file_ids, + file_service_key: None, + file_service_name: None, + reason: None, + } + } + + /// Sets the service to delete from. If none is given it deletes + /// from all files. + pub fn set_service(&mut self, service: ServiceIdentifier) { + match service { + ServiceIdentifier::Name(name) => self.file_service_name = Some(name), + ServiceIdentifier::Key(key) => self.file_service_key = Some(key), + } + } + + /// Sets the reason for deletion + pub fn set_reason(&mut self, reason: S) { + self.reason = Some(reason.to_string()); + } +} pub struct DeleteFiles; diff --git a/src/api_core/client.rs b/src/api_core/client.rs index fe616c1..4234213 100644 --- a/src/api_core/client.rs +++ b/src/api_core/client.rs @@ -106,9 +106,8 @@ impl Client { /// Moves files with matching hashes to the trash #[tracing::instrument(skip(self), level = "debug")] - pub async fn delete_files(&self, hashes: Vec) -> Result<()> { - self.post::(DeleteFilesRequest { hashes }) - .await?; + pub async fn delete_files(&self, request: DeleteFilesRequest) -> Result<()> { + self.post::(request).await?; Ok(()) } @@ -180,7 +179,7 @@ impl Client { ) -> Result { let mut args = options.into_query_args(); args.push(("tags", Self::serialize_query_object(query)?)); - args.push(("return_hashes", String::from("true"))); + args.push(("return_hashes", Self::serialize_query_object(true)?)); self.get_and_parse::(&args) .await } @@ -432,11 +431,13 @@ impl Client { fn serialize_query_object(obj: S) -> Result { #[cfg(feature = "json")] { + tracing::trace!("Serializing query to JSON"); serde_json::ser::to_string(&obj).map_err(|e| Error::Serialization(e.to_string())) } #[cfg(feature = "cbor")] { + tracing::trace!("Serializing query to CBOR"); let mut buf = Vec::new(); ciborium::ser::into_writer(&obj, &mut buf) .map_err(|e| Error::Serialization(e.to_string()))?; @@ -471,11 +472,19 @@ impl Client { #[tracing::instrument(skip(body), level = "trace")] fn serialize_body(body: S) -> Result> { let mut buf = Vec::new(); - #[cfg(feature = "cbor")] - ciborium::ser::into_writer(&body, &mut buf) - .map_err(|e| Error::Serialization(e.to_string()))?; + #[cfg(feature = "json")] - serde_json::to_writer(&mut buf, &body).map_err(|e| Error::Serialization(e.to_string()))?; + { + tracing::trace!("Serializing body to JSON"); + serde_json::to_writer(&mut buf, &body) + .map_err(|e| Error::Serialization(e.to_string()))?; + } + #[cfg(feature = "cbor")] + { + tracing::trace!("Serializing body to CBOR"); + ciborium::ser::into_writer(&body, &mut buf) + .map_err(|e| Error::Serialization(e.to_string()))?; + } Ok(buf) } @@ -525,11 +534,16 @@ impl Client { let bytes = response.bytes().await?; let reader = bytes.reader(); #[cfg(feature = "json")] - let content = serde_json::from_reader::<_, T>(reader) - .map_err(|e| Error::Deserialization(e.to_string()))?; + let content = { + tracing::trace!("Deserializing content from JSON"); + serde_json::from_reader::<_, T>(reader) + .map_err(|e| Error::Deserialization(e.to_string()))? + }; #[cfg(feature = "cbor")] - let content = - ciborium::de::from_reader(reader).map_err(|e| Error::Deserialization(e.to_string()))?; + let content = { + tracing::trace!("Deserializing content from CBOR"); + ciborium::de::from_reader(reader).map_err(|e| Error::Deserialization(e.to_string()))? + }; tracing::trace!("response content: {:?}", content); Ok(content) diff --git a/src/wrapper/builders/delete_files_builder.rs b/src/wrapper/builders/delete_files_builder.rs new file mode 100644 index 0000000..b9abd61 --- /dev/null +++ b/src/wrapper/builders/delete_files_builder.rs @@ -0,0 +1,72 @@ +use crate::api_core::adding_files::DeleteFilesRequest; +use crate::api_core::common::{FileIdentifier, ServiceIdentifier}; +use crate::error::Result; +use crate::Client; + +pub struct DeleteFilesBuilder { + client: Client, + hashes: Vec, + ids: Vec, + reason: Option, + service: Option, +} + +impl DeleteFilesBuilder { + pub(crate) fn new(client: Client) -> Self { + Self { + client, + hashes: Vec::new(), + ids: Vec::new(), + reason: None, + service: None, + } + } + + /// Adds a file to be deleted + pub fn add_file(mut self, identifier: FileIdentifier) -> Self { + match identifier { + FileIdentifier::ID(id) => self.ids.push(id), + FileIdentifier::Hash(hash) => self.hashes.push(hash), + } + + self + } + + /// Adds multiple files to be deleted + pub fn add_files(self, ids: Vec) -> Self { + ids.into_iter().fold(self, |acc, id| acc.add_file(id)) + } + + /// Restricts deletion to a single file service + pub fn service(mut self, service: ServiceIdentifier) -> Self { + self.service = Some(service); + + self + } + + /// Adds a reason for why the file was deleted + pub fn reason(mut self, reason: S) -> Self { + self.reason = Some(reason.to_string()); + + self + } + + /// Deletes all files specified in this builder + pub async fn run(self) -> Result<()> { + let mut request = DeleteFilesRequest { + reason: self.reason, + hashes: self.hashes, + file_ids: self.ids, + file_service_key: None, + file_service_name: None, + }; + if let Some(service) = self.service { + match service { + ServiceIdentifier::Name(name) => request.file_service_name = Some(name), + ServiceIdentifier::Key(key) => request.file_service_key = Some(key), + } + } + + self.client.delete_files(request).await + } +} diff --git a/src/wrapper/builders/mod.rs b/src/wrapper/builders/mod.rs index a58dde9..c3426b8 100644 --- a/src/wrapper/builders/mod.rs +++ b/src/wrapper/builders/mod.rs @@ -1,6 +1,7 @@ +pub mod delete_files_builder; pub mod import_builder; +pub mod notes_builder; pub mod or_chain_builder; pub mod search_builder; pub mod tag_builder; pub mod tagging_builder; -pub mod notes_builder; diff --git a/src/wrapper/hydrus.rs b/src/wrapper/hydrus.rs index 13dbeae..852c544 100644 --- a/src/wrapper/hydrus.rs +++ b/src/wrapper/hydrus.rs @@ -1,6 +1,7 @@ use crate::api_core::common::FileIdentifier; use crate::error::Result; use crate::wrapper::address::Address; +use crate::wrapper::builders::delete_files_builder::DeleteFilesBuilder; use crate::wrapper::builders::import_builder::ImportBuilder; use crate::wrapper::builders::search_builder::SearchBuilder; use crate::wrapper::builders::tagging_builder::TaggingBuilder; @@ -77,6 +78,11 @@ impl Hydrus { Ok(HydrusFile::from_metadata(self.client.clone(), metadata)) } + /// Creates a builder to delete files + pub async fn delete(&self) -> DeleteFilesBuilder { + DeleteFilesBuilder::new(self.client.clone()) + } + /// Starts a request to bulk add tags to files pub fn tagging(&self) -> TaggingBuilder { TaggingBuilder::new(self.client.clone()) diff --git a/src/wrapper/hydrus_file.rs b/src/wrapper/hydrus_file.rs index 70e2b4c..4b3e368 100644 --- a/src/wrapper/hydrus_file.rs +++ b/src/wrapper/hydrus_file.rs @@ -2,6 +2,7 @@ use crate::api_core::adding_tags::{AddTagsRequestBuilder, TagAction}; use crate::api_core::common::{FileIdentifier, FileMetadataInfo, FileRecord, ServiceIdentifier}; use crate::error::{Error, Result}; use crate::utils::tag_list_to_string_list; +use crate::wrapper::builders::delete_files_builder::DeleteFilesBuilder; use crate::wrapper::builders::notes_builder::AddNotesBuilder; use crate::wrapper::service::ServiceName; use crate::wrapper::tag::Tag; @@ -229,6 +230,19 @@ impl HydrusFile { Ok(naive_time_deleted) } + /// Creates a request builder to delete the file + pub fn delete(&mut self) -> DeleteFilesBuilder { + self.metadata = None; + DeleteFilesBuilder::new(self.client.clone()).add_file(self.id.clone()) + } + + /// Undeletes the file + pub async fn undelete(&mut self) -> Result<()> { + let hash = self.hash().await?; + self.metadata = None; + self.client.undelete_files(vec![hash]).await + } + /// Associates the file with a list of urls pub async fn associate_urls(&mut self, urls: Vec) -> Result<()> { let hash = self.hash().await?; diff --git a/tests/client/test_adding_files.rs b/tests/client/test_adding_files.rs index 285c038..0424fa8 100644 --- a/tests/client/test_adding_files.rs +++ b/tests/client/test_adding_files.rs @@ -1,6 +1,8 @@ use crate::common; use crate::common::create_testdata; use crate::common::test_data::get_test_hashes; +use hydrus_api::api_core::adding_files::DeleteFilesRequest; +use hydrus_api::wrapper::service::ServiceName; #[tokio::test] async fn it_adds_files() { @@ -22,7 +24,11 @@ async fn it_adds_binary_files() { #[tokio::test] async fn it_deletes_files() { let client = common::get_client(); - client.delete_files(get_test_hashes()).await.unwrap(); + create_testdata(&client).await; + let mut delete_request = DeleteFilesRequest::new(get_test_hashes(), vec![]); + delete_request.set_reason("Testing"); + delete_request.set_service(ServiceName::my_files().into()); + client.delete_files(delete_request).await.unwrap(); } #[tokio::test] diff --git a/tests/wrapper/test_files.rs b/tests/wrapper/test_files.rs index 536ff4a..ce64a76 100644 --- a/tests/wrapper/test_files.rs +++ b/tests/wrapper/test_files.rs @@ -1,14 +1,18 @@ use super::super::common; +use crate::common::test_data::TEST_HASH_2; +use crate::common::{create_testdata, get_client}; use hydrus_api::api_core::adding_tags::TagAction; use hydrus_api::api_core::common::FileIdentifier; use hydrus_api::wrapper::hydrus_file::HydrusFile; use hydrus_api::wrapper::service::ServiceName; async fn get_file() -> HydrusFile { + let client = get_client(); + create_testdata(&client).await; let hydrus = common::get_hydrus(); hydrus .file(FileIdentifier::hash( - "277a138cd1ee79fc1fdb2869c321b848d4861e45b82184487139ef66dd40b62d", // needs to exist + TEST_HASH_2, // needs to exist )) .await .unwrap() @@ -102,9 +106,20 @@ async fn it_retrieves_content() { async fn it_retrieves_metadata() { let mut file = get_file().await; assert!(file.dimensions().await.unwrap().is_some()); - assert!(file.stored_locally().await.unwrap()); assert!(file.duration().await.unwrap().is_none()); assert!(file.time_modified().await.is_ok()); assert!(file.time_deleted("000").await.is_ok()); assert!(file.time_imported("000").await.is_ok()); } + +#[tokio::test] +async fn it_deletes() { + let mut file = get_file().await; + file.delete() + .reason("I just don't like that file") + .service(ServiceName::all_local_files().into()) + .run() + .await + .unwrap(); + file.undelete().await.unwrap(); +}