|
|
@ -1,7 +1,6 @@
|
|
|
|
//mod errors;
|
|
|
|
mod errors;
|
|
|
|
mod http;
|
|
|
|
mod http;
|
|
|
|
mod post;
|
|
|
|
mod post;
|
|
|
|
mod db;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
use crate::errors::{HandlingError, RequestError};
|
|
|
|
use crate::errors::{HandlingError, RequestError};
|
|
|
|
use crate::http::{Form, Request, Response, Status};
|
|
|
|
use crate::http::{Form, Request, Response, Status};
|
|
|
@ -27,52 +26,148 @@ use threadpool::ThreadPool;
|
|
|
|
pub const INDEX: &str = include_str!("www/index.html");
|
|
|
|
pub const INDEX: &str = include_str!("www/index.html");
|
|
|
|
pub const FAVICON: &[u8] = include_bytes!("www/favicon.ico");
|
|
|
|
pub const FAVICON: &[u8] = include_bytes!("www/favicon.ico");
|
|
|
|
pub const STYLE: &str = include_str!("www/style.css");
|
|
|
|
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)]
|
|
|
|
#[derive(StructOpt, Debug)]
|
|
|
|
struct Opt {
|
|
|
|
struct Opt {
|
|
|
|
/// port on which to listen
|
|
|
|
|
|
|
|
/// defaults to 8000
|
|
|
|
|
|
|
|
#[structopt(short, long, default_value = "8000")]
|
|
|
|
#[structopt(short, long, default_value = "8000")]
|
|
|
|
port: u16,
|
|
|
|
port: u16,
|
|
|
|
|
|
|
|
|
|
|
|
/// database path
|
|
|
|
|
|
|
|
/// the database initializes as a folder in the filesystem
|
|
|
|
|
|
|
|
/// defaults to `data`
|
|
|
|
|
|
|
|
#[structopt(short, long, default_value = "data")]
|
|
|
|
#[structopt(short, long, default_value = "data")]
|
|
|
|
database: PathBuf,
|
|
|
|
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")]
|
|
|
|
#[structopt(short, long, default_value = "999999999")]
|
|
|
|
max_id: u32,
|
|
|
|
max_id: u32,
|
|
|
|
|
|
|
|
|
|
|
|
/// verbosity level
|
|
|
|
|
|
|
|
/// if set, will print info
|
|
|
|
|
|
|
|
#[structopt(short, long)]
|
|
|
|
|
|
|
|
verbose: bool
|
|
|
|
|
|
|
|
// TODO setup the verbosity levels
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// more or less safe
|
|
|
|
// get command line arguments and make them static
|
|
|
|
|
|
|
|
// safe because they're read-only
|
|
|
|
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 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");
|
|
|
|
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");
|
|
|
|
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<Vec<u8>> {
|
|
|
|
|
|
|
|
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<Post, DatabaseError> {
|
|
|
|
|
|
|
|
POSTS
|
|
|
|
|
|
|
|
.get(id.to_be_bytes())?
|
|
|
|
|
|
|
|
.ok_or(DatabaseError::NotInDb)
|
|
|
|
|
|
|
|
.and_then(|x| deserialize::<Post>(&x).map_err(|_| DatabaseError::SerError))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// returns thread from id
|
|
|
|
|
|
|
|
fn get_thread(id: u32) -> Result<Vec<Post>, DatabaseError> {
|
|
|
|
|
|
|
|
THREADS
|
|
|
|
|
|
|
|
.get(id.to_be_bytes())?
|
|
|
|
|
|
|
|
.ok_or(DatabaseError::NotInDb)
|
|
|
|
|
|
|
|
.and_then(|x| deserialize::<Thread>(&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<u32, DatabaseError> {
|
|
|
|
|
|
|
|
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<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()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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::<Thread>(
|
|
|
|
|
|
|
|
&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
|
|
|
|
/// top level handling of requests
|
|
|
|
fn handle(mut reader: BufReader<&mut TcpStream>) -> Result<Response, HandlingError> {
|
|
|
|
fn handle(mut reader: BufReader<&mut TcpStream>) -> Result<Response, HandlingError> {
|
|
|
|
let mut request: Request = reader
|
|
|
|
let mut request: Request = reader
|
|
|
@ -134,6 +229,11 @@ fn get(path: &str) -> Result<Response, HandlingError> {
|
|
|
|
vec![("content-type", "text/css; charset=utf-8")],
|
|
|
|
vec![("content-type", "text/css; charset=utf-8")],
|
|
|
|
String::from(STYLE).into(),
|
|
|
|
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(
|
|
|
|
"/favicon.ico" => Ok(Response::new(
|
|
|
|
Status::Ok,
|
|
|
|
Status::Ok,
|
|
|
|
vec![("content-type", "image/x-icon")],
|
|
|
|
vec![("content-type", "image/x-icon")],
|
|
|
@ -174,7 +274,7 @@ fn post(request: Request) -> Result<Response, HandlingError> {
|
|
|
|
// TODO check content-type
|
|
|
|
// TODO check content-type
|
|
|
|
|
|
|
|
|
|
|
|
let form = Form::try_from(&request)?;
|
|
|
|
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", "<br>"));
|
|
|
|
let mut post = Post::new(0, encode_text(form.content.trim()).replace("\r\n", "<br>"));
|
|
|
|
post.img = form.image;
|
|
|
|
post.img = form.image;
|
|
|
|
|
|
|
|
|
|
|
|