diff --git a/Cargo.lock b/Cargo.lock index c555fbd..44d4a3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "binascii" version = "0.1.4" @@ -244,6 +250,41 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -251,6 +292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -456,13 +498,19 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.0" @@ -481,6 +529,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.12" @@ -573,6 +627,23 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.6.0" @@ -580,7 +651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.0", "serde", ] @@ -740,6 +811,8 @@ dependencies = [ "log", "maud", "rocket", + "serde", + "serde_with", "toml", "uuid", ] @@ -1069,7 +1142,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap", + "indexmap 2.6.0", "log", "memchr", "multer", @@ -1101,7 +1174,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap", + "indexmap 2.6.0", "proc-macro2", "quote", "rocket_http", @@ -1121,7 +1194,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper", - "indexmap", + "indexmap 2.6.0", "log", "memchr", "pear", @@ -1182,18 +1255,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.211" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.211" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", @@ -1221,6 +1294,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1294,6 +1397,12 @@ dependencies = [ "loom", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.82" @@ -1438,7 +1547,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", diff --git a/Cargo.toml b/Cargo.toml index a83d903..f59744c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ chrono = { version = "0.4.38", features = ["serde"] } log = "0.4" maud = { version = "0.26", features = ["rocket"] } rocket = { version = "0.5", features = ["json"] } +serde = { version = "1.0.213", features = ["derive"] } +serde_with = { version = "3.11.0", features = ["chrono_0_4"] } toml = "0.8.19" uuid = { version = "1.11.0", features = ["v4"] } diff --git a/src/main.rs b/src/main.rs index dc3d408..5f5c990 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod settings; use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}, time::Duration}; use blake3::Hash; -use chrono::{DateTime, TimeDelta, Utc}; +use chrono::{DateTime, Utc}; use database::{clean_loop, Database, MochiFile}; use log::info; use rocket::{ @@ -45,17 +45,20 @@ fn home() -> Markup { (head("Mochi")) center { - div id="durationOptions" { + h1 { "Confetti Box" } + div id="durationBox" { } form id="uploadForm" { - label for="fileUpload" class="file_upload" onclick="document.getElementById('fileInput').click()" { + label for="fileUpload" + class="button" + onclick="document.getElementById('fileInput').click()" + { "Upload File" } input id="fileInput" type="file" name="fileUpload" onchange="formSubmit(this.parentNode)" style="display:none;"; - br; - input type="text" name="duration" minlength="2" maxlength="4"; + input id="fileDuration" type="text" name="duration" minlength="2" maxlength="7" value="" style="display:none;"; } div class="progress_box" { @@ -64,13 +67,13 @@ fn home() -> Markup { } div id="uploadedFilesDisplay" { - h2 class="sep center" { "Uploaded Files" } + h2 { "Uploaded Files" } } } } } -#[derive(FromForm)] +#[derive(Debug, FromForm)] struct Upload<'r> { #[field(name = "fileUpload")] file: TempFile<'r>, @@ -83,16 +86,22 @@ struct Upload<'r> { #[post("/upload", data = "")] async fn handle_upload( mut file_data: Form>, - db: &State>> + db: &State>>, + settings: &State, ) -> Result, std::io::Error> { let mut out_path = PathBuf::from("files/"); + let mut temp_dir = settings.temp_dir.clone(); let expire_time = if let Ok(t) = parse_time_string(&file_data.expire_time) { - if t < TimeDelta::days(365) { - t - } else { - TimeDelta::days(365) + if t > settings.duration.maximum { + return Ok(Json(ClientResponse { + status: false, + response: "Duration larger than maximum", + ..Default::default() + })) } + + t } else { return Ok(Json(ClientResponse { status: false, @@ -101,13 +110,20 @@ async fn handle_upload( })) }; + // TODO: Properly sanitize this... + let raw_name = &*file_data.file + .raw_name() + .unwrap() + .dangerous_unsafe_unsanitized_raw() + .as_str() + .to_string(); + // Get temp path and hash it - let temp_filename = "temp_files/".to_owned() + &Uuid::new_v4().to_string(); + temp_dir.push(&Uuid::new_v4().to_string()); + let temp_filename = temp_dir; file_data.file.persist_to(&temp_filename).await?; let hash = hash_file(&temp_filename).await?; - // TODO: Properly sanitize this... - let raw_name = file_data.file.raw_name().unwrap().dangerous_unsafe_unsanitized_raw().as_str(); let filename = get_id( raw_name, hash.0 @@ -122,6 +138,10 @@ async fn handle_upload( expire_time ); + if db.read().unwrap().files.contains_key(&constructed_file.get_key()) { + info!("Key already in DB"); + } + // Move it to the new proper place std::fs::rename(temp_filename, out_path)?; @@ -130,8 +150,8 @@ async fn handle_upload( Ok(Json(ClientResponse { status: true, - name: Some(constructed_file.name().clone()), - url: Some("files/".to_string() + &filename), + name: constructed_file.name().clone(), + url: "files/".to_string() + &filename, expires: Some(constructed_file.get_expiry()), ..Default::default() })) @@ -146,10 +166,10 @@ struct ClientResponse { pub response: &'static str, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, + #[serde(skip_serializing_if = "String::is_empty")] + pub name: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub url: String, #[serde(skip_serializing_if = "Option::is_none")] pub expires: Option>, } @@ -181,9 +201,9 @@ async fn hash_file>(input: &P) -> Result<(Hash, usize), std::io:: fn server_info(settings: &State) -> Json { Json(ServerInfo { max_filesize: settings.max_filesize, - max_duration: settings.duration.maximum, - default_duration: settings.duration.default, - allowed_durations: settings.duration.allowed.clone(), + max_duration: settings.duration.maximum.num_seconds() as u32, + default_duration: settings.duration.default.num_seconds() as u32, + allowed_durations: settings.duration.allowed.clone().into_iter().map(|t| t.num_seconds() as u32).collect(), }) } diff --git a/src/settings.rs b/src/settings.rs index 2a679d0..8b4192b 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,5 +1,7 @@ use std::{fs::{self, File}, io::{self, Read, Write}, path::{Path, PathBuf}}; +use chrono::TimeDelta; +use serde_with::serde_as; use rocket::serde::{Deserialize, Serialize}; /// A response to the client from the server @@ -10,6 +12,11 @@ pub struct Settings { #[serde(default)] pub max_filesize: u64, + /// Is overwiting already uploaded files with the same hash allowed, or is + /// this a no-op? + #[serde(default)] + pub overwrite: bool, + /// Settings pertaining to duration information pub duration: DurationSettings, @@ -34,6 +41,7 @@ impl Default for Settings { Self { max_filesize: 128_000_000, // 128 MB duration: DurationSettings::default(), + overwrite: true, server: ServerSettings::default(), path: "./settings.toml".into(), database_path: "./database.mochi".into(), @@ -94,29 +102,37 @@ impl Default for ServerSettings { } } +#[serde_as] #[derive(Deserialize, Serialize, Debug)] -#[serde(crate = "rocket::serde")] pub struct DurationSettings { /// Maximum file lifetime, seconds #[serde(default)] - pub maximum: u32, + #[serde_as(as = "serde_with::DurationSeconds")] + pub maximum: TimeDelta, /// Default file lifetime, seconds #[serde(default)] - pub default: u32, + #[serde_as(as = "serde_with::DurationSeconds")] + pub default: TimeDelta, /// List of allowed durations. An empty list means any are allowed. #[serde(default)] - pub allowed: Vec, + #[serde_as(as = "Vec>")] + pub allowed: Vec, } impl Default for DurationSettings { fn default() -> Self { Self { - maximum: 259_200, // 72 hours - default: 21_600, // 6 hours + maximum: TimeDelta::days(3), // 72 hours + default: TimeDelta::hours(6), // 6 hours // 1 hour, 6 hours, 24 hours, and 48 hours - allowed: vec![3600, 21_600, 86_400, 172_800], + allowed: vec![ + TimeDelta::hours(1), + TimeDelta::hours(6), + TimeDelta::days(1), + TimeDelta::days(2), + ], } } } diff --git a/src/static/main.css b/src/static/main.css index 4ba4449..497267a 100644 --- a/src/static/main.css +++ b/src/static/main.css @@ -1,16 +1,38 @@ @import url('https://g2games.dev/assets/fonts/fonts.css'); body { - background-color: black; - color: white; + font-family: sans-serif; } -.file_upload { +h1 { + +} + +.button { display: block; width: fit-content; - padding: 5px; + padding: 2px; border: 1px solid grey; + background-color: #EEE; cursor: pointer; - margin: 10px; + margin: 5px; border-radius: 5px; } + +.button:hover { + background-color: #CCC; +} + +#durationBox { + display: flex; + flex-direction: row; + width: fit-content; + gap: 10px; +} + +#durationBox > p { + font-size: 10pt; + width: 40px; + height: 40px; + vertical-align: center; +} diff --git a/src/static/request.js b/src/static/request.js index ffb3910..9d717e0 100644 --- a/src/static/request.js +++ b/src/static/request.js @@ -2,6 +2,7 @@ let progressBar; let progressValue; let statusNotifier; let uploadedFilesDisplay; +let durationBox; let uploadInProgress = false; @@ -9,11 +10,6 @@ const TOO_LARGE_TEXT = "File is too large!"; const ERROR_TEXT = "An error occured!"; let CAPABILITIES; -async function getServerCapabilities() { - CAPABILITIES = await fetch("info").then((response) => response.json()); - console.log(CAPABILITIES); -} -getServerCapabilities(); async function formSubmit(form) { if (uploadInProgress) { @@ -103,4 +99,34 @@ document.addEventListener("DOMContentLoaded", function(_event){ progressValue = document.getElementById("uploadProgressValue"); statusNotifier = document.getElementById("uploadStatus"); uploadedFilesDisplay = document.getElementById("uploadedFilesDisplay"); + durationBox = document.getElementById("durationBox"); + + getServerCapabilities(); }); + +function toPrettyTime(seconds) { + var days = Math.floor(seconds / 86400); + var hours = Math.floor((seconds - (days * 86400)) / 3600); + var mins = Math.floor((seconds - (hours * 3600) - (days * 86400)) / 60); + var secs = seconds - (hours * 3600) - (mins * 60) - (days * 86400); + + if(days == 0) {days = "";} else if(days == 1) {days += "
day"} else {days += "
days"} + if(hours == 0) {hours = "";} else if(hours == 1) {hours += "
hour"} else {hours += "
hours"} + if(mins == 0) {mins = "";} else if(mins == 1) {mins += "
minute"} else {mins += "
minutes"} + if(secs == 0) {secs = "";} else if(secs == 1) {secs += "
second"} else {secs += "
seconds"} + + return (days + " " + hours + " " + mins + " " + secs).trim(); +} + +async function getServerCapabilities() { + CAPABILITIES = await fetch("info").then((response) => response.json()); + + let file_duration = document.getElementById("fileDuration"); + file_duration.value = CAPABILITIES.default_duration + "s"; + + for (duration in CAPABILITIES.allowed_durations) { + const durationOption = durationBox.appendChild(document.createElement("p")); + durationOption.innerHTML = toPrettyTime(CAPABILITIES.allowed_durations[duration]); + durationOption.classList.add("button"); + } +} diff --git a/src/time_string.rs b/src/time_string.rs index 9549f46..0d82045 100644 --- a/src/time_string.rs +++ b/src/time_string.rs @@ -3,7 +3,7 @@ use std::error::Error; use chrono::TimeDelta; pub fn parse_time_string(string: &str) -> Result> { - if string.len() > 5 { + if string.len() > 7 { return Err("Not valid time string".into()) }