feat: todo's, add image cache control
This commit is contained in:
21
Cargo.lock
generated
21
Cargo.lock
generated
@@ -457,6 +457,26 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-str"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f"
|
||||
dependencies = [
|
||||
"const-str-proc-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-str-proc-macro"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c7e7913ec01ed98b697e62f8d3fd63c86dc6cccaf983c7eebc64d0e563b0ad9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
@@ -1155,6 +1175,7 @@ dependencies = [
|
||||
"anstyle",
|
||||
"anyhow",
|
||||
"clap",
|
||||
"const-str",
|
||||
"libc",
|
||||
"magic",
|
||||
"moka",
|
||||
|
||||
@@ -34,6 +34,7 @@ thiserror = "2.0.18"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
toml = "1.0.3"
|
||||
users = "0.11.0"
|
||||
const-str = { version = "1.1.0", features = ["proc"] }
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
|
||||
@@ -11,9 +11,19 @@ pub const MAX_PFP_SIZE: u64 = 8 * 1024 * 1024; // 8 MiB; TODO: might lower, high
|
||||
/// Default user image to use if users have none
|
||||
pub const DEFAULT_USER_PFP: &[u8] = include_bytes!("../assets/default-pfp.png");
|
||||
pub const DEFAULT_USER_PFP_MIME: &str = "image/png; charset=binary";
|
||||
/// Basically [`USER_CACHES_HEADER`] but much longer for the [`DEFAULT_USER_PFP`]
|
||||
pub const DEFAULT_USER_PFP_CACHES_HEADER: &str =
|
||||
crate::utils::web::make_static_cache_header!(Duration::from_hours(2), Duration::from_days(1));
|
||||
|
||||
/// TTL for cached user information
|
||||
/// TTL for cached user information, both in server memory and http's cache-control
|
||||
pub const USER_CACHES_TTL: Duration = Duration::from_mins(15);
|
||||
/// TTL for cached user information in CDNs, (stale-while-revalidate), only for non-urgent info
|
||||
/// (e.g. pfp's)
|
||||
pub const USER_CDN_CACHES_TTL: Duration = Duration::from_hours(1);
|
||||
/// Compile-time formatted string for the cache-control header of [`USER_CACHES_TTL`] and
|
||||
/// [`USER_CDN_CACHES_TTL`]
|
||||
pub const USER_CACHES_HEADER: &str =
|
||||
crate::utils::web::make_static_cache_header!(USER_CACHES_TTL, USER_CDN_CACHES_TTL);
|
||||
|
||||
/// Paths relative to a user's home to search profile pictures in
|
||||
pub const USER_PFP_PATHS: &[&str] = &[
|
||||
@@ -25,3 +35,7 @@ pub const USER_PFP_PATHS: &[&str] = &[
|
||||
"logo.jpg",
|
||||
".face",
|
||||
];
|
||||
|
||||
pub mod web_scopes {
|
||||
pub const IMAGES: &str = "/image";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![feature(seek_stream_len, once_cell_try)]
|
||||
#![feature(seek_stream_len, once_cell_try, decl_macro, duration_constructors)]
|
||||
#![allow(clippy::future_not_send)] // will get to fix these later
|
||||
|
||||
//! # About, Licensing and More
|
||||
@@ -33,6 +33,7 @@ pub mod consts;
|
||||
pub mod ext;
|
||||
pub mod serdes;
|
||||
pub mod server;
|
||||
pub mod utils;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
|
||||
@@ -14,13 +14,12 @@ pub struct AppState {
|
||||
/// Leaks memory for the sake of not atomic'ing all over.
|
||||
#[expect(clippy::missing_errors_doc)]
|
||||
pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) -> io::Result<()> {
|
||||
use crate::consts::web_scopes as ws;
|
||||
|
||||
let config = Box::leak(Box::new(config));
|
||||
|
||||
let cache = caches::AppCache::new(
|
||||
config.unix.magic_paths.clone(),
|
||||
// ["/usr/share/file/misc/magic"]
|
||||
// .try_into()
|
||||
// .expect("To be valid DB paths"),
|
||||
&config.unix.groups,
|
||||
);
|
||||
|
||||
@@ -38,8 +37,7 @@ pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) ->
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(app))
|
||||
.service(services::get_image)
|
||||
// .service(factory)
|
||||
.service(services::images::make_scope(ws::IMAGES))
|
||||
})
|
||||
.bind(&app.config.server.listen)?
|
||||
.run()
|
||||
|
||||
60
src/server/services/images.rs
Normal file
60
src/server/services/images.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Scope for the image get backend.
|
||||
//! - `/`: Gives the default image.
|
||||
//! - `/{username}`: Gives username's pfp or redirects to the default image's path (for better
|
||||
//! cache control) if there's no image.
|
||||
//!
|
||||
//! Must be scoped at [`ws::IMAGES`]
|
||||
|
||||
// TODO: etags
|
||||
|
||||
use actix_web::{
|
||||
HttpResponse, get,
|
||||
http::header,
|
||||
web::{self, Redirect},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
consts::{self, web_scopes as ws},
|
||||
server,
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
pub fn make_scope(path: &str) -> actix_web::Scope {
|
||||
web::scope(path)
|
||||
.service(get_default_image)
|
||||
.service(get_image)
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn get_default_image(
|
||||
_data: web::Data<&server::AppState>,
|
||||
_username: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.insert_header((
|
||||
header::CACHE_CONTROL,
|
||||
consts::DEFAULT_USER_PFP_CACHES_HEADER,
|
||||
))
|
||||
.content_type(consts::DEFAULT_USER_PFP_MIME)
|
||||
.body(web::Bytes::from_static(consts::DEFAULT_USER_PFP))
|
||||
}
|
||||
|
||||
#[get("/{username}")]
|
||||
async fn get_image(
|
||||
data: web::Data<&server::AppState>,
|
||||
username: web::Path<String>,
|
||||
) -> web::Either<Redirect, HttpResponse> {
|
||||
let cached_pfp = data.cache.get_pfp(username.to_string()).await;
|
||||
|
||||
cached_pfp.as_ref().map_or_else(
|
||||
|| web::Either::Left(web::Redirect::to(ws::IMAGES).temporary()),
|
||||
|img| {
|
||||
web::Either::Right(
|
||||
HttpResponse::Ok()
|
||||
.insert_header((header::CACHE_CONTROL, consts::USER_CACHES_HEADER))
|
||||
.content_type(img.mime.as_ref())
|
||||
.body(web::Bytes::copy_from_slice(img.bytes.as_ref())),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,30 +1 @@
|
||||
use actix_web::{HttpResponse, get, web};
|
||||
|
||||
use crate::{consts, server};
|
||||
|
||||
// TODO: cache control
|
||||
// TODO: etags
|
||||
// TODO: canonical redirect for default image and better cache control
|
||||
#[get("/image/{username}")]
|
||||
pub async fn get_image(
|
||||
data: web::Data<&server::AppState>,
|
||||
username: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
let cached_pfp = data.cache.get_pfp(username.to_string()).await;
|
||||
let (mime, bytes) = cached_pfp.as_ref().map_or_else(
|
||||
|| {
|
||||
(
|
||||
consts::DEFAULT_USER_PFP_MIME,
|
||||
web::Bytes::from_static(consts::DEFAULT_USER_PFP),
|
||||
)
|
||||
},
|
||||
|img| {
|
||||
(
|
||||
img.mime.as_ref(),
|
||||
web::Bytes::copy_from_slice(img.bytes.as_ref()),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
HttpResponse::Ok().content_type(mime).body(bytes)
|
||||
}
|
||||
pub mod images;
|
||||
|
||||
39
src/utils.rs
Normal file
39
src/utils.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
pub mod web {
|
||||
/// Macro shenanigans to format a str at compile time
|
||||
pub macro make_static_cache_header($max_age:expr, $stale_revalidate:expr) {{
|
||||
// type guards lmfao
|
||||
const _: ::std::time::Duration = $max_age;
|
||||
const _: ::std::time::Duration = $stale_revalidate;
|
||||
|
||||
// hopefully ident encapsulation will save my ass here
|
||||
const MAX_AGE: u64 = $max_age.as_secs();
|
||||
const STALE_REVALIDATE: u64 = $stale_revalidate.as_secs();
|
||||
|
||||
const_str::format!(
|
||||
"public, max-age={}, stale-while-revalidate={}",
|
||||
MAX_AGE,
|
||||
STALE_REVALIDATE
|
||||
)
|
||||
}}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn const_concat_worked() {
|
||||
const MA: Duration = Duration::from_mins(15);
|
||||
const SR: Duration = Duration::from_hours(1);
|
||||
|
||||
const NAME: &str = super::make_static_cache_header!(MA, SR);
|
||||
assert_eq!(
|
||||
NAME,
|
||||
format!(
|
||||
"public, max-age={}, stale-while-revalidate={}",
|
||||
MA.as_secs(),
|
||||
SR.as_secs()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user