Project start

This commit is contained in:
2024-11-30 20:40:23 +01:00
commit 231c392f23
11 changed files with 418 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target
Cargo.lock
.idea

23
Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "aweblib"
version = "0.1.0"
edition = "2021"
[features]
default = ["windows"]
windows = []
linux = ["dep:daemonize"]
[dependencies]
tokio = {version = "1.41.1", features = ["macros", "rt-multi-thread", "net"]}
hyper = {version = "1.5.1", features = ["server", "http1"]}
hyper-util = {version = "0.1.10", features = ["tokio"]}
serde_json = {version = "1.0.132"}
serde = { version = "1.0.214", features = ["derive"] }
bytes = "1.8.0"
http-body-util = "0.1.2"
sqlx = {version = "0.8.1", features = ["sqlite", "sqlx-macros", "runtime-tokio"]}
daemonize = {version = "0.5.0", optional = true}
[dev-dependencies]
futures = "0.3.31"

11
routes-test.json Normal file
View File

@@ -0,0 +1,11 @@
{
"": {
"resource": {"File": "test.html"}
},
"/test/test3": {
"resource": "None"
},
"/test2": {
"resource": "None"
}
}

35
src/body.rs Normal file
View File

@@ -0,0 +1,35 @@
use bytes::Bytes;
use http_body_util::Full;
use hyper::body::Frame;
use std::convert::Infallible;
use std::pin::Pin;
use std::task::{Context, Poll};
#[derive(Debug)]
pub enum Body {
Empty,
Full(Full<Bytes>),
}
impl Body {
pub(crate) fn new<T>(body: T) -> Body
where
Bytes: From<T>,
{
Body::Full(Full::new(Bytes::from(body)))
}
}
impl hyper::body::Body for Body {
type Data = Bytes;
type Error = Infallible;
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
match &mut *self.get_mut() {
Self::Full(body) => Pin::new(body).poll_frame(cx),
Self::Empty => Poll::Ready(None),
}
}
}

36
src/database.rs Normal file
View File

@@ -0,0 +1,36 @@
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
#[derive(Clone)]
enum DbType {
Sqlite,
}
impl Display for DbType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match *self {
DbType::Sqlite => f.write_str("sqlite"),
}
}
}
#[derive(Clone)]
pub(crate) struct Database {
db_type: DbType,
db_path: PathBuf,
}
impl From<PathBuf> for Database {
fn from(value: PathBuf) -> Self {
Database {
db_type: DbType::Sqlite,
db_path: value,
}
}
}
impl From<Database> for String {
fn from(value: Database) -> String {
format!("{}:{}", value.db_type, value.db_path.display())
}
}

36
src/lib.rs Normal file
View File

@@ -0,0 +1,36 @@
#![allow(dead_code)]
#![allow(unused_variables)]
use crate::database::Database;
use crate::routing::Routes;
use crate::server::Server;
use crate::settings::Settings;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
mod body;
mod database;
mod renderer;
mod routing;
mod server;
mod settings;
pub struct SimpleHttpServer {
server: Server,
}
impl SimpleHttpServer {
fn new() -> Self {
SimpleHttpServer {
server: Server::new(Settings {
address: SocketAddr::from(([127, 0, 0, 1], 80)),
routes: Arc::new(Mutex::new(Routes::default())),
database: Database::from(PathBuf::from("database.db")),
}),
}
}
fn run(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.server.run()
}
}

23
src/renderer.rs Normal file
View File

@@ -0,0 +1,23 @@
use crate::body::Body;
use hyper::{Response, StatusCode};
use std::fs::File;
use std::io::Read;
pub(crate) fn get_static(file: &String) -> Response<Body> {
let file = read_static(file);
match file {
Ok(file) => Response::new(Body::new(file)),
Err(_) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::Empty)
.unwrap(),
}
}
pub(crate) fn get_rendered() {}
fn read_static(file: &String) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
let mut buf = Vec::new();
let mut file = File::open(file)?;
file.read_to_end(&mut buf)?;
Ok(buf)
}

183
src/routing.rs Normal file
View File

