This commit is contained in:
2023-12-02 00:52:53 +01:00
parent 0e0754fa10
commit b51840d09a
14 changed files with 1113 additions and 107 deletions

View File

@@ -32,7 +32,6 @@ impl Fairing for Chromium {
async fn on_ignite(&self, rocket: Rocket<Build>) -> fairing::Result {
let new_rocket = rocket.manage(ChromiumCoordinator::new().await);
let coordinator = new_rocket.state::<ChromiumCoordinator>().unwrap();
Ok(new_rocket)
}
}

View File

@@ -1,15 +1,27 @@
use chromium::rocket::Chromium;
use nanobyte_opentelemetry::rocket::TracingFairing;
use rocket::{Rocket, Build, routes};
use rocket::{Rocket, Build, routes, fairing::AdHoc};
use serde::Deserialize;
pub mod routes;
mod chromium;
mod tools;
mod services;
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CVBackendConfig {
cv_backend_path: String,
}
pub fn rocket_builder() -> Rocket<Build> {
rocket::build()
.attach(TracingFairing::ignite())
.attach(Chromium::ignite())
.attach(
AdHoc::config::<CVBackendConfig>()
)
.mount("/", routes![
routes::pdf::render_pdf_cv,
routes::pdf::render_html_cv

View File

@@ -1,12 +1,15 @@
use headless_chrome::Browser;
use headless_chrome::types::PrintToPdfOptions;
use nanobyte_opentelemetry::rocket::TracingSpan;
use rocket::{get, response::stream::ByteStream, fairing::Fairing, fs::NamedFile, futures::TryFutureExt};
use nanobyte_opentelemetry::rocket::{TracingSpan, RequestId, OtelReqwestClient};
use ovlach_data::cv::cv::CV;
use reqwest::Client;
use rocket::fs::NamedFile;
use ::rocket::{State, http::Status};
use ::rocket::get;
use tempfile::NamedTempFile;
use tera::Context;
use tracing::{info_span, error, debug};
use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream}};
use urlencoding::encode;
use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream, rocket::RequestLanguage}, services::cv::fetch_cv_data_from_backend, CVBackendConfig};
// TODO: request-id
fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec<u8> {
@@ -38,29 +41,59 @@ fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec<u8> {
}
#[tracing::instrument]
fn render_template(template_name: &str, file: &NamedTempFile, tera: NanoTera) {
fn render_template(template_name: &str, file: &NamedTempFile, tera: NanoTera, cv: CV, language: String) {
// TODO: handle errors
tera.0.render_to("two_column.html.tera", &Context::new(), file);
let mut tera_context = Context::new();
tera_context.insert("cv", &cv);
tera_context.insert("lang", language.as_str());
match tera.0.render_to("two_column.html.tera", &tera_context, file) {
Ok(_) => {
debug!("Rendered template to {}", file.path().to_str().unwrap());
},
Err(e) => {
error!("Error rendering template {}: {}", &template_name, e);
}
}
}
#[get("/cv/<username>/output.pdf")]
pub async fn render_pdf_cv(username: &str, browser: BrowserHolder, tera: NanoTera, tracing: TracingSpan) -> PdfStream {
#[get("/cv/<username>/<language>/output.pdf")]
pub async fn render_pdf_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
cv_config: &State<CVBackendConfig>, language: RequestLanguage, browser: BrowserHolder) -> Result<PdfStream, Status> {
let entered_span = tracing.0.enter();
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
render_template("two_column", &file, tera);
let span = info_span!("render_pdf", username = username);
let pdf = span.in_scope(||{
generate_pdf(browser.browser, &file)
});
drop(entered_span);
PdfStream::new(pdf)
match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone(), request_client.0).await {
Ok(cv_data) => {
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
render_template("two_column", &file, tera, cv_data, language.language);
let span = info_span!("render_pdf", username = username);
let pdf = span.in_scope(||{
generate_pdf(browser.browser, &file)
});
drop(entered_span);
Ok(PdfStream::new(pdf))
},
Err(e) => {
error!("Error fetching cv data: {:?}", e);
Err(Status::InternalServerError)
}
}
}
/// Route only for debuging
#[get("/cv/<username>/output.html")]
pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan) -> NamedFile {
let entered_span = tracing.0.enter();
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
render_template("two_column", &file, tera);
NamedFile::open(file.path()).await.unwrap()
#[get("/cv/<username>/<language>/output.html")]
pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan, request_client: OtelReqwestClient,
cv_config: &State<CVBackendConfig>, language: RequestLanguage) -> Result<NamedFile, Status> {
let _ = tracing.0.enter();
match fetch_cv_data_from_backend(cv_config.cv_backend_path.clone(), request_client.0).await {
Ok(cv_data) => {
let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap();
render_template("two_column", &file, tera, cv_data, language.language);
Ok(NamedFile::open(file.path()).await.unwrap())
},
Err(e) => {
error!("Error fetching cv data: {:?}", e);
Err(Status::InternalServerError)
}
}
}

