init
This commit is contained in:
commit
3299d3cc4c
14 changed files with 3920 additions and 0 deletions
25
src/request/api.rs
Normal file
25
src/request/api.rs
Normal 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
56
src/request/assets.rs
Normal 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
121
src/request/cache.rs
Normal 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
22
src/request/context.rs
Normal 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
27
src/request/mod.rs
Normal 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
|
||||
}))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue