diff --git a/.gitignore b/.gitignore index c403c34..2d19f50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /target .idea/ +test_data/ +.env + diff --git a/Cargo.lock b/Cargo.lock index 69792b8..5b47607 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "parking_lot" @@ -1328,6 +1328,7 @@ dependencies = [ "dashmap", "lazy_static", "nanoid", + "once_cell", "parking_lot", "serde", "tera", diff --git a/Cargo.toml b/Cargo.toml index 66c1463..09fdbdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ axum = "0.6.1" dashmap = "5.4.0" lazy_static = "1.4.0" nanoid = "0.4.0" +once_cell = "1.17.0" parking_lot = "0.12.1" serde = { version = "1.0.152", features = ["derive", "serde_derive"] } tera = "1.17.1" diff --git a/src/admin.rs b/src/admin.rs new file mode 100644 index 0000000..c38ce51 --- /dev/null +++ b/src/admin.rs @@ -0,0 +1,8 @@ +use axum::{routing::{post, get}, Router}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", get(todo!())) + .route("/user", get(todo!()).post(todo!())) + .route("/user/:id", get(todo!()).post(todo!())) +} diff --git a/src/data.rs b/src/data.rs index 35f40d1..501a02e 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,16 +1,18 @@ use std::{ collections::HashMap, fs::{read_to_string, remove_file, DirBuilder, File}, - io::{self, BufRead}, + os::unix::fs::DirBuilderExt, path::PathBuf, }; -use std::os::unix::fs::DirBuilderExt; - use argon2::{ password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier, }; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; use dashmap::DashMap; use lazy_static::lazy_static; @@ -43,6 +45,18 @@ pub enum Error { BadPassword, } +impl IntoResponse for Error { + fn into_response(self) -> Response { + match self { + Error::ShitMissing(thing) => { + (StatusCode::NOT_FOUND, format!("404 not found: {}", thing)).into_response() + } + + _ => (StatusCode::INTERNAL_SERVER_ERROR, "shit went wrong sorry").into_response(), + } + } +} + fn meta_str_to_map(from: &str) -> Result, Error> { let map = from .lines() @@ -67,12 +81,14 @@ fn hash_password(password: String) -> Result { Ok(hash) } -struct TextPaste { +#[derive(Clone, Debug)] +pub struct TextPaste { pub text: String, pub meta: TextMeta, } -struct TextMeta { +#[derive(Clone, Debug)] +pub struct TextMeta { language: Option, } @@ -95,8 +111,10 @@ impl std::fmt::Display for TextMeta { } } -struct FileMeta { - file_name: String, +#[derive(Clone, Debug)] +pub struct FileMeta { + pub file_name: String, + pub size: u64, } impl FileMeta { @@ -104,17 +122,22 @@ impl FileMeta { let mut map = meta_str_to_map(from)?; let file_name = map.remove("file_name").ok_or(Error::Metadata)?; - Ok(Self { file_name }) + let size = map + .remove("size") + .and_then(|string| string.parse::().ok()) + .ok_or(Error::Metadata)?; + Ok(Self { file_name, size }) } } impl std::fmt::Display for FileMeta { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "file_name: {}\n", self.file_name) + write!(f, "file_name: {}\nsize: {}\n", self.file_name, self.size) } } -struct UserData { +#[derive(Clone, Debug)] +pub struct UserData { pub username: String, pub pw_hash: String, pub is_admin: bool, @@ -146,7 +169,7 @@ impl UserData { pub fn set_password_checked(&mut self, old: String, new: String) -> Result<(), Error> { self.validate_password(old)?; - self.set_password(new); + self.set_password(new)?; Ok(()) } @@ -187,15 +210,40 @@ impl DataStorage { } pub fn init(&mut self) -> Result<(), Error> { + println!("1"); self.ensure_folder_structure()?; + println!("2"); self.load_redirects()?; + println!("3"); self.load_text()?; + println!("4"); self.load_files()?; + println!("lets go"); self.load_users()?; + self.print_init_message(); + Ok(()) } + fn print_init_message(&self) { + println!( + "data storage finished initializing. data loaded from {}:\n- {} redirects\n- {} users\n- {} text pastes\n- {} file/media pastes", + self.base_dir.display(), + self.redirects.len(), + self.users.len(), + self.text.len(), + self.files.len(), + ); + } + + pub fn get_text(&self, id: String) -> Result { + self.text + .get(&id) + .map(|thing| thing.value().clone()) + .ok_or(Error::ShitMissing(id)) + } + pub fn create_text(&mut self, id: String, text: String, meta: TextMeta) -> Result<(), Error> { if self.text.contains_key(&id) { return Err(Error::IDCollision); @@ -232,6 +280,14 @@ impl DataStorage { Ok(old_text) } + pub fn get_file_meta(&self, id: String) -> Result { + Ok(self + .files + .get(&id) + .map(|item| item.value().clone()) + .ok_or(Error::ShitMissing(id))?) + } + pub fn create_file(&mut self, id: String, file_meta: FileMeta) -> Result<(), Error> { if self.files.contains_key(&id) { return Err(Error::IDCollision); @@ -239,16 +295,14 @@ impl DataStorage { self.files.insert(id.clone(), file_meta); - self.write_file_meta(id); + self.write_file_meta(id)?; Ok(()) } pub fn delete_file(&mut self, id: String) -> Result<(), Error> { - let file_meta = self - .files + self.files .remove(&id) - .ok_or(Error::ShitMissing(id.clone()))? - .1; + .ok_or(Error::ShitMissing(id.clone()))?; remove_file(self.base_dir.join(FILE_DIR.as_str()).join(id.clone()))?; remove_file(self.base_dir.join(FILE_DIR.as_str()).join(id + ".meta"))?; @@ -256,6 +310,13 @@ impl DataStorage { Ok(()) } + pub fn get_redirect(&self, id: String) -> Result { + self.redirects + .get(&id) + .map(|meow| meow.value().to_string()) + .ok_or(Error::ShitMissing(id)) + } + pub fn create_redirect(&mut self, id: String, target: String) -> Result<(), Error> { if self.redirects.contains_key(&id) { return Err(Error::IDCollision); @@ -335,18 +396,23 @@ impl DataStorage { dir_builder.create(self.base_dir.as_path())?; dir_builder.create(self.base_dir.join(FILE_DIR.as_str()))?; dir_builder.create(self.base_dir.join(TEXT_DIR.as_str()))?; + let redir_file = self.base_dir.join(REDIRECTS_FILE.as_str()); + if !redir_file.exists() { + File::create(redir_file)?; + } + let users_file = self.base_dir.join(USERS_FILE.as_str()); + if !users_file.exists() { + File::create(users_file)?; + } Ok(()) } fn load_redirects(&mut self) -> Result<(), Error> { - let f = File::options() - .create(true) - .read(true) - .open(self.base_dir.join(REDIRECTS_FILE.as_str()))?; + let contents = read_to_string(self.base_dir.join(REDIRECTS_FILE.as_str()))?; - for line in io::BufReader::new(f).lines() { - let (id, url) = line? + for line in contents.lines() { + let (id, url) = line .split_once(": ") .map(|(id, url)| (id.to_string(), url.to_string())) .ok_or(Error::Metadata)?; @@ -411,7 +477,7 @@ impl DataStorage { for line in read_to_string(self.base_dir.join(USERS_FILE.as_str()))?.lines() { if line.starts_with("---") { is_admin = false; - break; + continue; } let user = UserData::parse(line, is_admin)?; self.users.insert(user.username.clone(), user); @@ -420,7 +486,7 @@ impl DataStorage { } fn write_redirects(&self) -> Result<(), Error> { - let mut fuckery: String = self + let fuckery: String = self .redirects .iter() .map(|meow| format!("{}: {}\n", meow.key(), meow.value())) @@ -432,7 +498,7 @@ impl DataStorage { fn write_users(&self) -> Result<(), Error> { let mut fuckery = String::from("---"); - self.users.iter().map(|item| { + self.users.iter().for_each(|item| { if item.value().is_admin { fuckery = format!("{}\n{}", item.value().to_string(), fuckery) } else { diff --git a/src/down.rs b/src/down.rs index f257dea..6729562 100644 --- a/src/down.rs +++ b/src/down.rs @@ -1,11 +1,64 @@ -use axum::{routing::get, Router}; - -pub(crate) fn routes() -> Router { - Router::new() - .route("/t/:id", get(todo!())) - .route("/f/:id", get(todo!())) - .route("/l/:id", get(todo!())) - .route("/text/:id", get(todo!())) - .route("/file/:id", get(todo!())) - .route("/link/:id", get(todo!())) +use axum::{ + extract::{Path, State}, + http::{header, StatusCode}, + response::{Html, IntoResponse}, + routing::get, + Router, +}; + +use crate::{AppState, Error}; + +pub(crate) fn routes(router: Router) -> Router { + router + .route("/t/:id", get(get_text_page)) + .route("/t/:id/raw", get(get_text_raw)) + .route("/f/:id", get(get_file_page)) + .route("/f/:id/dl", get(|| async { "Hello world" })) + .route("/l/:id", get(get_redirect)) + .route("/text/:id", get(get_text_page)) + .route("/text/:id/raw", get(get_text_raw)) + .route("/file/:id", get(get_file_page)) + .route("/file/:id/download", get(|| async { "Hello world" })) + .route("/link/:id", get(get_redirect)) +} + +async fn get_redirect(State(state): State, Path(id): Path) -> impl IntoResponse { + match state.cache.get_redirect(id) { + Ok(target) => (StatusCode::FOUND, [(header::LOCATION, target)]).into_response(), + Err(e) => e.into_response(), + } +} + +async fn get_text_raw(State(state): State, Path(id): Path) -> impl IntoResponse { + match state.cache.get_text(id) { + Ok(text) => text.text.into_response(), + Err(e) => e.into_response(), + } +} + +async fn get_text_page( + State(state): State, + Path(id): Path, +) -> Result, Error> { + let text = state.cache.get_text(id)?; + + let mut ctx = tera::Context::new(); + + ctx.insert("text", &text.text); + + return Ok(Html(state.tera.render("text.html", &ctx)?)); +} + +async fn get_file_page( + State(state): State, + Path(id): Path, +) -> Result, Error> { + let file_meta = state.cache.get_file_meta(id)?; + + let mut ctx = tera::Context::new(); + + ctx.insert("size", &file_meta.size); + ctx.insert("file_name", &file_meta.file_name); + + return Ok(Html(state.tera.render("file.html", &ctx)?)); } diff --git a/src/main.rs b/src/main.rs index 7ecc724..60d2608 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,71 +1,94 @@ +#![allow(dead_code)] + mod data; mod down; -mod up; +//mod up; +//mod user; +//mod admin; -use std::{net::SocketAddr, path::PathBuf}; +use std::{net::SocketAddr, path::PathBuf, sync::Arc}; -use axum::{routing::get, Router}; -use lazy_static::lazy_static; +use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Router}; use tera::Tera; use data::DataStorage; -lazy_static! { - pub static ref TEMPLATES: Tera = { - let tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*.html")) - .expect("Parsing error(s)"); - - tera - }; -} - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("DataStorage error")] DataStorage(#[from] data::Error), - #[error("invalid env var")] - EnvVar, + #[error("invalid or missing env var: {0}")] + EnvVar(String), + #[error("tera error")] + Tera(#[from] tera::Error), +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + match self { + Error::DataStorage(ds_error) => ds_error.into_response(), + _ => (StatusCode::INTERNAL_SERVER_ERROR, "shit went wrong sorry").into_response(), + } + } } +#[derive(Clone)] struct AppState { alphabet: Vec, id_length: usize, - cache: DataStorage, + cache: Arc, + tera: Tera, } impl AppState { pub fn new() -> Result { - let base_dir_string = std::env::var("V_BASE_DIR").or(Err(Error::EnvVar))?; + let base_dir_string = + std::env::var("V_BASE_DIR").or(Err(Error::EnvVar("V_BASE_DIR".to_string())))?; let id_length: usize = std::env::var("V_ID_LENGTH") .unwrap_or("6".to_string()) .parse() - .or(Err(Error::EnvVar))?; + .or(Err(Error::EnvVar("V_ID_LENGTH".to_string())))?; let alphabet = std::env::var("V_ID_ALPHABET") .unwrap_or("abcdefghijkmnpqrstuwxyz123467890".to_string()) .chars() .collect(); - let base_dir = PathBuf::new(); + let mut base_dir = PathBuf::new(); base_dir.push(base_dir_string); let mut cache = DataStorage::new(base_dir); cache.init()?; + let tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*.html")) + .expect("Parsing error(s)"); + Ok(Self { alphabet, id_length, - cache, + cache: Arc::new(cache), + tera, }) } } -fn routes() -> Router { - let up = up::routes(); - let down = down::routes(); - Router::new() - .route("/", get(|| async { "Hello, World!" })) - .merge(down) - .nest("/up", up) +fn routes() -> Result { + let state = AppState::new()?; + // let up = up::routes(); + // let down = down::routes(); + // let user = user::routes(); + // let admin = admin::routes(); + let router = Router::new().route("/extrac", get(test_extrac)); + // .merge(down) + // .nest("/up", up) + // .nest("/user", user) + // .nest("/admin", admin) + let meow = down::routes(router); + + Ok(meow.with_state(state)) +} + +async fn test_extrac(State(state): State) -> String { + state.id_length.to_string() } #[tokio::main] @@ -75,7 +98,7 @@ async fn main() -> anyhow::Result<()> { .parse() .expect("LISTEN_ADDR parse error"); - let app = routes(); + let app = routes()?; axum::Server::bind(&addr) .serve(app.into_make_service()) diff --git a/src/up.rs b/src/up.rs index 3bb424f..f38de9d 100644 --- a/src/up.rs +++ b/src/up.rs @@ -1,8 +1,11 @@ -use axum::{routing::post, Router}; +use axum::{routing::{post, get}, Router}; pub(crate) fn routes() -> Router { Router::new() .route("/text", post(todo!())) .route("/file", post(todo!())) .route("/link", post(todo!())) + .route("/text/:id", get(todo!()).post(todo!())) + .route("/file/:id", get(todo!()).post(todo!())) + .route("/link/:id", get(todo!()).post(todo!())) } diff --git a/src/user.rs b/src/user.rs new file mode 100644 index 0000000..fd3b539 --- /dev/null +++ b/src/user.rs @@ -0,0 +1,7 @@ +use axum::{Router, routing::post}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/password", post(todo!())) + .route("/login", post(todo!())) +} diff --git a/templates/base.html b/templates/base.html index 3a3d7f8..88aa3bb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -10,13 +10,13 @@ {% endblock head %} - + {% block body %}
{% block header %}{% endblock header %}
- {% block main %}{% enblock main %} + {% block main %}{% endblock main %}