From af925677f97896d7748054e0876172b96fc7f738 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Mon, 21 Oct 2024 16:28:10 -0500 Subject: [PATCH] Added database for backend operations --- Cargo.lock | 31 +++++++++++- Cargo.toml | 14 ++++-- src/database.rs | 113 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 84 ++++++++++++++++++++++---------- src/static/main.css | 3 +- 5 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 src/database.rs diff --git a/Cargo.lock b/Cargo.lock index b80020c..5aadeee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,25 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" +[[package]] +name = "bincode" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +dependencies = [ + "bincode_derive", + "serde", +] + +[[package]] +name = "bincode_derive" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -145,6 +164,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", + "serde", ] [[package]] @@ -196,6 +216,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -713,11 +734,13 @@ dependencies = [ name = "mochihost" version = "0.1.0" dependencies = [ + "bincode", "blake3", "chrono", + "log", "maud", - "rand", "rocket", + "serde", "uuid", ] @@ -1546,6 +1569,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 6e9dd22..7541250 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,17 @@ version = "0.1.0" edition = "2021" [dependencies] -blake3 = "1.5.4" -chrono = "0.4.38" +bincode = { version = "2.0.0-rc.3", features = ["serde"] } +blake3 = { version = "1.5.4", features = ["serde"] } +chrono = { version = "0.4.38", features = ["serde"] } +log = "0.4" maud = { version = "0.26", features = ["rocket"] } -rand = "0.8" rocket = "0.5" +serde = { version = "1.0.210", features = ["derive"] } uuid = { version = "1.11.0", features = ["v4"] } + +[profile.production] +inherits = "release" +lto = true +opt-level = "z" +codegen-units = 1 diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..dc3abc2 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,113 @@ +use std::{collections::HashMap, fs::{self, File}, path::{Path, PathBuf}}; + +use bincode::{config::Configuration, decode_from_std_read, encode_into_std_write, Decode, Encode}; +use chrono::{DateTime, TimeDelta, Utc}; +use blake3::Hash; + +const BINCODE_CFG: Configuration = bincode::config::standard(); + +#[derive(Debug, Clone)] +#[derive(Decode, Encode)] +pub struct Database { + path: PathBuf, + #[bincode(with_serde)] + pub files: HashMap +} + +impl Database { + pub fn new>(path: &P) -> Self { + let mut file = File::create_new(path).expect("Could not create database!"); + + let output = Self { + path: path.as_ref().to_path_buf(), + files: HashMap::new() + }; + + encode_into_std_write(&output, &mut file, BINCODE_CFG).expect("Could not write database!"); + + output + } + + pub fn open>(path: &P) -> Self { + if !path.as_ref().exists() { + Self::new(path) + } else { + let mut file = File::open(path).expect("Could not get database file!"); + decode_from_std_read(&mut file, BINCODE_CFG).expect("Could not decode database") + } + } + + pub fn save(&self) { + let mut out_path = self.path.clone(); + out_path.set_extension(".bkp"); + let mut file = File::create(&out_path).expect("Could not save!"); + encode_into_std_write(&self, &mut file, BINCODE_CFG).expect("Could not write out!"); + + fs::rename(out_path, &self.path).unwrap(); + } +} + +#[derive(Debug, Clone)] +#[derive(Decode, Encode)] +#[derive(serde::Deserialize, serde::Serialize)] +pub struct MochiFile { + /// The original name of the file + name: String, + + /// Size of the file in bytes + size: usize, + + /// The location on disk (for deletion and management) + filename: PathBuf, + + /// The hashed contents of the file as a Blake3 hash + #[bincode(with_serde)] + hash: Hash, + + /// The datetime when the file was uploaded + #[bincode(with_serde)] + upload_datetime: DateTime, + + /// The datetime when the file is set to expire + #[bincode(with_serde)] + expiry_datetime: DateTime, +} + +impl MochiFile { + /// Create a new file that expires in `expiry`. + pub fn new_with_expiry( + name: &str, + size: usize, + hash: Hash, + filename: PathBuf, + expire_duration: TimeDelta + ) -> Self { + let current = Utc::now(); + let expiry = current + expire_duration; + + Self { + name: name.to_string(), + size, + filename, + hash, + upload_datetime: current, + expiry_datetime: expiry, + } + } + + pub fn get_key(&self) -> MochiKey { + MochiKey { + name: self.name.clone(), + hash: self.hash + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Decode, Encode)] +#[derive(serde::Deserialize, serde::Serialize)] +pub struct MochiKey { + name: String, + #[bincode(with_serde)] + hash: Hash, +} diff --git a/src/main.rs b/src/main.rs index 9091040..1ecd125 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,13 @@ -use std::path::{Path, PathBuf}; +mod database; + +use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}}; use blake3::Hash; +use chrono::TimeDelta; +use database::{Database, MochiFile}; +use log::info; use maud::{html, Markup, DOCTYPE, PreEscaped}; use rocket::{ - form::Form, fs::{FileServer, Options, TempFile}, get, post, routes, tokio::{fs::File, io::AsyncReadExt}, FromForm + form::Form, fs::{FileServer, Options, TempFile}, get, post, routes, tokio::{fs::File, io::AsyncReadExt}, FromForm, State }; use uuid::Uuid; @@ -10,6 +15,7 @@ fn head(page_title: &str) -> Markup { html! { (DOCTYPE) meta charset="UTF-8"; + meta name="viewport" content="width=device-width, initial-scale=1"; title { (page_title) } // Javascript stuff for client side handling script { (PreEscaped(include_str!("static/form_handler.js"))) } @@ -39,17 +45,6 @@ fn home() -> Markup { } } -struct Database { - files: Vec -} - -struct MochiFile { - /// The original name of the file - name: String, - /// The hashed contents of the file as a Blake3 hash - hash: Hash, -} - #[derive(FromForm)] struct Upload<'r> { #[field(name = "fileUpload")] @@ -58,7 +53,10 @@ struct Upload<'r> { /// Handle a file upload and store it #[post("/upload", data = "")] -async fn handle_upload(mut file_data: Form>) -> Result<(), std::io::Error> { +async fn handle_upload( + mut file_data: Form>, + db: &State>> +) -> Result<(), std::io::Error> { let mut out_path = PathBuf::from("files/"); // Get temp path and hash it @@ -66,42 +64,78 @@ async fn handle_upload(mut file_data: Form>) -> Result<(), std::io::E file_data.file.persist_to(&temp_filename).await?; let hash = hash_file(&temp_filename).await?; - out_path.push(get_filename( + let filename = get_filename( // TODO: Properly sanitize this... file_data.file.raw_name().unwrap().dangerous_unsafe_unsanitized_raw().as_str(), - hash - )); + hash.0 + ); + out_path.push(filename); + + let constructed_file = MochiFile::new_with_expiry( + file_data.file.raw_name().unwrap().dangerous_unsafe_unsanitized_raw().as_str(), + hash.1, + hash.0, + out_path.clone(), + TimeDelta::hours(24) + ); // Move it to the new proper place std::fs::rename(temp_filename, out_path)?; + db.write().unwrap().files.insert(constructed_file.get_key(), constructed_file); + db.write().unwrap().save(); + Ok(()) } -async fn hash_file>(input: &P) -> Result { +/// Get the Blake3 hash of a file, without reading it all into memory, and also get the size +async fn hash_file>(input: &P) -> Result<(Hash, usize), std::io::Error> { let mut file = File::open(input).await?; let mut buf = vec![0; 5000000]; let mut hasher = blake3::Hasher::new(); + let mut total = 0; let mut bytes_read = None; while bytes_read != Some(0) { bytes_read = Some(file.read(&mut buf).await?); + total += bytes_read.unwrap(); hasher.update(&buf[..bytes_read.unwrap()]); - dbg!(bytes_read); } - Ok(hasher.finalize()) + Ok((hasher.finalize(), total)) } /// Get a random filename for use as the uploaded file's name fn get_filename(name: &str, hash: Hash) -> String { - let uuid = hash.to_hex()[0..10].to_string() + "_" + name; - uuid + hash.to_hex()[0..10].to_string() + "_" + name } -#[rocket::launch] -fn launch() -> _ { - rocket::build() +/* +/// Handle a file upload and store it +#[post("/query", data = "")] +async fn handle_upload( + mut file_data: Form>, + db: &State>> +) -> Result<(), std::io::Error> { + + Ok(()) +} +*/ + +#[rocket::main] +async fn main() { + let database = Arc::new(RwLock::new(Database::open(&"database.mochi"))); + let local_db = database.clone(); + + let rocket = rocket::build() + .manage(database) .mount("/", routes![home, handle_upload]) .mount("/files", FileServer::new("files/", Options::Missing | Options::NormalizeDirs)) + .launch() + .await; + + rocket.expect("Server failed to shutdown gracefully"); + + info!("Saving database on shutdown..."); + local_db.write().unwrap().save(); } diff --git a/src/static/main.css b/src/static/main.css index d2fe2dc..32cae77 100644 --- a/src/static/main.css +++ b/src/static/main.css @@ -44,7 +44,8 @@ progress[value]::-webkit-progress-value { .progress-box { display: flex; position: relative; - width: 500px; + max-width: 500px; + width: 80vw; height: 20px; margin: 10px auto;