mirror of
https://github.com/Dangoware/confetti-box.git
synced 2025-04-19 15:22:57 -05:00
Compare commits
12 commits
Author | SHA1 | Date | |
---|---|---|---|
83237c5d5a | |||
4426192700 | |||
80a11fbb0b | |||
a282bc588b | |||
b76ebd339c | |||
a3071b4f26 | |||
|
4c4698ed8e | ||
710552fc1f | |||
db07d75fa7 | |||
e6ae15bea5 | |||
beee488402 | |||
8990ec0e47 |
13 changed files with 212 additions and 137 deletions
10
README.md
10
README.md
|
@ -1,5 +1,6 @@
|
||||||
# Confetti-Box 🎉
|
# Confetti-Box 🎉
|
||||||
A super simple file host. Inspired by [Catbox](https://catbox.moe) and [Uguu](https://uguu.se).
|
A super simple file host. Inspired by [Catbox](https://catbox.moe) and
|
||||||
|
[Uguu](https://uguu.se).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
### Current
|
### Current
|
||||||
|
@ -7,6 +8,7 @@ A super simple file host. Inspired by [Catbox](https://catbox.moe) and [Uguu](ht
|
||||||
- Customizable using a simple config file
|
- Customizable using a simple config file
|
||||||
- Only stores one copy of a given hash on the backend
|
- Only stores one copy of a given hash on the backend
|
||||||
- Chunked uploads of configurable size
|
- Chunked uploads of configurable size
|
||||||
|
- Websocket uploads
|
||||||
- Fast (enough), runs just fine on a Raspberry Pi
|
- Fast (enough), runs just fine on a Raspberry Pi
|
||||||
- Simple API for interfacing with it programmatically
|
- Simple API for interfacing with it programmatically
|
||||||
- No database setup required, uses self-contained in memory database
|
- No database setup required, uses self-contained in memory database
|
||||||
|
@ -18,10 +20,10 @@ A super simple file host. Inspired by [Catbox](https://catbox.moe) and [Uguu](ht
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img width="500px" src="https://github.com/user-attachments/assets/9b12d65f-257d-448f-a7d0-43068cc3f8a3">
|
<img width="500px" src="./images/Confetti-Box Screenshot.png">
|
||||||
<p align="center"><i>An example of a running instance</i></p>
|
<p align="center"><i>An example of a running instance</i></p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Confetti-Box is licensed under the terms of the GNU AGPL-3.0 license. Do what you want
|
Confetti-Box is licensed under the terms of the GNU AGPL-3.0 license. Do what
|
||||||
with it within the terms of that license.
|
you want with it within the terms of that license.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "confetti_box"
|
name = "confetti_box"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
repository = "https://github.com/Dangoware/confetti-box"
|
repository = "https://github.com/Dangoware/confetti-box"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
@ -16,7 +16,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||||
ciborium = "0.2"
|
ciborium = "0.2"
|
||||||
file-format = { version = "0.26", features = ["reader"] }
|
file-format = { version = "0.26", features = ["reader"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
maud = { version = "0.26", features = ["rocket"] }
|
maud = { version = "0.27", features = ["rocket"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rocket = { version = "0.5", features = ["json"] }
|
rocket = { version = "0.5", features = ["json"] }
|
||||||
rocket_ws = "0.1"
|
rocket_ws = "0.1"
|
||||||
|
|
|
@ -35,6 +35,7 @@ pub fn home(settings: &State<Settings>) -> Markup {
|
||||||
center {
|
center {
|
||||||
h1 { "Confetti-Box 🎉" }
|
h1 { "Confetti-Box 🎉" }
|
||||||
h2 { "Files up to " (settings.max_filesize.bytes()) " in size are allowed!" }
|
h2 { "Files up to " (settings.max_filesize.bytes()) " in size are allowed!" }
|
||||||
|
noscript { "Javascript must be enabled for this site to function!" }
|
||||||
hr;
|
hr;
|
||||||
button.main_file_upload #fileButton onclick="document.getElementById('fileInput').click()" {
|
button.main_file_upload #fileButton onclick="document.getElementById('fileInput').click()" {
|
||||||
h4 { "Upload File(s)" }
|
h4 { "Upload File(s)" }
|
||||||
|
@ -306,7 +307,7 @@ pub async fn websocket_upload(
|
||||||
|
|
||||||
hasher.update(&message);
|
hasher.update(&message);
|
||||||
|
|
||||||
stream.send(rocket_ws::Message::Text(json::serde_json::ser::to_string(&offset).unwrap())).await.unwrap();
|
stream.send(rocket_ws::Message::binary(offset.to_le_bytes().as_slice())).await.unwrap();
|
||||||
|
|
||||||
file.write_all(&message).await.unwrap();
|
file.write_all(&message).await.unwrap();
|
||||||
|
|
||||||
|
@ -346,6 +347,7 @@ pub async fn websocket_upload(
|
||||||
file.flush().await.unwrap();
|
file.flush().await.unwrap();
|
||||||
|
|
||||||
stream.send(rocket_ws::Message::Text(json::serde_json::ser::to_string(&constructed_file).unwrap())).await?;
|
stream.send(rocket_ws::Message::Text(json::serde_json::ser::to_string(&constructed_file).unwrap())).await?;
|
||||||
|
stream.close(None).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})))
|
})))
|
||||||
|
|
|
@ -68,7 +68,8 @@ async fn main() {
|
||||||
confetti_box::home,
|
confetti_box::home,
|
||||||
pages::api_info,
|
pages::api_info,
|
||||||
pages::about,
|
pages::about,
|
||||||
resources::favicon,
|
resources::favicon_svg,
|
||||||
|
resources::favicon_ico,
|
||||||
resources::form_handler_js,
|
resources::form_handler_js,
|
||||||
resources::stylesheet,
|
resources::stylesheet,
|
||||||
resources::font_static,
|
resources::font_static,
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub fn head(page_title: &str) -> Markup {
|
||||||
meta charset="UTF-8";
|
meta charset="UTF-8";
|
||||||
meta name="viewport" content="width=device-width, initial-scale=1";
|
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||||
title { (page_title) }
|
title { (page_title) }
|
||||||
link rel="icon" type="image/svg+xml" href="/resources/favicon.svg";
|
link rel="icon" type="image/svg+xml" href="/favicon.svg";
|
||||||
link rel="stylesheet" href="/resources/main.css";
|
link rel="stylesheet" href="/resources/main.css";
|
||||||
link rel="preload" href="/resources/fonts/Roboto.woff2" as="font" type="font/woff2" crossorigin;
|
link rel="preload" href="/resources/fonts/Roboto.woff2" as="font" type="font/woff2" crossorigin;
|
||||||
link rel="preload" href="/resources/fonts/FiraCode.woff2" as="font" type="font/woff2" crossorigin;
|
link rel="preload" href="/resources/fonts/FiraCode.woff2" as="font" type="font/woff2" crossorigin;
|
||||||
|
|
|
@ -31,7 +31,12 @@ pub fn form_handler_js() -> RawJavaScript<&'static str> {
|
||||||
RawJavaScript(include_str!("../web/request.js"))
|
RawJavaScript(include_str!("../web/request.js"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/resources/favicon.svg")]
|
#[get("/favicon.svg")]
|
||||||
pub fn favicon() -> (ContentType, &'static str) {
|
pub fn favicon_svg() -> (ContentType, &'static str) {
|
||||||
(ContentType::SVG, include_str!("../web/favicon.svg"))
|
(ContentType::SVG, include_str!("../web/favicon.svg"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/favicon.ico")]
|
||||||
|
pub fn favicon_ico() -> (ContentType, &'static [u8]) {
|
||||||
|
(ContentType::Icon, include_bytes!("../web/favicon.ico"))
|
||||||
|
}
|
||||||
|
|
BIN
confetti-box/web/favicon.ico
Normal file
BIN
confetti-box/web/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
confetti-box/web/favicon.png
Normal file
BIN
confetti-box/web/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
|
@ -55,7 +55,7 @@ hr {
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
font-weight: bolder;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
@ -75,6 +75,7 @@ button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.button {
|
button.button {
|
||||||
|
@ -209,12 +210,15 @@ h2 code {
|
||||||
.upload_failed {
|
.upload_failed {
|
||||||
color: black;
|
color: black;
|
||||||
background-color: #ffb2ae;
|
background-color: #ffb2ae;
|
||||||
|
|
||||||
a:link {
|
a:link {
|
||||||
all: revert;
|
all: revert;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
all: revert;
|
all: revert;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
all: revert;
|
all: revert;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
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!";
|
||||||
|
const USERAGENT = navigator.userAgent;
|
||||||
|
const USE_CHUNKS_COMPAT = /Ladybird/.test(USERAGENT);
|
||||||
|
|
||||||
async function formSubmit() {
|
async function formSubmit() {
|
||||||
const form = document.getElementById("uploadForm");
|
const form = document.getElementById("uploadForm");
|
||||||
|
@ -17,6 +19,11 @@ async function formSubmit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dragDropSubmit(evt) {
|
async function dragDropSubmit(evt) {
|
||||||
|
fileButton.style.backgroundColor = "#84E5FF";
|
||||||
|
fileButton.style.removeProperty("transitionDuration");
|
||||||
|
fileButton.style.removeProperty("scale");
|
||||||
|
fileButton.style.removeProperty("transitionTimingFunction");
|
||||||
|
|
||||||
const form = document.getElementById("uploadForm");
|
const form = document.getElementById("uploadForm");
|
||||||
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;
|
||||||
|
@ -58,27 +65,28 @@ async function pasteSubmit(evt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendFiles(files, duration, maxSize) {
|
async function sendFiles(files, duration, maxSize) {
|
||||||
|
if (USE_CHUNKS_COMPAT) {
|
||||||
|
console.warn("This browser is known to have problems with WebSockets, falling back to chunked upload");
|
||||||
|
}
|
||||||
|
|
||||||
const inProgressUploads = new Set();
|
const inProgressUploads = new Set();
|
||||||
const concurrencyLimit = 10;
|
const concurrencyLimit = 10;
|
||||||
|
|
||||||
// Create a reference for the Wake Lock.
|
// Try to get a wake-lock
|
||||||
let wakeLock = null;
|
let wakeLock = null;
|
||||||
|
|
||||||
// create an async function to request a wake lock
|
|
||||||
try {
|
try {
|
||||||
wakeLock = await navigator.wakeLock.request("screen");
|
wakeLock = await navigator.wakeLock.request("screen");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Failed to set wake-lock!");
|
console.warn("Failed to set wake-lock!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let start = performance.now();
|
let start = performance.now();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
console.log("Started upload for", file.name);
|
console.log("Started upload for", file.name);
|
||||||
|
|
||||||
// Start the upload and add it to the set of in-progress uploads
|
// Start the upload and add it to the set of in-progress uploads
|
||||||
let uploadPromise;
|
let uploadPromise;
|
||||||
if ('WebSocket' in window && window.WebSocket.CLOSING === 2) {
|
if ('WebSocket' in window && window.WebSocket.CLOSING === 2 && !USE_CHUNKS_COMPAT) {
|
||||||
console.log("Uploading file using Websockets");
|
console.log("Uploading file using Websockets");
|
||||||
uploadPromise = uploadFileWebsocket(file, duration, maxSize);
|
uploadPromise = uploadFileWebsocket(file, duration, maxSize);
|
||||||
} else {
|
} else {
|
||||||
|
@ -101,9 +109,13 @@ async function sendFiles(files, duration, maxSize) {
|
||||||
let end = performance.now();
|
let end = performance.now();
|
||||||
console.log(end - start);
|
console.log(end - start);
|
||||||
|
|
||||||
wakeLock.release().then(() => {
|
try {
|
||||||
wakeLock = null;
|
wakeLock.release().then(() => {
|
||||||
});
|
wakeLock = null;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to modify wake-lock!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadFileChunked(file, duration, maxSize) {
|
async function uploadFileChunked(file, duration, maxSize) {
|
||||||
|
@ -208,8 +220,15 @@ async function uploadFileWebsocket(file, duration, maxSize) {
|
||||||
new_uri += "//" + loc.host;
|
new_uri += "//" + loc.host;
|
||||||
new_uri += "/upload/websocket?name=" + file.name +"&size=" + file.size + "&duration=" + parseInt(duration);
|
new_uri += "/upload/websocket?name=" + file.name +"&size=" + file.size + "&duration=" + parseInt(duration);
|
||||||
const socket = new WebSocket(new_uri);
|
const socket = new WebSocket(new_uri);
|
||||||
|
socket.binaryType = "arraybuffer";
|
||||||
|
|
||||||
const chunkSize = 10_000_000;
|
// Ensure that the websocket gets closed if the page is unloaded
|
||||||
|
window.onbeforeunload = function() {
|
||||||
|
socket.onclose = function () {};
|
||||||
|
socket.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const chunkSize = 5_000_000;
|
||||||
socket.addEventListener("open", (_event) => {
|
socket.addEventListener("open", (_event) => {
|
||||||
for (let chunk_num = 0; chunk_num < Math.floor(file.size / chunkSize) + 1; chunk_num ++) {
|
for (let chunk_num = 0; chunk_num < Math.floor(file.size / chunkSize) + 1; chunk_num ++) {
|
||||||
const offset = Math.floor(chunk_num * chunkSize);
|
const offset = Math.floor(chunk_num * chunkSize);
|
||||||
|
@ -221,16 +240,20 @@ async function uploadFileWebsocket(file, duration, maxSize) {
|
||||||
socket.send("");
|
socket.send("");
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, _reject) {
|
||||||
socket.addEventListener("message", (event) => {
|
socket.addEventListener("message", (event) => {
|
||||||
const response = JSON.parse(event.data);
|
if (event.data instanceof ArrayBuffer) {
|
||||||
if (response.mmid == null) {
|
const view = new DataView(event.data);
|
||||||
const progress = parseInt(response);
|
console.log(view.getBigUint64(0, true));
|
||||||
|
const progress = parseInt(view.getBigUint64(0, true));
|
||||||
uploadProgressWebsocket(progress, progressBar, progressText, file.size);
|
uploadProgressWebsocket(progress, progressBar, progressText, file.size);
|
||||||
} else {
|
} else {
|
||||||
// It's so over
|
// It's so over
|
||||||
socket.close();
|
if (!socket.CLOSED) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = JSON.parse(event.data);
|
||||||
uploadComplete(response, 200, progressBar, progressText, linkRow);
|
uploadComplete(response, 200, progressBar, progressText, linkRow);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
@ -359,7 +382,20 @@ document.addEventListener("DOMContentLoaded", function(_event) {
|
||||||
let fileButton = document.getElementById("fileButton");
|
let fileButton = document.getElementById("fileButton");
|
||||||
document.addEventListener("drop", (e) => {e.preventDefault();}, false);
|
document.addEventListener("drop", (e) => {e.preventDefault();}, false);
|
||||||
document.addEventListener("dragover", (e) => {e.preventDefault()}, false);
|
document.addEventListener("dragover", (e) => {e.preventDefault()}, false);
|
||||||
fileButton.addEventListener("dragover", (e) => {e.preventDefault();}, false);
|
fileButton.addEventListener("dragover", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
fileButton.style.backgroundColor = "#9cff7e";
|
||||||
|
fileButton.style.transitionDuration = "0.5s";
|
||||||
|
fileButton.style.scale = "1.1";
|
||||||
|
fileButton.style.transitionTimingFunction = "cubic-bezier(.23,-0.09,.52,1.62)";
|
||||||
|
}, false);
|
||||||
|
fileButton.addEventListener("dragleave", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
fileButton.style.backgroundColor = "#84E5FF";
|
||||||
|
fileButton.style.removeProperty("transitionDuration");
|
||||||
|
fileButton.style.removeProperty("scale");
|
||||||
|
fileButton.style.removeProperty("transitionTimingFunction");
|
||||||
|
}, false);
|
||||||
fileButton.addEventListener("drop", dragDropSubmit, false);
|
fileButton.addEventListener("drop", dragDropSubmit, false);
|
||||||
|
|
||||||
initEverything();
|
initEverything();
|
||||||
|
|
|
@ -7,7 +7,7 @@ keywords = ["selfhost", "upload", "command_line"]
|
||||||
categories = ["command-line-utilities"]
|
categories = ["command-line-utilities"]
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "imu"
|
name = "imu"
|
||||||
|
@ -17,17 +17,21 @@ path = "src/main.rs"
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.92"
|
anyhow = "1.0"
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
base64 = "0.22.1"
|
||||||
clap = { version = "4.5.20", features = ["derive", "unicode"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
directories = "5.0.1"
|
clap = { version = "4.5", features = ["derive", "unicode"] }
|
||||||
indicatif = { version = "0.17.8", features = ["improved_unicode"] }
|
directories = "6.0"
|
||||||
owo-colors = { version = "4.1.0", features = ["supports-colors"] }
|
futures-util = "0.3.31"
|
||||||
reqwest = { version = "0.12.8", features = ["json", "stream"] }
|
indicatif = { version = "0.17", features = ["improved_unicode"] }
|
||||||
serde = { version = "1.0.213", features = ["derive"] }
|
owo-colors = { version = "4.1", features = ["supports-colors"] }
|
||||||
serde_json = "1.0.132"
|
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||||
thiserror = "1.0.68"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tokio = { version = "1.41.0", features = ["fs", "macros", "rt-multi-thread"] }
|
serde_json = "1.0"
|
||||||
tokio-util = { version = "0.7.12", features = ["codec"] }
|
thiserror = "1.0"
|
||||||
toml = "0.8.19"
|
tokio = { version = "1.41", features = ["fs", "macros", "rt-multi-thread"] }
|
||||||
uuid = { version = "1.11.0", features = ["serde", "v4"] }
|
tokio-tungstenite = { version = "0.26.2", features = ["native-tls"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
|
toml = "0.8"
|
||||||
|
url = { version = "2.5.4", features = ["serde"] }
|
||||||
|
uuid = { version = "1.11", features = ["serde", "v4"] }
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
use std::{error::Error, fs, io::{self, Read, Write}, os::unix::fs::MetadataExt, path::{Path, PathBuf}};
|
use std::{error::Error, fs, io::{self, Read, Write}, path::{Path, PathBuf}};
|
||||||
|
|
||||||
|
use base64::{prelude::BASE64_URL_SAFE, Engine};
|
||||||
use chrono::{DateTime, Datelike, Local, Month, TimeDelta, Timelike, Utc};
|
use chrono::{DateTime, Datelike, Local, Month, TimeDelta, Timelike, Utc};
|
||||||
|
|
||||||
|
use futures_util::{stream::FusedStream as _, SinkExt as _, StreamExt as _};
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::{fs::{create_dir, File}, io::{AsyncReadExt, AsyncWriteExt}, task::JoinSet};
|
use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}, join, task::JoinSet};
|
||||||
|
use tokio_tungstenite::{connect_async, tungstenite::{client::IntoClientRequest as _, Message}};
|
||||||
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use clap::{arg, builder::{styling::RgbColor, Styles}, Parser, Subcommand};
|
use clap::{arg, builder::{styling::RgbColor, Styles}, Parser, Subcommand};
|
||||||
use anyhow::{anyhow, bail, Context as _, Result};
|
use anyhow::{anyhow, bail, Context as _, Result};
|
||||||
|
@ -53,7 +57,7 @@ enum Commands {
|
||||||
/// Set the password for a server which requires login
|
/// Set the password for a server which requires login
|
||||||
#[arg(short, long, required = false)]
|
#[arg(short, long, required = false)]
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
/// Set the URL of the server to connect to
|
/// Set the URL of the server to connect to (assumes https://)
|
||||||
#[arg(long, required = false)]
|
#[arg(long, required = false)]
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
/// Set the directory to download into by default
|
/// Set the directory to download into by default
|
||||||
|
@ -82,17 +86,16 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Commands::Upload { files, duration } => {
|
Commands::Upload { files, duration } => {
|
||||||
if config.url.is_empty() {
|
let Some(url) = config.url.clone() else {
|
||||||
exit_error(
|
exit_error(
|
||||||
format!("URL is empty"),
|
format!("URL is empty"),
|
||||||
Some(format!("Please set it using the {} command", "set".truecolor(246,199,219).bold())),
|
Some(format!("Please set it using the {} command", "set".truecolor(246,199,219).bold())),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
get_info_if_expired(&mut config).await?;
|
get_info_if_expired(&mut config).await?;
|
||||||
|
|
||||||
let client = Client::new();
|
|
||||||
let duration = match parse_time_string(&duration) {
|
let duration = match parse_time_string(&duration) {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => return Err(anyhow!("Invalid duration: {e}")),
|
Err(e) => return Err(anyhow!("Invalid duration: {e}")),
|
||||||
|
@ -125,8 +128,7 @@ async fn main() -> Result<()> {
|
||||||
let response = upload_file(
|
let response = upload_file(
|
||||||
name.into_owned(),
|
name.into_owned(),
|
||||||
&path,
|
&path,
|
||||||
&client,
|
&url,
|
||||||
&config.url,
|
|
||||||
duration,
|
duration,
|
||||||
&config.login
|
&config.login
|
||||||
).await.with_context(|| "Failed to upload").unwrap();
|
).await.with_context(|| "Failed to upload").unwrap();
|
||||||
|
@ -141,11 +143,19 @@ async fn main() -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"{:>8} {}, {} (in {})\n{:>8} {}",
|
"{:>8} {}, {} (in {})\n{:>8} {}",
|
||||||
"Expires:".truecolor(174,196,223).bold(), date, time, pretty_time_long(duration.num_seconds()),
|
"Expires:".truecolor(174,196,223).bold(), date, time, pretty_time_long(duration.num_seconds()),
|
||||||
"URL:".truecolor(174,196,223).bold(), (config.url.clone() + "/f/" + &response.mmid.0).underline()
|
"URL:".truecolor(174,196,223).bold(), (url.to_string() + "/f/" + &response.mmid.0).underline()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Download { mmids, out_directory } => {
|
Commands::Download { mmids, out_directory } => {
|
||||||
|
let Some(url) = config.url else {
|
||||||
|
exit_error(
|
||||||
|
format!("URL is empty"),
|
||||||
|
Some(format!("Please set it using the {} command", "set".truecolor(246,199,219).bold())),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let out_directory = if let Some(dir) = out_directory {
|
let out_directory = if let Some(dir) = out_directory {
|
||||||
dir
|
dir
|
||||||
} else {
|
} else {
|
||||||
|
@ -167,7 +177,6 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let url = &config.url;
|
|
||||||
for mmid in mmids {
|
for mmid in mmids {
|
||||||
let mmid = if mmid.len() != 8 {
|
let mmid = if mmid.len() != 8 {
|
||||||
if mmid.contains(format!("{url}/f/").as_str()) {
|
if mmid.contains(format!("{url}/f/").as_str()) {
|
||||||
|
@ -203,10 +212,10 @@ async fn main() -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut file_res = if let Some(login) = &config.login {
|
let mut file_res = if let Some(login) = &config.login {
|
||||||
client.get(format!("{}/f/{mmid}", config.url))
|
client.get(format!("{}/f/{mmid}", url))
|
||||||
.basic_auth(&login.user, Some(&login.pass))
|
.basic_auth(&login.user, Some(&login.pass))
|
||||||
} else {
|
} else {
|
||||||
client.get(format!("{}/f/{mmid}", config.url))
|
client.get(format!("{}/f/{mmid}", url))
|
||||||
}
|
}
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
@ -305,7 +314,14 @@ async fn main() -> Result<()> {
|
||||||
url
|
url
|
||||||
};
|
};
|
||||||
|
|
||||||
config.url = url.to_string();
|
let new_url = if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||||
|
("https://".to_owned() + url).to_string()
|
||||||
|
} else {
|
||||||
|
url.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
config.url = Some(Url::parse(&new_url)?);
|
||||||
|
|
||||||
config.save().unwrap();
|
config.save().unwrap();
|
||||||
println!("URL set to \"{url}\"");
|
println!("URL set to \"{url}\"");
|
||||||
}
|
}
|
||||||
|
@ -351,7 +367,7 @@ async fn main() -> Result<()> {
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
enum UploadError {
|
enum UploadError {
|
||||||
#[error("request provided was invalid: {0}")]
|
#[error("request provided was invalid: {0}")]
|
||||||
InvalidRequest(String),
|
WebSocketFailed(String),
|
||||||
|
|
||||||
#[error("error on reqwest transaction: {0}")]
|
#[error("error on reqwest transaction: {0}")]
|
||||||
Reqwest(#[from] reqwest::Error),
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
@ -360,104 +376,101 @@ enum UploadError {
|
||||||
async fn upload_file<P: AsRef<Path>>(
|
async fn upload_file<P: AsRef<Path>>(
|
||||||
name: String,
|
name: String,
|
||||||
path: &P,
|
path: &P,
|
||||||
client: &Client,
|
url: &Url,
|
||||||
url: &String,
|
|
||||||
duration: TimeDelta,
|
duration: TimeDelta,
|
||||||
login: &Option<Login>,
|
login: &Option<Login>,
|
||||||
) -> Result<MochiFile, UploadError> {
|
) -> Result<MochiFile, UploadError> {
|
||||||
let mut file = File::open(path).await.unwrap();
|
let mut file = File::open(path).await.unwrap();
|
||||||
let size = file.metadata().await.unwrap().size() as u64;
|
let file_size = file.metadata().await.unwrap().len();
|
||||||
|
|
||||||
let ChunkedResponse {status, message, uuid, chunk_size} = {
|
// Construct the URL
|
||||||
client.post(format!("{url}/upload/chunked/"))
|
let mut url = url.clone();
|
||||||
.json(
|
if url.scheme() == "http" {
|
||||||
&ChunkedInfo {
|
url.set_scheme("ws").unwrap();
|
||||||
name: name.clone(),
|
} else if url.scheme() == "https" {
|
||||||
size,
|
url.set_scheme("wss").unwrap();
|
||||||
expire_duration: duration.num_seconds() as u64,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.basic_auth(&login.as_ref().unwrap().user, login.as_ref().unwrap().pass.clone().into())
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json()
|
|
||||||
.await?
|
|
||||||
};
|
|
||||||
|
|
||||||
if !status {
|
|
||||||
return Err(UploadError::InvalidRequest(message));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut i = 0;
|
url.set_path("/upload/websocket");
|
||||||
let post_url = format!("{url}/upload/chunked/{}", uuid.unwrap());
|
url.set_query(Some(&format!("name={}&size={}&duration={}", name, file_size, duration.num_seconds())));
|
||||||
let mut request_set = JoinSet::new();
|
|
||||||
|
let mut request = url.to_string().into_client_request().unwrap();
|
||||||
|
|
||||||
|
if let Some(l) = login {
|
||||||
|
request.headers_mut().insert(
|
||||||
|
"Authorization",
|
||||||
|
format!("Basic {}", BASE64_URL_SAFE.encode(format!("{}:{}", l.user, l.pass))).parse().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (stream, _response) = connect_async(request).await.map_err(|e| UploadError::WebSocketFailed(e.to_string()))?;
|
||||||
|
let (mut write, mut read) = stream.split();
|
||||||
|
|
||||||
|
// Upload the file in chunks
|
||||||
|
let upload_task = async move {
|
||||||
|
let mut chunk = vec![0u8; 20_000];
|
||||||
|
loop {
|
||||||
|
let read_len = file.read(&mut chunk).await.unwrap();
|
||||||
|
if read_len == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
write.send(Message::binary(chunk[..read_len].to_vec())).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the stream because sending is over
|
||||||
|
write.send(Message::binary(b"".as_slice())).await.unwrap();
|
||||||
|
write.flush().await.unwrap();
|
||||||
|
|
||||||
|
write
|
||||||
|
};
|
||||||
|
|
||||||
let bar = ProgressBar::new(100);
|
let bar = ProgressBar::new(100);
|
||||||
bar.set_style(ProgressStyle::with_template(
|
bar.set_style(ProgressStyle::with_template(
|
||||||
&format!("{} {{bar:40.cyan/blue}} {{pos:>3}}% {{msg}}", name)
|
&format!("{} {{bar:40.cyan/blue}} {{pos:>3}}% {{msg}}", name)
|
||||||
).unwrap());
|
).unwrap());
|
||||||
loop {
|
|
||||||
// Read the next chunk into a buffer
|
|
||||||
let mut chunk = vec![0u8; chunk_size.unwrap() as usize];
|
|
||||||
let bytes_read = fill_buffer(&mut chunk, &mut file).await.unwrap();
|
|
||||||
if bytes_read == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let chunk = chunk[..bytes_read].to_owned();
|
|
||||||
|
|
||||||
request_set.spawn({
|
// Get the progress of the file upload
|
||||||
let post_url = post_url.clone();
|
let progress_task = async move {
|
||||||
let user = login.as_ref().unwrap().user.clone();
|
let final_json = loop {
|
||||||
let pass = login.as_ref().unwrap().pass.clone();
|
let Some(p) = read.next().await else {
|
||||||
// Reuse the client for all the threads
|
break String::new()
|
||||||
let client = Client::clone(client);
|
};
|
||||||
|
|
||||||
async move {
|
let p = p.unwrap();
|
||||||
client.post(&post_url)
|
|
||||||
.query(&[("chunk", i)])
|
// Got the final json information, return that
|
||||||
.basic_auth(&user, pass.into())
|
if p.is_text() {
|
||||||
.body(chunk)
|
break p.into_text().unwrap().to_string()
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
i += 1;
|
// Get the progress information
|
||||||
|
let prog = p.into_data();
|
||||||
|
let prog = u64::from_le_bytes(prog.to_vec().try_into().unwrap());
|
||||||
|
let percent = f64::trunc((prog as f64 / file_size as f64) * 100.0);
|
||||||
|
if percent <= 100. {
|
||||||
|
bar.set_position(percent as u64);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Limit the number of concurrent uploads to 5
|
(read, final_json, bar)
|
||||||
if request_set.len() >= 5 {
|
};
|
||||||
bar.set_message("");
|
|
||||||
request_set.join_next().await;
|
|
||||||
bar.set_message("⏳");
|
|
||||||
}
|
|
||||||
|
|
||||||
let percent = f64::trunc(((i as f64 * chunk_size.unwrap() as f64) / size as f64) * 100.0);
|
// Wait for both of the tasks to finish
|
||||||
if percent <= 100. {
|
let (read, write) = join!(progress_task, upload_task);
|
||||||
bar.set_position(percent as u64);
|
let (read, final_json, bar) = read;
|
||||||
}
|
let mut stream = write.reunite(read).unwrap();
|
||||||
|
|
||||||
|
let file_info: MochiFile = serde_json::from_str(&final_json).unwrap();
|
||||||
|
|
||||||
|
// If the websocket isn't closed, do that
|
||||||
|
if !stream.is_terminated() {
|
||||||
|
stream.close(None).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all remaining uploads to finish
|
|
||||||
loop {
|
|
||||||
if let Some(t) = request_set.join_next().await {
|
|
||||||
match t {
|
|
||||||
Ok(_) => (),
|
|
||||||
Err(_) => todo!(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bar.finish_and_clear();
|
bar.finish_and_clear();
|
||||||
println!("[{}] - \"{}\"", "✓".bright_green(), name);
|
|
||||||
|
|
||||||
Ok(
|
Ok(file_info)
|
||||||
client.get(format!("{url}/upload/chunked/{}?finish", uuid.unwrap()))
|
|
||||||
.basic_auth(&login.as_ref().unwrap().user, login.as_ref().unwrap().pass.clone().into())
|
|
||||||
.send()
|
|
||||||
.await.unwrap()
|
|
||||||
.json::<MochiFile>()
|
|
||||||
.await?
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_info_if_expired(config: &mut Config) -> Result<()> {
|
async fn get_info_if_expired(config: &mut Config) -> Result<()> {
|
||||||
|
@ -477,7 +490,13 @@ async fn get_info_if_expired(config: &mut Config) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_info(config: &Config) -> Result<ServerInfo> {
|
async fn get_info(config: &Config) -> Result<ServerInfo> {
|
||||||
let url = config.url.clone();
|
let Some(url) = config.url.clone() else {
|
||||||
|
exit_error(
|
||||||
|
format!("URL is empty"),
|
||||||
|
Some(format!("Please set it using the {} command", "set".truecolor(246,199,219).bold())),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
};
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
let get_info = client.get(format!("{url}/info"));
|
let get_info = client.get(format!("{url}/info"));
|
||||||
|
@ -515,9 +534,11 @@ async fn fill_buffer<S: AsyncReadExt + Unpin>(buffer: &mut [u8], mut stream: S)
|
||||||
let mut bytes_read = 0;
|
let mut bytes_read = 0;
|
||||||
while bytes_read < buffer.len() {
|
while bytes_read < buffer.len() {
|
||||||
let len = stream.read(&mut buffer[bytes_read..]).await?;
|
let len = stream.read(&mut buffer[bytes_read..]).await?;
|
||||||
|
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes_read += len;
|
bytes_read += len;
|
||||||
}
|
}
|
||||||
Ok(bytes_read)
|
Ok(bytes_read)
|
||||||
|
@ -591,7 +612,7 @@ struct Login {
|
||||||
#[derive(Deserialize, Serialize, Debug, Default)]
|
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
struct Config {
|
struct Config {
|
||||||
url: String,
|
url: Option<Url>,
|
||||||
login: Option<Login>,
|
login: Option<Login>,
|
||||||
/// The time when the info was last fetched
|
/// The time when the info was last fetched
|
||||||
info_fetch: Option<DateTime<Utc>>,
|
info_fetch: Option<DateTime<Utc>>,
|
||||||
|
@ -606,7 +627,7 @@ impl Config {
|
||||||
str
|
str
|
||||||
} else {
|
} else {
|
||||||
let c = Config {
|
let c = Config {
|
||||||
url: String::new(),
|
url: None,
|
||||||
login: None,
|
login: None,
|
||||||
info_fetch: None,
|
info_fetch: None,
|
||||||
info: None,
|
info: None,
|
||||||
|
@ -639,7 +660,7 @@ impl Config {
|
||||||
|
|
||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
let c = Config {
|
let c = Config {
|
||||||
url: String::new(),
|
url: None,
|
||||||
login: None,
|
login: None,
|
||||||
info: None,
|
info: None,
|
||||||
info_fetch: None,
|
info_fetch: None,
|
||||||
|
|
BIN
images/Confetti-Box Screenshot.png
Normal file
BIN
images/Confetti-Box Screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
Loading…
Reference in a new issue