Moved time selection buttons mostly to the backend

This commit is contained in:
G2-Games 2024-10-23 22:43:11 -05:00
parent 6c7763d5e6
commit 2f785d78c1
8 changed files with 234 additions and 88 deletions

8
Cargo.lock generated
View file

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

View file

@ -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<Settings>) -> 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",

View file

@ -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<i64>")]
pub default: TimeDelta,
/// List of allowed durations. An empty list means any are allowed.
/// List of recommended lifetimes
#[serde(default)]
#[serde_as(as = "Vec<serde_with::DurationSeconds<i64>>")]
pub allowed: Vec<TimeDelta>,
/// 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,
}
}
}

1
src/static/favicon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#DD2E44" d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"/><path fill="#EA596E" d="M13 12L.416 32.506l-.282.635.011.011c-.208.403.14 1.223.853 1.937.232.232.473.408.709.557L17 17l-4-5z"/><path fill="#A0041E" d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124-1.473 1.474-6.453-1.118-11.126-5.788-4.671-4.672-7.263-9.654-5.79-11.127 1.474-1.473 6.454 1.119 11.127 5.791z"/><path fill="#AA8DD8" d="M18.59 13.609c-.199.161-.459.245-.734.215-.868-.094-1.598-.396-2.109-.873-.541-.505-.808-1.183-.735-1.862.128-1.192 1.324-2.286 3.363-2.066.793.085 1.147-.17 1.159-.292.014-.121-.277-.446-1.07-.532-.868-.094-1.598-.396-2.11-.873-.541-.505-.809-1.183-.735-1.862.13-1.192 1.325-2.286 3.362-2.065.578.062.883-.057 1.012-.134.103-.063.144-.123.148-.158.012-.121-.275-.446-1.07-.532-.549-.06-.947-.552-.886-1.102.059-.549.55-.946 1.101-.886 2.037.219 2.973 1.542 2.844 2.735-.13 1.194-1.325 2.286-3.364 2.067-.578-.063-.88.057-1.01.134-.103.062-.145.123-.149.157-.013.122.276.446 1.071.532 2.037.22 2.973 1.542 2.844 2.735-.129 1.192-1.324 2.286-3.362 2.065-.578-.062-.882.058-1.012.134-.104.064-.144.124-.148.158-.013.121.276.446 1.07.532.548.06.947.553.886 1.102-.028.274-.167.511-.366.671z"/><path fill="#77B255" d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478.324 1.154-.378 2.615-2.35 3.17-.77.216-1.001.584-.97.701.034.118.425.312 1.193.095 1.972-.555 3.333.325 3.657 1.479.326 1.155-.378 2.614-2.351 3.17-.769.216-1.001.585-.967.702.033.117.423.311 1.192.095.53-.149 1.084.16 1.233.691.148.532-.161 1.084-.693 1.234-1.971.555-3.333-.323-3.659-1.479-.324-1.154.379-2.613 2.353-3.169.77-.217 1.001-.584.967-.702-.032-.117-.422-.312-1.19-.096-1.974.556-3.334-.322-3.659-1.479-.325-1.154.378-2.613 2.351-3.17.768-.215.999-.585.967-.701-.034-.118-.423-.312-1.192-.096-.532.15-1.083-.16-1.233-.691-.149-.53.161-1.082.693-1.232z"/><path fill="#AA8DD8" d="M23.001 20.16c-.294 0-.584-.129-.782-.375-.345-.432-.274-1.061.156-1.406.218-.175 5.418-4.259 12.767-3.208.547.078.927.584.849 1.131-.078.546-.58.93-1.132.848-6.493-.922-11.187 2.754-11.233 2.791-.186.148-.406.219-.625.219z"/><path fill="#77B255" d="M5.754 16c-.095 0-.192-.014-.288-.042-.529-.159-.829-.716-.67-1.245 1.133-3.773 2.16-9.794.898-11.364-.141-.178-.354-.353-.842-.316-.938.072-.849 2.051-.848 2.071.042.551-.372 1.031-.922 1.072-.559.034-1.031-.372-1.072-.923-.103-1.379.326-4.035 2.692-4.214 1.056-.08 1.933.287 2.552 1.057 2.371 2.951-.036 11.506-.542 13.192-.13.433-.528.712-.958.712z"/><circle fill="#5C913B" cx="25.5" cy="9.5" r="1.5"/><circle fill="#9266CC" cx="2" cy="18" r="2"/><circle fill="#5C913B" cx="32.5" cy="19.5" r="1.5"/><circle fill="#5C913B" cx="23.5" cy="31.5" r="1.5"/><circle fill="#FFCC4D" cx="28" cy="4" r="2"/><circle fill="#FFCC4D" cx="32.5" cy="8.5" r="1.5"/><circle fill="#FFCC4D" cx="29.5" cy="12.5" r="1.5"/><circle fill="#FFCC4D" cx="7.5" cy="23.5" r="1.5"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

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

View file

@ -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 += "<br>day"} else {days += "<br>days"}
if(hours == 0) {hours = "";} else if(hours == 1) {hours += "<br>hour"} else {hours += "<br>hours"}
if(hour == 0) {hour = "";} else if(hour == 1) {hour += "<br>hour"} else {hour += "<br>hours"}
if(mins == 0) {mins = "";} else if(mins == 1) {mins += "<br>minute"} else {mins += "<br>minutes"}
if(secs == 0) {secs = "";} else if(secs == 1) {secs += "<br>second"} else {secs += "<br>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");
});
}
}

50
src/strings.rs Normal file
View file

@ -0,0 +1,50 @@
use std::error::Error;
use chrono::TimeDelta;
pub fn parse_time_string(string: &str) -> Result<TimeDelta, Box<dyn Error>> {
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::<i32>() {
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() + "<br>day"} else {days.to_string() + "<br>days"};
let hour = if hour == 0.0 {"".to_string()} else if hour == 1.0 {hour.to_string() + "<br>hour"} else {hour.to_string() + "<br>hours"};
let mins = if mins == 0.0 {"".to_string()} else if mins == 1.0 {mins.to_string() + "<br>minute"} else {mins.to_string() + "<br>minutes"};
let secs = if secs == 0.0 {"".to_string()} else if secs == 1.0 {secs.to_string() + "<br>second"} else {secs.to_string() + "<br>seconds"};
(days + " " + &hour + " " + &mins + " " + &secs).trim().to_string()
}

View file

@ -1,36 +0,0 @@
use std::error::Error;
use chrono::TimeDelta;
pub fn parse_time_string(string: &str) -> Result<TimeDelta, Box<dyn Error>> {
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::<i32>() {
n
} else {
return Err("Not valid time string".into())
};
let final_time = multiplier * time;
Ok(final_time)
}