Switch database to begin using CBOR, and begin work on chunked uploads

This commit is contained in:
G2-Games 2024-10-30 16:28:34 -05:00
parent 3892975fc2
commit 2b8d7255b8
4 changed files with 82 additions and 51 deletions

View file

@ -4,9 +4,9 @@ version = "0.1.2"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
blake3 = { version = "1.5.4", features = ["mmap", "rayon", "serde"] } blake3 = { version = "1.5.4", features = ["mmap", "rayon", "serde"] }
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
ciborium = "0.2.2"
file-format = { version = "0.25.0", features = ["reader"] } file-format = { version = "0.25.0", features = ["reader"] }
log = "0.4" log = "0.4"
lz4_flex = "0.11.3" lz4_flex = "0.11.3"
@ -16,7 +16,7 @@ rocket = { version = "0.5", features = ["json"] }
serde = { version = "1.0.213", features = ["derive"] } serde = { version = "1.0.213", features = ["derive"] }
serde_with = { version = "3.11.0", features = ["chrono_0_4"] } serde_with = { version = "3.11.0", features = ["chrono_0_4"] }
toml = "0.8.19" toml = "0.8.19"
uuid = { version = "1.11.0", features = ["v4"] } uuid = { version = "1.11.0", features = ["serde", "v4"] }
[profile.production] [profile.production]
inherits = "release" inherits = "release"

View file

