use bytes::Bytes;
use reqwest::multipart::{Form, Part};
use serde::Deserialize;
use std::{sync::Arc, time::Duration};
use thiserror::Error;
#[derive(Clone)]
pub struct OfficeConvertClient {
http: reqwest::Client,
host: Arc<str>,
}
#[derive(Debug, Error)]
pub enum CreateError {
#[error(transparent)]
Builder(reqwest::Error),
}
#[derive(Debug, Error)]
pub enum RequestError {
#[error(transparent)]
RequestFailed(reqwest::Error),
#[error(transparent)]
InvalidResponse(reqwest::Error),
#[error("server connection timed out")]
ServerConnectTimeout,
#[error("{reason}")]
ErrorResponse {
reason: String,
backtrace: Option<String>,
},
}
impl RequestError {
pub fn is_retry(&self) -> bool {
matches!(
self,
RequestError::RequestFailed(_)
| RequestError::InvalidResponse(_)
| RequestError::ServerConnectTimeout
)
}
}
#[derive(Debug, Deserialize)]
pub struct StatusResponse {
pub is_busy: bool,
}
#[derive(Debug, Deserialize)]
pub struct SupportedFormat {
pub name: String,
pub mime: String,
}
#[derive(Debug, Deserialize)]
pub struct VersionResponse {
pub major: u32,
pub minor: u32,
pub build_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ErrorResponse {
reason: String,
backtrace: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub connect_timeout: Option<Duration>,
pub read_timeout: Option<Duration>,
}
impl Default for ClientOptions {
fn default() -> Self {
Self {
connect_timeout: Some(Duration::from_millis(700)),
read_timeout: None,
}
}
}
impl OfficeConvertClient {
pub fn new<T>(host: T) -> Result<Self, CreateError>
where
T: Into<Arc<str>>,
{
Self::new_with_options(host, ClientOptions::default())
}
pub fn new_with_options<T>(host: T, options: ClientOptions) -> Result<Self, CreateError>
where
T: Into<Arc<str>>,
{
let mut builder = reqwest::Client::builder();
if let Some(connect_timeout) = options.connect_timeout {
builder = builder.connect_timeout(connect_timeout);
}
if let Some(connect_timeout) = options.read_timeout {
builder = builder.read_timeout(connect_timeout);
}
let client = builder.build().map_err(CreateError::Builder)?;
Ok(Self::from_client(host, client))
}
pub fn from_client<T>(host: T, client: reqwest::Client) -> Self
where
T: Into<Arc<str>>,
{
Self {
http: client,
host: host.into(),
}
}
pub async fn get_status(&self) -> Result<StatusResponse, RequestError> {
let route = format!("{}/status", self.host);
let response = self
.http
.get(route)
.send()
.await
.map_err(RequestError::RequestFailed)?;
let status = response.status();
if status.is_client_error() || status.is_server_error() {
let body: ErrorResponse = response
.json()
.await
.map_err(RequestError::InvalidResponse)?;
return Err(RequestError::ErrorResponse {
reason: body.reason,
backtrace: body.backtrace,
});
}
let response: StatusResponse = response
.json()
.await
.map_err(RequestError::InvalidResponse)?;
Ok(response)
}
pub async fn get_office_version(&self) -> Result<VersionResponse, RequestError> {
let route = format!("{}/office-version", self.host);
let response = self
.http
.get(route)
.send()
.await
.map_err(RequestError::RequestFailed)?;
let status = response.status();
if status.is_client_error() || status.is_server_error() {
let body: ErrorResponse = response
.json()
.await
.map_err(RequestError::InvalidResponse)?;
return Err(RequestError::ErrorResponse {
reason: body.reason,
backtrace: body.backtrace,
});
}
let response: VersionResponse = response
.json()
.await
.map_err(RequestError::InvalidResponse)?;
Ok(response)
}
pub async fn get_supported_formats(&self) -> Result<Vec<SupportedFormat>, RequestError> {
let route = format!("{}/supported-formats", self.host);
let response = self
.http
.get(route)
.send()
.await
.map_err(RequestError::RequestFailed)?;
let status = response.status();
if status.is_client_error() || status.is_server_error() {
let body: ErrorResponse = response
.json()
.await
.map_err(RequestError::InvalidResponse)?;
return Err(RequestError::ErrorResponse {
reason: body.reason,
backtrace: body.backtrace,
});
}
let response: Vec<SupportedFormat> = response
.json()
.await
.map_err(RequestError::InvalidResponse)?;
Ok(response)
}
pub async fn is_busy(&self) -> Result<bool, RequestError> {
let status = self.get_status().await?;
Ok(status.is_busy)
}
pub async fn collect_garbage(&self) -> Result<(), RequestError> {
let route = format!("{}/collect-garbage", self.host);
let response = self
.http
.post(route)
.send()
.await
.map_err(RequestError::RequestFailed)?;
let status = response.status();
if status.is_client_error() || status.is_server_error() {
let body: ErrorResponse = response
.json()
.await
.map_err(RequestError::InvalidResponse)?;
return Err(RequestError::ErrorResponse {
reason: body.reason,
backtrace: body.backtrace,
});
}
Ok(())
}
pub async fn convert(&self, file: Bytes) -> Result<Bytes, RequestError> {
let route = format!("{}/convert", self.host);
let form = Form::new().part("file", Part::stream(file));
let response = self
.http
.post(route)
.multipart(form)
.send()
.await
.map_err(RequestError::RequestFailed)?;
let status = response.status();
if status.is_client_error() || status.is_server_error() {
let body: ErrorResponse = response
.json()
.await
.map_err(RequestError::InvalidResponse)?;
return Err(RequestError::ErrorResponse {
reason: body.reason,
backtrace: body.backtrace,
});
}
let response = response
.bytes()
.await
.map_err(RequestError::InvalidResponse)?;
Ok(response)
}
}