mirror of
https://github.com/Dangoware/confetti-box.git
synced 2025-04-19 15:22:57 -05:00
Fixed relative paths. Fixed multi-file uploading. Fixed config application.
This commit is contained in:
parent
e356e9377d
commit
eb9db0f546
6 changed files with 142 additions and 91 deletions
|
@ -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<RwLock<Database>>) {
|
|||
pub async fn clean_loop(
|
||||
db: Arc<RwLock<Database>>,
|
||||
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! {
|
||||
|
|
65
src/main.rs
65
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<Settings>) -> Markup {
|
||||
dbg!(settings.duration.default);
|
||||
html! {
|
||||
(head("Confetti-Box"))
|
||||
|
||||
|
@ -55,6 +54,10 @@ fn home(settings: &State<Settings>) -> 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<Settings>) -> 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<Settings>) -> 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<Arc<RwLock<Database>>>,
|
||||
settings: &State<Settings>,
|
||||
) -> Result<Json<ClientResponse>, 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<DateTime<Utc>>,
|
||||
}
|
||||
|
@ -247,6 +249,27 @@ struct ServerInfo {
|
|||
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]
|
||||
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)
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
||||
for (const file of file_upload.files) {
|
||||
let [linkRow, progressBar, progressText] = addNewToList(file.name);
|
||||
if (file.size > file_upload.dataset.maxFilesize) {
|
||||
progressValue.textContent = TOO_LARGE_TEXT;
|
||||
makeErrored(progressBar, progressText, linkRow, TOO_LARGE_TEXT);
|
||||
console.error(
|
||||
"Provided file is too large", file.size, "bytes; max",
|
||||
CAPABILITIES.max_filesize, "bytes"
|
||||
file_upload.dataset.maxFilesize, "bytes"
|
||||
);
|
||||
return;
|
||||
continue
|
||||
}
|
||||
|
||||
let [progressBar, progressText] = addNewToList(file.name);
|
||||
|
||||
let url = "/upload";
|
||||
let request = new XMLHttpRequest();
|
||||
request.open('POST', url, true);
|
||||
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);
|
||||
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) {
|
||||
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 += "<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() {
|
||||
async function initEverything() {
|
||||
const durationButtons = durationBox.getElementsByTagName("button");
|
||||
for (const b of durationButtons) {
|
||||
b.addEventListener("click", function (_e) {
|
||||
|
|
|
@ -37,9 +37,9 @@ pub fn parse_time_string(string: &str) -> Result<TimeDelta, Box<dyn Error>> {
|
|||
|
||||
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() + "<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"};
|
||||
|
|
Loading…
Reference in a new issue