@ -7,30 +7,29 @@ use std::{
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use bincode::{config::Configuration, decode_from_std_read, encode_into_std_write, Decode, Encode};
use blake3::Hash; use blake3::Hash;
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use ciborium::{from_reader, into_writer};
use log::{error, info, warn}; use log::{error, info, warn};
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use rocket::{ use rocket::{
serde::{Deserialize, Serialize}, form::{self, FromFormField, ValueField}, serde::{Deserialize, Serialize}, tokio::{select, sync::mpsc::Receiver, time}
tokio::{select, sync::mpsc::Receiver, time},
}; };
use serde_with::{serde_as, DisplayFromStr}; use serde_with::{serde_as, DisplayFromStr};
use uuid::Uuid;
const BINCODE_CFG: Configuration = bincode::config::standard(); #[derive(Debug, Clone)]
#[derive(Deserialize, Serialize)]
#[derive(Debug, Clone, Decode, Encode)]
pub struct Mochibase { pub struct Mochibase {
path: PathBuf, path: PathBuf,
/// Every hash in the database along with the [`Mmid`]s associated with them /// Every hash in the database along with the [`Mmid`]s associated with them
#[bincode(with_serde)]
hashes: HashMap<Hash, HashSet<Mmid>>, hashes: HashMap<Hash, HashSet<Mmid>>,
/// All entries in the database /// All entries in the database
#[bincode(with_serde)]
entries: HashMap<Mmid, MochiFile>, entries: HashMap<Mmid, MochiFile>,
chunks: HashMap<Uuid, DateTime<Utc>>,
} }
impl Mochibase { impl Mochibase {
@ -39,6 +38,7 @@ impl Mochibase {
path: path.as_ref().to_path_buf(), path: path.as_ref().to_path_buf(),
entries: HashMap::new(), entries: HashMap::new(),
hashes: HashMap::new(), hashes: HashMap::new(),
chunks: HashMap::new(),
}; };
// Save the database initially after creating it // Save the database initially after creating it
@ -52,7 +52,7 @@ impl Mochibase {
let file = File::open(path)?; let file = File::open(path)?;
let mut lz4_file = lz4_flex::frame::FrameDecoder::new(file); let mut lz4_file = lz4_flex::frame::FrameDecoder::new(file);
decode_from_std_read(&mut lz4_file, BINCODE_CFG) from_reader(&mut lz4_file)
.map_err(|e| io::Error::other(format!("failed to open database: {e}"))) .map_err(|e| io::Error::other(format!("failed to open database: {e}")))
} }
@ -70,7 +70,7 @@ impl Mochibase {
// Create a file and write the LZ4 compressed stream into it // Create a file and write the LZ4 compressed stream into it
let file = File::create(self.path.with_extension("bkp"))?; let file = File::create(self.path.with_extension("bkp"))?;
let mut lz4_file = lz4_flex::frame::FrameEncoder::new(file); let mut lz4_file = lz4_flex::frame::FrameEncoder::new(file);
encode_into_std_write(self, &mut lz4_file, BINCODE_CFG) into_writer(self, &mut lz4_file)
.map_err(|e| io::Error::other(format!("failed to save database: {e}")))?; .map_err(|e| io::Error::other(format!("failed to save database: {e}")))?;
lz4_file.try_finish()?; lz4_file.try_finish()?;
@ -154,7 +154,8 @@ impl Mochibase {
/// An entry in the database storing metadata about a file /// An entry in the database storing metadata about a file
#[serde_as] #[serde_as]
#[derive(Debug, Clone, Decode, Encode, Deserialize, Serialize)] #[derive(Debug, Clone)]
#[derive(Deserialize, Serialize)]
pub struct MochiFile { pub struct MochiFile {
/// A unique identifier describing this file /// A unique identifier describing this file
mmid: Mmid, mmid: Mmid,
@ -166,16 +167,13 @@ pub struct MochiFile {
mime_type: String, mime_type: String,
/// The Blake3 hash of the file /// The Blake3 hash of the file
#[bincode(with_serde)]
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "DisplayFromStr")]
hash: Hash, hash: Hash,
/// The datetime when the file was uploaded /// The datetime when the file was uploaded
#[bincode(with_serde)]
upload_datetime: DateTime<Utc>, upload_datetime: DateTime<Utc>,
/// The datetime when the file is set to expire /// The datetime when the file is set to expire
#[bincode(with_serde)]
expiry_datetime: DateTime<Utc>, expiry_datetime: DateTime<Utc>,
} }
@ -285,7 +283,8 @@ pub async fn clean_loop(
/// A unique identifier for an entry in the database, 8 characters long, /// A unique identifier for an entry in the database, 8 characters long,
/// consists of ASCII alphanumeric characters (`a-z`, `A-Z`, and `0-9`). /// consists of ASCII alphanumeric characters (`a-z`, `A-Z`, and `0-9`).
#[derive(Debug, PartialEq, Eq, Clone, Hash, Decode, Encode, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Hash)]
#[derive(Deserialize, Serialize)]
pub struct Mmid(String); pub struct Mmid(String);
impl Mmid { impl Mmid {
@ -347,3 +346,12 @@ impl std::fmt::Display for Mmid {
write!(f, "{}", self.0) write!(f, "{}", self.0)
} }
} }
#[rocket::async_trait]
impl<'r> FromFormField<'r> for Mmid {
fn from_value(field: ValueField<'r>) -> form::Result<'r, Self> {
Ok(
Self::try_from(field.value).map_err(|_| form::Error::validation("Invalid MMID"))?
)
}
}

View file

@ -8,20 +8,17 @@ pub mod utils;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use crate::database::{Mmid, MochiFile, Mochibase}; use crate::{
use crate::pages::{footer, head}; database::{Mmid, MochiFile, Mochibase},
use crate::settings::Settings; pages::{footer, head},
use crate::strings::{parse_time_string, to_pretty_time}; settings::Settings,
use crate::utils::hash_file; strings::{parse_time_string, to_pretty_time},
utils::hash_file,
};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use maud::{html, Markup, PreEscaped}; use maud::{html, Markup, PreEscaped};
use rocket::{ use rocket::{
data::ToByteUnit, data::ToByteUnit, form::Form, fs::TempFile, get, post, serde::{json::Json, Serialize}, FromForm, State
form::Form,
fs::TempFile,
get, post,
serde::{json::Json, Serialize},
FromForm, State,
}; };
use uuid::Uuid; use uuid::Uuid;
@ -78,6 +75,34 @@ pub struct Upload<'r> {
file: TempFile<'r>, file: TempFile<'r>,
} }
/// A response to the client from the server
#[derive(Serialize, Default, Debug)]
pub struct ClientResponse {
/// Success or failure
pub status: bool,
pub response: &'static str,
#[serde(skip_serializing_if = "str::is_empty")]
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mmid: Option<Mmid>,
#[serde(skip_serializing_if = "str::is_empty")]
pub hash: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<DateTime<Utc>>,
}
impl ClientResponse {
fn failure(response: &'static str) -> Self {
Self {
status: false,
response,
..Default::default()
}
}
}
/// Handle a file upload and store it /// Handle a file upload and store it
#[post("/upload", data = "<file_data>")] #[post("/upload", data = "<file_data>")]
pub async fn handle_upload( pub async fn handle_upload(
@ -152,30 +177,28 @@ pub async fn handle_upload(
})) }))
} }
/// A response to the client from the server pub struct ChunkedResponse {
#[derive(Serialize, Default, Debug)] /// UUID used for associating the chunk with the final file
pub struct ClientResponse { uuid: Uuid,
/// Success or failure
pub status: bool,
pub response: &'static str, /// Valid max chunk size in bytes
chunk_size: u64,
#[serde(skip_serializing_if = "str::is_empty")] /// The datetime at which the upload will be invalidated unless new
pub name: String, /// chunks have come in
#[serde(skip_serializing_if = "Option::is_none")] timeout: DateTime<Utc>,
pub mmid: Option<Mmid>,
#[serde(skip_serializing_if = "str::is_empty")] /// The datetime at which the upload will be invalidated even if new
pub hash: String, /// chunks have come in
#[serde(skip_serializing_if = "Option::is_none")] hard_timeout: DateTime<Utc>,
pub expires: Option<DateTime<Utc>>,
} }
impl ClientResponse { /// Start a chunked upload. Response contains all the info you need to continue
fn failure(response: &'static str) -> Self { /// uploading chunks.
Self { #[get("/upload/chunked")]
status: false, pub async fn chunked_start() -> Result<Json<ClientResponse>, std::io::Error> {
response,
..Default::default()
}
} todo!()
} }

View file

@ -48,7 +48,7 @@ pub struct Settings {
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
Self { Self {
max_filesize: 1.megabytes().into(), // 128 MB max_filesize: 1.megabytes().into(), // 1 MB
overwrite: true, overwrite: true,
duration: DurationSettings::default(), duration: DurationSettings::default(),
server: ServerSettings::default(), server: ServerSettings::default(),