use super::*;
use reqwest::blocking::multipart;
use reqwest::blocking::{Client as ReqwestClient, Response};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
#[derive(Clone)]
pub struct BlockingClient {
client: ReqwestClient,
base_url: String,
username: Option<String>,
password: Option<String>,
}
impl Drop for BlockingClient {
fn drop(&mut self) {
#[cfg(feature = "zeroize")]
{
if let Some(username) = &mut self.username {
username.zeroize();
}
if let Some(password) = &mut self.password {
password.zeroize();
}
}
}
}
impl Debug for BlockingClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BlockingClient")
.field("base_url", &self.base_url)
.field("username", &self.username)
.finish()
}
}
impl BlockingClient {
pub fn new(base_url: &str) -> Self {
let base_url = base_url.trim_end_matches('/');
let client = ReqwestClient::builder()
.pool_idle_timeout(Some(std::time::Duration::from_secs(25))) .build()
.unwrap();
BlockingClient {
client,
base_url: base_url.to_string(),
username: None,
password: None,
}
}
pub fn new_with_client(base_url: &str, client: ReqwestClient) -> Self {
let base_url = base_url.trim_end_matches('/');
BlockingClient {
client,
base_url: base_url.to_string(),
username: None,
password: None,
}
}
pub fn auth(self, username: &str, password: &str) -> Self {
let mut client = self;
client.username = Some(username.to_string());
client.password = Some(password.to_string());
client
}
fn post(
&self,
endpoint: &str,
form: multipart::Form,
trace: Option<String>,
) -> Result<Bytes, Error> {
let url = format!("{}/{}", self.base_url, endpoint);
let mut req = self.client.post(&url).multipart(form);
if let Some(trace) = trace {
req = req.header("Gotenberg-Trace", trace);
}
if let (Some(username), Some(password)) = (&self.username, &self.password) {
req = req.basic_auth(username, Some(password));
}
let response: Response = req.send().map_err(Into::into)?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().unwrap_or_default();
return Err(Error::RenderingError(format!(
"Failed to render PDF: {} - {}",
status, body
)));
}
response.bytes().map_err(Into::into)
}
pub fn pdf_from_url(&self, url: &str, options: WebOptions) -> Result<Bytes, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new().text("url", url.to_string());
let form = options.fill_form_blocking(form);
self.post("forms/chromium/convert/url", form, trace)
}
pub fn pdf_from_html(&self, html: &str, options: WebOptions) -> Result<Bytes, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new();
let file_bytes = html.to_string().into_bytes();
let part = multipart::Part::bytes(file_bytes)
.file_name("index.html")
.mime_str("text/html")
.unwrap();
let form = form.part("index.html", part);
let form = options.fill_form_blocking(form);
self.post("forms/chromium/convert/html", form, trace)
}
pub fn pdf_from_markdown(
&self,
html_template: &str,
markdown: HashMap<&str, &str>,
options: WebOptions,
) -> Result<Bytes, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new();
let file_bytes = html_template.to_string().into_bytes();
let part = multipart::Part::bytes(file_bytes)
.file_name("index.html")
.mime_str("text/html")
.unwrap();
let form = form.part("index.html", part);
let form = options.fill_form_blocking(form);
let form = {
let mut form = form;
for (filename, content) in markdown {
if !filename.ends_with(".md") {
return Err(Error::FilenameError(
"Markdown filename must end with '.md'".to_string(),
));
}
let file_bytes = content.to_string().into_bytes();
let part = multipart::Part::bytes(file_bytes)
.file_name(filename.to_string())
.mime_str("text/markdown")
.unwrap();
form = form.part(filename.to_string(), part);
}
form
};
self.post("forms/chromium/convert/markdown", form, trace)
}
pub fn screenshot_url(&self, url: &str, options: ScreenshotOptions) -> Result<Bytes, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new().text("url", url.to_string());
let form = options.fill_form_blocking(form);
self.post("forms/chromium/screenshot/url", form, trace)
}
pub fn screenshot_html(&self, html: &str, options: ScreenshotOptions) -> Result<Bytes, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new();
let file_bytes = html.to_string().into_bytes();
let part = multipart::Part::bytes(file_bytes)
.file_name("index.html")
.mime_str("text/html")
.unwrap();
let form = form.part("index.html", part);
let form = options.fill_form_blocking(form);
self.post("forms/chromium/screenshot/html", form, trace)
}
pub fn screenshot_markdown(
&self,
html_template: &str,
markdown: HashMap<&str, &str>,
options: ScreenshotOptions,
) -> Result<Bytes, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new();
let file_bytes = html_template.to_string().into_bytes();
let part = multipart::Part::bytes(file_bytes)
.file_name("index.html")
.mime_str("text/html")
.unwrap();
let form = form.part("index.html", part);
let form = options.fill_form_blocking(form);
let form = {
let mut form = form;
for (filename, content) in markdown {
if !filename.ends_with(".md") {
return Err(Error::FilenameError(
"Markdown filename must end with '.md'".to_string(),
));
}
let file_bytes = content.to_string().into_bytes();
let part = multipart::Part::bytes(file_bytes)
.file_name(filename.to_string())
.mime_str("text/markdown")
.unwrap();
form = form.part(filename.to_string(), part);
}
form
};
self.post("forms/chromium/screenshot/markdown", form, trace)
}
pub fn pdf_from_doc(
&self,
filename: &str,
bytes: Vec<u8>,
options: DocumentOptions,
) -> Result<Bytes, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new();
let part = multipart::Part::bytes(bytes).file_name(filename.to_string());
let form = form.part("files", part);
let form = options.fill_form_blocking(form);
self.post("forms/libreoffice/convert", form, trace)
}
pub fn convert_pdf(
&self,
pdf_bytes: Vec<u8>,
pdfa: Option<PDFFormat>,
pdfua: bool,
) -> Result<Bytes, Error> {
let form = multipart::Form::new();
let part = multipart::Part::bytes(pdf_bytes).file_name("file.pdf".to_string());
let mut form = form.part("file.pdf", part);
if let Some(pdfa) = pdfa {
form = form.text("pdfa", pdfa.to_string());
}
let form = form.text("pdfua", pdfua.to_string());
self.post("forms/pdfengines/convert", form, None)
}
pub fn read_metadata(
&self,
pdf_bytes: Vec<u8>,
) -> Result<HashMap<String, serde_json::Value>, Error> {
let form = multipart::Form::new();
let part = multipart::Part::bytes(pdf_bytes).file_name("file.pdf".to_string());
let form = form.part("file.pdf", part);
#[derive(Debug, Deserialize)]
pub struct MetadataContainer {
#[serde(rename = "file.pdf")]
pub filepdf: HashMap<String, serde_json::Value>,
}
let bytes = self.post("forms/pdfengines/metadata/read", form, None)?;
let metadata: MetadataContainer = serde_json::from_slice(&bytes).map_err(|e| {
Error::ParseError(
"Metadata".to_string(),
String::from_utf8_lossy(&bytes).to_string(),
e.to_string(),
)
})?;
Ok(metadata.filepdf)
}
pub fn write_metadata(
&self,
pdf_bytes: Vec<u8>,
metadata: HashMap<String, serde_json::Value>,
) -> Result<Bytes, Error> {
let form = multipart::Form::new();
let part = multipart::Part::bytes(pdf_bytes).file_name("file.pdf".to_string());
let form = form.part("file.pdf", part);
let metadata = serde_json::to_string(&metadata).map_err(|e| {
Error::ParseError("Metadata".to_string(), "".to_string(), e.to_string())
})?;
let part = multipart::Part::text(metadata);
let form = form.part("metadata", part);
self.post("forms/pdfengines/metadata/write", form, None)
}
pub fn health_check(&self) -> Result<health::Health, Error> {
let url = format!("{}/health", self.base_url);
let response = self.client.get(&url).send().map_err(Into::into)?;
let body = response.text().map_err(Into::into)?;
serde_json::from_str(&body)
.map_err(|e| Error::ParseError("Health".to_string(), body, e.to_string()))
}
pub fn version(&self) -> Result<String, Error> {
let url = format!("{}/version", self.base_url);
let response = self.client.get(&url).send().map_err(Into::into)?;
let body = response.text().map_err(Into::into)?;
Ok(body)
}
pub fn metrics(&self) -> Result<String, Error> {
let url = format!("{}/prometheus/metrics", self.base_url);
let response = self.client.get(&url).send().map_err(Into::into)?;
let body = response.text().map_err(Into::into)?;
Ok(body)
}
}