Compare commits

...

5 commits

Author SHA1 Message Date
0f780d027d Only save database on clean and shutdown 2024-10-28 03:30:36 -05:00
8de33459eb Version bump, 0.1.2 2024-10-28 03:09:41 -05:00
a2148d4227 Minor tweaks to the API docs 2024-10-28 02:57:33 -05:00
24253020f4 Shortened API link name 2024-10-28 02:49:11 -05:00
2f70967834 Add API documentation page 2024-10-28 02:45:43 -05:00
8 changed files with 144 additions and 43 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "confetti_box"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
[dependencies]

View file

@ -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.

View file

@ -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,

View file

@ -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)

View file

@ -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;

View file

@ -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,

View file

@ -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 {

View file

@ -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);