23
src/services/cv.rs Normal file
View File

@@ -0,0 +1,23 @@
use ovlach_data::cv::cv::CV;
use reqwest::Client;
#[derive(Debug)]
pub enum FetchError {
ReqwestError(reqwest::Error)
}
impl From<reqwest::Error> for FetchError {
fn from(e: reqwest::Error) -> Self {
FetchError::ReqwestError(e)
}
}
pub async fn fetch_cv_data_from_backend(backend_host: String, client: Client) -> Result<CV, FetchError> {
let resp = client
.get(format!("{}/{}", backend_host, "/api/cv/ovlach")).send()
.await?
.json::<CV>()
.await?;
Ok(resp)
}

1
src/services/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod cv;

View File

@@ -1,2 +1,3 @@
pub mod tera;
pub mod pdf;
pub mod pdf;
pub(crate) mod rocket;

26
src/tools/rocket.rs Normal file
View File

@@ -0,0 +1,26 @@
use phf::phf_map;
use rocket::request::FromParam;
pub struct RequestLanguage {
pub language: String,
}
static LANG_TO_CODES: phf::Map<&'static str, &'static str> = phf_map! {
"cs" => "cs-CZ",
"en" => "en-US",
};
impl<'r> FromParam<'r> for RequestLanguage {
type Error = &'r str;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
match LANG_TO_CODES.get(param) {
Some(val) => Ok(RequestLanguage {
language: val.to_string(),
}),
None => Ok(RequestLanguage {
language: LANG_TO_CODES["en"].to_string(),
}),
}
}
}

View File

@@ -1,5 +1,9 @@
use std::{collections::HashMap};
use nanobyte_tera::{l18n::translate_filter, date::{calculate_age, get_year}, string::insert_space_every, gravatar::gravatar_link};
use ovlach_tera::entity::lang_entity;
use rocket::{request::{FromRequest, Outcome}, Request};
use tera::Tera;
use tera::{Tera, Value, Error};
#[derive(Debug)]
pub struct NanoTera(pub Tera);
@@ -11,6 +15,33 @@ impl<'r> FromRequest<'r> for NanoTera {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, ()> {
rocket::outcome::Outcome::Success(NanoTera(Tera::new("templates/*").unwrap()))
let mut tera = Tera::new("templates/*").unwrap();
tera.register_filter("translate", translate_filter);
tera.register_filter("calculate_age", calculate_age);
tera.register_filter("insert_space_every", insert_space_every);
tera.register_filter("gravatar_link", gravatar_link);
// filters specific to API
tera.register_filter("lang_entity", lang_entity);
tera.register_filter("format_date", get_year); // deprecated
tera.register_filter("get_year", get_year);
tera.register_filter("strip_proto", strip_proto);
rocket::outcome::Outcome::Success(NanoTera(tera))
}
}
}
/// Strip protocol from URL (value)
pub fn strip_proto(
tera_value: &Value,
_: &HashMap<String, Value>
) -> Result<Value, Error> {
let value = tera_value.as_str().unwrap();
if value.starts_with("http://") {
Ok(Value::String(value.strip_prefix("http://").unwrap().to_string()))
} else if value.starts_with("https://") {
Ok(Value::String(value.strip_prefix("https://").unwrap().to_string()))
} else {
Ok(Value::String(value.to_string()))
}
}