mirror of
https://github.com/Dangoware/confetti-box.git
synced 2025-04-19 23:32:58 -05:00
Compare commits
No commits in common. "0f780d027d629eeecc15ed674b9e6b3f76d0142a" and "4b07ba4c462fb4bb1570237853b6f49ca7686b8b" have entirely different histories.
0f780d027d
...
4b07ba4c46
8 changed files with 43 additions and 144 deletions
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "confetti_box"
|
name = "confetti_box"
|
||||||
version = "0.1.2"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -245,7 +245,6 @@ fn clean_database(db: &Arc<RwLock<Database>>, file_path: &Path) {
|
||||||
info!("Cleaned database. Removed {removed_entries} expired entries. Removed {removed_files} no longer referenced files.");
|
info!("Cleaned database. Removed {removed_entries} expired entries. Removed {removed_files} no longer referenced files.");
|
||||||
|
|
||||||
database.save();
|
database.save();
|
||||||
drop(database); // Just to be sure
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A loop to clean the database periodically.
|
/// A loop to clean the database periodically.
|
||||||
|
|
|
@ -42,6 +42,7 @@ pub struct ServerInfo {
|
||||||
allowed_durations: Vec<u32>,
|
allowed_durations: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Look up the [`Mmid`] of a file to find it.
|
||||||
#[get("/f/<mmid>")]
|
#[get("/f/<mmid>")]
|
||||||
pub async fn lookup_mmid(db: &State<Arc<RwLock<Database>>>, mmid: &str) -> Option<Redirect> {
|
pub async fn lookup_mmid(db: &State<Arc<RwLock<Database>>>, mmid: &str) -> Option<Redirect> {
|
||||||
let mmid: Mmid = mmid.try_into().ok()?;
|
let mmid: Mmid = mmid.try_into().ok()?;
|
||||||
|
@ -53,31 +54,9 @@ pub async fn lookup_mmid(db: &State<Arc<RwLock<Database>>>, mmid: &str) -> Optio
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/f/<mmid>?noredir")]
|
/// Look up the [`Mmid`] of a file to find it, along with the name of the file
|
||||||
pub async fn lookup_mmid_noredir(
|
|
||||||
db: &State<Arc<RwLock<Database>>>,
|
|
||||||
settings: &State<Settings>,
|
|
||||||
mmid: &str,
|
|
||||||
) -> Option<(ContentType, File)> {
|
|
||||||
let mmid: Mmid = mmid.try_into().ok()?;
|
|
||||||
|
|
||||||
let entry = db.read().unwrap().get(&mmid).cloned()?;
|
|
||||||
|
|
||||||
let file = File::open(settings.file_dir.join(entry.hash().to_string()))
|
|
||||||
.await
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
dbg!(entry.extension());
|
|
||||||
|
|
||||||
Some((
|
|
||||||
ContentType::from_extension(entry.extension()).unwrap_or(ContentType::Binary),
|
|
||||||
file,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/f/<mmid>/<name>")]
|
#[get("/f/<mmid>/<name>")]
|
||||||
pub async fn lookup_mmid_name(
|
pub async fn lookup_mmid_name(db: &State<Arc<RwLock<Database>>>,
|
||||||
db: &State<Arc<RwLock<Database>>>,
|
|
||||||
settings: &State<Settings>,
|
settings: &State<Settings>,
|
||||||
mmid: &str,
|
mmid: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -12,12 +12,12 @@ use std::{
|
||||||
|
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use database::{clean_loop, Database, Mmid, MochiFile};
|
use database::{clean_loop, Database, Mmid, MochiFile};
|
||||||
use endpoints::{lookup_mmid, lookup_mmid_name, lookup_mmid_noredir, server_info};
|
use endpoints::{lookup_mmid, lookup_mmid_name, server_info};
|
||||||
use log::info;
|
use log::info;
|
||||||
use maud::{html, Markup, PreEscaped};
|
use maud::{html, Markup, PreEscaped};
|
||||||
use pages::{api_info, footer, head};
|
use pages::{api_info, footer, head};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
data::{Limits, ToByteUnit}, form::Form, fs::TempFile, get, http::ContentType, post, response::content::{RawCss, RawJavaScript}, routes, serde::{json::Json, Serialize}, tokio, Config, FromForm, State
|
data::{Limits, ToByteUnit}, form::Form, fs::{FileServer, Options, TempFile}, get, http::ContentType, post, response::content::{RawCss, RawJavaScript}, routes, serde::{json::Json, Serialize}, tokio, 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};
|
||||||
|
@ -147,6 +147,7 @@ async fn handle_upload(
|
||||||
std::fs::rename(temp_filename, settings.file_dir.join(file_hash.to_string()))?;
|
std::fs::rename(temp_filename, settings.file_dir.join(file_hash.to_string()))?;
|
||||||
|
|
||||||
db.write().unwrap().insert(&file_mmid, constructed_file.clone());
|
db.write().unwrap().insert(&file_mmid, constructed_file.clone());
|
||||||
|
db.write().unwrap().save();
|
||||||
|
|
||||||
Ok(Json(ClientResponse {
|
Ok(Json(ClientResponse {
|
||||||
status: true,
|
status: true,
|
||||||
|
@ -234,10 +235,16 @@ async fn main() {
|
||||||
server_info,
|
server_info,
|
||||||
favicon,
|
favicon,
|
||||||
lookup_mmid,
|
lookup_mmid,
|
||||||
lookup_mmid_noredir,
|
|
||||||
lookup_mmid_name,
|
lookup_mmid_name,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
.mount(
|
||||||
|
config.server.root_path.clone() + "/files",
|
||||||
|
FileServer::new(
|
||||||
|
config.file_dir.clone(),
|
||||||
|
Options::Missing | Options::NormalizeDirs,
|
||||||
|
),
|
||||||
|
)
|
||||||
.manage(database)
|
.manage(database)
|
||||||
.manage(config)
|
.manage(config)
|
||||||
.configure(rocket_config)
|
.configure(rocket_config)
|
||||||
|
|
87
src/pages.rs
87
src/pages.rs
|
@ -1,7 +1,5 @@
|
||||||
use maud::{html, Markup, DOCTYPE};
|
use maud::{html, Markup, DOCTYPE};
|
||||||
use rocket::{get, State};
|
use rocket::get;
|
||||||
|
|
||||||
use crate::settings::Settings;
|
|
||||||
|
|
||||||
pub fn head(page_title: &str) -> Markup {
|
pub fn head(page_title: &str) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
|
@ -20,16 +18,14 @@ pub fn footer() -> Markup {
|
||||||
p {a href="/" {"Home"}}
|
p {a href="/" {"Home"}}
|
||||||
p {a href="https://github.com/G2-Games/confetti-box" {"Source"}}
|
p {a href="https://github.com/G2-Games/confetti-box" {"Source"}}
|
||||||
p {a href="https://g2games.dev/" {"My Website"}}
|
p {a href="https://g2games.dev/" {"My Website"}}
|
||||||
p {a href="api" {"API"}}
|
p {a href="api_info" {"API Info"}}
|
||||||
p {a href="https://ko-fi.com/g2_games" {"Donate"}}
|
p {a href="https://ko-fi.com/g2_games" {"Donate"}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/api")]
|
#[get("/api_info")]
|
||||||
pub fn api_info(settings: &State<Settings>) -> Markup {
|
pub fn api_info() -> Markup {
|
||||||
let domain = &settings.server.domain;
|
|
||||||
let root = &settings.server.root_path;
|
|
||||||
html! {
|
html! {
|
||||||
(head("Confetti-Box | API"))
|
(head("Confetti-Box | API"))
|
||||||
|
|
||||||
|
@ -39,77 +35,14 @@ pub fn api_info(settings: &State<Settings>) -> Markup {
|
||||||
|
|
||||||
div style="text-align: left;" {
|
div style="text-align: left;" {
|
||||||
p {
|
p {
|
||||||
"Confetti-Box is designed to be simple to access using its
|
"""
|
||||||
API. All endpoints are accessed following "
|
The API for this service can be used by POST ing a form
|
||||||
code{"https://"(domain) (root)} ". All responses are encoded
|
with an expiration time and file to upload to the upload
|
||||||
in JSON. MMIDs are a unique identifier for a file returned by
|
endpoint:
|
||||||
the server after a successful " code{"/upload"} " request."
|
"""
|
||||||
}
|
|
||||||
p {
|
|
||||||
"The following endpoints are supported:"
|
|
||||||
}
|
}
|
||||||
|
pre { "/upload POST duration=\"6h\" fileUpload=(file data)" }
|
||||||
|
|
||||||
h2 { code {"/upload"} }
|
|
||||||
pre { r#"POST duration=String fileUpload=Bytes -> JSON"# }
|
|
||||||
p {
|
|
||||||
"To upload files, " code{"POST"} " a multipart form
|
|
||||||
containing the fields " code{"duration"} " and "
|
|
||||||
code{"fileData"} " to this endpoint. " code{"duration"}
|
|
||||||
" MUST be a string formatted like " code{"1H"}", where
|
|
||||||
the number MUST be a valid number and the letter MUST be
|
|
||||||
one of " b{"S"} "(econd), " b{"M"}"(inute), " b{"H"}"(our), "
|
|
||||||
b{"D"}"(ay). The " code{"/info"} " endpoint returns valid
|
|
||||||
durations and maximum file sizes."
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
"Example successful response:"
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
"{\n\t\"status\": true,\n\t\"response\": \"\",\n\t\"name\": \"1600-1200.jpg\",\n\t\"mmid\": \"xNLF6ogx\",\n\t\"hash\": \"1f12137f2c263d9e6d686e90c687a55d46d064fe6eeda7e4c39158d20ce1f071\",\n\t\"expires\": \"2024-10-28T11:59:25.024373438Z\"\n}"
|
|
||||||
}
|
|
||||||
p {"Example failure response:"}
|
|
||||||
pre {
|
|
||||||
"{\n\t\"status\": false,\n\t\"response\": \"Duration invalid\",\n}"
|
|
||||||
}
|
|
||||||
|
|
||||||
hr;
|
|
||||||
h2 { code {"/info"} }
|
|
||||||
pre { r#"GET -> JSON"# }
|
|
||||||
p {
|
|
||||||
"Returns the capabilities of the server."
|
|
||||||
}
|
|
||||||
p {"Example response:"}
|
|
||||||
pre {
|
|
||||||
"{\n\t\"max_filesize\": 5000000000,\n\t\"max_duration\": 259200,\n\t\"default_duration\": 21600,\n\t\"allowed_durations\": [\n\t\t3600,\n\t\t21600,\n\t\t86400,\n\t\t172800\n\t]\n}"
|
|
||||||
}
|
|
||||||
|
|
||||||
hr;
|
|
||||||
h2 { code {"/f/<mmid>"} }
|
|
||||||
pre { r#"GET mmid=MMID -> Redirect or File"# }
|
|
||||||
p {
|
|
||||||
"By default issues a redirect to the full URL for a file. This
|
|
||||||
behavior can be modified by appending " code{"?noredir"} " to
|
|
||||||
the end of this request, like " code{"/f/<mmid>?noredir"} ",
|
|
||||||
in which case it behaves just like " code{"/f/<mmid>/<filename>"}
|
|
||||||
}
|
|
||||||
p {"Example default response:"}
|
|
||||||
pre {"303: /f/xNLF6ogx/1600-1200.jpg"}
|
|
||||||
|
|
||||||
p {"Example modified response:"}
|
|
||||||
pre {"<File Bytes>"}
|
|
||||||
|
|
||||||
hr;
|
|
||||||
h2 { code {"/f/<mmid>/<filename>"} }
|
|
||||||
pre { r#"GET mmid=MMID filename=String -> File"# }
|
|
||||||
p {
|
|
||||||
"Returns the contents of the file corresponding to the
|
|
||||||
requested MMID, but with the corresponding filename so as
|
|
||||||
to preserve it for downloads. Mostly for use by browsers."
|
|
||||||
}
|
|
||||||
p {"Example response:"}
|
|
||||||
pre {
|
|
||||||
"<File Bytes>"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hr;
|
hr;
|
||||||
|
|
|
@ -95,7 +95,6 @@ impl Settings {
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct ServerSettings {
|
pub struct ServerSettings {
|
||||||
pub domain: String,
|
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
|
||||||
|
@ -106,7 +105,6 @@ pub struct ServerSettings {
|
||||||
impl Default for ServerSettings {
|
impl Default for ServerSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
domain: "example.com".into(),
|
|
||||||
address: "127.0.0.1".into(),
|
address: "127.0.0.1".into(),
|
||||||
root_path: "/".into(),
|
root_path: "/".into(),
|
||||||
port: 8950,
|
port: 8950,
|
||||||
|
|
18
web/main.css
18
web/main.css
|
@ -26,10 +26,6 @@ h1 {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
button p {
|
button p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -78,21 +74,9 @@ button.main_file_upload {
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #161b22;
|
background-color: black;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow: scroll;
|
|
||||||
tab-size: 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
p code {
|
|
||||||
background-color: lightgray;
|
|
||||||
font-size: 12pt;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 code {
|
|
||||||
font-size: 15pt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#durationBox {
|
#durationBox {
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
/*jshint esversion: 11 */
|
|
||||||
|
|
||||||
const TOO_LARGE_TEXT = "Too large!";
|
const TOO_LARGE_TEXT = "Too large!";
|
||||||
const ZERO_TEXT = "File is blank!";
|
const ZERO_TEXT = "File is blank!";
|
||||||
const ERROR_TEXT = "Error!";
|
const ERROR_TEXT = "Error!";
|
||||||
|
|
||||||
async function formSubmit() {
|
async function formSubmit() {
|
||||||
const form = document.getElementById("uploadForm");
|
const form = document.getElementById("uploadForm");
|
||||||
const files = form.elements.fileUpload.files;
|
const files = form.elements["fileUpload"].files;
|
||||||
const duration = form.elements.duration.value;
|
const duration = form.elements["duration"].value;
|
||||||
const maxSize = form.elements.fileUpload.dataset.maxFilesize;
|
const maxSize = form.elements["fileUpload"].dataset.maxFilesize;
|
||||||
|
|
||||||
await fileSend(files, duration, maxSize);
|
await fileSend(files, duration, maxSize);
|
||||||
|
|
||||||
// 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 = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dragDropSubmit(evt) {
|
async function dragDropSubmit(evt) {
|
||||||
const form = document.getElementById("uploadForm");
|
const form = document.getElementById("uploadForm");
|
||||||
const duration = form.elements.duration.value;
|
const duration = form.elements["duration"].value;
|
||||||
|
|
||||||
const files = getDroppedFiles(evt);
|
const files = getDroppedFiles(evt);
|
||||||
|
|
||||||
|
@ -53,11 +51,11 @@ async function fileSend(files, duration, maxSize) {
|
||||||
if (file.size > maxSize) {
|
if (file.size > maxSize) {
|
||||||
makeErrored(progressBar, progressText, linkRow, TOO_LARGE_TEXT);
|
makeErrored(progressBar, progressText, linkRow, TOO_LARGE_TEXT);
|
||||||
console.error("Provided file is too large", file.size, "bytes; max", maxSize, "bytes");
|
console.error("Provided file is too large", file.size, "bytes; max", maxSize, "bytes");
|
||||||
continue;
|
continue
|
||||||
} else if (file.size == 0) {
|
} else if (file.size == 0) {
|
||||||
makeErrored(progressBar, progressText, linkRow, ZERO_TEXT);
|
makeErrored(progressBar, progressText, linkRow, ZERO_TEXT);
|
||||||
console.error("Provided file has 0 bytes");
|
console.error("Provided file has 0 bytes");
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new XMLHttpRequest();
|
const request = new XMLHttpRequest();
|
||||||
|
@ -65,11 +63,11 @@ async function fileSend(files, duration, maxSize) {
|
||||||
|
|
||||||
// Set up event listeners
|
// Set up event listeners
|
||||||
request.upload.addEventListener('progress',
|
request.upload.addEventListener('progress',
|
||||||
(p) => {uploadProgress(p, progressBar, progressText, linkRow);}, false);
|
(p) => {uploadProgress(p, progressBar, progressText, linkRow)}, false);
|
||||||
request.addEventListener('load',
|
request.addEventListener('load',
|
||||||
(c) => {uploadComplete(c, progressBar, progressText, linkRow);}, false);
|
(c) => {uploadComplete(c, progressBar, progressText, linkRow)}, false);
|
||||||
request.addEventListener('error',
|
request.addEventListener('error',
|
||||||
(e) => {networkErrorHandler(e, progressBar, progressText, linkRow);}, false);
|
(e) => {networkErrorHandler(e, progressBar, progressText, linkRow)}, false);
|
||||||
|
|
||||||
// Create and send FormData
|
// Create and send FormData
|
||||||
try {
|
try {
|
||||||
|
@ -92,7 +90,7 @@ function makeErrored(progressBar, progressText, linkRow, errorMessage) {
|
||||||
|
|
||||||
function makeFinished(progressBar, progressText, linkRow, response) {
|
function makeFinished(progressBar, progressText, linkRow, response) {
|
||||||
progressText.textContent = "";
|
progressText.textContent = "";
|
||||||
const _name = encodeURIComponent(response.name);
|
const name = encodeURIComponent(response.name);
|
||||||
const link = progressText.appendChild(document.createElement("a"));
|
const link = progressText.appendChild(document.createElement("a"));
|
||||||
link.textContent = response.mmid;
|
link.textContent = response.mmid;
|
||||||
link.href = "/f/" + response.mmid;
|
link.href = "/f/" + response.mmid;
|
||||||
|
@ -104,16 +102,16 @@ function makeFinished(progressBar, progressText, linkRow, response) {
|
||||||
button.addEventListener('click', function(_e) {
|
button.addEventListener('click', function(_e) {
|
||||||
const mmid = response.mmid;
|
const mmid = response.mmid;
|
||||||
if (buttonTimeout) {
|
if (buttonTimeout) {
|
||||||
clearTimeout(buttonTimeout);
|
clearTimeout(buttonTimeout)
|
||||||
}
|
}
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
window.location.protocol + "//" + window.location.host + "/f/" + mmid
|
window.location.protocol + "//" + window.location.host + "/f/" + mmid
|
||||||
);
|
)
|
||||||
button.textContent = "✅";
|
button.textContent = "✅";
|
||||||
buttonTimeout = setTimeout(function() {
|
buttonTimeout = setTimeout(function() {
|
||||||
button.textContent = "📝";
|
button.textContent = "📝";
|
||||||
}, 750);
|
}, 750);
|
||||||
});
|
})
|
||||||
|
|
||||||
progressBar.style.display = "none";
|
progressBar.style.display = "none";
|
||||||
linkRow.style.background = "#a4ffbb";
|
linkRow.style.background = "#a4ffbb";
|
||||||
|
@ -179,9 +177,10 @@ async function initEverything() {
|
||||||
for (const b of durationButtons) {
|
for (const b of durationButtons) {
|
||||||
b.addEventListener("click", function (_e) {
|
b.addEventListener("click", function (_e) {
|
||||||
if (this.classList.contains("selected")) {
|
if (this.classList.contains("selected")) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
document.getElementById("uploadForm").elements.duration.value = this.dataset.durationSeconds + "s";
|
document.getElementById("uploadForm").elements["duration"].value
|
||||||
|
= this.dataset.durationSeconds + "s";
|
||||||
let selected = this.parentNode.getElementsByClassName("selected");
|
let selected = this.parentNode.getElementsByClassName("selected");
|
||||||
selected[0].classList.remove("selected");
|
selected[0].classList.remove("selected");
|
||||||
this.classList.add("selected");
|
this.classList.add("selected");
|
||||||
|
@ -193,7 +192,7 @@ async function initEverything() {
|
||||||
document.addEventListener("DOMContentLoaded", function(_event) {
|
document.addEventListener("DOMContentLoaded", function(_event) {
|
||||||
const form = document.getElementById("uploadForm");
|
const form = document.getElementById("uploadForm");
|
||||||
form.addEventListener("submit", formSubmit);
|
form.addEventListener("submit", formSubmit);
|
||||||
let fileButton = document.getElementById("fileButton");
|
fileButton = document.getElementById("fileButton");
|
||||||
|
|
||||||
document.addEventListener("drop", (e) => {e.preventDefault();}, false);
|
document.addEventListener("drop", (e) => {e.preventDefault();}, false);
|
||||||
fileButton.addEventListener("dragover", (e) => {e.preventDefault();}, false);
|
fileButton.addEventListener("dragover", (e) => {e.preventDefault();}, false);
|
||||||
|
|
Loading…
Reference in a new issue