diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..8493602 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,136 @@ +use crate::{THREADS, POSTS, MEDIA, HEAD}; + +/// increments the id stored inside the db +fn increment(old: Option<&[u8]>) -> Option> { + let number = match old { + Some(s) => { + let buf: [u8; 4] = s.try_into().unwrap(); + u32::from_be_bytes(buf) + 1 + // FIXME + } + None => 0, + }; + Some(number.to_be_bytes().to_vec()) +} + +// DATABASE ACCESS FUNCTIONS +/// returns post from id +fn get_post(id: u32) -> Result { + POSTS + .get(id.to_be_bytes())? + .ok_or(DatabaseError::NotInDb) + .and_then(|x| deserialize::(&x).map_err(|_| DatabaseError::SerError)) +} + +/// returns thread from id +fn get_thread(id: u32) -> Result, DatabaseError> { + THREADS + .get(id.to_be_bytes())? + .ok_or(DatabaseError::NotInDb) + .and_then(|x| deserialize::(&x).map_err(|_| DatabaseError::SerError))? + .into_iter() + .map(|x| get_post(x)) + .filter(|x| x.is_ok()) + .collect() + // let thread = THREADS.get(id.to_be_bytes()).and_then() +} + +/// generates next id +fn next_id() -> Result { + DB.update_and_fetch(b"id", increment) + .map(|x| match x { + Some(s) => { + let buf: [u8; 4] = (*s).try_into().unwrap(); + u32::from_be_bytes(buf) + } + None => 0u32, + }) + .map_err(|e| DatabaseError::from(e)) +} + +/// lists all threads +fn list_threads() -> Result, 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::(&i).map_err(|_| DatabaseError::SerError)) + }) + .collect() +} + +// NOTE worst out of everything, but I guess it's not so bad +fn add_post(mut post: Post, thread_id: u32) -> Result<(), DatabaseError> { + let id = next_id()?; + post.id = id; + let mut thread = deserialize::( + &THREADS + .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(()) +} + +fn delete_post(id: u32) -> Result<(), DatabaseError> { + POSTS.remove(id.to_be_bytes())?; + if THREADS.contains_key(id.to_be_bytes())? { + THREADS.remove(id.to_be_bytes())?; + } else { + } + Ok(()) +} + +fn add_thread(mut post: Post) -> Result<(), DatabaseError> { + let id = next_id()?; + let thread = Thread::new(id); + post.id = id; + THREADS.insert( + id.to_be_bytes(), + serialize(&thread).map_err(|_| DatabaseError::SerError)?, + )?; + POSTS.insert( + id.to_be_bytes(), + serialize(&post).map_err(|_| DatabaseError::SerError)?, + )?; + Ok(()) +} + +#[derive(Debug, Error)] +pub enum DatabaseError { + #[error("failed to read from database")] + SledError(#[from] sled::Error), + #[error("not found in database")] + NotInDb, + #[error("serialization error")] + SerError, +} + +impl From for Status { + fn from(err: DatabaseError) -> Self { + match err { + DatabaseError::SledError(_) => Status::InternalServerError, + DatabaseError::NotInDb => Status::BadRequest, + DatabaseError::SerError => Status::InternalServerError, + } + } +} diff --git a/src/errors.rs b/src/errors.rs index 83d19fc..033609f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -21,16 +21,6 @@ pub enum RequestError { MissingBody, } -#[derive(Debug, Error)] -pub enum DatabaseError { - #[error("failed to read from database")] - SledError(#[from] sled::Error), - #[error("not found in database")] - NotInDb, - #[error("serialization error")] - SerError, -} - #[derive(Debug, Error)] pub enum HandlingError { #[error("client error: {0}")] @@ -41,15 +31,6 @@ pub enum HandlingError { Io(#[from] std::io::Error), } -impl From for Status { - fn from(err: DatabaseError) -> Self { - match err { - DatabaseError::SledError(_) => Status::InternalServerError, - DatabaseError::NotInDb => Status::BadRequest, - DatabaseError::SerError => Status::InternalServerError, - } - } -} impl From for Status { fn from(err: RequestError) -> Self { diff --git a/src/main.rs b/src/main.rs index 31a647e..4ded76f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -mod errors; +//mod errors; mod http; mod post; +mod db; use crate::errors::{HandlingError, RequestError}; use crate::http::{Form, Request, Response, Status}; @@ -26,148 +27,52 @@ use threadpool::ThreadPool; pub const INDEX: &str = include_str!("www/index.html"); pub const FAVICON: &[u8] = include_bytes!("www/favicon.ico"); pub const STYLE: &str = include_str!("www/style.css"); -pub const FAQ: &str = include_str!("www/faq.html"); -// represents command line arguments +/// represents command line arguments #[derive(StructOpt, Debug)] struct Opt { + /// port on which to listen + /// defaults to 8000 #[structopt(short, long, default_value = "8000")] port: u16, + /// database path + /// the database initializes as a folder in the filesystem + /// defaults to `data` #[structopt(short, long, default_value = "data")] database: PathBuf, + /// the maximum id in the pool + /// this defines the size of the pool, although it is not guaranteed to be used in its entirety + /// defaults to 999_999_999 #[structopt(short, long, default_value = "999999999")] max_id: u32, + + /// verbosity level + /// if set, will print info + #[structopt(short, long)] + verbose: bool + // TODO setup the verbosity levels } -// get command line arguments and make them static -// safe because they're read-only +// more or less safe lazy_static! { // parse command line arguments static ref OPT: Opt = Opt::from_args(); static ref DB: sled::Db = sled::open(&OPT.database).expect("failed to open db"); + /// a tree that descibes the relation between posts + /// each post id either points to the next post in thread, or it points to 0, in which case it is the last post in the thread + /// 0 is understood as a null, and need to be reserved so that the additive group of static ref THREADS: sled::Tree = DB.open_tree(b"threads").expect("failed to initialize threads"); + /// a tree of id - posts::Post, serialized into bytes static ref POSTS: sled::Tree = DB.open_tree(b"posts").expect("failed to intialize posts"); + /// a tree of id -> post::Media, serialized into bytes + static ref MEDIA: sled::Tree = DB.open_tree(b"media").expect("failed to initialize media"); + /// a tree of the first posts in each thread; id -> post::Thread + /// important when listing threads and when deciding to delete threads to free up space in the id pool + static ref HEAD: sled::Tree = DB.open_tree(b"heads").expect("failed to initialize heads"); } -/// increments the id stored inside the db -fn increment(old: Option<&[u8]>) -> Option> { - let number = match old { - Some(s) => { - let buf: [u8; 4] = s.try_into().unwrap(); - u32::from_be_bytes(buf) + 1 - // FIXME - } - None => 0, - }; - Some(number.to_be_bytes().to_vec()) -} - -// DATABASE ACCESS FUNCTIONS -/// returns post from id -fn get_post(id: u32) -> Result { - POSTS - .get(id.to_be_bytes())? - .ok_or(DatabaseError::NotInDb) - .and_then(|x| deserialize::(&x).map_err(|_| DatabaseError::SerError)) -} - -/// returns thread from id -fn get_thread(id: u32) -> Result, DatabaseError> { - THREADS - .get(id.to_be_bytes())? - .ok_or(DatabaseError::NotInDb) - .and_then(|x| deserialize::(&x).map_err(|_| DatabaseError::SerError))? - .into_iter() - .map(|x| get_post(x)) - .filter(|x| x.is_ok()) - .collect() - // let thread = THREADS.get(id.to_be_bytes()).and_then() -} - -/// generates next id -fn next_id() -> Result { - DB.update_and_fetch(b"id", increment) - .map(|x| match x { - Some(s) => { - let buf: [u8; 4] = (*s).try_into().unwrap(); - u32::from_be_bytes(buf) - } - None => 0u32, - }) - .map_err(|e| DatabaseError::from(e)) -} - -/// lists all threads -fn list_threads() -> Result, 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::(&i).map_err(|_| DatabaseError::SerError)) - }) - .collect() -} - -// NOTE worst out of everything, but I guess it's not so bad -fn add_post(mut post: Post, thread_id: u32) -> Result<(), DatabaseError> { - let id = next_id()?; - post.id = id; - let mut thread = deserialize::( - &THREADS - .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(()) -} - -fn delete_post(id: u32) -> Result<(), DatabaseError> { - POSTS.remove(id.to_be_bytes())?; - if THREADS.contains_key(id.to_be_bytes())? { - THREADS.remove(id.to_be_bytes())?; - } else { - } - Ok(()) -} - -fn add_thread(mut post: Post) -> Result<(), DatabaseError> { - let id = next_id()?; - let thread = Thread::new(id); - post.id = id; - THREADS.insert( - id.to_be_bytes(), - serialize(&thread).map_err(|_| DatabaseError::SerError)?, - )?; - POSTS.insert( - id.to_be_bytes(), - serialize(&post).map_err(|_| DatabaseError::SerError)?, - )?; - Ok(()) -} - -// END DATABASE ACCESS FUNCTIONS - /// top level handling of requests fn handle(mut reader: BufReader<&mut TcpStream>) -> Result { let mut request: Request = reader @@ -229,11 +134,6 @@ fn get(path: &str) -> Result { vec![("content-type", "text/css; charset=utf-8")], String::from(STYLE).into(), )), - "/faq" => Ok(Response::new( - Status::Ok, - vec![("content-type", "text/html; charset=utf-8")], - String::from(FAQ).into(), - )), "/favicon.ico" => Ok(Response::new( Status::Ok, vec![("content-type", "image/x-icon")], @@ -274,7 +174,7 @@ fn post(request: Request) -> Result { // TODO check content-type let form = Form::try_from(&request)?; - + // FIXME pass form into add_thread let mut post = Post::new(0, encode_text(form.content.trim()).replace("\r\n", "
")); post.img = form.image; diff --git a/src/post.rs b/src/post.rs index 941e3c0..8d735db 100644 --- a/src/post.rs +++ b/src/post.rs @@ -10,36 +10,33 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Post { - pub id: u32, // technically reduntant but whatever - pub img: Option>, + //pub id: u32, // technically reduntant but whatever pub body: String, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Thread(Vec); - -impl IntoIterator for Thread { - type Item = u32; - type IntoIter = std::vec::IntoIter; +pub struct Media { + pub content_type: String, + pub data: Vec, +} - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Thread { + pub replies: u32, } impl Thread { - pub fn new(op: u32) -> Self { - Thread(vec![op]) + pub fn new() -> Self { + Self { replies: 0 } } - pub fn push(&mut self, thing: u32) { - self.0.push(thing) + pub fn increment(&mut self) { + self.replies += 1; } } impl Post { pub fn new(id: u32, body: String) -> Self { Self { - id, img: None, body, } diff --git a/src/www/faq.html b/src/www/faq.html deleted file mode 100644 index 9f5284f..0000000 --- a/src/www/faq.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - kchan - {name} - - - - -
-
- -
-
-
-
-

FAQ

-
-
- -