2024-11-02 00:56:25 +00:00
|
|
|
|
use std::{net::TcpListener, process, rc::Rc};
|
2024-10-29 20:58:10 +00:00
|
|
|
|
|
|
|
|
|
use ctrlc;
|
2024-11-01 11:51:12 +00:00
|
|
|
|
use http::responses::{Body, Response};
|
2024-10-29 20:58:10 +00:00
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
use resvg::{
|
|
|
|
|
tiny_skia::{Color, Pixmap, Transform},
|
|
|
|
|
usvg,
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-02 06:33:44 +00:00
|
|
|
|
use indoc::indoc;
|
|
|
|
|
|
2024-11-01 08:35:25 +00:00
|
|
|
|
mod handlers;
|
2024-10-29 20:58:10 +00:00
|
|
|
|
mod http;
|
2024-11-01 11:51:12 +00:00
|
|
|
|
mod icons;
|
2024-10-29 20:58:10 +00:00
|
|
|
|
|
|
|
|
|
fn main() {
|
2024-11-01 10:59:38 +00:00
|
|
|
|
println!("luciders starting...");
|
|
|
|
|
|
2024-11-02 06:15:42 +00:00
|
|
|
|
let port = match std::env::var("PORT") {
|
|
|
|
|
Ok(port) => port,
|
|
|
|
|
Err(_) => "7878".to_string(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let listener = TcpListener::bind(format!("[::]:{}", port))
|
|
|
|
|
.unwrap_or_else(|_| fatal("Failed to bind to address"));
|
2024-10-29 20:58:10 +00:00
|
|
|
|
|
|
|
|
|
ctrlc::set_handler(move || {
|
|
|
|
|
println!("Shutting down...");
|
|
|
|
|
process::exit(0);
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|_| fatal("Failed to set termination signal handler"));
|
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
let icons_rc = Rc::new(match icons::Icons::build() {
|
2024-11-01 11:51:12 +00:00
|
|
|
|
Ok(icons) => icons,
|
|
|
|
|
Err(e) => fatal(e),
|
2024-11-02 00:56:25 +00:00
|
|
|
|
});
|
2024-11-01 11:51:12 +00:00
|
|
|
|
|
2024-11-01 08:35:25 +00:00
|
|
|
|
let mut handlers = handlers::Handlers::new();
|
2024-10-29 20:58:10 +00:00
|
|
|
|
|
2024-11-01 08:35:25 +00:00
|
|
|
|
handlers.add_handler("/", |req| {
|
2024-11-01 10:59:38 +00:00
|
|
|
|
if req.method != "GET" {
|
|
|
|
|
return Response::new(req, 405, Body::Static("Method Not Allowed"));
|
2024-10-29 20:58:10 +00:00
|
|
|
|
}
|
2024-11-01 10:59:38 +00:00
|
|
|
|
|
|
|
|
|
Response::new(req, 200, Body::Static("OK - luciders is running"))
|
|
|
|
|
});
|
|
|
|
|
|
2024-11-02 06:33:44 +00:00
|
|
|
|
const INFO: &str = indoc!(
|
|
|
|
|
"
|
|
|
|
|
luciders - Lucide SVG icons server <https://lucide.dev/>
|
|
|
|
|
|
|
|
|
|
GET /info - Get server information
|
|
|
|
|
GET /icons/[icon].svg - Get an icon in SVG format (equivalent to fetching them directly from Lucide)
|
|
|
|
|
GET /icons/[icon].png - Get an icon rendered in PNG format
|
|
|
|
|
Query parameters:
|
|
|
|
|
scale - integer - Scale the icon (default: 1, min: 1, max: 100)
|
|
|
|
|
padding - integer - Add padding (px) around the icon (default: 0, min: 0, max: 100)
|
|
|
|
|
background - hex - Set the background color in hex w/o the hashtag (e.g. FFFFFF) (default: transparent)
|
|
|
|
|
discord_compatibility - boolean - Set padding to 8px for Discord compatibility. Typically for use in embed author icons as these have a circle clip applied by Discord. Overrides padding if set. (default: false)
|
|
|
|
|
cache_key - string - Technically this can have any name. The server does not interpret this at all. Useful for cache busting. (default: none)
|
|
|
|
|
Example:
|
|
|
|
|
/icons/apple.png?scale=2&background=FF0000&discord_compatibility
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*** mantained with ❤️ by Compositr (Jim) ***
|
|
|
|
|
"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
handlers.add_handler("/info", |req| {
|
|
|
|
|
if req.method != "GET" {
|
|
|
|
|
return Response::new(req, 405, Body::Static("Method Not Allowed"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Response::new(req, 200, Body::Static(INFO))
|
|
|
|
|
});
|
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
handlers.add_handler("/icons/[icon].svg", {
|
|
|
|
|
let icons = Rc::clone(&icons_rc);
|
|
|
|
|
move |req| {
|
|
|
|
|
if req.method != "GET" {
|
|
|
|
|
return Response::new(req, 405, Body::Static("Method Not Allowed"));
|
|
|
|
|
}
|
2024-11-01 10:59:38 +00:00
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
let icon_filename = match req.url.get_path_segment(1) {
|
|
|
|
|
Some(icon) => icon,
|
|
|
|
|
None => return Response::new(req, 404, Body::Static("Icon Segment Not Found")),
|
|
|
|
|
};
|
2024-11-01 10:59:38 +00:00
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
let icon_name = &icon_filename[..icon_filename.len() - icons::ICON_FILE_EXTENSION_LEN];
|
2024-11-01 11:51:12 +00:00
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
if !icons.has_iconname(icon_name) {
|
|
|
|
|
return Response::new(req, 404, Body::Static("Icon Not Found"));
|
|
|
|
|
}
|
2024-11-01 10:59:38 +00:00
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
let icon = match icons.get_icon(icon_name) {
|
|
|
|
|
Some(icon) => icon,
|
2024-11-02 06:15:42 +00:00
|
|
|
|
None => {
|
|
|
|
|
error("Failed to read icon");
|
|
|
|
|
return Response::new(req, 500, Body::Static("Failed to read icon"));
|
|
|
|
|
}
|
2024-11-02 00:56:25 +00:00
|
|
|
|
};
|
2024-11-01 11:51:12 +00:00
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
return Response::new(req, 200, Body::Bytes(icon, "image/svg+xml"));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
handlers.add_handler("/icons/[icon].png", {
|
|
|
|
|
let icons = Rc::clone(&icons_rc);
|
|
|
|
|
move |req| {
|
|
|
|
|
if req.method != "GET" {
|
|
|
|
|
return Response::new(req, 405, Body::Static("Method Not Allowed"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let icon_filename = match req.url.get_path_segment(1) {
|
|
|
|
|
Some(icon) => icon,
|
|
|
|
|
None => return Response::new(req, 404, Body::Static("Icon Segment Not Found")),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let icon_name = &icon_filename[..icon_filename.len() - icons::ICON_FILE_EXTENSION_LEN];
|
|
|
|
|
|
|
|
|
|
if !icons.has_iconname(icon_name) {
|
|
|
|
|
return Response::new(req, 404, Body::Static("Icon Not Found"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let icon = match icons.get_icon(icon_name) {
|
|
|
|
|
Some(icon) => icon,
|
2024-11-02 06:15:42 +00:00
|
|
|
|
None => {
|
|
|
|
|
error("Failed to read icon");
|
|
|
|
|
return Response::new(req, 500, Body::Static("Failed to read icon"))
|
|
|
|
|
},
|
2024-11-02 00:56:25 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let tree = match usvg::Tree::from_data(&icon, &usvg::Options::default()) {
|
|
|
|
|
Ok(tree) => tree,
|
2024-11-02 06:15:42 +00:00
|
|
|
|
Err(e) => {
|
|
|
|
|
error(&format!("Failed to load icon into tree: {}", e));
|
2024-11-02 00:56:25 +00:00
|
|
|
|
return Response::new(req, 500, Body::Static("Failed to load icon into tree"))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Options handling
|
|
|
|
|
let mut scale = 1;
|
|
|
|
|
match req.url.query.get("scale") {
|
|
|
|
|
Some(scale_str) => {
|
|
|
|
|
match scale_str.parse::<u32>() {
|
|
|
|
|
Ok(scale_val) => {
|
|
|
|
|
if scale_val <= 0 || scale_val > 100 {
|
|
|
|
|
return Response::new(req, 400, Body::Static("Invalid scale value. Scale value must be an integer > 0 and <= 100"));
|
|
|
|
|
}
|
|
|
|
|
scale = scale_val;
|
|
|
|
|
}
|
2024-11-02 06:15:42 +00:00
|
|
|
|
Err(_) => return Response::new(req, 400, Body::Static("Invalid scale value. Scale value must be an integer > 0 and <= 100"))
|
2024-11-02 00:56:25 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut padding = 0;
|
|
|
|
|
match req.url.query.get("padding") {
|
|
|
|
|
Some(padding_str) => {
|
|
|
|
|
match padding_str.parse::<u32>() {
|
|
|
|
|
Ok(padding_val) => {
|
|
|
|
|
if padding_val > 100 {
|
|
|
|
|
return Response::new(req, 400, Body::Static("Invalid padding value. Padding value must be an integer >= 0 and <= 100"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
padding = padding_val;
|
|
|
|
|
}
|
2024-11-02 06:15:42 +00:00
|
|
|
|
Err(_) => return Response::new(req, 400, Body::Static("Invalid padding value. Padding value must be an integer >= 0 and <= 100"))
|
2024-11-02 00:56:25 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-02 06:33:44 +00:00
|
|
|
|
match req.url.query.get("discord_compatibility") {
|
|
|
|
|
Some(_) => {
|
|
|
|
|
padding = 8;
|
|
|
|
|
}
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-02 01:34:16 +00:00
|
|
|
|
let mut background = Color::TRANSPARENT;
|
|
|
|
|
match req.url.query.get("background") {
|
|
|
|
|
Some(background_str) => {
|
|
|
|
|
match u32::from_str_radix(&background_str, 16) {
|
|
|
|
|
Ok(background_val) => {
|
|
|
|
|
if background_val > 0xFFFFFF {
|
|
|
|
|
return Response::new(req, 400, Body::Static("Invalid background value. Background value must be a valid hex color 000000 - FFFFFF"));
|
|
|
|
|
}
|
|
|
|
|
let red = ((background_val >> 16) & 0xFF) as u8;
|
|
|
|
|
let green = ((background_val >> 8) & 0xFF) as u8;
|
|
|
|
|
let blue = (background_val & 0xFF) as u8;
|
|
|
|
|
background = Color::from_rgba8(red, green, blue, 255);
|
|
|
|
|
}
|
2024-11-02 06:15:42 +00:00
|
|
|
|
Err(_) => return Response::new(req, 400, Body::Static("Invalid background value. Background value must be a valid hex color"))
|
2024-11-02 01:34:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-02 00:56:25 +00:00
|
|
|
|
let length = 24 * scale + padding;
|
|
|
|
|
|
|
|
|
|
let mut pixmap = match Pixmap::new(length, length) {
|
|
|
|
|
Some(pixmap) => pixmap,
|
2024-11-02 06:15:42 +00:00
|
|
|
|
None => {
|
|
|
|
|
error("Failed to create pixmap");
|
|
|
|
|
return Response::new(req, 500, Body::Static("Failed to create pixmap"))
|
|
|
|
|
},
|
2024-11-02 00:56:25 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut pixmap_mut = pixmap.as_mut();
|
2024-11-02 01:34:16 +00:00
|
|
|
|
pixmap_mut.fill(background);
|
2024-11-02 00:56:25 +00:00
|
|
|
|
|
|
|
|
|
resvg::render(
|
|
|
|
|
&tree,
|
|
|
|
|
Transform::from_scale(scale as f32, scale as f32)
|
|
|
|
|
.post_concat(Transform::from_translate(length as f32 / 2.0 - 12.0 * scale as f32, length as f32 / 2.0 - 12.0 * scale as f32)),
|
|
|
|
|
&mut pixmap_mut,
|
|
|
|
|
);
|
|
|
|
|
let png = match pixmap.encode_png() {
|
|
|
|
|
Ok(png) => png,
|
2024-11-02 06:15:42 +00:00
|
|
|
|
Err(e) => {
|
|
|
|
|
error(&format!("Failed to encode PNG: {}", e));
|
|
|
|
|
return Response::new(req, 500, Body::Static("Failed to encode PNG"))
|
|
|
|
|
},
|
2024-11-02 00:56:25 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return Response::new(req, 200, Body::Bytes(png, "image/png"));
|
|
|
|
|
}
|
2024-11-01 08:35:25 +00:00
|
|
|
|
});
|
2024-10-30 02:49:27 +00:00
|
|
|
|
|
2024-11-01 08:35:25 +00:00
|
|
|
|
handlers.bind(listener);
|
2024-11-02 06:15:42 +00:00
|
|
|
|
|
|
|
|
|
println!("luciders started on port {}", port);
|
|
|
|
|
println!("Serving {} icons", icons_rc.icons_len())
|
2024-10-29 20:58:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fatal(msg: &'static str) -> ! {
|
|
|
|
|
eprintln!("[FATAL] {}", msg);
|
|
|
|
|
process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-02 06:15:42 +00:00
|
|
|
|
fn error(msg: &str) {
|
2024-10-29 20:58:10 +00:00
|
|
|
|
eprintln!("[ERROR] {}", msg);
|
|
|
|
|
}
|