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:
Scott Lyons 2024-08-10 18:12:42 -07:00
commit a534d6ebd8
9 changed files with 462 additions and 51 deletions

View file

@ -597,16 +597,15 @@ impl<'a> SzurubooruRequest<'a> {
.await
}
/// Update an existing post based on the `contentUrl` field, which the server will use to
/// download and update the post
/// Update an existing post
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn update_post_from_url(
pub async fn update_post(
&self,
post_id: u32,
update_post: &CreateUpdatePost,
) -> 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)
.await
}
@ -619,28 +618,37 @@ impl<'a> SzurubooruRequest<'a> {
Ok(Part::stream(bytes))
}
async fn create_update_post_from_file(
async fn create_update_post_from_file<T>(
&self,
file: &mut File,
file: Option<&mut File>,
thumbnail: Option<&mut File>,
file_name: Option<T>,
path: &str,
method: Method,
cupost: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
) -> SzurubooruResult<PostResource>
where
T: AsRef<str>,
{
let request = self.prep_request(method, path, None);
let metadata_str =
serde_json::to_string(cupost).map_err(SzurubooruClientError::JSONSerializationError)?;
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()
.part("content", content_part)
.part("metadata", metadata_part);
if let Some(file) = file {
let content_part = self
.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 {
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);
}
@ -650,14 +658,25 @@ impl<'a> SzurubooruRequest<'a> {
/// Create a new post from a file handle
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn create_post_from_file(
pub async fn create_post_from_file<T>(
&self,
file: &mut File,
thumbnail: Option<&mut File>,
file_name: T,
new_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
self.create_update_post_from_file(file, thumbnail, "/posts", Method::POST, new_post)
.await
) -> SzurubooruResult<PostResource>
where
T: AsRef<str>,
{
self.create_update_post_from_file(
Some(file),
thumbnail,
Some(file_name),
"/posts",
Method::POST,
new_post,
)
.await
}
/// Create a new post from a file path
@ -669,30 +688,57 @@ impl<'a> SzurubooruRequest<'a> {
thumbnail: Option<impl AsRef<Path>>,
new_post: &CreateUpdatePost,
) -> 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 {
Some(File::open(t).map_err(SzurubooruClientError::IOError)?)
} else {
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
}
/// Update an existing post from an open File handle
/// See [SzurubooruRequest::create_post_from_url] for more details about the fields in
/// (CreateUpdatePost)[models::CreateUpdatePost]
pub async fn update_post_from_file(
&self,
post_id: u32,
file: &mut File,
file: Option<&mut File>,
thumbnail: Option<&mut File>,
file_name: impl AsRef<str>,
update_post: &CreateUpdatePost,
) -> SzurubooruResult<PostResource> {
let path = format!("/posts/{post_id}");
self.create_update_post_from_file(file, thumbnail, &path, Method::PUT, update_post)
.await
self.create_update_post_from_file(
file,
thumbnail,
Some(file_name),
&path,
Method::PUT,
update_post,
)
.await
}
/// Update an existing post from a file path
@ -701,25 +747,60 @@ impl<'a> SzurubooruRequest<'a> {
pub async fn update_post_from_file_path(
&self,
post_id: u32,
file_path: impl AsRef<Path>,
file_path: Option<impl AsRef<Path>>,
thumbnail: Option<impl AsRef<Path>>,
update_post: &CreateUpdatePost,
) -> 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 {
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)?)
} else {
None
};
self.update_post_from_file(post_id, &mut file, thumbnail_file.as_mut(), update_post)
.await
self.update_post_from_file(
post_id,
file.as_mut(),
thumbnail_file.as_mut(),
filename.unwrap(),
update_post,
)
.await
}
async fn get_post_content(&self, post_id: u32) -> SzurubooruResult<Response> {
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
.build()
.map_err(SzurubooruClientError::RequestBuilderError)?;
@ -729,7 +810,7 @@ impl<'a> SzurubooruRequest<'a> {
.client
.execute(request)
.await
.map_err(|e| SzurubooruClientError::RequestError(e))?;
.map_err(SzurubooruClientError::RequestError)?;
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<()> {
let path = format!("/post/{post_id}");
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
.map(|_| ())
}
///
@ -935,13 +1017,19 @@ impl<'a> SzurubooruRequest<'a> {
}
/// 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
T: AsRef<str> + Display,
{
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
.map(|_| ())
}
/// 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(
&self,
file: &mut File,
file_name: impl AsRef<str>,
) -> SzurubooruResult<TemporaryFileUpload> {
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);
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

View file

@ -77,6 +77,8 @@ impl<T> IntoClientResult<T> for SzuruEither<T, SzurubooruServerError> {
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
/// An error type returned by the server
pub enum SzurubooruServerErrorType {
/// Inavlid pool category color
InvalidPoolCategoryColorError,
/// Missing required file
MissingRequiredFileError,
/// Missing required parameter

View file

@ -46,7 +46,7 @@ pub struct PagedSearchResult<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.
pub struct MicroTagResource {
/// The tag names and aliases
@ -219,7 +219,8 @@ pub struct TagSibling {
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
pub enum PostType {
/// Image post
@ -240,7 +241,8 @@ pub enum PostType {
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
pub enum PostSafety {
/// Post is SFW
@ -253,7 +255,7 @@ pub enum PostSafety {
Unsafe,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
/// A post resource stripped down to `id` and `thumbnailUrl` fields.
pub struct MicroPostResource {
@ -269,7 +271,7 @@ pub(crate) struct PostId {
pub id: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
/// A post resource
pub struct PostResource {
@ -313,6 +315,8 @@ pub struct PostResource {
pub user: Option<MicroUserResource>,
/// The collective score (+1/-1 rating) of the given post
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
pub own_favorite: Option<bool>,
/// 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
/// the [version](CreateUpdatePost::version) field is required
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
/// created. Tags created automatically have no implications, no suggestions, one name and
/// their category is set to the first tag category found
#[builder(default)]
pub tags: Vec<String>,
/// Required field, represents the SFW/NSFW state of a post
pub safety: PostSafety,
/// The origin of the post's content
#[builder(default)]
pub source: Option<String>,
/// The IDs of related posts
pub relations: Option<Vec<u32>>,
#[builder(default)]
pub relations: Vec<u32>,
/// 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
pub flags: Option<Vec<String>>,
#[builder(default)]
pub flags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// The URL to download the content from
#[builder(default)]
pub content_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// The token returned from
/// [upload_temporary_file](crate::SzurubooruRequest::upload_temporary_file)
#[builder(default)]
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)]
@ -379,7 +391,7 @@ pub struct CreateUpdatePost {
/// A token representing a temporary file upload
pub struct TemporaryFileUpload {
/// Temporary upload token
token: String,
pub token: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
@ -409,7 +421,7 @@ pub struct RateResource {
pub score: i8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
/// A text annotation rendered on top of the post
pub struct NoteResource {
@ -516,7 +528,7 @@ pub struct CreateUpdateUser {
pub avatar_style: Option<UserAvatarStyle>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
/// A user resource stripped down to `name` and `avatarUrl` fields
pub struct MicroUserResource {
@ -663,17 +675,20 @@ pub struct PoolCategoryResource {
/// ```
pub struct CreateUpdatePoolCategory {
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
/// Category version (used for updating)
pub version: Option<u32>,
/// Category name
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
pub name: Option<String>,
/// Category color
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
pub color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
/// Type that represents a Pool resource
pub struct PoolResource {
@ -771,7 +786,7 @@ pub struct MicroPoolResource {
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
/// A type representing a Comment on a post
pub struct CommentResource {