diff --git a/szurubooru-client/src/client.rs b/szurubooru-client/src/client.rs index 360427e..13130ee 100644 --- a/szurubooru-client/src/client.rs +++ b/szurubooru-client/src/client.rs @@ -1,8 +1,9 @@ #![warn(missing_docs)] +use crate::models::WithBaseURL; use crate::{errors::*, models::*, tokens::*}; use base64::{engine::general_purpose::STANDARD, Engine as _}; -use futures_util::{TryFutureExt, TryStreamExt}; +use futures_util::TryStreamExt; use reqwest::header::CONTENT_TYPE; use reqwest::{ header::{HeaderMap, ACCEPT, AUTHORIZATION}, @@ -309,8 +310,13 @@ impl<'a> SzurubooruRequest<'a> { where T: AsRef + Display, { - let mut req_url = self.client.base_url.clone(); - req_url.set_path(path.as_ref()); + let mut req_url = if !path.as_ref().contains(&self.client.base_url.to_string()) { + let mut url = self.client.base_url.clone(); + url.set_path(path.as_ref()); + url + } else { + Url::parse(path.as_ref()).unwrap() + }; if let Some(query_vec) = query { let mut qpm = req_url.query_pairs_mut(); @@ -412,6 +418,14 @@ impl<'a> SzurubooruRequest<'a> { .into_result() } + fn propagate_urls(&self, wbu: T) -> T + where + T: WithBaseURL, + { + #[allow(clippy::unnecessary_to_owned)] + wbu.with_base_url(&self.client.base_url.to_string()) + } + /// Lists all tag categories. Doesn't use paging. pub async fn list_tag_categories( &self, @@ -579,6 +593,7 @@ impl<'a> SzurubooruRequest<'a> { ) -> SzurubooruResult> { self.do_request(Method::GET, "/api/posts", query, None::<&String>) .await + .map(|pr| self.propagate_urls(pr)) } async fn create_update_post_from_url( @@ -607,6 +622,7 @@ impl<'a> SzurubooruRequest<'a> { ) -> SzurubooruResult { self.create_update_post_from_url("/api/posts", Method::POST, new_post) .await + .map(|pr| self.propagate_urls(pr)) } /// Update an existing post @@ -620,6 +636,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/post/{post_id}"); self.create_update_post_from_url(&path, Method::PUT, update_post) .await + .map(|pr| self.propagate_urls(pr)) } fn part_from_file(&self, file: &mut File) -> SzurubooruResult { @@ -689,6 +706,7 @@ impl<'a> SzurubooruRequest<'a> { new_post, ) .await + .map(|pr| self.propagate_urls(pr)) } /// Create a new post from a file path @@ -709,6 +727,7 @@ impl<'a> SzurubooruRequest<'a> { }; self.create_post_from_file(&mut file, thumbnail_file.as_mut(), filename, new_post) .await + .map(|pr| self.propagate_urls(pr)) } /// Create a post from a token previously generated by @@ -728,6 +747,7 @@ impl<'a> SzurubooruRequest<'a> { new_post, ) .await + .map(|pr| self.propagate_urls(pr)) } /// Update an existing post from an open File handle @@ -751,6 +771,7 @@ impl<'a> SzurubooruRequest<'a> { update_post, ) .await + .map(|pr| self.propagate_urls(pr)) } /// Update an existing post from a file path @@ -801,6 +822,7 @@ impl<'a> SzurubooruRequest<'a> { update_post, ) .await + .map(|pr| self.propagate_urls(pr)) } async fn get_post_content( @@ -872,26 +894,6 @@ impl<'a> SzurubooruRequest<'a> { .map_err(SzurubooruClientError::RequestError) } - ///Gets a post's image's URL - pub async fn get_image_url(&self, post_id: u32) -> SzurubooruResult { - let post_resource = self.get_post(post_id).await?; - Ok(format!( - "{}{}", - self.client.base_url, - post_resource.content_url.unwrap() - )) - } - - ///Gets a post's image's URL - pub async fn get_thumbnail_url(&self, post_id: u32) -> SzurubooruResult { - let post_resource = self.get_post(post_id).await?; - Ok(format!( - "{}{}", - self.client.base_url, - post_resource.thumbnail_url.unwrap() - )) - } - async fn write_content_to_file( &self, file: &mut File, @@ -970,7 +972,9 @@ impl<'a> SzurubooruRequest<'a> { .file_name(file_path.as_ref().to_string()); let form = Form::new().part("content", image_part); - self.handle_request(request.multipart(form)).await + self.handle_request(request.multipart(form)) + .await + .map(|isr| self.propagate_urls(isr)) } /// Retrieves posts that look like the input image from the given file path @@ -980,7 +984,9 @@ impl<'a> SzurubooruRequest<'a> { ) -> SzurubooruResult { let mut file = File::open(&file_path).map_err(SzurubooruClientError::IOError)?; let filename = file_path.as_ref().file_name().unwrap().to_str().unwrap(); - self.reverse_search_file(&mut file, filename).await + self.reverse_search_file(&mut file, filename) + .await + .map(|isr| self.propagate_urls(isr)) } /// Searches for an exact match of a file based on the SHA1 checksum @@ -994,7 +1000,9 @@ impl<'a> SzurubooruRequest<'a> { let hex_string = hex::encode(hash); let qt = QueryToken::token(PostNamedToken::ContentChecksum, hex_string); - self.list_posts(Some(&vec![qt])).await + self.list_posts(Some(&vec![qt])) + .await + .map(|psr| self.propagate_urls(psr)) } /// Searches for an exact match of a file path based on the SHA1 checksum @@ -1004,7 +1012,9 @@ impl<'a> SzurubooruRequest<'a> { ) -> SzurubooruResult> { let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?; - self.posts_for_file(&mut file).await + self.posts_for_file(&mut file) + .await + .map(|psr| self.propagate_urls(psr)) } /// Retrieves information about an existing post. @@ -1012,6 +1022,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/post/{post_id}"); self.do_request(Method::GET, &path, None, None::<&String>) .await + .map(|pr| self.propagate_urls(pr)) } /// Retrieves information about posts that are before or after an existing post. @@ -1040,6 +1051,7 @@ impl<'a> SzurubooruRequest<'a> { pub async fn merge_post(&self, merge_opts: &MergePost) -> SzurubooruResult { self.do_request(Method::POST, "/api/post-merge/", None, Some(merge_opts)) .await + .map(|pr| self.propagate_urls(pr)) } /// Updates score of authenticated user for given post. Valid scores are -1, 0 and 1. @@ -1048,6 +1060,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/post/{post_id}/score"); self.do_request(Method::PUT, &path, None, Some(&rating_obj)) .await + .map(|pr| self.propagate_urls(pr)) } /// Marks the post as favorite for authenticated user. @@ -1055,6 +1068,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/post/{post_id}/favorite"); self.do_request(Method::POST, &path, None, None::<&String>) .await + .map(|pr| self.propagate_urls(pr)) } /// Unmarks the post as favorite for authenticated user. @@ -1062,6 +1076,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/post/{post_id}/favorite"); self.do_request(Method::DELETE, &path, None, None::<&String>) .await + .map(|pr| self.propagate_urls(pr)) } /// Retrieves the post that is currently featured on the main page in web client. If no post is @@ -1071,6 +1086,7 @@ impl<'a> SzurubooruRequest<'a> { pub async fn get_featured_post(&self) -> SzurubooruResult> { self.do_request(Method::GET, "/api/featured-post", None, None::<&String>) .await + .map(|r| self.propagate_urls(r)) } /// Features a post on the main page @@ -1078,6 +1094,7 @@ impl<'a> SzurubooruRequest<'a> { let id_object = PostId { id: post_id }; self.do_request(Method::POST, "/api/featured-post", None, Some(&id_object)) .await + .map(|r| self.propagate_urls(r)) } /// Lists all pool categories. Doesn't use paging. @@ -1167,6 +1184,7 @@ impl<'a> SzurubooruRequest<'a> { ) -> SzurubooruResult> { self.do_request(Method::GET, "/api/pools", query, None::<&String>) .await + .map(|r| self.propagate_urls(r)) } /// Creates a new pool using specified parameters. Names, suggestions and implications must @@ -1180,6 +1198,7 @@ impl<'a> SzurubooruRequest<'a> { ) -> SzurubooruResult { self.do_request(Method::POST, "/api/pool", None, Some(create_update_pool)) .await + .map(|r| self.propagate_urls(r)) } /// Updates an existing pool using specified parameters. [name](models::CreateUpdatePool::name), @@ -1199,6 +1218,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/pool/{pool_id}"); self.do_request(Method::PUT, &path, None, Some(create_update_pool)) .await + .map(|r| self.propagate_urls(r)) } /// Retrieves information about an existing pool. @@ -1206,6 +1226,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/pool/{pool_id}"); self.do_request(Method::GET, &path, None, None::<&String>) .await + .map(|r| self.propagate_urls(r)) } /// Deletes existing pool. All posts in the pool will only have their relation to the pool @@ -1223,6 +1244,7 @@ impl<'a> SzurubooruRequest<'a> { pub async fn merge_pools(&self, merge_pool: &MergePool) -> SzurubooruResult { self.do_request(Method::POST, "/api/pool-merge", None, Some(merge_pool)) .await + .map(|r| self.propagate_urls(r)) } /// Searches for comments. @@ -1293,6 +1315,7 @@ impl<'a> SzurubooruRequest<'a> { ) -> SzurubooruResult> { self.do_request(Method::GET, "/api/users", query, None::<&String>) .await + .map(|r| self.propagate_urls(r)) } async fn create_update_user( @@ -1336,6 +1359,7 @@ impl<'a> SzurubooruRequest<'a> { pub async fn create_user(&self, new_user: &CreateUpdateUser) -> SzurubooruResult { self.do_request(Method::POST, "/api/users", None, Some(new_user)) .await + .map(|r| self.propagate_urls(r)) } /// Create a [UserResource](models::UserResource) with the included Avatar file @@ -1355,6 +1379,7 @@ impl<'a> SzurubooruRequest<'a> { Some(file_name), ) .await + .map(|r| self.propagate_urls(r)) } /// Create a [UserResource](models::UserResource) with the included Avatar file path @@ -1375,6 +1400,7 @@ impl<'a> SzurubooruRequest<'a> { Some(filename), ) .await + .map(|r| self.propagate_urls(r)) } /// Updates user using specified parameters. Names and passwords must match @@ -1395,6 +1421,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/user/{name}"); self.do_request(Method::PUT, path, None, Some(update_user)) .await + .map(|r| self.propagate_urls(r)) } /// Update a [UserResource](models::UserResource) with the included Avatar file @@ -1419,6 +1446,7 @@ impl<'a> SzurubooruRequest<'a> { Some(file_name), ) .await + .map(|r| self.propagate_urls(r)) } /// Update a [UserResource](models::UserResource) with the included Avatar file path @@ -1444,6 +1472,7 @@ impl<'a> SzurubooruRequest<'a> { Some(filename), ) .await + .map(|r| self.propagate_urls(r)) } /// Retrieves information about an existing user @@ -1454,19 +1483,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/user/{name}"); self.do_request(Method::GET, &path, None, None::<&String>) .await - } - - /// Retrieves the user's avatar URL - pub async fn get_user_avatar_url(&self, name: T) -> SzurubooruResult - where - T: AsRef + Display, - { - let user = self.get_user(name).await?; - Ok(format!( - "{}{}", - self.client.base_url, - user.avatar_url.unwrap() - )) + .map(|r| self.propagate_urls(r)) } /// Deletes existing user @@ -1492,6 +1509,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/user-tokens/{name}"); self.do_request(Method::GET, &path, None, None::<&String>) .await + .map(|r| self.propagate_urls(r)) } /// Creates a new user token that can be used for authentication of API endpoints @@ -1507,6 +1525,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/user-token/{name}"); self.do_request(Method::POST, &path, None, Some(create_token)) .await + .map(|r| self.propagate_urls(r)) } /// Updates an existing user token using specified parameters. All fields except the @@ -1524,6 +1543,7 @@ impl<'a> SzurubooruRequest<'a> { let path = format!("/api/user-token/{name}/{token}"); self.do_request(Method::PUT, &path, None, Some(update_token)) .await + .map(|r| self.propagate_urls(r)) } /// Deletes an existing user token using specified parameters. All fields except the @@ -1588,6 +1608,7 @@ impl<'a> SzurubooruRequest<'a> { ) -> SzurubooruResult> { self.do_request(Method::GET, "/api/snapshots", query, None::<&String>) .await + .map(|r| self.propagate_urls(r)) } /// Retrieves simple statistics. [featured_post](models::GlobalInfo::featured_post) is diff --git a/szurubooru-client/src/models.rs b/szurubooru-client/src/models.rs index 49ed2e2..fc318b4 100644 --- a/szurubooru-client/src/models.rs +++ b/szurubooru-client/src/models.rs @@ -28,6 +28,14 @@ pub struct UnpagedSearchResult { pub results: Vec, } +impl WithBaseURL for UnpagedSearchResult { + fn with_base_url(self, url: &str) -> Self { + Self { + results: self.results.with_base_url(url), + } + } +} + #[derive(Debug, Serialize, Deserialize)] /// A result of search operation that involves paging /// @@ -46,6 +54,33 @@ pub struct PagedSearchResult { pub results: Vec, } +impl WithBaseURL for PagedSearchResult { + fn with_base_url(self, url: &str) -> Self { + Self { + results: self.results.with_base_url(url), + ..self + } + } +} + +pub(crate) trait WithBaseURL { + fn with_base_url(self, url: &str) -> Self; +} + +impl WithBaseURL for Option { + fn with_base_url(self, url: &str) -> Self { + self.map(|inner| inner.with_base_url(url)) + } +} + +impl WithBaseURL for Vec { + fn with_base_url(self, url: &str) -> Self { + self.into_iter() + .map(|inner| inner.with_base_url(url)) + .collect() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] /// A [tag resource](TagResource) stripped down to `names`, `category` and `usages` fields. pub struct MicroTagResource { @@ -265,6 +300,19 @@ pub struct MicroPostResource { pub thumbnail_url: String, } +impl WithBaseURL for MicroPostResource { + fn with_base_url(self, url: &str) -> Self { + if !self.thumbnail_url.contains(url) { + MicroPostResource { + id: self.id, + thumbnail_url: format!("{}{}", url, self.thumbnail_url), + } + } else { + self + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[doc(hidden)] pub(crate) struct PostId { @@ -346,6 +394,40 @@ pub struct PostResource { pub pools: Option>, } +impl WithBaseURL for PostResource { + fn with_base_url(self, url: &str) -> Self { + let curl = self.content_url.map(|cu| { + if !cu.contains(url) { + format!("{}{}", url, cu) + } else { + cu + } + }); + let turl = self.thumbnail_url.map(|tu| { + if !tu.contains(url) { + format!("{}{}", url, tu) + } else { + tu + } + }); + + let user = self.user.with_base_url(url); + let relations = self.relations.with_base_url(url); + let fv_by = self.favorited_by.with_base_url(url); + let pools = self.pools.with_base_url(url); + + PostResource { + content_url: curl, + thumbnail_url: turl, + user, + relations, + favorited_by: fv_by, + pools, + ..self + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Builder)] #[builder(setter(strip_option))] #[serde(rename_all = "camelCase")] @@ -509,6 +591,22 @@ pub struct UserResource { pub favorite_post_count: Option>, } +impl WithBaseURL for UserResource { + fn with_base_url(self, url: &str) -> Self { + let av_url = self.avatar_url.map(|au| { + if !au.contains(url) { + format!("{}{}", url, au) + } else { + au + } + }); + UserResource { + avatar_url: av_url, + ..self + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Default, Builder)] #[builder(setter(strip_option))] #[serde(rename_all = "camelCase")] @@ -548,6 +646,19 @@ pub struct MicroUserResource { pub avatar_url: String, } +impl WithBaseURL for MicroUserResource { + fn with_base_url(self, url: &str) -> Self { + if !self.avatar_url.contains(url) { + MicroUserResource { + name: self.name, + avatar_url: format!("{}{}", url, self.avatar_url), + } + } else { + self + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] /// A single user token @@ -572,6 +683,15 @@ pub struct UserAuthTokenResource { pub last_usage_time: Option>, } +impl WithBaseURL for UserAuthTokenResource { + fn with_base_url(self, url: &str) -> Self { + Self { + user: self.user.with_base_url(url), + ..self + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)] #[builder(setter(into, strip_option))] #[serde(rename_all = "kebab-case")] @@ -727,6 +847,15 @@ pub struct PoolResource { pub description: Option, } +impl WithBaseURL for PoolResource { + fn with_base_url(self, url: &str) -> Self { + PoolResource { + posts: self.posts.with_base_url(url), + ..self + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Builder, Default)] #[builder(setter(strip_option))] #[serde(rename_all = "camelCase")] @@ -912,6 +1041,20 @@ pub enum SnapshotCreationDeletionData { PoolCategory(PoolCategoryResource), } +impl WithBaseURL for SnapshotCreationDeletionData { + fn with_base_url(self, url: &str) -> Self { + match self { + SnapshotCreationDeletionData::Pool(pool) => { + SnapshotCreationDeletionData::Pool(pool.with_base_url(url)) + } + SnapshotCreationDeletionData::Post(post) => { + SnapshotCreationDeletionData::Post(post.with_base_url(url)) + } + _ => self, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] /// Data for a modified resource @@ -940,6 +1083,17 @@ pub enum SnapshotData { Merge(Vec), } +impl WithBaseURL for SnapshotData { + fn with_base_url(self, url: &str) -> Self { + match self { + SnapshotData::CreateOrDelete(cod) => { + SnapshotData::CreateOrDelete(cod.with_base_url(url)) + } + _ => self, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] /// Overall type representing some sort of change to a resource @@ -959,6 +1113,16 @@ pub struct SnapshotResource { pub time: Option>, } +impl WithBaseURL for SnapshotResource { + fn with_base_url(self, url: &str) -> Self { + SnapshotResource { + user: self.user.with_base_url(url), + data: self.data.with_base_url(url), + ..self + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] /// A result when searching for similar posts to a given image @@ -969,6 +1133,15 @@ pub struct ImageSearchSimilarPost { pub post: PostResource, } +impl WithBaseURL for ImageSearchSimilarPost { + fn with_base_url(self, url: &str) -> Self { + Self { + post: self.post.with_base_url(url), + ..self + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] /// A type to represent the result from an Image search request @@ -981,6 +1154,15 @@ pub struct ImageSearchResult { pub similar_posts: Vec, } +impl WithBaseURL for ImageSearchResult { + fn with_base_url(self, url: &str) -> Self { + Self { + exact_post: self.exact_post.with_base_url(url), + similar_posts: self.similar_posts.with_base_url(url), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] /// A type that represents posts that are before or after an existing post pub struct AroundPostResult { diff --git a/szurubooru-integration-test/src/main.rs b/szurubooru-integration-test/src/main.rs index 4638e57..072e6ec 100644 --- a/szurubooru-integration-test/src/main.rs +++ b/szurubooru-integration-test/src/main.rs @@ -913,14 +913,14 @@ async fn test_downloads(client: &SzurubooruClient) { let mut dl_file = tempfile().expect("Could not create temporary file"); //let mut dl_thumb_file = tempfile().expect("Could not create temporary thumbnail file"); - let dlfolly3_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("dlfolly3.jpg"); - let mut dl_file = File::options() - .create(true) - .truncate(true) - .read(true) - .write(true) - .open(dlfolly3_path) - .expect("unable to open download file"); + //let dlfolly3_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("dlfolly3.jpg"); + /*let mut dl_file = File::options() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(dlfolly3_path) + .expect("unable to open download file");*/ client .request()