Add API documentation page

This commit is contained in:
G2-Games 2024-10-28 02:45:43 -05:00
parent 4b07ba4c46
commit 2f70967834
6 changed files with 142 additions and 41 deletions

View file

@ -42,7 +42,6 @@ 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()?;
@ -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>")] #[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>, settings: &State<Settings>,
mmid: &str, mmid: &str,
name: &str, name: &str,

View file

@ -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, server_info}; use endpoints::{lookup_mmid, lookup_mmid_name, lookup_mmid_noredir, 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::{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 settings::Settings;
use strings::{parse_time_string, to_pretty_time}; use strings::{parse_time_string, to_pretty_time};
@ -235,16 +235,10 @@ 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)

View file

@ -1,5 +1,7 @@
use maud::{html, Markup, DOCTYPE}; use maud::{html, Markup, DOCTYPE};
use rocket::get; use rocket::{get, State};
use crate::settings::Settings;
pub fn head(page_title: &str) -> Markup { pub fn head(page_title: &str) -> Markup {
html! { html! {
@ -18,14 +20,16 @@ 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_info" {"API Info"}} p {a href="api" {"API Info"}}
p {a href="https://ko-fi.com/g2_games" {"Donate"}} p {a href="https://ko-fi.com/g2_games" {"Donate"}}
} }
} }
} }
#[get("/api_info")] #[get("/api")]
pub fn api_info() -> Markup { pub fn api_info(settings: &State<Settings>) -> Markup {
let domain = &settings.server.domain;
let root = &settings.server.root_path;
html! { html! {
(head("Confetti-Box | API")) (head("Confetti-Box | API"))
@ -35,14 +39,77 @@ pub fn api_info() -> Markup {
div style="text-align: left;" { div style="text-align: left;" {
p { p {
""" "Confetti-Box is designed to be simple to access using its
The API for this service can be used by POST ing a form API. All endpoints are accessed following "
with an expiration time and file to upload to the upload code{"https://"(domain) (root)} ". All responses are encoded
endpoint: 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"# }
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 -> Redirect"# }
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;

View file

@ -95,6 +95,7 @@ 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,
@ -105,6 +106,7 @@ 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,

View file

@ -26,6 +26,10 @@ h1 {
font-weight: bolder; font-weight: bolder;
} }
p {
line-height: 1.5;
}
button p { button p {
margin: 0; margin: 0;
} }
@ -74,9 +78,21 @@ button.main_file_upload {
pre { pre {
color: white; color: white;
background-color: black; background-color: #161b22;
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 {

View file

@ -1,22 +1,24 @@
/*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);
@ -51,11 +53,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();
@ -63,11 +65,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 {
@ -90,7 +92,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;
@ -102,16 +104,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";
@ -177,10 +179,9 @@ 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 document.getElementById("uploadForm").elements.duration.value = this.dataset.durationSeconds + "s";
= 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");
@ -192,7 +193,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);
fileButton = document.getElementById("fileButton"); let 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);