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",
|
"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]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -1155,6 +1175,7 @@ dependencies = [
|
|||||||
"anstyle",
|
"anstyle",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
"const-str",
|
||||||
"libc",
|
"libc",
|
||||||
"magic",
|
"magic",
|
||||||
"moka",
|
"moka",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ thiserror = "2.0.18"
|
|||||||
tokio = { version = "1.49.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
toml = "1.0.3"
|
toml = "1.0.3"
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
|
const-str = { version = "1.1.0", features = ["proc"] }
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
cargo = { level = "warn", priority = -1 }
|
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
|
/// 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: &[u8] = include_bytes!("../assets/default-pfp.png");
|
||||||
pub const DEFAULT_USER_PFP_MIME: &str = "image/png; charset=binary";
|
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);
|
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
|
/// Paths relative to a user's home to search profile pictures in
|
||||||
pub const USER_PFP_PATHS: &[&str] = &[
|
pub const USER_PFP_PATHS: &[&str] = &[
|
||||||
@@ -25,3 +35,7 @@ pub const USER_PFP_PATHS: &[&str] = &[
|
|||||||
"logo.jpg",
|
"logo.jpg",
|
||||||
".face",
|
".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
|
#![allow(clippy::future_not_send)] // will get to fix these later
|
||||||
|
|
||||||
//! # About, Licensing and More
|
//! # About, Licensing and More
|
||||||
@@ -24,7 +24,7 @@ use std::fs::File;
|
|||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use crate::ext::FileExt;
|
use crate::ext::FileExt as _;
|
||||||
|
|
||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
@@ -33,6 +33,7 @@ pub mod consts;
|
|||||||
pub mod ext;
|
pub mod ext;
|
||||||
pub mod serdes;
|
pub mod serdes;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ pub struct AppState {
|
|||||||
/// Leaks memory for the sake of not atomic'ing all over.
|
/// Leaks memory for the sake of not atomic'ing all over.
|
||||||
#[expect(clippy::missing_errors_doc)]
|
#[expect(clippy::missing_errors_doc)]
|
||||||
pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) -> io::Result<()> {
|
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 config = Box::leak(Box::new(config));
|
||||||
|
|
||||||
let cache = caches::AppCache::new(
|
let cache = caches::AppCache::new(
|
||||||
config.unix.magic_paths.clone(),
|
config.unix.magic_paths.clone(),
|
||||||
// ["/usr/share/file/misc/magic"]
|
|
||||||
// .try_into()
|
|
||||||
// .expect("To be valid DB paths"),
|
|
||||||
&config.unix.groups,
|
&config.unix.groups,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -38,8 +37,7 @@ pub async fn start_app(args: crate::args::Args, config: crate::conf::Config) ->
|
|||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(app))
|
.app_data(web::Data::new(app))
|
||||||
.service(services::get_image)
|
.service(services::images::make_scope(ws::IMAGES))
|
||||||
// .service(factory)
|
|
||||||
})
|
})
|
||||||
.bind(&app.config.server.listen)?
|
.bind(&app.config.server.listen)?
|
||||||
.run()
|
.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};
|
pub mod images;
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
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