Improved thread performance, database performance, isolated database interacion into specific functions,

added ability to parse POST data efficiently, support for binary data (images), removed the url crate,
added better errors, implemented From<HandlingError> for Response, better http request parsing,
commented out useless pieces of code
doctorpavel
Dawid J. Kubis 2 years ago
parent 12635886f9
commit e08c368365

84
Cargo.lock generated

@ -101,15 +101,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
[[package]] [[package]]
name = "fs2" name = "fs2"
version = "0.4.3" version = "0.4.3"
@ -165,16 +156,6 @@ dependencies = [
"utf8-width", "utf8-width",
] ]
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@ -195,7 +176,6 @@ name = "kchan"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"byteorder",
"html-escape", "html-escape",
"lazy_static", "lazy_static",
"log", "log",
@ -206,7 +186,6 @@ dependencies = [
"structopt", "structopt",
"thiserror", "thiserror",
"threadpool", "threadpool",
"url",
"urlencoding", "urlencoding",
] ]
@ -294,12 +273,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
version = "1.0.4" version = "1.0.4"
@ -326,18 +299,18 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.58" version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.27" version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -374,7 +347,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.16", "syn 2.0.17",
] ]
[[package]] [[package]]
@ -453,9 +426,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.16" version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" checksum = "45b6ddbb36c5b969c182aec3c4a0bce7df3fbad4b77114706a49aacc80567388"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -497,7 +470,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.16", "syn 2.0.17",
] ]
[[package]] [[package]]
@ -538,42 +511,12 @@ dependencies = [
"time-core", "time-core",
] ]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "unicode-bidi"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.9" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.10.1" version = "1.10.1"
@ -586,17 +529,6 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "url"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]] [[package]]
name = "urlencoding" name = "urlencoding"
version = "2.1.2" version = "2.1.2"

@ -18,8 +18,6 @@ log = "0.4.17"
simplelog = "0.12.1" simplelog = "0.12.1"
html-escape = "0.2.13" html-escape = "0.2.13"
urlencoding = "2.1.2" urlencoding = "2.1.2"
byteorder = "1.4.3"
url = "2.3.1"
#ctrlc = "3.3.1" #ctrlc = "3.3.1"
#image = "0.24.6" #image = "0.24.6"

@ -1,9 +1,4 @@
### TODO: ### TODO:
1. proper logging
2. working threads and posts
3. add authorization and ip whitelist
4. parse http form `multipart/form-data`
5. add images
6. limit storage, maybe with pseudorandomness, find a way to clean old threads 6. limit storage, maybe with pseudorandomness, find a way to clean old threads
7. better html and css 7. better html and css
8. optimize/clean code, improve memory usage 8. optimize/clean code, improve memory usage
@ -14,7 +9,6 @@
capacity - the least active thread will then be removed. capacity - the least active thread will then be removed.
+ id's will be attached to every thread and every response + id's will be attached to every thread and every response
based on the order of creation. based on the order of creation.
+ specific posts will be searchable under the `/<thread_id>/<response_id>` uri.
### POST usage ### POST usage

