diff --git a/Cargo.lock b/Cargo.lock index 44d4a3b..2c5c92e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -996,9 +996,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -1470,9 +1470,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", diff --git a/src/main.rs b/src/main.rs index 5f5c990..231ad7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ mod database; -mod time_string; +mod strings; mod settings; use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}, time::Duration}; @@ -8,12 +8,12 @@ use chrono::{DateTime, Utc}; use database::{clean_loop, Database, MochiFile}; use log::info; use rocket::{ - data::{Limits, ToByteUnit}, form::Form, fs::{FileServer, Options, TempFile}, get, post, response::content::{RawCss, RawJavaScript}, routes, serde::{json::Json, Serialize}, tokio::{self, fs::File, io::AsyncReadExt}, Config, FromForm, State + data::{Limits, ToByteUnit}, form::Form, fs::{FileServer, Options, TempFile}, get, post, response::content::{RawCss, RawJavaScript}, routes, serde::{json::Json, Serialize}, tokio::{self, fs::File, io::AsyncReadExt}, Config, FromForm, State, http::ContentType, }; use settings::Settings; -use time_string::parse_time_string; +use strings::{parse_time_string, to_pretty_time}; use uuid::Uuid; -use maud::{html, Markup, DOCTYPE}; +use maud::{html, Markup, DOCTYPE, PreEscaped}; fn head(page_title: &str) -> Markup { html! { @@ -23,6 +23,7 @@ fn head(page_title: &str) -> Markup { title { (page_title) } // Javascript stuff for client side handling script src="request.js" { } + link rel="icon" type="image/svg+xml" href="favicon.svg"; link rel="stylesheet" href="main.css"; } } @@ -39,35 +40,55 @@ fn form_handler_js() -> RawJavaScript<&'static str> { RawJavaScript(include_str!("static/request.js")) } +#[get("/favicon.svg")] +fn favicon() -> (ContentType, &'static str) { + (ContentType::SVG, include_str!("static/favicon.svg")) +} + #[get("/")] -fn home() -> Markup { +fn home(settings: &State) -> Markup { html! { - (head("Mochi")) + (head("Confetti-Box")) center { - h1 { "Confetti Box" } + h1 { "Confetti-Box 🎉" } + h2 { "Files up to " (settings.max_filesize.bytes()) " in size are allowed!" } + hr; + h3 { "Expire after:" } div id="durationBox" { - - } - - form id="uploadForm" { - label for="fileUpload" - class="button" - onclick="document.getElementById('fileInput').click()" - { - "Upload File" + @for d in &settings.duration.allowed { + button.button.{@if settings.duration.default == *d { "selected" }} + data-duration-seconds=(d.num_seconds()) + { + (PreEscaped(to_pretty_time(d.num_seconds() as u32))) + } } - input id="fileInput" type="file" name="fileUpload" onchange="formSubmit(this.parentNode)" style="display:none;"; - input id="fileDuration" type="text" name="duration" minlength="2" maxlength="7" value="" style="display:none;"; + } + form #uploadForm { + // It's stupid how these can't be styled so they're just hidden here... + input id="fileInput" type="file" name="fileUpload" + onchange="formSubmit(this.parentNode)" data-max-filesize=(settings.max_filesize) style="display:none;"; + input id="fileDuration" type="text" name="duration" minlength="2" + maxlength="7" value=(settings.duration.default) style="display:none;"; + } + button.main_file_upload onclick="document.getElementById('fileInput').click()" { + h4 { "Upload File" } + p { "Click or Drag and Drop" } + } + hr; + + h3 { "Uploaded Files" } + div #uploadedFilesDisplay { + div { p {"File Name Here"} span {" "} div {p {"File Link Here"} button {"copy"}} } } - div class="progress_box" { - progress id="uploadProgress" value="0" max="100" {} - p id="uploadProgressValue" class="progress_value" { "" } - } - - div id="uploadedFilesDisplay" { - h2 { "Uploaded Files" } + hr; + footer { + p {a href="https://github.com/G2-Games/confetti-box" {"Source"}} + p {a href="https://g2games.dev/" {"My Website"}} + p {a href="#" {"Links"}} + p {a href="#" {"Go"}} + p {a href="#" {"Here"}} } } } @@ -101,6 +122,14 @@ async fn handle_upload( })) } + if settings.duration.restrict_to_allowed && !settings.duration.allowed.contains(&t) { + return Ok(Json(ClientResponse { + status: false, + response: "Duration is disallowed", + ..Default::default() + })) + } + t } else { return Ok(Json(ClientResponse { @@ -247,7 +276,7 @@ async fn main() { let rocket = rocket::build() .mount( config.server.root_path.clone() + "/", - routes![home, handle_upload, form_handler_js, stylesheet, server_info] + routes![home, handle_upload, form_handler_js, stylesheet, server_info, favicon] ) .mount( config.server.root_path.clone() + "/files", diff --git a/src/settings.rs b/src/settings.rs index 8b4192b..1782afa 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -3,6 +3,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}; +use rocket::data::ToByteUnit; /// A response to the client from the server #[derive(Deserialize, Serialize, Debug)] @@ -39,9 +40,9 @@ pub struct Settings { impl Default for Settings { fn default() -> Self { Self { - max_filesize: 128_000_000, // 128 MB - duration: DurationSettings::default(), + max_filesize: 1.megabytes().into(), // 128 MB overwrite: true, + duration: DurationSettings::default(), server: ServerSettings::default(), path: "./settings.toml".into(), database_path: "./database.mochi".into(), @@ -97,7 +98,7 @@ impl Default for ServerSettings { Self { address: "127.0.0.1".into(), root_path: "/".into(), - port: 8955 + port: 8950 } } } @@ -115,10 +116,14 @@ pub struct DurationSettings { #[serde_as(as = "serde_with::DurationSeconds")] pub default: TimeDelta, - /// List of allowed durations. An empty list means any are allowed. + /// List of recommended lifetimes #[serde(default)] #[serde_as(as = "Vec>")] pub allowed: Vec, + + /// Restrict the input durations to the allowed ones or not + #[serde(default)] + pub restrict_to_allowed: bool, } impl Default for DurationSettings { @@ -133,6 +138,7 @@ impl Default for DurationSettings { TimeDelta::days(1), TimeDelta::days(2), ], + restrict_to_allowed: true, } } } diff --git a/src/static/favicon.svg b/src/static/favicon.svg new file mode 100644 index 0000000..a4b8305 --- /dev/null +++ b/src/static/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/main.css b/src/static/main.css index 497267a..33454e0 100644 --- a/src/static/main.css +++ b/src/static/main.css @@ -4,8 +4,32 @@ body { font-family: sans-serif; } -h1 { +center { + margin: auto; + max-width: 500px; +} +footer { + display: flex; + width: fit-content; + + p { + border-right: 1px dotted grey; + padding: 0 10px; + } + + p:last-child { + border-right: none; + } +} + +h1 { + font-size: 3em; + font-weight: bolder; +} + +button p { + margin: 0; } .button { @@ -19,11 +43,38 @@ h1 { border-radius: 5px; } -.button:hover { - background-color: #CCC; +button.button { + width: 50px; + height: 50px; +} + +button:hover { + filter: brightness(0.9); +} + +button.main_file_upload { + border: 1px solid grey; + border-radius: 10px; + margin: 20px 0; + width: 250px; + height: 75px; + cursor: pointer; + background-color: #84E5FF; + + h4 { + margin: 0; + font-size: 1.9em; + font-weight: bold; + } +} + +.button.selected { + background-color: #84FFAE; + border: 2px dashed grey; } #durationBox { + margin-top: 0; display: flex; flex-direction: row; width: fit-content; @@ -36,3 +87,43 @@ h1 { height: 40px; vertical-align: center; } + +.progress_box { + display: flex; + width: 300px; + gap: 5px; + align-items: center; + height: 70px; + + progress { + width: 100%; + } + + .progress_value { + width: 90px; + margin: 0; + } +} + +#uploadedFilesDisplay { + text-align: left; + min-height: 2em; +} + +#uploadedFilesDisplay div { + display: flex; + flex-direction: row; + gap: 10px; +} + +#uploadedFilesDisplay > div > span { + flex-grow: 2; + border-top: 2px dotted grey; + height: 0px; + margin: auto; +} + +#uploadedFilesDisplay button { + height: 50px; + width: 50px; +} diff --git a/src/static/request.js b/src/static/request.js index 9d717e0..93833bc 100644 --- a/src/static/request.js +++ b/src/static/request.js @@ -13,7 +13,7 @@ let CAPABILITIES; async function formSubmit(form) { if (uploadInProgress) { - return; + return; // TODO: REMOVE THIS ONCE MULTIPLE CAN WORK! } // Get file size and don't upload if it's too large @@ -57,6 +57,7 @@ function networkErrorHandler(_err) { function uploadComplete(response) { let target = response.target; + progressBar.value = 0; if (target.status === 200) { const response = JSON.parse(target.responseText); @@ -93,10 +94,9 @@ function uploadProgress(progress) { } } +// This is the entrypoint for everything basically document.addEventListener("DOMContentLoaded", function(_event){ document.getElementById("uploadForm").addEventListener("submit", formSubmit); - progressBar = document.getElementById("uploadProgress"); - progressValue = document.getElementById("uploadProgressValue"); statusNotifier = document.getElementById("uploadStatus"); uploadedFilesDisplay = document.getElementById("uploadedFilesDisplay"); durationBox = document.getElementById("durationBox"); @@ -106,27 +106,32 @@ document.addEventListener("DOMContentLoaded", function(_event){ 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); + var hour = Math.floor((seconds - (days * 86400)) / 3600); + var mins = Math.floor((seconds - (hour * 3600) - (days * 86400)) / 60); + var secs = seconds - (hour * 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(hour == 0) {hour = "";} else if(hour == 1) {hour += "
hour"} else {hour += "
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(); + return (days + " " + hour + " " + 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"); + const durationButtons = durationBox.getElementsByTagName("button"); + for (const b of durationButtons) { + b.addEventListener("click", function (_e) { + if (this.classList.contains("selected")) { + return + } + let selected = this.parentNode.getElementsByClassName("selected"); + selected[0].classList.remove("selected"); + + file_duration.value = this.dataset.durationSeconds + "s"; + this.classList.add("selected"); + }); } } diff --git a/src/strings.rs b/src/strings.rs new file mode 100644 index 0000000..e142a66 --- /dev/null +++ b/src/strings.rs @@ -0,0 +1,50 @@ +use std::error::Error; + +use chrono::TimeDelta; + +pub fn parse_time_string(string: &str) -> Result> { + if string.len() > 7 { + return Err("Not valid time string".into()) + } + + let unit = string.chars().last(); + let multiplier = if let Some(u) = unit { + if !u.is_ascii_alphabetic() { + return Err("Not valid time string".into()) + } + + match u { + 'D' | 'd' => TimeDelta::days(1), + 'H' | 'h' => TimeDelta::hours(1), + 'M' | 'm' => TimeDelta::minutes(1), + 'S' | 's' => TimeDelta::seconds(1), + _ => return Err("Not valid time string".into()), + } + } else { + return Err("Not valid time string".into()) + }; + + let time = if let Ok(n) = string[..string.len() - 1].parse::() { + n + } else { + return Err("Not valid time string".into()) + }; + + let final_time = multiplier * time; + + Ok(final_time) +} + +pub fn to_pretty_time(seconds: u32) -> String { + let days = (seconds as f32 / 86400.0).floor(); + let hour = ((seconds as f32 - (days as f32 * 86400.0)) / 3600.0).floor(); + let mins = ((seconds as f32 - (hour * 3600.0) - (days * 86400.0)) / 60.0).floor(); + let secs = seconds as f32 - (hour as f32 * 3600.0) - (mins as f32 * 60.0) - (days as f32 * 86400.0); + + let days = if days == 0.0 {"".to_string()} else if days == 1.0 {days.to_string() + "
day"} else {days.to_string() + "
days"}; + let hour = if hour == 0.0 {"".to_string()} else if hour == 1.0 {hour.to_string() + "
hour"} else {hour.to_string() + "
hours"}; + let mins = if mins == 0.0 {"".to_string()} else if mins == 1.0 {mins.to_string() + "
minute"} else {mins.to_string() + "
minutes"}; + let secs = if secs == 0.0 {"".to_string()} else if secs == 1.0 {secs.to_string() + "
second"} else {secs.to_string() + "
seconds"}; + + (days + " " + &hour + " " + &mins + " " + &secs).trim().to_string() +} diff --git a/src/time_string.rs b/src/time_string.rs deleted file mode 100644 index 0d82045..0000000 --- a/src/time_string.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::error::Error; - -use chrono::TimeDelta; - -pub fn parse_time_string(string: &str) -> Result> { - if string.len() > 7 { - return Err("Not valid time string".into()) - } - - let unit = string.chars().last(); - let multiplier = if let Some(u) = unit { - if !u.is_ascii_alphabetic() { - return Err("Not valid time string".into()) - } - - match u { - 'D' | 'd' => TimeDelta::days(1), - 'H' | 'h' => TimeDelta::hours(1), - 'M' | 'm' => TimeDelta::minutes(1), - 'S' | 's' => TimeDelta::seconds(1), - _ => return Err("Not valid time string".into()), - } - } else { - return Err("Not valid time string".into()) - }; - - let time = if let Ok(n) = string[..string.len() - 1].parse::() { - n - } else { - return Err("Not valid time string".into()) - }; - - let final_time = multiplier * time; - - Ok(final_time) -}