#![deny(missing_docs)]
use {
derive_builder::Builder,
log::{debug, info, trace},
reqwest::{multipart::{Form, Part}, Client, StatusCode},
serde::{de::{Error as DeserializationError, Unexpected}, Deserialize, Deserializer},
std::{convert::TryFrom, path::PathBuf},
thiserror::Error,
url::Url,
};
pub const DEFAULT_HOST: &str = "uploads.im";
pub type ThumbnailDimension = u32;
pub type FullSizeDimension = u64;
#[derive(Builder, Clone, Debug)]
pub struct UploadOptions {
pub host: String,
pub resize_width: Option<FullSizeDimension>,
pub thumbnail_width: Option<ThumbnailDimension>,
pub family_unsafe: Option<bool>,
}
impl Default for UploadOptions {
fn default() -> Self {
Self {
host: DEFAULT_HOST.to_owned(),
resize_width: None,
thumbnail_width: None,
family_unsafe: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ImageReference<Dimension> {
pub dimensions: Rectangle<Dimension>,
pub url: Url,
}
#[derive(Debug, Clone)]
pub struct UploadedImage {
pub name: String,
pub full_size: ImageReference<FullSizeDimension>,
pub view_url: Url,
pub thumbnail: ImageReference<ThumbnailDimension>,
pub was_resized: bool,
}
#[derive(Debug, Clone)]
pub struct Rectangle<T> {
height: T,
width: T,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum RawUploadResponse {
Failure {
#[serde(deserialize_with = "parse_status_code_string")]
status_code: StatusCode,
status_txt: String,
},
Success {
data: Box<RawUploadResponseSuccess>,
},
}
fn parse_status_code_string<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<StatusCode, D::Error> {
let status_code_number = u16::deserialize(deserializer)?;
StatusCode::from_u16(status_code_number).map_err(|_| {
D::Error::invalid_value(
Unexpected::Unsigned(u64::from(status_code_number)),
&"valid HTTP status code",
)
})
}
#[derive(Debug, Clone, Deserialize)]
struct RawUploadResponseSuccess {
img_name: String,
img_url: Url,
img_view: Url,
#[serde(deserialize_with = "parse_u64_string")]
img_height: FullSizeDimension,
#[serde(deserialize_with = "parse_u64_string")]
img_width: FullSizeDimension,
thumb_url: Url,
thumb_height: ThumbnailDimension,
thumb_width: ThumbnailDimension,
#[serde(deserialize_with = "parse_bool_number_string")]
resized: bool,
}
fn parse_u64_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
use std::error::Error as StdError;
use std::num::ParseIntError;
let string_value = String::deserialize(deserializer)?;
Ok(string_value.parse().map_err(|e: ParseIntError| {
let unexpected = Unexpected::Str(&string_value);
D::Error::invalid_value(unexpected, &e.description())
}))?
}
fn parse_bool_number_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
let parsed_number = parse_u64_string(deserializer)?;
Ok(match parsed_number {
0 => false,
1 => true,
_ => {
let unexpected = Unexpected::Unsigned(parsed_number);
Err(D::Error::invalid_value(
unexpected,
&"boolean integral value",
))?
}
})
}
impl TryFrom<RawUploadResponse> for UploadedImage {
type Error = UploadError;
fn try_from(response: RawUploadResponse) -> Result<Self, Self::Error> {
match response {
RawUploadResponse::Failure {
status_code,
status_txt,
} => Err(UploadError::ResponseReturnedFailure {
status_code,
status_text: status_txt,
}),
RawUploadResponse::Success { data } => {
let d = *data;
let RawUploadResponseSuccess {
img_name,
img_url,
img_view,
img_height,
img_width,
thumb_url,
thumb_height,
thumb_width,
resized,
} = d;
Ok(UploadedImage {
name: img_name,
full_size: ImageReference {
url: img_url,
dimensions: Rectangle {
height: img_height,
width: img_width,
},
},
thumbnail: ImageReference {
url: thumb_url,
dimensions: Rectangle {
height: thumb_height,
width: thumb_width,
},
},
view_url: img_view,
was_resized: resized,
})
}
}
}
}
#[derive(Debug, Error)]
pub enum UploadRequestURLBuildError {
#[error("URL params serialization failed")]
URLParamsBuildingFailed(#[source] serde_urlencoded::ser::Error),
#[error("URL validation failed")]
URLValidationFailed(#[source] url::ParseError),
}
impl From<url::ParseError> for UploadRequestURLBuildError {
fn from(e: url::ParseError) -> Self {
UploadRequestURLBuildError::URLValidationFailed(e)
}
}
impl From<serde_urlencoded::ser::Error> for UploadRequestURLBuildError {
fn from(e: serde_urlencoded::ser::Error) -> Self {
UploadRequestURLBuildError::URLParamsBuildingFailed(e)
}
}
#[derive(Debug, Error)]
pub enum UploadError {
#[error("failed building upload request")]
BuildingRequest(
#[from]
#[source]
UploadRequestURLBuildError
),
#[error("invalid filename \"{}\"", _0.display())]
InvalidFilename(PathBuf),
#[error("could not transmit upload request")]
SendingRequest(
#[from]
#[source]
reqwest::Error
),
#[error(
"the server returned HTTP error code {} (\"{}\")",
status_code, status_text
)]
ResponseReturnedFailure {
status_code: StatusCode,
status_text: String,
},
#[error("cannot access file to upload")]
Io(
#[from]
#[source]
std::io::Error
),
#[error("internal error: unable to parse upload response")]
ParsingResponse(
#[from]
#[source]
serde_json::Error
),
}
pub fn build_upload_url(options: &UploadOptions) -> Result<Url, UploadRequestURLBuildError> {
let url_string = {
let params = {
let &UploadOptions {
ref resize_width,
ref family_unsafe,
..
} = options;
macro_rules! generate_string_keyed_pairs {
($($arg: tt),*) => { [$(generate_string_keyed_pairs!(@inside $arg)),*] };
(@inside $e: ident) => { (stringify!($e), $e.map(|x| x.to_string())) };
(@inside $e: expr) => { $e };
}
let params_tuple = generate_string_keyed_pairs![
resize_width,
family_unsafe,
(
"thumb_width",
options.thumbnail_width.map(|x| x.to_string())
)
];
serde_urlencoded::to_string(params_tuple)?
};
let initial_params_separator = if params.is_empty() { "" } else { "&" };
format!(
"http://{}/api?upload{}{}",
options.host, initial_params_separator, params
)
};
Ok(Url::parse(&url_string)?)
}
pub async fn upload(
client: &mut Client,
file_path: PathBuf,
options: &UploadOptions,
) -> Result<UploadedImage, UploadError> {
info!(
"Beginning upload of file \"{}\" with {:#?}",
file_path.display(),
options
);
let file_name = file_path
.file_name()
.and_then(|n| n.to_str().map(ToOwned::to_owned))
.ok_or_else(|| UploadError::InvalidFilename(file_path))?;
let endpoint_url = build_upload_url(options)?;
debug!("Upload URL: {}", endpoint_url.as_str());
let form = Form::new().part("fileupload", Part::stream("asdf").file_name(file_name));
trace!("Request built, sending now...");
let response = client
.post(endpoint_url.as_str())
.multipart(form)
.send().await?;
debug!("Got upload response: {:#?}", response);
let response_body_text = response.text().await?;
debug!("Upload response data: {:#?}", response_body_text);
let raw_upload_response: RawUploadResponse = serde_json::from_str(&response_body_text)?;
debug!("Parsed response: {:#?}", raw_upload_response);
let uploaded_image = UploadedImage::try_from(raw_upload_response)?;
Ok(uploaded_image)
}
pub async fn upload_with_default_options(
client: &mut Client,
file_path: PathBuf,
) -> Result<UploadedImage, UploadError> {
upload(client, file_path, &UploadOptions::default()).await
}