Fixed relative paths. Fixed multi-file uploading. Fixed config application.

This commit is contained in:
G2-Games 2024-10-24 02:29:49 -05:00
parent e356e9377d
commit eb9db0f546
6 changed files with 142 additions and 91 deletions

View file

@ -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 bincode::{config::Configuration, decode_from_std_read, encode_into_std_write, Decode, Encode};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
@ -121,6 +121,10 @@ impl MochiFile {
let datetime = Utc::now(); let datetime = Utc::now();
datetime > self.expiry_datetime datetime > self.expiry_datetime
} }
pub fn hash(&self) -> &Hash {
&self.hash
}
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -175,9 +179,9 @@ fn clean_database(db: &Arc<RwLock<Database>>) {
pub async fn clean_loop( pub async fn clean_loop(
db: Arc<RwLock<Database>>, db: Arc<RwLock<Database>>,
mut shutdown_signal: Receiver<()>, mut shutdown_signal: Receiver<()>,
interval: Duration, interval: TimeDelta,
) { ) {
let mut interval = time::interval(interval); let mut interval = time::interval(interval.to_std().unwrap());
loop { loop {
select! { select! {

View file

@ -2,13 +2,13 @@ mod database;
mod strings; mod strings;
mod settings; mod settings;
use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}, time::Duration}; use std::{path::Path, sync::{Arc, RwLock}};
use blake3::Hash; use blake3::Hash;
use chrono::{DateTime, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use database::{clean_loop, Database, MochiFile}; use database::{clean_loop, Database, MochiFile};
use log::info; use log::info;
use rocket::{ 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 settings::Settings;
use strings::{parse_time_string, to_pretty_time}; 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"; meta name="viewport" content="width=device-width, initial-scale=1";
title { (page_title) } title { (page_title) }
// Javascript stuff for client side handling // 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="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("/")] #[get("/")]
fn home(settings: &State<Settings>) -> Markup { fn home(settings: &State<Settings>) -> Markup {
dbg!(settings.duration.default);
html! { html! {
(head("Confetti-Box")) (head("Confetti-Box"))
@ -55,6 +54,10 @@ fn home(settings: &State<Settings>) -> Markup {
h1 { "Confetti-Box 🎉" } h1 { "Confetti-Box 🎉" }
h2 { "Files up to " (settings.max_filesize.bytes()) " in size are allowed!" } h2 { "Files up to " (settings.max_filesize.bytes()) " in size are allowed!" }
hr; hr;
button.main_file_upload onclick="document.getElementById('fileInput').click()" {
h4 { "Upload File" }
p { "Click or Drag and Drop" }
}
h3 { "Expire after:" } h3 { "Expire after:" }
div id="durationBox" { div id="durationBox" {
@for d in &settings.duration.allowed { @for d in &settings.duration.allowed {
@ -67,15 +70,11 @@ fn home(settings: &State<Settings>) -> Markup {
} }
form #uploadForm { form #uploadForm {
// It's stupid how these can't be styled so they're just hidden here... // 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;"; onchange="formSubmit(this.parentNode)" data-max-filesize=(settings.max_filesize) style="display:none;";
input id="fileDuration" type="text" name="duration" minlength="2" input id="fileDuration" type="text" name="duration" minlength="2"
maxlength="7" value=(settings.duration.default.num_seconds().to_string() + "s") style="display:none;"; 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; hr;
h3 { "Uploaded Files" } h3 { "Uploaded Files" }
@ -97,11 +96,11 @@ fn home(settings: &State<Settings>) -> Markup {
#[derive(Debug, FromForm)] #[derive(Debug, FromForm)]
struct Upload<'r> { struct Upload<'r> {
#[field(name = "fileUpload")]
file: TempFile<'r>,
#[field(name = "duration")] #[field(name = "duration")]
expire_time: String, expire_time: String,
#[field(name = "fileUpload")]
file: TempFile<'r>,
} }
/// Handle a file upload and store it /// Handle a file upload and store it
@ -111,8 +110,8 @@ async fn handle_upload(
db: &State<Arc<RwLock<Database>>>, db: &State<Arc<RwLock<Database>>>,
settings: &State<Settings>, settings: &State<Settings>,
) -> Result<Json<ClientResponse>, std::io::Error> { ) -> Result<Json<ClientResponse>, std::io::Error> {
let mut out_path = PathBuf::from("files/");
let mut temp_dir = settings.temp_dir.clone(); 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) { let expire_time = if let Ok(t) = parse_time_string(&file_data.expire_time) {
if t > settings.duration.maximum { if t > settings.duration.maximum {
@ -149,7 +148,7 @@ async fn handle_upload(
.to_string(); .to_string();
// Get temp path and hash it // 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; let temp_filename = temp_dir;
file_data.file.persist_to(&temp_filename).await?; file_data.file.persist_to(&temp_filename).await?;
let hash = hash_file(&temp_filename).await?; let hash = hash_file(&temp_filename).await?;
@ -182,6 +181,7 @@ async fn handle_upload(
status: true, status: true,
name: constructed_file.name().clone(), name: constructed_file.name().clone(),
url: "files/".to_string() + &filename, url: "files/".to_string() + &filename,
hash: hash.0.to_hex()[0..10].to_string(),
expires: Some(constructed_file.get_expiry()), expires: Some(constructed_file.get_expiry()),
..Default::default() ..Default::default()
})) }))
@ -200,6 +200,8 @@ struct ClientResponse {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "String::is_empty")] #[serde(skip_serializing_if = "String::is_empty")]
pub url: String, pub url: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub hash: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<DateTime<Utc>>, pub expires: Option<DateTime<Utc>>,
} }
@ -247,6 +249,27 @@ struct ServerInfo {
allowed_durations: Vec<u32>, allowed_durations: Vec<u32>,
} }
/// 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/<id>")]
fn lookup(
db: &State<Arc<RwLock<Database>>>,
id: &str
) -> Result<Redirect, NotFound<()>> {
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] #[rocket::main]
async fn main() { async fn main() {
// Get or create config file // Get or create config file
@ -264,24 +287,24 @@ async fn main() {
..Default::default() ..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(); 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); let (shutdown, rx) = tokio::sync::mpsc::channel(1);
tokio::spawn({ tokio::spawn({
let cleaner_db = database.clone(); 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() let rocket = rocket::build()
.mount( .mount(
config.server.root_path.clone() + "/", 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( .mount(
config.server.root_path.clone() + "/files", 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(database)
.manage(config) .manage(config)

View file

@ -29,6 +29,10 @@ pub struct Settings {
#[serde(default)] #[serde(default)]
pub temp_dir: PathBuf, pub temp_dir: PathBuf,
/// Directory in which to store hosted files
#[serde(default)]
pub file_dir: PathBuf,
/// Settings pertaining to the server configuration /// Settings pertaining to the server configuration
#[serde(default)] #[serde(default)]
pub server: ServerSettings, pub server: ServerSettings,
@ -46,7 +50,8 @@ impl Default for Settings {
server: ServerSettings::default(), server: ServerSettings::default(),
path: "./settings.toml".into(), path: "./settings.toml".into(),
database_path: "./database.mochi".into(), database_path: "./database.mochi".into(),
temp_dir: std::env::temp_dir() temp_dir: std::env::temp_dir(),
file_dir: "./files/".into(),
} }
} }
} }

View file

@ -60,6 +60,7 @@ button.main_file_upload {
height: 75px; height: 75px;
cursor: pointer; cursor: pointer;
background-color: #84E5FF; background-color: #84E5FF;
margin-bottom: 0;
h4 { h4 {
margin: 0; margin: 0;
@ -110,10 +111,27 @@ button.main_file_upload {
min-height: 2em; 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 { #uploadedFilesDisplay div {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
padding: 10px;
margin-bottom: 10px;
} }
#uploadedFilesDisplay > div > progress { #uploadedFilesDisplay > div > progress {
@ -123,8 +141,9 @@ button.main_file_upload {
} }
#uploadedFilesDisplay button { #uploadedFilesDisplay button {
height: 50px; height: fit-content;
width: 50px; margin: auto;
background-color: white;
} }
progress[value] { progress[value] {
@ -134,6 +153,7 @@ progress[value] {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
appearance: none; appearance: none;
border-radius: 5px;
background-color: var(--background); background-color: var(--background);
} }

View file

@ -2,76 +2,89 @@ let statusNotifier;
let uploadedFilesDisplay; let uploadedFilesDisplay;
let durationBox; let durationBox;
let uploadInProgress = false; const TOO_LARGE_TEXT = "Too large!";
const ERROR_TEXT = "Error!";
const TOO_LARGE_TEXT = "File is too large!";
const ERROR_TEXT = "An error occured!";
async function formSubmit(form) { 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 // Get file size and don't upload if it's too large
let file_upload = document.getElementById("fileInput"); 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();
let request = new XMLHttpRequest(); request.open('POST', "./upload", true);
request.open('POST', url, true);
// Set up event listeners // Set up event listeners
request.upload.addEventListener('progress', (p) => {uploadProgress(p, progressBar, progressText)}, false); request.upload.addEventListener('progress', (p) => {uploadProgress(p, progressBar, progressText, linkRow)}, false);
request.addEventListener('load', (c) => {uploadComplete(c, progressBar, progressText)}, false); request.addEventListener('load', (c) => {uploadComplete(c, progressBar, progressText, linkRow)}, false);
request.addEventListener('error', networkErrorHandler, false); request.addEventListener('error', (e) => {networkErrorHandler(e, progressBar, progressText, linkRow)}, false);
uploadInProgress = true; // Create and send FormData
// Create and send FormData try {
try { request.send(new FormData(form));
request.send(new FormData(form)); } catch (e) {
} catch (e) { makeErrored(progressBar, progressText, linkRow, ERROR_TEXT);
console.error("An error occured while uploading", e); console.error("An error occured while uploading", e);
}
} }
// Reset the form file data since we've successfully submitted it // Reset the form file data since we've successfully submitted it
form.elements["fileUpload"].value = ""; form.elements["fileUpload"].value = "";
} }
function networkErrorHandler(_err) { function makeErrored(progressBar, progressText, linkRow, errorMessage) {
uploadInProgress = false; progressText.textContent = errorMessage;
console.error("A network error occured while uploading"); progressBar.style.display = "none";
progressValue.textContent = "A network error occured!"; 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; let target = response.target;
if (target.status === 200) { if (target.status === 200) {
const response = JSON.parse(target.responseText); const response = JSON.parse(target.responseText);
if (response.status) { if (response.status) {
progressText.textContent = "Success"; makeFinished(progressBar, progressText, linkRow, response.url, response.hash);
} else { } else {
console.error("Error uploading", response) console.error("Error uploading", response)
progressText.textContent = response.response; makeErrored(progressBar, progressText, linkRow, response.response);
} }
} else if (target.status === 413) { } else if (target.status === 413) {
progressText.textContent = TOO_LARGE_TEXT; makeErrored(progressBar, progressText, linkRow, TOO_LARGE_TEXT);
} else { } else {
progressText.textContent = ERROR_TEXT; makeErrored(progressBar, progressText, linkRow, ERROR_TEXT);
} }
uploadInProgress = false;
} }
function addNewToList(origFileName) { function addNewToList(origFileName) {
@ -81,18 +94,18 @@ function addNewToList(origFileName) {
const progressTxt = linkRow.appendChild(document.createElement("p")); const progressTxt = linkRow.appendChild(document.createElement("p"));
fileName.textContent = origFileName; fileName.textContent = origFileName;
fileName.classList.add("file_name");
progressTxt.classList.add("status");
progressBar.max="100"; progressBar.max="100";
progressBar.value="0"; progressBar.value="0";
return [progressBar, progressTxt]; return [linkRow, progressBar, progressTxt];
} }
function uploadProgress(progress, progressBar, progressText) { function uploadProgress(progress, progressBar, progressText, linkRow) {
console.log(progress);
if (progress.lengthComputable) { if (progress.lengthComputable) {
const progressPercent = Math.floor((progress.loaded / progress.total) * 100); const progressPercent = Math.floor((progress.loaded / progress.total) * 100);
progressBar.value = progressPercent; progressBar.value = progressPercent;
console.log(progressBar.value);
progressText.textContent = progressPercent + "%"; progressText.textContent = progressPercent + "%";
} }
} }
@ -104,24 +117,10 @@ document.addEventListener("DOMContentLoaded", function(_event){
uploadedFilesDisplay = document.getElementById("uploadedFilesDisplay"); uploadedFilesDisplay = document.getElementById("uploadedFilesDisplay");
durationBox = document.getElementById("durationBox"); durationBox = document.getElementById("durationBox");
getServerCapabilities(); initEverything();
}); });
function toPrettyTime(seconds) { async function initEverything() {
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 += "<br>day"} else {days += "<br>days"}
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 + " " + hour + " " + mins + " " + secs).trim();
}
async function getServerCapabilities() {
const durationButtons = durationBox.getElementsByTagName("button"); const durationButtons = durationBox.getElementsByTagName("button");
for (const b of durationButtons) { for (const b of durationButtons) {
b.addEventListener("click", function (_e) { b.addEventListener("click", function (_e) {

View file

@ -37,9 +37,9 @@ pub fn parse_time_string(string: &str) -> Result<TimeDelta, Box<dyn Error>> {
pub fn to_pretty_time(seconds: u32) -> String { pub fn to_pretty_time(seconds: u32) -> String {
let days = (seconds as f32 / 86400.0).floor(); 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 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() + "<br>day"} else {days.to_string() + "<br>days"}; 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 hour = if hour == 0.0 {"".to_string()} else if hour == 1.0 {hour.to_string() + "<br>hour"} else {hour.to_string() + "<br>hours"};