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