1#![deny(missing_docs)]
2use {
27 derive_builder::Builder,
28 log::{debug, info, trace},
29 reqwest::{multipart::{Form, Part}, Client, StatusCode},
30 serde::{de::{Error as DeserializationError, Unexpected}, Deserialize, Deserializer},
31 std::{convert::TryFrom, path::PathBuf},
32 thiserror::Error,
33 url::Url,
34};
35
36pub const DEFAULT_HOST: &str = "uploads.im";
38
39pub type ThumbnailDimension = u32;
41pub type FullSizeDimension = u64;
43
44#[derive(Builder, Clone, Debug)]
46pub struct UploadOptions {
47 pub host: String,
49 pub resize_width: Option<FullSizeDimension>,
51 pub thumbnail_width: Option<ThumbnailDimension>,
54 pub family_unsafe: Option<bool>,
57}
58
59impl Default for UploadOptions {
60 fn default() -> Self {
61 Self {
62 host: DEFAULT_HOST.to_owned(),
63 resize_width: None,
64 thumbnail_width: None,
65 family_unsafe: None,
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct ImageReference<Dimension> {
73 pub dimensions: Rectangle<Dimension>,
75 pub url: Url,
77}
78
79#[derive(Debug, Clone)]
81pub struct UploadedImage {
82 pub name: String,
87 pub full_size: ImageReference<FullSizeDimension>,
89 pub view_url: Url,
91 pub thumbnail: ImageReference<ThumbnailDimension>,
93 pub was_resized: bool,
95}
96
97#[derive(Debug, Clone)]
99pub struct Rectangle<T> {
100 height: T,
102 width: T,
104}
105
106#[derive(Debug, Clone, Deserialize)]
108#[serde(untagged)]
109enum RawUploadResponse {
110 Failure {
112 #[serde(deserialize_with = "parse_status_code_string")]
113 status_code: StatusCode,
114 status_txt: String,
115 },
116 Success {
118 data: Box<RawUploadResponseSuccess>,
120 },
121}
122
123fn parse_status_code_string<'de, D: serde::Deserializer<'de>>(
125 deserializer: D,
126) -> Result<StatusCode, D::Error> {
127 let status_code_number = u16::deserialize(deserializer)?;
128 StatusCode::from_u16(status_code_number).map_err(|_| {
129 D::Error::invalid_value(
130 Unexpected::Unsigned(u64::from(status_code_number)),
131 &"valid HTTP status code",
132 )
133 })
134}
135
136#[derive(Debug, Clone, Deserialize)]
138struct RawUploadResponseSuccess {
139 img_name: String,
140 img_url: Url,
141 img_view: Url,
142 #[serde(deserialize_with = "parse_u64_string")]
143 img_height: FullSizeDimension,
144 #[serde(deserialize_with = "parse_u64_string")]
145 img_width: FullSizeDimension,
146 thumb_url: Url,
147 thumb_height: ThumbnailDimension,
148 thumb_width: ThumbnailDimension,
149 #[serde(deserialize_with = "parse_bool_number_string")]
150 resized: bool,
151}
152
153fn parse_u64_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
155 use std::error::Error as StdError;
156 use std::num::ParseIntError;
157
158 let string_value = String::deserialize(deserializer)?;
159 Ok(string_value.parse().map_err(|e: ParseIntError| {
160 let unexpected = Unexpected::Str(&string_value);
161 D::Error::invalid_value(unexpected, &e.description())
162 }))?
163}
164
165fn parse_bool_number_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
167 let parsed_number = parse_u64_string(deserializer)?;
168 Ok(match parsed_number {
169 0 => false,
170 1 => true,
171 _ => {
172 let unexpected = Unexpected::Unsigned(parsed_number);
173 Err(D::Error::invalid_value(
174 unexpected,
175 &"boolean integral value",
176 ))?
177 }
178 })
179}
180
181impl TryFrom<RawUploadResponse> for UploadedImage {
182 type Error = UploadError;
183 fn try_from(response: RawUploadResponse) -> Result<Self, Self::Error> {
184 match response {
185 RawUploadResponse::Failure {
186 status_code,
187 status_txt,
188 } => Err(UploadError::ResponseReturnedFailure {
189 status_code,
190 status_text: status_txt,
191 }),
192 RawUploadResponse::Success { data } => {
193 let d = *data;
194 let RawUploadResponseSuccess {
195 img_name,
196 img_url,
197 img_view,
198 img_height,
199 img_width,
200 thumb_url,
201 thumb_height,
202 thumb_width,
203 resized,
204 } = d;
205
206 Ok(UploadedImage {
207 name: img_name,
208 full_size: ImageReference {
209 url: img_url,
210 dimensions: Rectangle {
211 height: img_height,
212 width: img_width,
213 },
214 },
215 thumbnail: ImageReference {
216 url: thumb_url,
217 dimensions: Rectangle {
218 height: thumb_height,
219 width: thumb_width,
220 },
221 },
222 view_url: img_view,
223 was_resized: resized,
224 })
225 }
226 }
227 }
228}
229
230#[derive(Debug, Error)]
232pub enum UploadRequestURLBuildError {
233 #[error("URL params serialization failed")]
235 URLParamsBuildingFailed(#[source] serde_urlencoded::ser::Error),
236 #[error("URL validation failed")]
238 URLValidationFailed(#[source] url::ParseError),
239}
240
241impl From<url::ParseError> for UploadRequestURLBuildError {
242 fn from(e: url::ParseError) -> Self {
243 UploadRequestURLBuildError::URLValidationFailed(e)
244 }
245}
246
247impl From<serde_urlencoded::ser::Error> for UploadRequestURLBuildError {
248 fn from(e: serde_urlencoded::ser::Error) -> Self {
249 UploadRequestURLBuildError::URLParamsBuildingFailed(e)
250 }
251}
252
253#[derive(Debug, Error)]
256pub enum UploadError {
257 #[error("failed building upload request")]
259 BuildingRequest(
260 #[from]
261 #[source]
262 UploadRequestURLBuildError
263 ),
264 #[error("invalid filename \"{}\"", _0.display())]
266 InvalidFilename(PathBuf),
267 #[error("could not transmit upload request")]
269 SendingRequest(
270 #[from]
271 #[source]
272 reqwest::Error
273 ),
274 #[error(
276 "the server returned HTTP error code {} (\"{}\")",
277 status_code, status_text
278 )]
279 ResponseReturnedFailure {
280 status_code: StatusCode,
283 status_text: String,
285 },
286 #[error("cannot access file to upload")]
288 Io(
289 #[from]
290 #[source]
291 std::io::Error
292 ),
293 #[error("internal error: unable to parse upload response")]
295 ParsingResponse(
296 #[from]
297 #[source]
298 serde_json::Error
299 ),
300}
301
302pub fn build_upload_url(options: &UploadOptions) -> Result<Url, UploadRequestURLBuildError> {
305 let url_string = {
306 let params = {
307 let &UploadOptions {
308 ref resize_width,
309 ref family_unsafe,
310 ..
311 } = options;
312
313 macro_rules! generate_string_keyed_pairs {
314 ($($arg: tt),*) => { [$(generate_string_keyed_pairs!(@inside $arg)),*] };
315 (@inside $e: ident) => { (stringify!($e), $e.map(|x| x.to_string())) };
316 (@inside $e: expr) => { $e };
317 }
318
319 let params_tuple = generate_string_keyed_pairs![
320 resize_width,
321 family_unsafe,
322 (
323 "thumb_width",
324 options.thumbnail_width.map(|x| x.to_string())
325 )
326 ];
327
328 serde_urlencoded::to_string(params_tuple)?
329 };
330 let initial_params_separator = if params.is_empty() { "" } else { "&" };
331
332 format!(
333 "http://{}/api?upload{}{}",
334 options.host, initial_params_separator, params
335 )
336 };
337
338 Ok(Url::parse(&url_string)?)
339}
340
341pub async fn upload(
344 client: &mut Client,
345 file_path: PathBuf,
346 options: &UploadOptions,
347) -> Result<UploadedImage, UploadError> {
348 info!(
349 "Beginning upload of file \"{}\" with {:#?}",
350 file_path.display(),
351 options
352 );
353
354 let file_name = file_path
355 .file_name()
356 .and_then(|n| n.to_str().map(ToOwned::to_owned))
357 .ok_or_else(|| UploadError::InvalidFilename(file_path))?;
358
359 let endpoint_url = build_upload_url(options)?;
360
361 debug!("Upload URL: {}", endpoint_url.as_str());
362 let form = Form::new().part("fileupload", Part::stream("asdf").file_name(file_name));
363
364 trace!("Request built, sending now...");
365
366 let response = client
367 .post(endpoint_url.as_str())
368 .multipart(form)
369 .send().await?;
370
371 debug!("Got upload response: {:#?}", response);
372
373 let response_body_text = response.text().await?;
374
375 debug!("Upload response data: {:#?}", response_body_text);
376
377 let raw_upload_response: RawUploadResponse = serde_json::from_str(&response_body_text)?;
378
379 debug!("Parsed response: {:#?}", raw_upload_response);
380
381 let uploaded_image = UploadedImage::try_from(raw_upload_response)?;
382
383 Ok(uploaded_image)
384}
385
386pub async fn upload_with_default_options(
389 client: &mut Client,
390 file_path: PathBuf,
391) -> Result<UploadedImage, UploadError> {
392 upload(client, file_path, &UploadOptions::default()).await
393}