@ -1,3 +1,5 @@
use crate::http::{Response, Status};
use std::string::FromUtf8Error; use std::string::FromUtf8Error;
use thiserror::Error; use thiserror::Error;
// TODO add NotFound and InternalServerError response here, // TODO add NotFound and InternalServerError response here,
@ -9,26 +11,71 @@ pub enum RequestError {
NotAForm, NotAForm,
#[error("resource not found")] #[error("resource not found")]
NotFound, NotFound,
#[error("unused method")]
UnusedMethod,
#[error("request not authorized, incorred hash: {0}")] #[error("request not authorized, incorred hash: {0}")]
NotAuthorized(String), NotAuthorized(String),
#[error("urldecoding error")] #[error("urldecoding error")]
UrlDecodeErr(#[from] FromUtf8Error), UrlDecodeErr(#[from] FromUtf8Error),
#[error("bad request")] #[error("bad request")]
BadRequest, BadRequest,
#[error("missing body in POST request")]
MissingBody,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum InternalError { pub enum DatabaseError {
#[error("failed to read from database")] #[error("failed to read from database")]
DatabaseReadError, SledError(#[from] sled::Error),
#[error("failed to write to database")] #[error("not found in database")]
DatabaseWriteError, NotInDb,
#[error("serialization error")]
SerError,
} }
pub enum InternalError {}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum HandlingError { pub enum HandlingError {
#[error("client error: {0}")] #[error("client error: {0}")]
ClientError(#[from] RequestError), ClientError(#[from] RequestError),
#[error("server error: {0}")] #[error("server error: {0}")]
ServerError(#[from] InternalError), ServerError(#[from] DatabaseError),
#[error("problem reading stream")]
Io(#[from] std::io::Error),
}
impl From<DatabaseError> for Status {
fn from(err: DatabaseError) -> Self {
match err {
DatabaseError::SledError(e) => Status::InternalServerError,
DatabaseError::NotInDb => Status::BadRequest,
DatabaseError::SerError => Status::InternalServerError,
}
}
}
impl From<RequestError> for Status {
fn from(err: RequestError) -> Self {
match err {
RequestError::NotAForm => Status::BadRequest,
RequestError::NotFound => Status::BadRequest,
RequestError::UnusedMethod => Status::BadRequest,
RequestError::NotAuthorized(_) => Status::Unauthorized,
RequestError::UrlDecodeErr(_) => Status::BadRequest,
RequestError::BadRequest => Status::BadRequest,
RequestError::MissingBody => Status::BadRequest,
}
}
}
impl From<HandlingError> for Response {
fn from(err: HandlingError) -> Self {
let status = match err {
HandlingError::ClientError(e) => Status::from(e),
HandlingError::ServerError(e) => Status::from(e),
HandlingError::Io(e) => Status::InternalServerError,
};
Response::new(status, vec![], status.message().to_string())
}
} }

@ -8,17 +8,17 @@ const HTTP_VERSION: &'static str = "HTTP/1.1";
#[derive(Debug)] #[derive(Debug)]
pub struct Request { pub struct Request {
pub method: String, pub method: Method,
pub uri: String, pub uri: String,
pub headers: Vec<(String, String)>, pub headers: Vec<(String, String)>,
pub body: String, pub body: Option<Vec<u8>>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct Response { pub struct Response {
pub status: Status, pub status: Status,
pub headers: Vec<(String, String)>, pub headers: Vec<(String, String)>,
pub body: String, pub body: String, // make into Option<Vec<u8>>
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -31,6 +31,25 @@ pub enum Status {
InternalServerError = 500, InternalServerError = 500,
} }
#[derive(Debug)]
pub enum Method {
Get,
Post,
Delete,
}
impl FromStr for Method {
type Err = RequestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"GET" => Ok(Self::Get),
"POST" => Ok(Self::Post),
"DELETE" => Ok(Self::Delete),
_ => Err(Self::Err::UnusedMethod),
}
}
}
impl Status { impl Status {
pub fn message(&self) -> &'static str { pub fn message(&self) -> &'static str {
match self { match self {
@ -58,34 +77,38 @@ impl Status {
// } // }
//} //}
impl FromStr for Request { impl Request {
type Err = RequestError; pub fn add_body(&mut self, body: Vec<u8>) {
self.body = Some(body);
}
}
fn from_str(s: &str) -> Result<Self, Self::Err> { impl TryFrom<Vec<String>> for Request {
type Error = RequestError;
fn try_from(s: Vec<String>) -> Result<Self, Self::Error> {
//dbg!(&s); //dbg!(&s);
let mut iter = s.lines(); let mut iter = s.iter();
let mut first = iter let mut first = iter
.next() .next()
.ok_or(RequestError::BadRequest)? .ok_or(RequestError::BadRequest)?
.split_whitespace(); .split_whitespace();
let method = first.next().ok_or(RequestError::BadRequest)?; let method: Method = first.next().ok_or(RequestError::BadRequest)?.parse()?;
let uri = first.next().ok_or(RequestError::BadRequest)?; let uri = first.next().ok_or(RequestError::BadRequest)?.to_string();
let mut headers: Vec<(String, String)> = vec![]; let mut headers: Vec<(String, String)> = vec![];
for line in &mut iter { for line in iter {
if line.is_empty() { let mut line = line.split(":").take(2);
break; let left = line.next().ok_or(RequestError::BadRequest)?.trim();
let right = line.next().ok_or(RequestError::BadRequest)?.trim();
headers.push((left.to_string(), right.to_string()));
} }
let line: Vec<&str> = line.split(":").take(2).collect();
headers.push((line[0].into(), line[1].into()));
}
let body: String = iter.fold(String::new(), |a, b| format!("{}{}", a, b));
Ok(Self { Ok(Self {
method: method.to_string(), method,
uri: uri.to_string(), uri,
headers, headers,
body: body.to_string(), body: None,
}) })
} }
} }
@ -103,12 +126,6 @@ impl Response {
} }
} }
impl From<HandlingError> for Response {
fn from(err: HandlingError) -> Self {
todo!();
}
}
impl fmt::Display for Response { impl fmt::Display for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let headers: String = self let headers: String = self

@ -4,22 +4,24 @@ mod http;
mod post; mod post;
use crate::errors::{HandlingError, InternalError, RequestError}; use crate::errors::{HandlingError, InternalError, RequestError};
use crate::html::{FAQ, INDEX, STYLE, FAVICON}; use crate::html::{FAQ, FAVICON, INDEX, STYLE};
use crate::http::{Request, Response, Status}; use crate::http::{Request, Response, Status};
use crate::post::{Post, Thread}; use crate::post::{Post, Thread};
use std::error::Error; use std::error::Error;
use std::io::{Read, Write}; use std::io;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream}; use std::net::{SocketAddr, TcpListener, TcpStream};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str; use std::str;
use std::str::from_utf8; use std::str::from_utf8;
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, RwLock};
use std::thread::JoinHandle; use std::thread::JoinHandle;
use bincode::{deserialize, serialize}; use bincode::{deserialize, serialize};
use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; use errors::DatabaseError;
use html_escape::encode_text; use html_escape::encode_text;
use http::Method;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::*; use log::*;
use simplelog::*; use simplelog::*;
@ -27,6 +29,9 @@ use structopt::StructOpt;
use threadpool::ThreadPool; use threadpool::ThreadPool;
use urlencoding::decode; use urlencoding::decode;
// use this instead of hard-coding
type ID_TYPE = u32;
// represents command line arguments // represents command line arguments
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
struct Opt { struct Opt {
@ -45,129 +50,217 @@ struct Opt {
lazy_static! { lazy_static! {
// parse command line arguments // parse command line arguments
static ref OPT: Opt = Opt::from_args(); static ref OPT: Opt = Opt::from_args();
static ref DB: sled::Db = sled::open(&OPT.database).expect("failed to open db");
static ref THREADS: sled::Tree = DB.open_tree(b"threads").expect("failed to initialize threads");
static ref POSTS: sled::Tree = DB.open_tree(b"posts").expect("failed to intialize posts");
} }
#[derive(Debug)] /// increments the id stored inside the db
struct State { fn increment(old: Option<&[u8]>) -> Option<Vec<u8>> {
db: sled::Db, let number = match old {
id_counter: u32, Some(s) => {
// cache here? let buf: [u8; 4] = s.try_into().unwrap();
u32::from_be_bytes(buf) + 1
// FIXME
}
None => 0,
};
Some(number.to_be_bytes().to_vec())
} }
unsafe impl Send for State {} // DATABASE ACCESS FUNCTIONS
unsafe impl Sync for State {} /// returns post from id
fn get_post(id: u32) -> Result<Post, DatabaseError> {
POSTS
.get(id.to_be_bytes())?
.ok_or(DatabaseError::NotInDb)
.and_then(|x| deserialize::<Post>(&x).map_err(|_| DatabaseError::SerError))
}
impl State { /// returns thread from id
fn new(db_name: impl AsRef<Path>) -> Self { fn get_thread(id: u32) -> Result<Vec<Post>, DatabaseError> {
let mut s = Self { THREADS
db: sled::open(db_name).unwrap(), .get(id.to_be_bytes())?
id_counter: 0, .ok_or(DatabaseError::NotInDb)
}; .and_then(|x| deserialize::<Thread>(&x).map_err(|_| DatabaseError::SerError))?
s.init_id(); .into_iter()
s .map(|x| get_post(x))
.collect()
// let thread = THREADS.get(id.to_be_bytes()).and_then()
} }
fn next_id(&mut self) -> String { /// generates next id
// TODO improve this fn next_id() -> Result<u32, DatabaseError> {
// means the highest possible id is 999_999_999, then it loops back to zero DB.fetch_and_update(b"id", increment)
// NOTE this could technically be used to limit the storage required, .map(|x| match x {
// might possibly be a good feature Some(s) => {
self.id_counter = (self.id_counter + 1) % 1_000_000_000; let buf: [u8; 4] = (*s).try_into().unwrap();
let width = u32::MAX.ilog10() as usize; // happens to be 9 in this case u32::from_be_bytes(buf)
//let mut v = vec![];
//v.write_u32::<BigEndian>(self.id_counter).unwrap()
//self.db.insert(b"id", v);
format!("{:0width$}", self.id_counter)
} }
fn init_id(&mut self) { None => 0u32,
self.id_counter = match self.db.get(b"id").unwrap() { })
Some(s) => (&(*s)).read_u32::<BigEndian>().unwrap(), .map_err(|e| DatabaseError::from(e))
None => 0,
} }
/// returns current id
/// not used
fn get_id() -> Result<u32, DatabaseError> {
let s = DB.get(b"id")?.unwrap();
let buf: [u8; 4] = (*s).try_into().unwrap();
Ok(u32::from_be_bytes(buf))
}
/// lists all threads
fn list_threads() -> Result<Vec<Post>, DatabaseError> {
THREADS
.iter()
.take_while(|x| x.is_ok())
.map(|x| x.unwrap().0)
.map(|x| {
POSTS.get(x).map(|x| match x {
Some(s) => s,
None => {
error!("couldn't find thread op among posts");
panic!("unrecoverable error");
} }
})
})
.map(|x| {
x.map_err(|e| DatabaseError::from(e))
.and_then(|i| deserialize::<Post>(&i).map_err(|_| DatabaseError::SerError))
})
.collect()
} }
/// top level handling of requests // NOTE worst out of everything, but I guess it's not so bad
fn handle(request: &str, state: Arc<Mutex<State>>) -> Response { fn add_post(mut post: Post, thread_id: u32) -> Result<(), DatabaseError> {
let request = match request.parse::<Request>() { let id = next_id()?;
Ok(s) => s, post.id = id;
Err(e) => return Response::new(Status::BadRequest, vec![], e.to_string()), let mut thread = deserialize::<Thread>(
}; &THREADS
let state = state.lock().unwrap(); .get(thread_id.to_be_bytes())?
.ok_or(DatabaseError::NotInDb)?,
)
.map_err(|_| DatabaseError::SerError)?;
thread.push(post.id);
let thread = serialize(&thread).map_err(|_| DatabaseError::SerError)?;
THREADS.insert(thread_id.to_be_bytes(), thread)?;
POSTS.insert(
id.to_be_bytes(),
serialize(&post).map_err(|_| DatabaseError::SerError)?,
)?;
Ok(())
}
match request.method.as_str() { fn add_thread(mut post: Post) -> Result<(), DatabaseError> {
"GET" => match get(&request.uri, state) { let id = next_id()?;
Ok(s) => s, let thread = Thread::new(id);
Err(e) => Response::new(Status::NotFound, vec![], dbg!(e).to_string()), post.id = id;
}, THREADS.insert(
"POST" => match post(request, state) { id.to_be_bytes(),
Ok(s) => s, serialize(&thread).map_err(|_| DatabaseError::SerError)?,
Err(e) => Response::new(Status::Ok, vec![], "".to_string()), )?;
}, POSTS.insert(
// check admin hash id.to_be_bytes(),
"DELETE" => Response::new(Status::Ok, vec![], "".to_string()), serialize(&post).map_err(|_| DatabaseError::SerError)?,
_ => Response::new(Status::Ok, vec![], RequestError::BadRequest.to_string()), )?;
Ok(())
}
// END DATABASE ACCESS FUNCTIONS
/// top level handling of requests
fn handle(mut reader: BufReader<&mut TcpStream>) -> Result<Response, HandlingError> {
let mut request: Request = reader
.by_ref()
.lines()
.take_while(|l| l.is_ok())
.map(|l| l.unwrap())
.take_while(|l| !l.is_empty())
.collect::<Vec<_>>()
.try_into()?;
if let Some(s) = request
.headers
.iter()
.find(|(a, b)| a.to_lowercase() == "content-length")
{
let body: Vec<u8> = reader
.bytes()
.take(s.1.parse::<usize>().unwrap())
.collect::<Result<Vec<u8>, io::Error>>()
.unwrap();
// TODO test if it works
// FIXME deunwrap
// TODO handle body too large
request.add_body(body);
}
dbg!(&request);
match request.method {
Method::Get => get(&request.uri),
Method::Post => post(request),
// TODO check admin hash
_ => Ok(Response::from(HandlingError::from(
RequestError::BadRequest,
))),
} }
} }
/// get "/" returns head of every thread /// get "/" returns head of every thread
/// or returns posts from thread with id in uri /// or returns posts from thread with id in uri
fn get(path: &str, state: MutexGuard<State>) -> Result<Response, HandlingError> { fn get(path: &str) -> Result<Response, HandlingError> {
match path { match path {
// list threads in this case // list threads in this case
"/" => { "/" => {
let content = content( // FIXME absolute unwrap hell
"index", let ops = list_threads()?
&state
.db
.iter() .iter()
.map(|x| x.unwrap()) //FIXME unwraps .fold(String::from(""), |a, b| format!("{a}\n{b}"));
.map(|x| deserialize::<Thread>(&x.1).unwrap().head()) // FIXME here too let c = content("index", &ops);
.fold(String::from(""), |a, b| format!("{}{}", a, b)),
);
Ok(Response::new(Status::Ok, vec![], content)) Ok(Response::new(Status::Ok, vec![], c))
} }
// TODO favicon.ico // TODO favicon.ico
"/css" => Ok(Response::new(Status::Ok, vec![], String::from(STYLE))), "/css" => Ok(Response::new(Status::Ok, vec![], String::from(STYLE))),
"/faq" => Ok(Response::new(Status::Ok, vec![], String::from(FAQ))), "/faq" => Ok(Response::new(Status::Ok, vec![], String::from(FAQ))),
//"/favicon.ico" => Ok(Response::new(Status::Ok, vec![("content-type", "image/x-icon")], FAVICON)), //"/favicon.ico" => Ok(Response::new(Status::Ok, vec![("content-type", "image/x-icon")], FAVICON)),
// TODO favicon
// list specific thread here // list specific thread here
// FIXME unwrap hell // FIXME unwrap hell
s => { s => {
let content = content( let id = s
s, .trim_start_matches("/")
&deserialize::<Thread>( .split("/")
&state .next()
.db
.get(&s[1..])
.map_err(|_| HandlingError::ServerError(InternalError::DatabaseReadError))?
.ok_or(HandlingError::ClientError(RequestError::NotFound))?,
)
.unwrap() .unwrap()
.to_string(), .parse::<u32>()
); .map_err(|_| RequestError::NotFound)?;
let c = get_thread(id)?
.iter()
.fold(String::from(""), |a, b| format!("{a}\n{b}"));
let c = content(&id.to_string(), &c);
Ok(Response::new(Status::Ok, vec![], content)) Ok(Response::new(Status::Ok, vec![], c))
} }
} }
} }
fn post(request: Request, mut state: MutexGuard<State>) -> Result<Response, HandlingError> { fn post(request: Request) -> Result<Response, HandlingError> {
let content = encode_text(&request.body).into_owned(); let binding = &request.body.ok_or(RequestError::MissingBody)?;
let c = String::from_utf8_lossy(&binding);
let c = encode_text(&c).into_owned();
let post = Post::new(0, c);
match request.uri.as_str() { match request.uri.as_str() {
// means we wish to create a new thread // means we wish to create a new thread
"/" => { "/" => {
// TODO fix unwrap add_thread(post)?;
let id = state.next_id(); info!("Created new thread");
let thread = Thread(vec![Post::new(&id, content)]);
state.db.insert(&id, serialize(&thread).unwrap()).unwrap();
info!("Created new thread, id: {}", id);
// TODO add redirect // TODO add redirect
Ok(Response::new( Ok(Response::new(
@ -179,41 +272,33 @@ fn post(request: Request, mut state: MutexGuard<State>) -> Result<Response, Hand
// means we wish to post in specific thread // means we wish to post in specific thread
s => { s => {
// first we increment id let id = s
let id = state.next_id(); .trim_start_matches("/")
let post = Post::new(&id, content); .split("/")
.next()
let mut thread = deserialize::<Thread>( .unwrap()
&state .parse::<u32>()
.db .map_err(|_| RequestError::NotFound)?;
.remove(&s[1..]) add_post(post, id)?;
.map_err(|_| HandlingError::ServerError(InternalError::DatabaseWriteError))?
.ok_or(HandlingError::ClientError(RequestError::NotFound))?,
)
.unwrap();
thread.0.push(post);
state info!("Added new post to thread {id}");
.db
.insert(&s[1..], serialize(&thread).unwrap())
.unwrap();
// make this less weird
info!("Added new post to thread {}", s);
// TODO add redirect // TODO add redirect
Ok(Response::new(Status::SeeOther, vec![("location", s)], String::from(""))) Ok(Response::new(
Status::SeeOther,
vec![("location", s)],
String::from(""),
))
} }
} }
} }
fn delete(path: &str, database: Arc<Mutex<sled::Db>>) -> Response { fn delete(path: &str) -> Response {
todo!(); todo!();
} }
#[inline]
fn content(name: &str, main: &str) -> String { fn content(name: &str, main: &str) -> String {
INDEX.replace("{name}", &name).replace("{}", &main) INDEX.replace("{name}", name).replace("{}", main)
} }
fn main() { fn main() {
@ -229,10 +314,7 @@ fn main() {
TerminalMode::Mixed, TerminalMode::Mixed,
ColorChoice::Auto, ColorChoice::Auto,
) )
.unwrap(); .expect("failed to setup logger");
// open database
let state = Arc::new(Mutex::new(State::new(&OPT.database)));
// TODO setup cache
// wait for requests // wait for requests
for stream in listener.incoming() { for stream in listener.incoming() {
@ -244,23 +326,16 @@ fn main() {
} }
}; };
let state = Arc::clone(&state);
pool.execute(move || { pool.execute(move || {
let ip = stream.peer_addr().unwrap(); let reader = BufReader::new(&mut stream);
// TODO check if allowed ip before reading request
let mut buffer = [0; 1 << 10]; // 2 to the power of 10
// how do I implement images with this?
stream.read(&mut buffer).unwrap();
// NOTE possibly problematic when we cosider images let response = match handle(reader) {
let text = str::from_utf8(&buffer).unwrap().trim_matches(0 as char); Ok(s) => s,
// ^-- gets rid of null at the end of buffer Err(e) => Response::from(dbg!(e)),
};
// handle request // handle request
stream stream.write(response.to_string().as_bytes()).unwrap();
.write(handle(text, state).to_string().as_bytes())
.unwrap();
}); });
} }
} }

@ -10,29 +10,47 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Post { pub struct Post {
pub id: String, pub id: u32, // technically reduntant but whatever
//pub img: Option<Image>, //pub img: Option<Image>,
pub body: String, pub body: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Thread(pub Vec<Post>); pub struct Thread(Vec<u32>);
impl IntoIterator for Thread {
type Item = u32;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Thread { impl Thread {
pub fn head(&self) -> String { pub fn new(op: u32) -> Self {
let first = &self.0[0]; Thread(vec![op])
format!( }
"<article><div><span class=\"info\">{}</span></div>{}</article>", pub fn push(&mut self, thing: u32) {
first.id, self.0.push(thing)
first.to_string(),
)
} }
} }
//impl Thread {
// pub fn head(&self) -> String {
// let first = &self.0[0];
// format!(
// "<article><div><span class=\"info\">{}</span></div>{}</article>",
// first.id,
// first.to_string(),
// )
// }
//}
impl Post { impl Post {
pub fn new(id: &str, body: String) -> Self { pub fn new(id: u32, body: String) -> Self {
Self { Self {
id: id.to_string(), id,
//img, //img,
body, body,
} }
@ -75,13 +93,3 @@ impl fmt::Display for Post {
) )
} }
} }
impl fmt::Display for Thread {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let posts: String = self
.0
.iter()
.fold(String::from(""), |a, b| format!("{}{}", a, b));
write!(f, "{}", posts)
}
}

Loading…
Cancel
Save