From eb9db0f546034b867a4a64701688cc3fcb507e07 Mon Sep 17 00:00:00 2001 From: G2-Games Date: Thu, 24 Oct 2024 02:29:49 -0500 Subject: [PATCH] Fixed relative paths. Fixed multi-file uploading. Fixed config application. --- src/database.rs | 10 ++-- src/main.rs | 65 ++++++++++++++-------- src/settings.rs | 7 ++- src/static/main.css | 24 ++++++++- src/static/request.js | 123 +++++++++++++++++++++--------------------- src/strings.rs | 4 +- 6 files changed, 142 insertions(+), 91 deletions(-) diff --git a/src/database.rs b/src/database.rs index c072ad7..b42eb09 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, fs::{self, File}, path::{Path, PathBuf}, sync::{Arc, RwLock}, time::Duration}; +use std::{collections::HashMap, fs::{self, File}, path::{Path, PathBuf}, sync::{Arc, RwLock}}; use bincode::{config::Configuration, decode_from_std_read, encode_into_std_write, Decode, Encode}; use chrono::{DateTime, TimeDelta, Utc}; @@ -121,6 +121,10 @@ impl MochiFile { let datetime = Utc::now(); datetime > self.expiry_datetime } + + pub fn hash(&self) -> &Hash { + &self.hash + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -175,9 +179,9 @@ fn clean_database(db: &Arc>) { pub async fn clean_loop( db: Arc>, mut shutdown_signal: Receiver<()>, - interval: Duration, + interval: TimeDelta, ) { - let mut interval = time::interval(interval); + let mut interval = time::interval(interval.to_std().unwrap()); loop { select! { diff --git a/src/main.rs b/src/main.rs index 7ad4b3c..21887f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,13 @@ mod database; mod strings; mod settings; -use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}, time::Duration}; +use std::{path::Path, sync::{Arc, RwLock}}; use blake3::Hash; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, TimeDelta, 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, http::ContentType, + data::{Limits, ToByteUnit}, form::Form, fs::{FileServer, Options, TempFile}, get, http::{ContentType, RawStr}, post, response::{content::{RawCss, RawJavaScript}, status::NotFound, Redirect}, routes, serde::{json::Json, Serialize}, tokio::{self, fs::File, io::AsyncReadExt}, Config, FromForm, State }; use settings::Settings; use strings::{parse_time_string, to_pretty_time}; @@ -22,9 +22,9 @@ fn head(page_title: &str) -> Markup { meta name="viewport" content="width=device-width, initial-scale=1"; title { (page_title) } // Javascript stuff for client side handling - script src="request.js" { } + script src="./request.js" { } link rel="icon" type="image/svg+xml" href="favicon.svg"; - link rel="stylesheet" href="main.css"; + link rel="stylesheet" href="./main.css"; } } @@ -47,7 +47,6 @@ fn favicon() -> (ContentType, &'static str) { #[get("/")] fn home(settings: &State) -> Markup { - dbg!(settings.duration.default); html! { (head("Confetti-Box")) @@ -55,6 +54,10 @@ fn home(settings: &State) -> Markup { h1 { "Confetti-Box 🎉" } h2 { "Files up to " (settings.max_filesize.bytes()) " in size are allowed!" } hr; + button.main_file_upload onclick="document.getElementById('fileInput').click()" { + h4 { "Upload File" } + p { "Click or Drag and Drop" } + } h3 { "Expire after:" } div id="durationBox" { @for d in &settings.duration.allowed { @@ -67,15 +70,11 @@ fn home(settings: &State) -> Markup { } form #uploadForm { // It's stupid how these can't be styled so they're just hidden here... - input id="fileInput" type="file" name="fileUpload" + input id="fileInput" type="file" name="fileUpload" multiple 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.num_seconds().to_string() + "s") 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" } @@ -97,11 +96,11 @@ fn home(settings: &State) -> Markup { #[derive(Debug, FromForm)] struct Upload<'r> { - #[field(name = "fileUpload")] - file: TempFile<'r>, - #[field(name = "duration")] expire_time: String, + + #[field(name = "fileUpload")] + file: TempFile<'r>, } /// Handle a file upload and store it @@ -111,8 +110,8 @@ async fn handle_upload( db: &State>>, settings: &State, ) -> Result, std::io::Error> { - let mut out_path = PathBuf::from("files/"); let mut temp_dir = settings.temp_dir.clone(); + let mut out_path = settings.file_dir.clone(); let expire_time = if let Ok(t) = parse_time_string(&file_data.expire_time) { if t > settings.duration.maximum { @@ -149,7 +148,7 @@ async fn handle_upload( .to_string(); // Get temp path and hash it - temp_dir.push(&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?; @@ -182,6 +181,7 @@ async fn handle_upload( status: true, name: constructed_file.name().clone(), url: "files/".to_string() + &filename, + hash: hash.0.to_hex()[0..10].to_string(), expires: Some(constructed_file.get_expiry()), ..Default::default() })) @@ -200,6 +200,8 @@ struct ClientResponse { pub name: String, #[serde(skip_serializing_if = "String::is_empty")] pub url: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub hash: String, #[serde(skip_serializing_if = "Option::is_none")] pub expires: Option>, } @@ -247,6 +249,27 @@ struct ServerInfo { allowed_durations: Vec, } +/// Look up the hash of a file to find it. This only returns the first +/// hit for a hash, so different filenames may not be found. +#[get("/f/")] +fn lookup( + db: &State>>, + id: &str +) -> Result> { + for file in db.read().unwrap().files.values() { + if file.hash().to_hex()[0..10].to_string() == id { + let filename = get_id( + file.name(), + *file.hash() + ); + let filename = RawStr::new(&filename).percent_encode().to_string(); + return Ok(Redirect::to(format!("/files/{}", filename))) + } + } + + Err(NotFound(())) +} + #[rocket::main] async fn main() { // Get or create config file @@ -264,24 +287,24 @@ async fn main() { ..Default::default() }; - let database = Arc::new(RwLock::new(Database::open(&"database.mochi"))); + let database = Arc::new(RwLock::new(Database::open(&config.database_path))); let local_db = database.clone(); - // Start monitoring thread + // Start monitoring thread, cleaning the database every 2 minutes let (shutdown, rx) = tokio::sync::mpsc::channel(1); tokio::spawn({ let cleaner_db = database.clone(); - async move { clean_loop(cleaner_db, rx, Duration::from_secs(120)).await } + async move { clean_loop(cleaner_db, rx, TimeDelta::minutes(2)).await } }); let rocket = rocket::build() .mount( config.server.root_path.clone() + "/", - routes![home, handle_upload, form_handler_js, stylesheet, server_info, favicon] + routes![home, handle_upload, form_handler_js, stylesheet, server_info, favicon, lookup] ) .mount( config.server.root_path.clone() + "/files", - FileServer::new("files/", Options::Missing | Options::NormalizeDirs) + FileServer::new(config.file_dir.clone(), Options::Missing | Options::NormalizeDirs) ) .manage(database) .manage(config) diff --git a/src/settings.rs b/src/settings.rs index 1782afa..f918eeb 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -29,6 +29,10 @@ pub struct Settings { #[serde(default)] pub temp_dir: PathBuf, + /// Directory in which to store hosted files + #[serde(default)] + pub file_dir: PathBuf, + /// Settings pertaining to the server configuration #[serde(default)] pub server: ServerSettings, @@ -46,7 +50,8 @@ impl Default for Settings { server: ServerSettings::default(), path: "./settings.toml".into(), database_path: "./database.mochi".into(), - temp_dir: std::env::temp_dir() + temp_dir: std::env::temp_dir(), + file_dir: "./files/".into(), } } } diff --git a/src/static/main.css b/src/static/main.css index fb6a3ba..3a2433b 100644 --- a/src/static/main.css +++ b/src/static/main.css @@ -60,6 +60,7 @@ button.main_file_upload { height: 75px; cursor: pointer; background-color: #84E5FF; + margin-bottom: 0; h4 { margin: 0; @@ -110,10 +111,27 @@ button.main_file_upload { min-height: 2em; } +#uploadedFilesDisplay p.file_name { + width: 50%; + overflow: clip; + text-overflow: ellipsis; + white-space: nowrap; +} + +#uploadedFilesDisplay p.status { + overflow: clip; + text-overflow: ellipsis; + white-space: nowrap; + text-align: right; + width: 100%; +} + #uploadedFilesDisplay div { display: flex; flex-direction: row; gap: 10px; + padding: 10px; + margin-bottom: 10px; } #uploadedFilesDisplay > div > progress { @@ -123,8 +141,9 @@ button.main_file_upload { } #uploadedFilesDisplay button { - height: 50px; - width: 50px; + height: fit-content; + margin: auto; + background-color: white; } progress[value] { @@ -134,6 +153,7 @@ progress[value] { -webkit-appearance: none; -moz-appearance: none; appearance: none; + border-radius: 5px; background-color: var(--background); } diff --git a/src/static/request.js b/src/static/request.js index 665bc45..ed8819d 100644 --- a/src/static/request.js +++ b/src/static/request.js @@ -2,76 +2,89 @@ let statusNotifier; let uploadedFilesDisplay; let durationBox; -let uploadInProgress = false; - -const TOO_LARGE_TEXT = "File is too large!"; -const ERROR_TEXT = "An error occured!"; +const TOO_LARGE_TEXT = "Too large!"; +const ERROR_TEXT = "Error!"; async function formSubmit(form) { - if (uploadInProgress) { - return; // TODO: REMOVE THIS ONCE MULTIPLE CAN WORK! - } - // Get file size and don't upload if it's too large let file_upload = document.getElementById("fileInput"); - let file = file_upload.files[0]; - if (file.size > file_upload.dataset.maxFilesize) { - progressValue.textContent = TOO_LARGE_TEXT; - console.error( - "Provided file is too large", file.size, "bytes; max", - CAPABILITIES.max_filesize, "bytes" - ); - return; - } - let [progressBar, progressText] = addNewToList(file.name); + for (const file of file_upload.files) { + let [linkRow, progressBar, progressText] = addNewToList(file.name); + if (file.size > file_upload.dataset.maxFilesize) { + makeErrored(progressBar, progressText, linkRow, TOO_LARGE_TEXT); + console.error( + "Provided file is too large", file.size, "bytes; max", + file_upload.dataset.maxFilesize, "bytes" + ); + continue + } - let url = "/upload"; - let request = new XMLHttpRequest(); - request.open('POST', url, true); + let request = new XMLHttpRequest(); + request.open('POST', "./upload", true); - // Set up event listeners - request.upload.addEventListener('progress', (p) => {uploadProgress(p, progressBar, progressText)}, false); - request.addEventListener('load', (c) => {uploadComplete(c, progressBar, progressText)}, false); - request.addEventListener('error', networkErrorHandler, false); + // Set up event listeners + request.upload.addEventListener('progress', (p) => {uploadProgress(p, progressBar, progressText, linkRow)}, false); + request.addEventListener('load', (c) => {uploadComplete(c, progressBar, progressText, linkRow)}, false); + request.addEventListener('error', (e) => {networkErrorHandler(e, progressBar, progressText, linkRow)}, false); - uploadInProgress = true; - // Create and send FormData - try { - request.send(new FormData(form)); - } catch (e) { - console.error("An error occured while uploading", e); + // Create and send FormData + try { + request.send(new FormData(form)); + } catch (e) { + makeErrored(progressBar, progressText, linkRow, ERROR_TEXT); + console.error("An error occured while uploading", e); + } } // Reset the form file data since we've successfully submitted it form.elements["fileUpload"].value = ""; } -function networkErrorHandler(_err) { - uploadInProgress = false; - console.error("A network error occured while uploading"); - progressValue.textContent = "A network error occured!"; +function makeErrored(progressBar, progressText, linkRow, errorMessage) { + progressText.textContent = errorMessage; + progressBar.style.display = "none"; + linkRow.style.background = "#ffb2ae"; } -function uploadComplete(response, _progressBar, progressText) { +function makeFinished(progressBar, progressText, linkRow, linkAddress, hash) { + progressText.textContent = ""; + const link = progressText.appendChild(document.createElement("a")); + link.textContent = hash; + link.href = linkAddress; + + let button = linkRow.appendChild(document.createElement("button")); + button.textContent = "📝"; + button.addEventListener('click', function(e) { + navigator.clipboard.writeText("https://" + window.location.host + "/" + linkAddress) + }) + + progressBar.style.display = "none"; + linkRow.style.background = "#a4ffbb"; +} + +function networkErrorHandler(err, progressBar, progressText, linkRow) { + makeErrored(progressBar, progressText, linkRow, "A network error occured"); + console.error("A network error occured while uploading", err); +} + +function uploadComplete(response, progressBar, progressText, linkRow) { let target = response.target; if (target.status === 200) { const response = JSON.parse(target.responseText); if (response.status) { - progressText.textContent = "Success"; + makeFinished(progressBar, progressText, linkRow, response.url, response.hash); } else { console.error("Error uploading", response) - progressText.textContent = response.response; + makeErrored(progressBar, progressText, linkRow, response.response); } } else if (target.status === 413) { - progressText.textContent = TOO_LARGE_TEXT; + makeErrored(progressBar, progressText, linkRow, TOO_LARGE_TEXT); } else { - progressText.textContent = ERROR_TEXT; + makeErrored(progressBar, progressText, linkRow, ERROR_TEXT); } - - uploadInProgress = false; } function addNewToList(origFileName) { @@ -81,18 +94,18 @@ function addNewToList(origFileName) { const progressTxt = linkRow.appendChild(document.createElement("p")); fileName.textContent = origFileName; + fileName.classList.add("file_name"); + progressTxt.classList.add("status"); progressBar.max="100"; progressBar.value="0"; - return [progressBar, progressTxt]; + return [linkRow, progressBar, progressTxt]; } -function uploadProgress(progress, progressBar, progressText) { - console.log(progress); +function uploadProgress(progress, progressBar, progressText, linkRow) { if (progress.lengthComputable) { const progressPercent = Math.floor((progress.loaded / progress.total) * 100); progressBar.value = progressPercent; - console.log(progressBar.value); progressText.textContent = progressPercent + "%"; } } @@ -104,24 +117,10 @@ document.addEventListener("DOMContentLoaded", function(_event){ uploadedFilesDisplay = document.getElementById("uploadedFilesDisplay"); durationBox = document.getElementById("durationBox"); - getServerCapabilities(); + initEverything(); }); -function toPrettyTime(seconds) { - var days = Math.floor(seconds / 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(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 + " " + hour + " " + mins + " " + secs).trim(); -} - -async function getServerCapabilities() { +async function initEverything() { const durationButtons = durationBox.getElementsByTagName("button"); for (const b of durationButtons) { b.addEventListener("click", function (_e) { diff --git a/src/strings.rs b/src/strings.rs index e142a66..e0033fd 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -37,9 +37,9 @@ pub fn parse_time_string(string: &str) -> Result> { 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 hour = ((seconds as f32 - (days * 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 secs = seconds as f32 - (hour * 3600.0) - (mins * 60.0) - (days * 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"};