Added database for backend operations

This commit is contained in:
G2-Games 2024-10-21 16:28:10 -05:00
parent 0681af1633
commit af925677f9
5 changed files with 215 additions and 30 deletions

31
Cargo.lock generated
View file

@ -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"

View file

@ -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

113
src/database.rs Normal file
View file

@ -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<MochiKey, MochiFile>
}
impl Database {
pub fn new<P: AsRef<Path>>(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<P: AsRef<Path>>(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<Utc>,
/// The datetime when the file is set to expire
#[bincode(with_serde)]
expiry_datetime: DateTime<Utc>,
}
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,
}

View file

@ -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<File>
}
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 = "<file_data>")]
async fn handle_upload(mut file_data: Form<Upload<'_>>) -> Result<(), std::io::Error> {
async fn handle_upload(
mut file_data: Form<Upload<'_>>,
db: &State<Arc<RwLock<Database>>>
) -> 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<Upload<'_>>) -> 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<P: AsRef<Path>>(input: &P) -> Result<Hash, std::io::Error> {
/// Get the Blake3 hash of a file, without reading it all into memory, and also get the size
async fn hash_file<P: AsRef<Path>>(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 = "<file_data>")]
async fn handle_upload(
mut file_data: Form<Upload<'_>>,
db: &State<Arc<RwLock<Database>>>
) -> 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();
}

View file

@ -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;