use super::*;
use futures::Stream;
use reqwest::multipart;
use reqwest::{Client as ReqwestClient, Error as ReqwestError, Response};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
#[derive(Clone)]
pub struct StreamingClient {
client: ReqwestClient,
base_url: String,
username: Option<String>,
password: Option<String>,
}
impl Drop for StreamingClient {
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 StreamingClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StreamingClient")
.field("base_url", &self.base_url)
.field("username", &self.username)
.finish()
}
}
impl StreamingClient {
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();
Self {
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('/');
Self {
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
}
async fn post_stream(
&self,
endpoint: &str,
form: multipart::Form,
trace: Option<String>,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, 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 = req.send().await.map_err(Into::into)?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::RenderingError(format!(
"Failed to render PDF: {} - {}",
status, body
)));
}
Ok(response.bytes_stream())
}
async 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().await.map_err(Into::into)?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(Error::RenderingError(format!(
"Failed to render PDF: {} - {}",
status, body
)));
}
response.bytes().await.map_err(Into::into)
}
pub async fn pdf_from_url(
&self,
url: &str,
options: WebOptions,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new().text("url", url.to_string());
let form = options.fill_form(form);
self.post_stream("forms/chromium/convert/url", form, trace)
.await
}
pub async fn pdf_from_html(
&self,
html: &str,
options: WebOptions,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, Error> {
let trace = options.trace_id.clone();
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 = multipart::Form::new().part("index.html", part);
let form = options.fill_form(form);
self.post_stream("forms/chromium/convert/html", form, trace)
.await
}
pub async fn pdf_from_markdown(
&self,
html_template: &str,
markdown: HashMap<&str, &str>,
options: WebOptions,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, Error> {
let trace = options.trace_id.clone();
let file_bytes = html_template.to_string().into_bytes();
let html_part = multipart::Part::bytes(file_bytes)
.file_name("index.html")
.mime_str("text/html")
.unwrap();
let mut form = multipart::Form::new().part("index.html", html_part);
for (filename, content) in markdown {
if !filename.ends_with(".md") {
return Err(Error::FilenameError(
"Markdown filename must end with '.md'".to_string(),
));
}
let part = multipart::Part::bytes(content.as_bytes().to_vec())
.file_name(filename.to_string())
.mime_str("text/markdown")
.unwrap();
form = form.part(filename.to_string(), part);
}
let form = options.fill_form(form);
self.post_stream("forms/chromium/convert/markdown", form, trace)
.await
}
pub async fn screenshot_url(
&self,
url: &str,
options: ScreenshotOptions,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, Error> {
let trace = options.trace_id.clone();
let form = multipart::Form::new().text("url", url.to_string());
let form = options.fill_form(form);
self.post_stream("forms/chromium/screenshot/url", form, trace)
.await
}
pub async fn screenshot_html(
&self,
html: &str,
options: ScreenshotOptions,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, Error> {
let trace = options.trace_id.clone();
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 = multipart::Form::new().part("index.html", part);
let form = options.fill_form(form);
self.post_stream("forms/chromium/screenshot/html", form, trace)
.await
}
pub async fn screenshot_markdown(
&self,
html_template: &str,
markdown: HashMap<&str, &str>,
options: ScreenshotOptions,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, Error> {
let trace = options.trace_id.clone();
let file_bytes = html_template.to_string().into_bytes();
let html_part = multipart::Part::bytes(file_bytes)
.file_name("index.html")
.mime_str("text/html")
.unwrap();
let mut form = multipart::Form::new().part("index.html", html_part);
for (filename, content) in markdown {
if !filename.ends_with(".md") {
return Err(Error::FilenameError(
"Markdown filename must end with '.md'".to_string(),
));
}
let part = multipart::Part::bytes(content.as_bytes().to_vec())
.file_name(filename.to_string())
.mime_str("text/markdown")
.unwrap();
form = form.part(filename.to_string(), part);
}
let form = options.fill_form(form);
self.post_stream("forms/chromium/screenshot/markdown", form, trace)
.await
}
pub async fn pdf_from_doc(
&self,
filename: &str,
bytes: Vec<u8>,
options: DocumentOptions,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, Error> {
let trace = options.trace_id.clone();
let part = multipart::Part::bytes(bytes).file_name(filename.to_string());
let form = multipart::Form::new().part("files", part);
let form = options.fill_form(form);
self.post_stream("forms/libreoffice/convert", form, trace)
.await
}
pub async fn convert_pdf(
&self,
pdf_bytes: Vec<u8>,
pdfa: Option<PDFFormat>,
pdfua: bool,
) -> Result<impl Stream<Item = Result<Bytes, ReqwestError>>, Error> {
let pdf_part = multipart::Part::bytes(pdf_bytes).file_name("file.pdf".to_string());
let mut form = multipart::Form::new().part("file.pdf", pdf_part);
if let Some(pdfa) = pdfa {
form = form.text("pdfa", pdfa.to_string());
}
form = form.text("pdfua", pdfua.to_string());
self.post_stream("forms/pdfengines/convert", form, None)
.await
}
pub async 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 MeatadataContainer {
#[serde(rename = "file.pdf")]
pub filepdf: HashMap<String, serde_json::Value>,
}
let bytes = self
.post("forms/pdfengines/metadata/read", form, None)
.await?;
let metadata: MeatadataContainer = 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 async 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)
.await
}
pub async fn health_check(&self) -> Result<health::Health, Error> {
let url = format!("{}/health", self.base_url);
let response = self.client.get(&url).send().await.map_err(Into::into)?;
let body = response.text().await.map_err(Into::into)?;
serde_json::from_str(&body)
.map_err(|e| Error::ParseError("Health".to_string(), body, e.to_string()))
}
pub async fn version(&self) -> Result<String, Error> {
let url = format!("{}/version", self.base_url);
let response = self.client.get(&url).send().await.map_err(Into::into)?;
let body = response.text().await.map_err(Into::into)?;
Ok(body)
}
pub async fn metrics(&self) -> Result<String, Error> {
let url = format!("{}/prometheus/metrics", self.base_url);
let response = self.client.get(&url).send().await.map_err(Into::into)?;
let body = response.text().await.map_err(Into::into)?;
Ok(body)
}
}