@@ -0,0 +1,183 @@
use crate::body::Body;
use crate::renderer::get_static;
use hyper::{Request, Response};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs::File;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
#[derive(Clone, Deserialize, Default, Debug, Eq, PartialEq)]
pub(crate) enum Resource {
Page(String),
File(String),
Static,
#[default]
None,
}
#[derive(Clone, Deserialize, Debug, Default, Eq, PartialEq)]
pub(crate) struct Route {
resource: Resource,
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub(crate) struct RouteTree {
route: Route,
route_nodes: HashMap<String, RouteTree>,
}
impl From<HashMap<String, Route>> for RouteTree {
fn from(value: HashMap<String, Route>) -> Self {
let mut route_tree = RouteTree::default();
let urls: Vec<String> = value.keys().cloned().collect();
let splits: Vec<(String, Vec<String>)> = urls
.into_iter()
.map(|x| {
let sp = x.split("/").map(|x| x.to_string()).collect();
(x, sp)
})
.collect::<Vec<(String, Vec<String>)>>()
.into_iter()
.map(|(k, x)| {
let subs: Vec<String> = x.into_iter().map(|x| format!("/{}", x)).collect();
(k, subs)
})
.collect();
for split in splits {
let key = split.0;
let split = split.1;
let route = value.get(&key).unwrap().clone();
route_tree.insert(&split, route);
}
route_tree
}
}
impl RouteTree {
fn insert(&mut self, item: &[String], route: Route) {
let key = item[0].clone();
let entry = self.route_nodes.entry(key).or_default();
if item.len() > 1 {
entry.insert(&item[1..item.len()], route);
} else {
entry.route = route;
}
}
}
#[derive(Clone, Default)]
pub(crate) struct Routes {
tree: RouteTree,
}
impl From<PathBuf> for Routes {
fn from(path: PathBuf) -> Self {
let file = File::open(path);
match file {
Ok(file) => {
if let Ok(routes) = serde_json::from_reader::<File, HashMap<String, Route>>(file) {
let tree = RouteTree::from(routes);
Self { tree }
} else {
Self::default()
}
}
Err(_) => Self::default(),
}
}
}
pub(crate) async fn route<T>(routes: Arc<Mutex<Routes>>, req: Request<T>) -> Response<Body>
where
T: hyper::body::Body,
{
let url = req.uri().path();
let split = url
.split("/")
.map(|x| format!("/{}", x))
.collect::<Vec<String>>();
let routes = routes.lock().unwrap();
let mut tree = &routes.tree;
let mut i = 0;
while let Some(route) = tree.route_nodes.get(&split[i]) {
tree = route;
if i == split.len() {
break;
}
i += 1;
}
let route = &tree.route;
match &route.resource {
Resource::File(file) => get_static(file),
_ => Response::new(Body::Empty),
}
}
#[cfg(test)]
mod test {
use crate::body::Body;
use crate::renderer::get_static;
use crate::routing::{route, Resource, Route, RouteTree, Routes};
use futures::executor::block_on;
use http_body_util::BodyExt;
use hyper::Request;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
#[test]
fn load_from_file() {
let routes = Routes::from(PathBuf::from("routes-test.json"));
let tree = routes.tree;
// Bellow is:
// "/": {"/test": {"/test3"}, "/test2"}
// with
// "/" = File("test.html")
// "/test" = None
// "/test2" = None
// "/test3" = None
let mut to_match = HashMap::new();
let mut root_tree = RouteTree::default();
root_tree.route = Route {
resource: Resource::File("test.html".into()),
};
let root = to_match.entry("/".into()).or_insert(root_tree);
let mut not_empty = RouteTree::default();
not_empty.insert(&["/test3".to_string()], Route::default());
root.route_nodes.insert("/test".to_string(), not_empty);
root.route_nodes
.insert("/test2".to_string(), RouteTree::default());
assert_eq!(tree.route_nodes, to_match);
}
#[test]
fn wrong_path() {
let routes = Routes::from(PathBuf::from("a_wrong_file.json"));
assert_eq!(routes.tree.route_nodes, HashMap::new());
}
#[test]
fn route_file() {
let routes = Routes::from(PathBuf::from("routes-test.json"));
let request = Request::builder().uri("/").body(Body::Empty).unwrap();
let from_test = block_on(Box::pin(async {
let response = route(Arc::new(Mutex::new(routes)), request).await;
let body = response.into_body().collect().await.unwrap();
let bytes = body.to_bytes();
let string = String::from_utf8(Vec::from(bytes)).unwrap();
string
}));
let actual = block_on(Box::pin(async {
let response = get_static(&"test.html".into());
let body = response.into_body().collect().await.unwrap();
let bytes = body.to_bytes();
let string = String::from_utf8(Vec::from(bytes)).unwrap();
string
}));
assert_eq!(from_test, actual);
}
}

47
src/server.rs Normal file
View File

@@ -0,0 +1,47 @@
use crate::body::Body;
use crate::routing::route;
use crate::settings::Settings;
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::Service;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use std::future::Future;
use std::pin::Pin;
use tokio::net::TcpListener;
#[derive(Clone)]
pub struct Server {
settings: Settings,
}
impl Server {
pub fn new(settings: Settings) -> Server {
Server { settings }
}
#[tokio::main]
pub async fn run(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind(self.settings.address).await?;
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
let clone = self.clone();
tokio::task::spawn(async move {
if let Err(error) = http1::Builder::new().serve_connection(io, clone).await {
println!("failed to serve connection: {}", error);
}
});
}
}
}
impl Service<Request<Incoming>> for Server {
type Response = Response<Body>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn call(&self, req: Request<Incoming>) -> Self::Future {
let routes = self.settings.routes.clone();
Box::pin(async move { Ok(route(routes, req).await) })
}
}

11
src/settings.rs Normal file
View File

@@ -0,0 +1,11 @@
use crate::database::Database;
use crate::routing::Routes;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub struct Settings {
pub(crate) address: SocketAddr,
pub(crate) routes: Arc<Mutex<Routes>>,
pub(crate) database: Database,
}

10
test.html Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TEST</title>
</head>
<body>
</body>
</html>