This commit is contained in:
JMARyA 2024-12-17 23:28:43 +01:00
commit 3299d3cc4c
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
14 changed files with 3920 additions and 0 deletions

25
src/request/api.rs Normal file
View file

@ -0,0 +1,25 @@
use rocket::http::{ContentType, Status};
/// A trait to generate a Model API representation in JSON format.
pub trait ToAPI: Sized {
/// Generate public API JSON
fn api(&self) -> impl std::future::Future<Output = serde_json::Value>;
}
/// Converts a slice of items implementing the `ToAPI` trait into a `Vec` of JSON values.
pub async fn vec_to_api(items: &[impl ToAPI]) -> Vec<serde_json::Value> {
let mut ret = Vec::with_capacity(items.len());
for e in items {
ret.push(e.api().await);
}
ret
}
pub fn api_response(json: &serde_json::Value) -> (Status, (ContentType, String)) {
(
Status::Ok,
(ContentType::JSON, serde_json::to_string(json).unwrap()),
)
}

56
src/request/assets.rs Normal file
View file

@ -0,0 +1,56 @@
use rocket::{
fs::NamedFile,
get,
http::{ContentType, Status},
State,
};
use tokio::{fs::File, io::AsyncReadExt};
/*
#[get("/video/raw?<v>")]
pub async fn video_file(
v: &str,
library: &State<Library>,
) -> Option<(Status, (ContentType, Vec<u8>))> {
let video = if let Some(video) = library.get_video_by_id(v).await {
video
} else {
library.get_video_by_youtube_id(v).await.unwrap()
};
if let Ok(mut file) = File::open(&video.path).await {
let mut buf = Vec::with_capacity(51200);
file.read_to_end(&mut buf).await.ok()?;
let content_type = if video.path.ends_with("mp4") {
ContentType::new("video", "mp4")
} else {
ContentType::new("video", "webm")
};
return Some((Status::Ok, (content_type, buf)));
}
None
}
#[get("/video/thumbnail?<v>")]
pub async fn video_thumbnail(
v: &str,
library: &State<Library>,
) -> Option<(Status, (ContentType, Vec<u8>))> {
let video = if let Some(video) = library.get_video_by_id(v).await {
video
} else {
library.get_video_by_youtube_id(v).await.unwrap()
};
if let Some(data) = library.get_thumbnail(&video).await {
return Some((Status::Ok, (ContentType::PNG, data)));
}
None
}
*/

121
src/request/cache.rs Normal file
View file

@ -0,0 +1,121 @@
use std::collections::HashMap;
use rocket::tokio::sync::RwLock;
#[macro_export]
macro_rules! use_api_cache {
($route:literal, $id:ident, $cache:ident) => {
if let Some(ret) = $cache.get_only($route, $id).await {
return Ok(serde_json::from_str(&ret).unwrap());
}
};
($route:literal, $id:literal, $cache:ident) => {
if let Some(ret) = $cache.get_only($route, $id).await {
return Ok(serde_json::from_str(&ret).unwrap());
}
};
}
pub struct RouteCache {
inner: RwLock<HashMap<String, HashMap<String, Option<String>>>>,
}
impl RouteCache {
pub fn new() -> Self {
Self {
inner: RwLock::new(HashMap::new()),
}
}
pub async fn get<F, Fut>(&self, route: &str, id: &str, generator: F) -> String
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = String>,
{
{
// Try to get a read lock first.
let lock = self.inner.read().await;
if let Some(inner_map) = lock.get(route) {
if let Some(cached_value) = inner_map.get(id) {
log::info!("Using cached value for {route} / {id}");
return cached_value.clone().unwrap();
}
}
}
// If the value was not found, acquire a write lock to insert the computed value.
let mut lock = self.inner.write().await;
log::info!("Computing value for {route} / {id}");
let computed = generator().await;
lock.entry(route.to_string())
.or_insert_with(HashMap::new)
.insert(id.to_string(), Some(computed.clone()));
computed
}
pub async fn get_only(&self, route: &str, id: &str) -> Option<String> {
let lock = self.inner.read().await;
if let Some(inner_map) = lock.get(route) {
if let Some(cached_value) = inner_map.get(id) {
log::info!("Using cached value for {route} / {id}");
return cached_value.clone();
}
}
None
}
pub async fn get_option<F, Fut>(&self, route: &str, id: &str, generator: F) -> Option<String>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Option<String>>,
{
{
// Try to get a read lock first.
let lock = self.inner.read().await;
if let Some(inner_map) = lock.get(route) {
if let Some(cached_value) = inner_map.get(id) {
log::info!("Using cached value for {route} / {id}");
return cached_value.clone();
}
}
}
// If the value was not found, acquire a write lock to insert the computed value.
let mut lock = self.inner.write().await;
log::info!("Computing value for {route} / {id}");
let computed = generator().await;
lock.entry(route.to_string())
.or_insert_with(HashMap::new)
.insert(id.to_string(), computed.clone());
computed
}
pub async fn insert(&self, route: &str, id: &str, value: String) {
let mut lock = self.inner.write().await;
log::info!("Inserting value for {route} / {id}");
lock.entry(route.to_string())
.or_insert_with(HashMap::new)
.insert(id.to_string(), Some(value));
}
pub async fn invalidate(&self, route: &str, id: &str) {
let mut lock = self.inner.write().await;
if let Some(inner_map) = lock.get_mut(route) {
inner_map.remove(id);
// If the inner map is empty, remove the route entry as well.
if inner_map.is_empty() {
lock.remove(route);
}
}
}
}

22
src/request/context.rs Normal file
View file

@ -0,0 +1,22 @@
use rocket::{request::{self, FromRequest}, Request};
use crate::user::{Session, User};
pub struct RequestContext {
pub is_htmx: bool
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for RequestContext {
type Error = ();
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
rocket::outcome::Outcome::Success(RequestContext {
is_htmx: !req
.headers()
.get("HX-Request")
.collect::<Vec<&str>>()
.is_empty()
})
}
}

27
src/request/mod.rs Normal file
View file

@ -0,0 +1,27 @@
use std::str::FromStr;
use rocket::response::status::BadRequest;
use serde_json::json;
pub mod context;
pub mod assets;
pub mod cache;
pub mod api;
pub fn to_uuid(id: &str) -> Result<uuid::Uuid, ApiError> {
uuid::Uuid::from_str(id).map_err(|_| no_uuid_error())
}
type ApiError = BadRequest<serde_json::Value>;
type FallibleApiResponse = Result<serde_json::Value, ApiError>;
pub fn no_uuid_error() -> ApiError {
api_error("No valid UUID")
}
pub fn api_error(msg: &str) -> ApiError {
BadRequest(json!({
"error": msg
}))
}