mirror of
https://github.com/Dangoware/confetti-box.git
synced 2025-04-19 07:12:58 -05:00
Compare commits
5 commits
4b07ba4c46
...
0f780d027d
Author | SHA1 | Date | |
---|---|---|---|
0f780d027d | |||
8de33459eb | |||
a2148d4227 | |||
24253020f4 | |||
2f70967834 |
8 changed files with 144 additions and 43 deletions
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "confetti_box"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -245,6 +245,7 @@ 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.");
|
||||
|
||||
database.save();
|
||||
drop(database); // Just to be sure
|
||||
}
|
||||
|
||||
/// A loop to clean the database periodically.
|
||||
|
|
|
@ -42,7 +42,6 @@ pub struct ServerInfo {
|
|||
allowed_durations: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Look up the [`Mmid`] of a file to find it.
|
||||
#[get("/f/<mmid>")]
|
||||
pub async fn lookup_mmid(db: &State<Arc<RwLock<Database>>>, mmid: &str) -> Option<Redirect> {
|
||||
let mmid: Mmid = mmid.try_into().ok()?;
|
||||
|
@ -54,9 +53,31 @@ pub async fn lookup_mmid(db: &State<Arc<RwLock<Database>>>, mmid: &str) -> Optio
|
|||
))))
|
||||
}
|
||||
|
||||
/// Look up the [`Mmid`] of a file to find it, along with the name of the file
|
||||
#[get("/f/<mmid>?noredir")]
|
||||
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>")]
|
||||
pub async fn lookup_mmid_name(db: &State<Arc<RwLock<Database>>>,
|
||||
pub async fn lookup_mmid_name(
|
||||
db: &State<Arc<RwLock<Database>>>,
|
||||
settings: &State<Settings>,
|
||||
mmid: &str,
|
||||
name: &str,
|
||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -12,12 +12,12 @@ use std::{
|
|||
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use database::{clean_loop, Database, Mmid, MochiFile};
|
||||
use endpoints::{lookup_mmid, lookup_mmid_name, server_info};
|
||||
use endpoints::{lookup_mmid, lookup_mmid_name, lookup_mmid_noredir, server_info};
|
||||
use log::info;
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use pages::{api_info, footer, head};
|
||||
use rocket::{
|
||||
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
|
||||
data::{Limits, ToByteUnit}, form::Form, fs::TempFile, get, http::ContentType, post, response::content::{RawCss, RawJavaScript}, routes, serde::{json::Json, Serialize}, tokio, Config, FromForm, State
|
||||
};
|
||||
use settings::Settings;
|
||||
use strings::{parse_time_string, to_pretty_time};
|
||||
|
@ -147,7 +147,6 @@ async fn handle_upload(
|
|||
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().save();
|
||||
|
||||
Ok(Json(ClientResponse {
|
||||
status: true,
|
||||
|
@ -235,16 +234,10 @@ async fn main() {
|
|||
server_info,
|
||||
favicon,
|
||||
lookup_mmid,
|
||||
lookup_mmid_noredir,
|
||||
lookup_mmid_name,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
config.server.root_path.clone() + "/files",
|
||||
FileServer::new(
|
||||
config.file_dir.clone(),
|
||||
Options::Missing | Options::NormalizeDirs,
|
||||
),
|
||||
)
|
||||
.manage(database)
|
||||
.manage(config)
|
||||
.configure(rocket_config)
|
||||
|
|
87
src/pages.rs
87
src/pages.rs
|
@ -1,5 +1,7 @@
|
|||
use maud::{html, Markup, DOCTYPE};
|
||||
use rocket::get;
|
||||
use rocket::{get, State};
|
||||
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub fn head(page_title: &str) -> Markup {
|
||||
html! {
|
||||
|
@ -18,14 +20,16 @@ pub fn footer() -> Markup {
|
|||
p {a href="/" {"Home"}}
|
||||
p {a href="https://github.com/G2-Games/confetti-box" {"Source"}}
|
||||
p {a href="https://g2games.dev/" {"My Website"}}
|
||||
p {a href="api_info" {"API Info"}}
|
||||
p {a href="api" {"API"}}
|
||||
p {a href="https://ko-fi.com/g2_games" {"Donate"}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/api_info")]
|
||||
pub fn api_info() -> Markup {
|
||||
#[get("/api")]
|
||||
pub fn api_info(settings: &State<Settings>) -> Markup {
|
||||
let domain = &settings.server.domain;
|
||||
let root = &settings.server.root_path;
|
||||
html! {
|
||||
(head("Confetti-Box | API"))
|
||||
|
||||
|
@ -35,14 +39,77 @@ pub fn api_info() -> Markup {
|
|||
|
||||
div style="text-align: left;" {
|
||||
p {
|
||||
"""
|
||||
The API for this service can be used by POST ing a form
|
||||
with an expiration time and file to upload to the upload
|
||||
endpoint:
|
||||
"""
|
||||
"Confetti-Box is designed to be simple to access using its
|
||||
API. All endpoints are accessed following "
|
||||
code{"https://"(domain) (root)} ". All responses are encoded
|
||||
in JSON. MMIDs are a unique identifier for a file returned by
|
||||
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;
|
||||
|
|
|
@ -95,6 +95,7 @@ impl Settings {
|
|||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct ServerSettings {
|
||||
pub domain: String,
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
|
||||
|
@ -105,6 +106,7 @@ pub struct ServerSettings {
|
|||
impl Default for ServerSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
domain: "example.com".into(),
|
||||
address: "127.0.0.1".into(),
|
||||
root_path: "/".into(),
|
||||
port: 8950,
|
||||
|
|
18
web/main.css
18
web/main.css
|
@ -26,6 +26,10 @@ h1 {
|
|||
font-weight: bolder;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
button p {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -74,9 +78,21 @@ button.main_file_upload {
|
|||
|
||||
pre {
|
||||
color: white;
|
||||
background-color: black;
|
||||
background-color: #161b22;
|
||||
font-size: 11pt;
|
||||
padding: 10px;
|
||||
overflow: scroll;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
p code {
|
||||
background-color: lightgray;
|
||||
font-size: 12pt;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
h2 code {
|
||||
font-size: 15pt;
|
||||
}
|
||||
|
||||
#durationBox {
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
/*jshint esversion: 11 */
|
||||
|
||||
const TOO_LARGE_TEXT = "Too large!";
|
||||
const ZERO_TEXT = "File is blank!";
|
||||
const ERROR_TEXT = "Error!";
|
||||
|
||||
async function formSubmit() {
|
||||
const form = document.getElementById("uploadForm");
|
||||
const files = form.elements["fileUpload"].files;
|
||||
const duration = form.elements["duration"].value;
|
||||
const maxSize = form.elements["fileUpload"].dataset.maxFilesize;
|
||||
const files = form.elements.fileUpload.files;
|
||||
const duration = form.elements.duration.value;
|
||||
const maxSize = form.elements.fileUpload.dataset.maxFilesize;
|
||||
|
||||
await fileSend(files, duration, maxSize);
|
||||
|
||||
// Reset the form file data since we've successfully submitted it
|
||||
form.elements["fileUpload"].value = "";
|
||||
form.elements.fileUpload.value = "";
|
||||
}
|
||||
|
||||
async function dragDropSubmit(evt) {
|
||||
const form = document.getElementById("uploadForm");
|
||||
const duration = form.elements["duration"].value;
|
||||
const duration = form.elements.duration.value;
|
||||
|
||||
const files = getDroppedFiles(evt);
|
||||
|
||||
|
@ -51,11 +53,11 @@ async function fileSend(files, duration, maxSize) {
|
|||
if (file.size > maxSize) {
|
||||
makeErrored(progressBar, progressText, linkRow, TOO_LARGE_TEXT);
|
||||
console.error("Provided file is too large", file.size, "bytes; max", maxSize, "bytes");
|
||||
continue
|
||||
continue;
|
||||
} else if (file.size == 0) {
|
||||
makeErrored(progressBar, progressText, linkRow, ZERO_TEXT);
|
||||
console.error("Provided file has 0 bytes");
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
|
@ -63,11 +65,11 @@ async function fileSend(files, duration, maxSize) {
|
|||
|
||||
// Set up event listeners
|
||||
request.upload.addEventListener('progress',
|
||||
(p) => {uploadProgress(p, progressBar, progressText, linkRow)}, false);
|
||||
(p) => {uploadProgress(p, progressBar, progressText, linkRow);}, false);
|
||||
request.addEventListener('load',
|
||||
(c) => {uploadComplete(c, progressBar, progressText, linkRow)}, false);
|
||||
(c) => {uploadComplete(c, progressBar, progressText, linkRow);}, false);
|
||||
request.addEventListener('error',
|
||||
(e) => {networkErrorHandler(e, progressBar, progressText, linkRow)}, false);
|
||||
(e) => {networkErrorHandler(e, progressBar, progressText, linkRow);}, false);
|
||||
|
||||
// Create and send FormData
|
||||
try {
|
||||
|
@ -90,7 +92,7 @@ function makeErrored(progressBar, progressText, linkRow, errorMessage) {
|
|||
|
||||
function makeFinished(progressBar, progressText, linkRow, response) {
|
||||
progressText.textContent = "";
|
||||
const name = encodeURIComponent(response.name);
|
||||
const _name = encodeURIComponent(response.name);
|
||||
const link = progressText.appendChild(document.createElement("a"));
|
||||
link.textContent = response.mmid;
|
||||
link.href = "/f/" + response.mmid;
|
||||
|
@ -102,16 +104,16 @@ function makeFinished(progressBar, progressText, linkRow, response) {
|
|||
button.addEventListener('click', function(_e) {
|
||||
const mmid = response.mmid;
|
||||
if (buttonTimeout) {
|
||||
clearTimeout(buttonTimeout)
|
||||
clearTimeout(buttonTimeout);
|
||||
}
|
||||
navigator.clipboard.writeText(
|
||||
window.location.protocol + "//" + window.location.host + "/f/" + mmid
|
||||
)
|
||||
);
|
||||
button.textContent = "✅";
|
||||
buttonTimeout = setTimeout(function() {
|
||||
button.textContent = "📝";
|
||||
}, 750);
|
||||
})
|
||||
});
|
||||
|
||||
progressBar.style.display = "none";
|
||||
linkRow.style.background = "#a4ffbb";
|
||||
|
@ -177,10 +179,9 @@ async function initEverything() {
|
|||
for (const b of durationButtons) {
|
||||
b.addEventListener("click", function (_e) {
|
||||
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");
|
||||
selected[0].classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
|
@ -192,7 +193,7 @@ async function initEverything() {
|
|||
document.addEventListener("DOMContentLoaded", function(_event) {
|
||||
const form = document.getElementById("uploadForm");
|
||||
form.addEventListener("submit", formSubmit);
|
||||
fileButton = document.getElementById("fileButton");
|
||||
let fileButton = document.getElementById("fileButton");
|
||||
|
||||
document.addEventListener("drop", (e) => {e.preventDefault();}, false);
|
||||
fileButton.addEventListener("dragover", (e) => {e.preventDefault();}, false);
|
||||
|
|
Loading…
Reference in a new issue