Project start
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
.idea
|
||||
23
Cargo.toml
Normal file
23
Cargo.toml
Normal 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
11
routes-test.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"": {
|
||||
"resource": {"File": "test.html"}
|
||||
},
|
||||
"/test/test3": {
|
||||
"resource": "None"
|
||||
},
|
||||
"/test2": {
|
||||
"resource": "None"
|
||||
}
|
||||
}
|
||||
35
src/body.rs
Normal file
35
src/body.rs
Normal 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
36
src/database.rs
Normal 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
36
src/lib.rs
Normal 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
23
src/renderer.rs
Normal 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
183
src/routing.rs
Normal 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
47
src/server.rs
Normal 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
11
src/settings.rs
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user