Compare commits

..

1 Commits

3
.gitignore vendored

@ -1,2 +1,3 @@
/target
data*
data

@ -0,0 +1,136 @@
use crate::{THREADS, POSTS, MEDIA, HEAD};
/// 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(())
}
#[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<DatabaseError> for Status {
fn from(err: DatabaseError) -> Self {
match err {
DatabaseError::SledError(_) => Status::InternalServerError,
DatabaseError::NotInDb => Status::BadRequest,
DatabaseError::SerError => Status::InternalServerError,
}
}
}

@ -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<DatabaseError> for Status {
fn from(err: DatabaseError) -> Self {
match err {
DatabaseError::SledError(_) => Status::InternalServerError,
DatabaseError::NotInDb => Status::BadRequest,
DatabaseError::SerError => Status::InternalServerError,
}
}
}
impl From<RequestError> for Status {
fn from(err: RequestError) -> Self {

@ -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<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
fn handle(mut reader: BufReader<&mut TcpStream>) -> Result<Response, HandlingError> {
let mut request: Request = reader
@ -229,11 +134,6 @@ fn get(path: &str) -> Result<Response, HandlingError> {
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<Response, HandlingError> {
// 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", "<br>"));
post.img = form.image;

@ -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<Vec<u8>>,
//pub id: u32, // technically reduntant but whatever
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Thread(Vec<u32>);
impl IntoIterator for Thread {
type Item = u32;
type IntoIter = std::vec::IntoIter<Self::Item>;
pub struct Media {
pub content_type: String,
pub data: Vec<u8>,
}
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,
}

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>kchan - {name}</title>
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div class="wrap">
<nav>
<span>kchan &ndash;</span>
<a href="/">index</a>
<a href="faq.html">faq</a>
<a href="new-thread.html">new thread</a>
</nav>
</div>
</header>
<main>
<div class="wrap">
<h1>FAQ</h1>
</div>
</main>
</head>
</html>
Loading…
Cancel
Save