fix(client.rs): fixing file uploads requiring file name
Adding a file name parameter for the `File`-based uploads. Required for MIME type introspection BREAKING CHANGE:
This commit is contained in:
parent
9feea56056
commit
a534d6ebd8
9 changed files with 462 additions and 51 deletions
|
|
@ -597,16 +597,15 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update an existing post based on the `contentUrl` field, which the server will use to
|
/// Update an existing post
|
||||||
/// download and update the post
|
|
||||||
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
|
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
|
||||||
/// (CreateUpdatePost)[models::CreateUpdatePost]
|
/// (CreateUpdatePost)[models::CreateUpdatePost]
|
||||||
pub async fn update_post_from_url(
|
pub async fn update_post(
|
||||||
&self,
|
&self,
|
||||||
post_id: u32,
|
post_id: u32,
|
||||||
update_post: &CreateUpdatePost,
|
update_post: &CreateUpdatePost,
|
||||||
) -> SzurubooruResult<PostResource> {
|
) -> SzurubooruResult<PostResource> {
|
||||||
let path = format!("/posts/{post_id}");
|
let path = format!("/post/{post_id}");
|
||||||
self.create_update_post_from_url(&path, Method::PUT, update_post)
|
self.create_update_post_from_url(&path, Method::PUT, update_post)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -619,28 +618,37 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
Ok(Part::stream(bytes))
|
Ok(Part::stream(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_update_post_from_file(
|
async fn create_update_post_from_file<T>(
|
||||||
&self,
|
&self,
|
||||||
file: &mut File,
|
file: Option<&mut File>,
|
||||||
thumbnail: Option<&mut File>,
|
thumbnail: Option<&mut File>,
|
||||||
|
file_name: Option<T>,
|
||||||
path: &str,
|
path: &str,
|
||||||
method: Method,
|
method: Method,
|
||||||
cupost: &CreateUpdatePost,
|
cupost: &CreateUpdatePost,
|
||||||
) -> SzurubooruResult<PostResource> {
|
) -> SzurubooruResult<PostResource>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
let request = self.prep_request(method, path, None);
|
let request = self.prep_request(method, path, None);
|
||||||
|
|
||||||
let metadata_str =
|
let metadata_str =
|
||||||
serde_json::to_string(cupost).map_err(SzurubooruClientError::JSONSerializationError)?;
|
serde_json::to_string(cupost).map_err(SzurubooruClientError::JSONSerializationError)?;
|
||||||
let metadata_part = Part::text(metadata_str);
|
let metadata_part = Part::text(metadata_str);
|
||||||
|
|
||||||
let content_part = self.part_from_file(file)?;
|
let mut form = Form::new().part("metadata", metadata_part);
|
||||||
|
|
||||||
let mut form = Form::new()
|
if let Some(file) = file {
|
||||||
.part("content", content_part)
|
let content_part = self
|
||||||
.part("metadata", metadata_part);
|
.part_from_file(file)?
|
||||||
|
.file_name(file_name.as_ref().unwrap().as_ref().to_string());
|
||||||
|
form = form.part("content", content_part);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(thumbnail) = thumbnail {
|
if let Some(thumbnail) = thumbnail {
|
||||||
let thumbnail_part = self.part_from_file(thumbnail)?;
|
let thumbnail_part = self
|
||||||
|
.part_from_file(thumbnail)?
|
||||||
|
.file_name(format!("thumbnail_{}", file_name.unwrap().as_ref()));
|
||||||
form = form.part("thumbnail", thumbnail_part);
|
form = form.part("thumbnail", thumbnail_part);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -650,13 +658,24 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
/// Create a new post from a file handle
|
/// Create a new post from a file handle
|
||||||
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
|
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
|
||||||
/// (CreateUpdatePost)[models::CreateUpdatePost]
|
/// (CreateUpdatePost)[models::CreateUpdatePost]
|
||||||
pub async fn create_post_from_file(
|
pub async fn create_post_from_file<T>(
|
||||||
&self,
|
&self,
|
||||||
file: &mut File,
|
file: &mut File,
|
||||||
thumbnail: Option<&mut File>,
|
thumbnail: Option<&mut File>,
|
||||||
|
file_name: T,
|
||||||
new_post: &CreateUpdatePost,
|
new_post: &CreateUpdatePost,
|
||||||
) -> SzurubooruResult<PostResource> {
|
) -> SzurubooruResult<PostResource>
|
||||||
self.create_update_post_from_file(file, thumbnail, "/posts", Method::POST, new_post)
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
self.create_update_post_from_file(
|
||||||
|
Some(file),
|
||||||
|
thumbnail,
|
||||||
|
Some(file_name),
|
||||||
|
"/posts",
|
||||||
|
Method::POST,
|
||||||
|
new_post,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -669,14 +688,33 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
thumbnail: Option<impl AsRef<Path>>,
|
thumbnail: Option<impl AsRef<Path>>,
|
||||||
new_post: &CreateUpdatePost,
|
new_post: &CreateUpdatePost,
|
||||||
) -> SzurubooruResult<PostResource> {
|
) -> SzurubooruResult<PostResource> {
|
||||||
let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?;
|
let mut file = File::open(&file_path).map_err(SzurubooruClientError::IOError)?;
|
||||||
|
let filename = file_path.as_ref().file_name().unwrap().to_str().unwrap();
|
||||||
let mut thumbnail_file = if let Some(t) = thumbnail {
|
let mut thumbnail_file = if let Some(t) = thumbnail {
|
||||||
Some(File::open(t).map_err(SzurubooruClientError::IOError)?)
|
Some(File::open(t).map_err(SzurubooruClientError::IOError)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
self.create_post_from_file(&mut file, thumbnail_file.as_mut(), new_post)
|
self.create_post_from_file(&mut file, thumbnail_file.as_mut(), filename, new_post)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a post from a token previously generated by
|
||||||
|
/// (upload_temporary_file_from_path)[SzurubooruRequest::upload_temporary_file_from_path]
|
||||||
|
pub async fn create_post_from_token(
|
||||||
|
&self,
|
||||||
|
new_post: &CreateUpdatePost,
|
||||||
|
) -> SzurubooruResult<PostResource> {
|
||||||
|
assert!(new_post.content_token.is_some());
|
||||||
|
|
||||||
|
self.create_update_post_from_file(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None::<String>,
|
||||||
|
"/posts",
|
||||||
|
Method::POST,
|
||||||
|
new_post,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -686,12 +724,20 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
pub async fn update_post_from_file(
|
pub async fn update_post_from_file(
|
||||||
&self,
|
&self,
|
||||||
post_id: u32,
|
post_id: u32,
|
||||||
file: &mut File,
|
file: Option<&mut File>,
|
||||||
thumbnail: Option<&mut File>,
|
thumbnail: Option<&mut File>,
|
||||||
|
file_name: impl AsRef<str>,
|
||||||
update_post: &CreateUpdatePost,
|
update_post: &CreateUpdatePost,
|
||||||
) -> SzurubooruResult<PostResource> {
|
) -> SzurubooruResult<PostResource> {
|
||||||
let path = format!("/posts/{post_id}");
|
let path = format!("/posts/{post_id}");
|
||||||
self.create_update_post_from_file(file, thumbnail, &path, Method::PUT, update_post)
|
self.create_update_post_from_file(
|
||||||
|
file,
|
||||||
|
thumbnail,
|
||||||
|
Some(file_name),
|
||||||
|
&path,
|
||||||
|
Method::PUT,
|
||||||
|
update_post,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -701,25 +747,60 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
pub async fn update_post_from_file_path(
|
pub async fn update_post_from_file_path(
|
||||||
&self,
|
&self,
|
||||||
post_id: u32,
|
post_id: u32,
|
||||||
file_path: impl AsRef<Path>,
|
file_path: Option<impl AsRef<Path>>,
|
||||||
thumbnail: Option<impl AsRef<Path>>,
|
thumbnail: Option<impl AsRef<Path>>,
|
||||||
update_post: &CreateUpdatePost,
|
update_post: &CreateUpdatePost,
|
||||||
) -> SzurubooruResult<PostResource> {
|
) -> SzurubooruResult<PostResource> {
|
||||||
let mut file = File::open(file_path).map_err(SzurubooruClientError::IOError)?;
|
let mut filename = None;
|
||||||
|
let mut file = if let Some(f) = file_path {
|
||||||
|
filename = Some(
|
||||||
|
f.as_ref()
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
Some(File::open(f).map_err(SzurubooruClientError::IOError)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut thumbnail_file = if let Some(t) = thumbnail {
|
let mut thumbnail_file = if let Some(t) = thumbnail {
|
||||||
|
if let None = filename {
|
||||||
|
filename = Some(
|
||||||
|
t.as_ref()
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
Some(File::open(t).map_err(SzurubooruClientError::IOError)?)
|
Some(File::open(t).map_err(SzurubooruClientError::IOError)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
self.update_post_from_file(post_id, &mut file, thumbnail_file.as_mut(), update_post)
|
self.update_post_from_file(
|
||||||
|
post_id,
|
||||||
|
file.as_mut(),
|
||||||
|
thumbnail_file.as_mut(),
|
||||||
|
filename.unwrap(),
|
||||||
|
update_post,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_post_content(&self, post_id: u32) -> SzurubooruResult<Response> {
|
async fn get_post_content(&self, post_id: u32) -> SzurubooruResult<Response> {
|
||||||
let post_resource = self.get_post(post_id).await?;
|
let post_resource = self.get_post(post_id).await?;
|
||||||
|
|
||||||
let req = self.prep_request(Method::GET, post_resource.content_url.unwrap(), None);
|
let data_url = format!(
|
||||||
|
"{}/{}",
|
||||||
|
self.client.base_url,
|
||||||
|
post_resource.content_url.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let req = self.prep_request(Method::GET, data_url, None);
|
||||||
let request = req
|
let request = req
|
||||||
.build()
|
.build()
|
||||||
.map_err(SzurubooruClientError::RequestBuilderError)?;
|
.map_err(SzurubooruClientError::RequestBuilderError)?;
|
||||||
|
|
@ -729,7 +810,7 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
.client
|
.client
|
||||||
.execute(request)
|
.execute(request)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| SzurubooruClientError::RequestError(e))?;
|
.map_err(SzurubooruClientError::RequestError)?;
|
||||||
self.handle_response(resp_res).await
|
self.handle_response(resp_res).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -831,8 +912,9 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
pub async fn delete_post(&self, post_id: u32, version: u32) -> SzurubooruResult<()> {
|
pub async fn delete_post(&self, post_id: u32, version: u32) -> SzurubooruResult<()> {
|
||||||
let path = format!("/post/{post_id}");
|
let path = format!("/post/{post_id}");
|
||||||
let version_obj = ResourceVersion { version };
|
let version_obj = ResourceVersion { version };
|
||||||
self.do_request(Method::DELETE, &path, None, Some(&version_obj))
|
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&version_obj))
|
||||||
.await
|
.await
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
@ -935,13 +1017,19 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes existing pool category. The pool category to be deleted must have no usages.
|
/// Deletes existing pool category. The pool category to be deleted must have no usages.
|
||||||
pub async fn delete_pool_category<T>(&self, category_name: T) -> SzurubooruResult<()>
|
pub async fn delete_pool_category<T>(
|
||||||
|
&self,
|
||||||
|
category_name: T,
|
||||||
|
version: u32,
|
||||||
|
) -> SzurubooruResult<()>
|
||||||
where
|
where
|
||||||
T: AsRef<str> + Display,
|
T: AsRef<str> + Display,
|
||||||
{
|
{
|
||||||
let path = format!("/pool-category/{category_name}");
|
let path = format!("/pool-category/{category_name}");
|
||||||
self.do_request(Method::DELETE, &path, None, None::<&String>)
|
let resource_obj = ResourceVersion { version };
|
||||||
|
self.do_request::<Value, _, _>(Method::DELETE, &path, None, Some(&resource_obj))
|
||||||
.await
|
.await
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets given pool category as default. All new pools created manually or automatically will
|
/// Sets given pool category as default. All new pools created manually or automatically will
|
||||||
|
|
@ -1353,15 +1441,32 @@ impl<'a> SzurubooruRequest<'a> {
|
||||||
pub async fn upload_temporary_file(
|
pub async fn upload_temporary_file(
|
||||||
&self,
|
&self,
|
||||||
file: &mut File,
|
file: &mut File,
|
||||||
|
file_name: impl AsRef<str>,
|
||||||
) -> SzurubooruResult<TemporaryFileUpload> {
|
) -> SzurubooruResult<TemporaryFileUpload> {
|
||||||
let request = self.prep_request(Method::POST, "/uploads", None);
|
let request = self.prep_request(Method::POST, "/uploads", None);
|
||||||
|
|
||||||
let content_part = self.part_from_file(file)?;
|
let content_part = self
|
||||||
|
.part_from_file(file)?
|
||||||
|
.file_name(file_name.as_ref().to_string());
|
||||||
|
|
||||||
let form = Form::new().part("content", content_part);
|
let form = Form::new().part("content", content_part);
|
||||||
|
|
||||||
self.handle_request(request.multipart(form)).await
|
self.handle_request(request.multipart(form)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Puts a file from a given file path in temporary storage and assigns it a token that can be
|
||||||
|
/// used in other requests.
|
||||||
|
/// The files uploaded that way are deleted after a short while so clients shouldn't use it
|
||||||
|
/// as a free upload service.
|
||||||
|
pub async fn upload_temporary_file_from_path(
|
||||||
|
&self,
|
||||||
|
file_path: impl AsRef<Path>,
|
||||||
|
) -> SzurubooruResult<TemporaryFileUpload> {
|
||||||
|
let mut file = File::open(&file_path).map_err(SzurubooruClientError::IOError)?;
|
||||||
|
let filename = file_path.as_ref().file_name().unwrap().to_str().unwrap();
|
||||||
|
|
||||||
|
self.upload_temporary_file(&mut file, filename).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which kind of authentication is used. Automatically hides any sensitive information when printed
|
/// Which kind of authentication is used. Automatically hides any sensitive information when printed
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ impl<T> IntoClientResult<T> for SzuruEither<T, SzurubooruServerError> {
|
||||||
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
/// An error type returned by the server
|
/// An error type returned by the server
|
||||||
pub enum SzurubooruServerErrorType {
|
pub enum SzurubooruServerErrorType {
|
||||||
|
/// Inavlid pool category color
|
||||||
|
InvalidPoolCategoryColorError,
|
||||||
/// Missing required file
|
/// Missing required file
|
||||||
MissingRequiredFileError,
|
MissingRequiredFileError,
|
||||||
/// Missing required parameter
|
/// Missing required parameter
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ pub struct PagedSearchResult<T> {
|
||||||
pub results: Vec<T>,
|
pub results: Vec<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
/// A [tag resource](TagResource) stripped down to `names`, `category` and `usages` fields.
|
/// A [tag resource](TagResource) stripped down to `names`, `category` and `usages` fields.
|
||||||
pub struct MicroTagResource {
|
pub struct MicroTagResource {
|
||||||
/// The tag names and aliases
|
/// The tag names and aliases
|
||||||
|
|
@ -219,7 +219,8 @@ pub struct TagSibling {
|
||||||
pub occurrences: u32,
|
pub occurrences: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr)]
|
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
/// The type of post
|
/// The type of post
|
||||||
pub enum PostType {
|
pub enum PostType {
|
||||||
/// Image post
|
/// Image post
|
||||||
|
|
@ -240,7 +241,8 @@ pub enum PostType {
|
||||||
Webm,
|
Webm,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr)]
|
#[derive(Debug, Clone, Serialize, Deserialize, AsRefStr, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
/// How SFW/NSFW the post is
|
/// How SFW/NSFW the post is
|
||||||
pub enum PostSafety {
|
pub enum PostSafety {
|
||||||
/// Post is SFW
|
/// Post is SFW
|
||||||
|
|
@ -253,7 +255,7 @@ pub enum PostSafety {
|
||||||
Unsafe,
|
Unsafe,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
/// A post resource stripped down to `id` and `thumbnailUrl` fields.
|
/// A post resource stripped down to `id` and `thumbnailUrl` fields.
|
||||||
pub struct MicroPostResource {
|
pub struct MicroPostResource {
|
||||||
|
|
@ -269,7 +271,7 @@ pub(crate) struct PostId {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
/// A post resource
|
/// A post resource
|
||||||
pub struct PostResource {
|
pub struct PostResource {
|
||||||
|
|
@ -313,6 +315,8 @@ pub struct PostResource {
|
||||||
pub user: Option<MicroUserResource>,
|
pub user: Option<MicroUserResource>,
|
||||||
/// The collective score (+1/-1 rating) of the given post
|
/// The collective score (+1/-1 rating) of the given post
|
||||||
pub score: Option<i32>,
|
pub score: Option<i32>,
|
||||||
|
/// The user's score for this post
|
||||||
|
pub own_score: Option<i32>,
|
||||||
/// Whether the authenticated user has given post in their favorites
|
/// Whether the authenticated user has given post in their favorites
|
||||||
pub own_favorite: Option<bool>,
|
pub own_favorite: Option<bool>,
|
||||||
/// How many tags the post is tagged with
|
/// How many tags the post is tagged with
|
||||||
|
|
@ -348,30 +352,38 @@ pub struct PostResource {
|
||||||
/// A `struct` used to create or update a post. For updating purposes
|
/// A `struct` used to create or update a post. For updating purposes
|
||||||
/// the [version](CreateUpdatePost::version) field is required
|
/// the [version](CreateUpdatePost::version) field is required
|
||||||
pub struct CreateUpdatePost {
|
pub struct CreateUpdatePost {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Resource version. See [versioning](ResourceVersion)
|
||||||
|
#[builder(default)]
|
||||||
|
pub version: Option<u32>,
|
||||||
/// Tags to use for this post. If specified tags do not exist yet, they will be automatically
|
/// Tags to use for this post. If specified tags do not exist yet, they will be automatically
|
||||||
/// created. Tags created automatically have no implications, no suggestions, one name and
|
/// created. Tags created automatically have no implications, no suggestions, one name and
|
||||||
/// their category is set to the first tag category found
|
/// their category is set to the first tag category found
|
||||||
|
#[builder(default)]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
/// Required field, represents the SFW/NSFW state of a post
|
/// Required field, represents the SFW/NSFW state of a post
|
||||||
pub safety: PostSafety,
|
pub safety: PostSafety,
|
||||||
/// The origin of the post's content
|
/// The origin of the post's content
|
||||||
|
#[builder(default)]
|
||||||
pub source: Option<String>,
|
pub source: Option<String>,
|
||||||
/// The IDs of related posts
|
/// The IDs of related posts
|
||||||
pub relations: Option<Vec<u32>>,
|
#[builder(default)]
|
||||||
|
pub relations: Vec<u32>,
|
||||||
/// Notes to be displayed on the post
|
/// Notes to be displayed on the post
|
||||||
pub notes: Option<Vec<NoteResource>>,
|
#[builder(default)]
|
||||||
|
pub notes: Vec<NoteResource>,
|
||||||
/// Flags relevant to the post. If omitted they will be auto-detected
|
/// Flags relevant to the post. If omitted they will be auto-detected
|
||||||
pub flags: Option<Vec<String>>,
|
#[builder(default)]
|
||||||
|
pub flags: Vec<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
/// The URL to download the content from
|
/// The URL to download the content from
|
||||||
|
#[builder(default)]
|
||||||
pub content_url: Option<String>,
|
pub content_url: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
/// The token returned from
|
/// The token returned from
|
||||||
/// [upload_temporary_file](crate::SzurubooruRequest::upload_temporary_file)
|
/// [upload_temporary_file](crate::SzurubooruRequest::upload_temporary_file)
|
||||||
|
#[builder(default)]
|
||||||
pub content_token: Option<String>,
|
pub content_token: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
/// Resource version. See [versioning](ResourceVersion)
|
|
||||||
pub version: Option<u32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -379,7 +391,7 @@ pub struct CreateUpdatePost {
|
||||||
/// A token representing a temporary file upload
|
/// A token representing a temporary file upload
|
||||||
pub struct TemporaryFileUpload {
|
pub struct TemporaryFileUpload {
|
||||||
/// Temporary upload token
|
/// Temporary upload token
|
||||||
token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
|
||||||
|
|
@ -409,7 +421,7 @@ pub struct RateResource {
|
||||||
pub score: i8,
|
pub score: i8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
/// A text annotation rendered on top of the post
|
/// A text annotation rendered on top of the post
|
||||||
pub struct NoteResource {
|
pub struct NoteResource {
|
||||||
|
|
@ -516,7 +528,7 @@ pub struct CreateUpdateUser {
|
||||||
pub avatar_style: Option<UserAvatarStyle>,
|
pub avatar_style: Option<UserAvatarStyle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
/// A user resource stripped down to `name` and `avatarUrl` fields
|
/// A user resource stripped down to `name` and `avatarUrl` fields
|
||||||
pub struct MicroUserResource {
|
pub struct MicroUserResource {
|
||||||
|
|
@ -663,17 +675,20 @@ pub struct PoolCategoryResource {
|
||||||
/// ```
|
/// ```
|
||||||
pub struct CreateUpdatePoolCategory {
|
pub struct CreateUpdatePoolCategory {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[builder(default)]
|
||||||
/// Category version (used for updating)
|
/// Category version (used for updating)
|
||||||
pub version: Option<u32>,
|
pub version: Option<u32>,
|
||||||
/// Category name
|
/// Category name
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[builder(default)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
/// Category color
|
/// Category color
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[builder(default)]
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
/// Type that represents a Pool resource
|
/// Type that represents a Pool resource
|
||||||
pub struct PoolResource {
|
pub struct PoolResource {
|
||||||
|
|
@ -771,7 +786,7 @@ pub struct MicroPoolResource {
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
/// A type representing a Comment on a post
|
/// A type representing a Comment on a post
|
||||||
pub struct CommentResource {
|
pub struct CommentResource {
|
||||||
|
|
|
||||||
BIN
szurubooru-integration-test/folly1.jpg
Normal file
BIN
szurubooru-integration-test/folly1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 317 KiB |
BIN
szurubooru-integration-test/folly2.jpg
Normal file
BIN
szurubooru-integration-test/folly2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
BIN
szurubooru-integration-test/folly3.jpg
Normal file
BIN
szurubooru-integration-test/folly3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 272 KiB |
BIN
szurubooru-integration-test/folly3_thumb.jpg
Normal file
BIN
szurubooru-integration-test/folly3_thumb.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
szurubooru-integration-test/folly4.jpg
Normal file
BIN
szurubooru-integration-test/folly4.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 310 KiB |
|
|
@ -1,13 +1,15 @@
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::Path;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use szurubooru_client::models::*;
|
use szurubooru_client::models::*;
|
||||||
|
use szurubooru_client::tokens::QueryToken;
|
||||||
use szurubooru_client::*;
|
use szurubooru_client::*;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use tracing::{error, info, instrument};
|
use tracing::{info, instrument};
|
||||||
use tracing_subscriber::filter::EnvFilter;
|
use tracing_subscriber::filter::EnvFilter;
|
||||||
use tracing_subscriber::fmt::format::FmtSpan;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
|
|
@ -48,6 +50,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
|
||||||
test_tag_categories(&auth_client).await;
|
test_tag_categories(&auth_client).await;
|
||||||
test_tags(&auth_client).await;
|
test_tags(&auth_client).await;
|
||||||
|
test_creating_posts(&auth_client).await;
|
||||||
|
test_pool_categories(&auth_client).await;
|
||||||
|
|
||||||
Command::new("sh")
|
Command::new("sh")
|
||||||
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
|
@ -283,4 +287,289 @@ async fn test_tags(client: &SzurubooruClient) {
|
||||||
.expect("Could not delete tag");
|
.expect("Could not delete tag");
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: test Tag siblings once we've tested a post
|
#[instrument(skip(client))]
|
||||||
|
async fn test_creating_posts(client: &SzurubooruClient) {
|
||||||
|
info!("Testing creating posts");
|
||||||
|
|
||||||
|
info!("Listing posts (should be empty)");
|
||||||
|
let post_list = client
|
||||||
|
.request()
|
||||||
|
.list_posts(None)
|
||||||
|
.await
|
||||||
|
.expect("Could not list posts");
|
||||||
|
assert_eq!(post_list.total, 0);
|
||||||
|
|
||||||
|
info!("Testing upload by URL");
|
||||||
|
let wiki_post_obj = CreateUpdatePostBuilder::default()
|
||||||
|
.content_url("https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Maine_Coon_cat_by_Tomitheos.JPG/225px-Maine_Coon_cat_by_Tomitheos.JPG".to_string())
|
||||||
|
.tags(vec!["maine_coon".to_string(), "cat".to_string()])
|
||||||
|
.safety(PostSafety::Safe)
|
||||||
|
.build()
|
||||||
|
.expect("Could not build wiki post object");
|
||||||
|
let wiki_post = client
|
||||||
|
.request()
|
||||||
|
.create_post_from_url(&wiki_post_obj)
|
||||||
|
.await
|
||||||
|
.expect("Unable to create wiki post object");
|
||||||
|
|
||||||
|
info!("Updating existing post");
|
||||||
|
let wiki_post_update = CreateUpdatePostBuilder::default()
|
||||||
|
.version(wiki_post.version.unwrap())
|
||||||
|
.safety(wiki_post.safety.unwrap())
|
||||||
|
.source("Wikipedia".to_string())
|
||||||
|
.build()
|
||||||
|
.expect("Could not build wiki post update object");
|
||||||
|
let wiki_post = client
|
||||||
|
.request()
|
||||||
|
.update_post(wiki_post.id.unwrap(), &wiki_post_update)
|
||||||
|
.await
|
||||||
|
.expect("Unable to up wiki post object");
|
||||||
|
let post_list = client
|
||||||
|
.request()
|
||||||
|
.list_posts(None)
|
||||||
|
.await
|
||||||
|
.expect("Could not list posts");
|
||||||
|
assert_eq!(post_list.total, 1);
|
||||||
|
|
||||||
|
info!("Deleting wikipedia image");
|
||||||
|
client
|
||||||
|
.request()
|
||||||
|
.delete_post(wiki_post.id.unwrap(), wiki_post.version.unwrap())
|
||||||
|
.await
|
||||||
|
.expect("Could not delete wiki post");
|
||||||
|
|
||||||
|
info!("Test upload by File type");
|
||||||
|
let folly1_obj = CreateUpdatePostBuilder::default()
|
||||||
|
.tags(vec![
|
||||||
|
"maine_coon".to_string(),
|
||||||
|
"cat".to_string(),
|
||||||
|
"folly1".to_string(),
|
||||||
|
])
|
||||||
|
.safety(PostSafety::Safe)
|
||||||
|
.build()
|
||||||
|
.expect("Could not build first upload object");
|
||||||
|
let folly1_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly1.jpg");
|
||||||
|
let mut folly1_file =
|
||||||
|
File::open(&folly1_path).expect(&format!("Could not open file {folly1_path:?}"));
|
||||||
|
let folly1_post = client
|
||||||
|
.request()
|
||||||
|
.create_post_from_file(&mut folly1_file, None, "folly1.jpg", &folly1_obj)
|
||||||
|
.await
|
||||||
|
.expect("Could not create post from folly1 file");
|
||||||
|
|
||||||
|
info!("Test upload by file path");
|
||||||
|
let folly2_obj = CreateUpdatePostBuilder::default()
|
||||||
|
.tags(vec![
|
||||||
|
"maine_coon".to_string(),
|
||||||
|
"cat".to_string(),
|
||||||
|
"folly2".to_string(),
|
||||||
|
])
|
||||||
|
.safety(PostSafety::Safe)
|
||||||
|
.build()
|
||||||
|
.expect("Could not build second upload object");
|
||||||
|
let folly2_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly2.jpg");
|
||||||
|
let folly2_post = client
|
||||||
|
.request()
|
||||||
|
.create_post_from_file_path(folly2_path, None::<String>, &folly2_obj)
|
||||||
|
.await
|
||||||
|
.expect("Could not create post from folly2 path");
|
||||||
|
|
||||||
|
info!("Test upload by file path with thumbnail");
|
||||||
|
let folly3_obj = CreateUpdatePostBuilder::default()
|
||||||
|
.tags(vec![
|
||||||
|
"maine_coon".to_string(),
|
||||||
|
"cat".to_string(),
|
||||||
|
"folly3".to_string(),
|
||||||
|
])
|
||||||
|
.safety(PostSafety::Safe)
|
||||||
|
.build()
|
||||||
|
.expect("Could not build third upload object");
|
||||||
|
let folly3_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly3.jpg");
|
||||||
|
let folly3_thumbnail = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly3_thumb.jpg");
|
||||||
|
let folly3_post = client
|
||||||
|
.request()
|
||||||
|
.create_post_from_file_path(folly3_path, Some(folly3_thumbnail), &folly3_obj)
|
||||||
|
.await
|
||||||
|
.expect("Could not create post with thumbnail");
|
||||||
|
|
||||||
|
info!("Testing temporary upload");
|
||||||
|
let folly4_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("folly4.jpg");
|
||||||
|
let folly4_temp_upload = client
|
||||||
|
.request()
|
||||||
|
.upload_temporary_file_from_path(folly4_path)
|
||||||
|
.await
|
||||||
|
.expect("Could not create temporary upload for folly4");
|
||||||
|
let folly4_obj = CreateUpdatePostBuilder::default()
|
||||||
|
.tags(vec![
|
||||||
|
"maine_coon".to_string(),
|
||||||
|
"cat".to_string(),
|
||||||
|
"folly4".to_string(),
|
||||||
|
])
|
||||||
|
.content_token(folly4_temp_upload.token)
|
||||||
|
.safety(PostSafety::Safe)
|
||||||
|
.build()
|
||||||
|
.expect("Could not build fourth upload object");
|
||||||
|
let folly4_post = client
|
||||||
|
.request()
|
||||||
|
.create_post_from_token(&folly4_obj)
|
||||||
|
.await
|
||||||
|
.expect("Could not create upload from temporary token");
|
||||||
|
|
||||||
|
info!("Querying by tag");
|
||||||
|
let f4_results = client
|
||||||
|
.request()
|
||||||
|
.list_posts(Some(&vec![QueryToken::anonymous("cat")]))
|
||||||
|
.await
|
||||||
|
.expect("Could not list posts by tag cat");
|
||||||
|
assert_eq!(f4_results.total, 4);
|
||||||
|
|
||||||
|
info!("Testing pagination");
|
||||||
|
let post_list = client
|
||||||
|
.request()
|
||||||
|
.with_limit(1)
|
||||||
|
.list_posts(None)
|
||||||
|
.await
|
||||||
|
.expect("Could not list posts page 1");
|
||||||
|
assert_eq!(post_list.results.len(), 1);
|
||||||
|
let post_list2 = client
|
||||||
|
.request()
|
||||||
|
.with_limit(1)
|
||||||
|
.with_offset(1)
|
||||||
|
.list_posts(None)
|
||||||
|
.await
|
||||||
|
.expect("Could not list posts page 2");
|
||||||
|
assert_ne!(post_list.results, post_list2.results);
|
||||||
|
|
||||||
|
info!("Testing tag siblings");
|
||||||
|
let tag_occurrences = client
|
||||||
|
.request()
|
||||||
|
.get_tag_siblings("maine_coon")
|
||||||
|
.await
|
||||||
|
.expect("Could not fetch tag siblings");
|
||||||
|
let occurrences_filtered = tag_occurrences
|
||||||
|
.results
|
||||||
|
.iter()
|
||||||
|
.filter(|oc| {
|
||||||
|
oc.tag
|
||||||
|
.names
|
||||||
|
.as_ref()
|
||||||
|
.map(|names| names.contains(&"cat".to_string()))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
assert_eq!(occurrences_filtered, 1);
|
||||||
|
|
||||||
|
info!("Rating post");
|
||||||
|
let folly3_post = client
|
||||||
|
.request()
|
||||||
|
.rate_post(folly3_post.id.unwrap(), 1)
|
||||||
|
.await
|
||||||
|
.expect("Could not rate post");
|
||||||
|
assert_eq!(folly3_post.own_score, Some(1));
|
||||||
|
|
||||||
|
info!("Favoriting post");
|
||||||
|
let folly3_post = client
|
||||||
|
.request()
|
||||||
|
.favorite_post(folly3_post.id.unwrap())
|
||||||
|
.await
|
||||||
|
.expect("Could not favorite post");
|
||||||
|
assert_eq!(folly3_post.own_favorite, Some(true));
|
||||||
|
|
||||||
|
info!("Unfavorite post");
|
||||||
|
let folly3_post = client
|
||||||
|
.request()
|
||||||
|
.unfavorite_post(folly3_post.id.unwrap())
|
||||||
|
.await
|
||||||
|
.expect("Could not unfavorite post");
|
||||||
|
assert_eq!(folly3_post.own_favorite, Some(false));
|
||||||
|
|
||||||
|
info!("Featuring post");
|
||||||
|
let featured_post = client
|
||||||
|
.request()
|
||||||
|
.get_featured_post()
|
||||||
|
.await
|
||||||
|
.expect("Could not get featured post");
|
||||||
|
assert!(featured_post.is_none());
|
||||||
|
|
||||||
|
client
|
||||||
|
.request()
|
||||||
|
.set_featured_post(folly4_post.id.unwrap())
|
||||||
|
.await
|
||||||
|
.expect("Could not set featured post");
|
||||||
|
|
||||||
|
let featured_post = client
|
||||||
|
.request()
|
||||||
|
.get_featured_post()
|
||||||
|
.await
|
||||||
|
.expect("Could not get featured post");
|
||||||
|
assert!(featured_post.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(client))]
|
||||||
|
async fn test_pool_categories(client: &SzurubooruClient) {
|
||||||
|
info!("Testing pool categories");
|
||||||
|
|
||||||
|
info!("Listing pool categories");
|
||||||
|
let pool_cats = client
|
||||||
|
.request()
|
||||||
|
.list_pool_categories()
|
||||||
|
.await
|
||||||
|
.expect("Could not list pool categories");
|
||||||
|
assert!(!pool_cats.results.is_empty());
|
||||||
|
|
||||||
|
info!("Creating pool category");
|
||||||
|
let create_cat = CreateUpdatePoolCategoryBuilder::default()
|
||||||
|
.name("cat_pool_category".to_string())
|
||||||
|
.color("purple".to_string())
|
||||||
|
.build()
|
||||||
|
.expect("Could not build pool category object");
|
||||||
|
let pool_cat = client
|
||||||
|
.request()
|
||||||
|
.create_pool_category(&create_cat)
|
||||||
|
.await
|
||||||
|
.expect("Could not create pool category");
|
||||||
|
|
||||||
|
let create_dog_cat = CreateUpdatePoolCategoryBuilder::default()
|
||||||
|
.name("dog_category".to_string())
|
||||||
|
.color("orange".to_string())
|
||||||
|
.build()
|
||||||
|
.expect("Could not build pool category object");
|
||||||
|
let dog_pool_cat = client
|
||||||
|
.request()
|
||||||
|
.create_pool_category(&create_dog_cat)
|
||||||
|
.await
|
||||||
|
.expect("Could not create pool category");
|
||||||
|
|
||||||
|
info!("Updating pool category");
|
||||||
|
let update_cat = CreateUpdatePoolCategoryBuilder::default()
|
||||||
|
.version(pool_cat.version.unwrap())
|
||||||
|
.color("white".to_string())
|
||||||
|
.build()
|
||||||
|
.expect("Could not build pool category update");
|
||||||
|
let pool_cat = client
|
||||||
|
.request()
|
||||||
|
.update_pool_category(pool_cat.name.unwrap(), &update_cat)
|
||||||
|
.await
|
||||||
|
.expect("Could not update pool category");
|
||||||
|
|
||||||
|
info!("Getting pool category");
|
||||||
|
let pool_cat = client
|
||||||
|
.request()
|
||||||
|
.get_pool_category(pool_cat.name.unwrap())
|
||||||
|
.await
|
||||||
|
.expect("Could not get pool category");
|
||||||
|
|
||||||
|
info!("Deleting pool category");
|
||||||
|
client
|
||||||
|
.request()
|
||||||
|
.delete_pool_category(dog_pool_cat.name.unwrap(), dog_pool_cat.version.unwrap())
|
||||||
|
.await
|
||||||
|
.expect("Could not delete pool category");
|
||||||
|
|
||||||
|
info!("Setting default pool category");
|
||||||
|
let pool_cat = client
|
||||||
|
.request()
|
||||||
|
.set_default_pool_category(pool_cat.name.unwrap())
|
||||||
|
.await
|
||||||
|
.expect("Could not set default pool category");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue