Compare commits

..

No commits in common. "main" and "0.2.0" have entirely different histories.
main ... 0.2.0

13 changed files with 137 additions and 212 deletions

View file

@ -1,6 +1,5 @@
# Confetti-Box 🎉 # Confetti-Box 🎉
A super simple file host. Inspired by [Catbox](https://catbox.moe) and A super simple file host. Inspired by [Catbox](https://catbox.moe) and [Uguu](https://uguu.se).
[Uguu](https://uguu.se).
## Features ## Features
### Current ### Current
@ -8,7 +7,6 @@ A super simple file host. Inspired by [Catbox](https://catbox.moe) and
- 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
@ -20,10 +18,10 @@ A super simple file host. Inspired by [Catbox](https://catbox.moe) and
## Screenshot ## Screenshot
<p align="center"> <p align="center">
<img width="500px" src="./images/Confetti-Box Screenshot.png"> <img width="500px" src="https://github.com/user-attachments/assets/9b12d65f-257d-448f-a7d0-43068cc3f8a3">
<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 Confetti-Box is licensed under the terms of the GNU AGPL-3.0 license. Do what you want
you want with it within the terms of that license. with it within the terms of that license.

View file

@ -1,6 +1,6 @@
[package] [package]
name = "confetti_box" name = "confetti_box"
version = "0.2.2" version = "0.2.1"
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.27", features = ["rocket"] } maud = { version = "0.26", 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"

View file

@ -35,7 +35,6 @@ 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)" }
@ -307,7 +306,7 @@ pub async fn websocket_upload(
hasher.update(&message); hasher.update(&message);
stream.send(rocket_ws::Message::binary(offset.to_le_bytes().as_slice())).await.unwrap(); stream.send(rocket_ws::Message::Text(json::serde_json::ser::to_string(&offset).unwrap())).await.unwrap();
file.write_all(&message).await.unwrap(); file.write_all(&message).await.unwrap();
@ -347,7 +346,6 @@ 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(())
}))) })))

View file

@ -68,8 +68,7 @@ async fn main() {
confetti_box::home, confetti_box::home,
pages::api_info, pages::api_info,
pages::about, pages::about,
resources::favicon_svg, resources::favicon,
resources::favicon_ico,
resources::form_handler_js, resources::form_handler_js,
resources::stylesheet, resources::stylesheet,
resources::font_static, resources::font_static,

View file

@ -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="/favicon.svg"; link rel="icon" type="image/svg+xml" href="/resources/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;

View file

@ -31,12 +31,7 @@ pub fn form_handler_js() -> RawJavaScript<&'static str> {
RawJavaScript(include_str!("../web/request.js")) RawJavaScript(include_str!("../web/request.js"))
} }
#[get("/favicon.svg")] #[get("/resources/favicon.svg")]
pub fn favicon_svg() -> (ContentType, &'static str) { pub fn favicon() -> (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"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -55,7 +55,7 @@ hr {
h1 { h1 {
font-size: 3em; font-size: 3em;
font-weight: bold; font-weight: bolder;
} }
p { p {
@ -75,7 +75,6 @@ button {
cursor: pointer; cursor: pointer;
margin: 5px; margin: 5px;
border-radius: 5px; border-radius: 5px;
color: black;
} }
button.button { button.button {
@ -210,15 +209,12 @@ 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;
} }

View file

@ -3,8 +3,6 @@
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");
@ -19,11 +17,6 @@ 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;
@ -65,28 +58,27 @@ 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;
// Try to get a wake-lock // Create a reference for the 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 && !USE_CHUNKS_COMPAT) { if ('WebSocket' in window && window.WebSocket.CLOSING === 2) {
console.log("Uploading file using Websockets"); console.log("Uploading file using Websockets");
uploadPromise = uploadFileWebsocket(file, duration, maxSize); uploadPromise = uploadFileWebsocket(file, duration, maxSize);
} else { } else {
@ -109,13 +101,9 @@ async function sendFiles(files, duration, maxSize) {
let end = performance.now(); let end = performance.now();
console.log(end - start); console.log(end - start);
try {
wakeLock.release().then(() => { wakeLock.release().then(() => {
wakeLock = null; wakeLock = null;
}); });
} catch (err) {
console.warn("Failed to modify wake-lock!");
}
} }
async function uploadFileChunked(file, duration, maxSize) { async function uploadFileChunked(file, duration, maxSize) {
@ -220,15 +208,8 @@ 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";
// Ensure that the websocket gets closed if the page is unloaded const chunkSize = 10_000_000;
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);
@ -240,20 +221,16 @@ 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) => {
if (event.data instanceof ArrayBuffer) { const response = JSON.parse(event.data);
const view = new DataView(event.data); if (response.mmid == null) {
console.log(view.getBigUint64(0, true)); const progress = parseInt(response);
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
if (!socket.CLOSED) {
socket.close(); socket.close();
}
const response = JSON.parse(event.data);
uploadComplete(response, 200, progressBar, progressText, linkRow); uploadComplete(response, 200, progressBar, progressText, linkRow);
resolve(); resolve();
} }
@ -382,20 +359,7 @@ 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) => { fileButton.addEventListener("dragover", (e) => {e.preventDefault();}, false);
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();

View file

@ -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 = "2024" edition = "2021"
[[bin]] [[bin]]
name = "imu" name = "imu"
@ -17,21 +17,17 @@ path = "src/main.rs"
workspace = true workspace = true
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0.92"
base64 = "0.22.1" chrono = { version = "0.4.38", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.5.20", features = ["derive", "unicode"] }
clap = { version = "4.5", features = ["derive", "unicode"] } directories = "5.0.1"
directories = "6.0" indicatif = { version = "0.17.8", features = ["improved_unicode"] }
futures-util = "0.3.31" owo-colors = { version = "4.1.0", features = ["supports-colors"] }
indicatif = { version = "0.17", features = ["improved_unicode"] } reqwest = { version = "0.12.8", features = ["json", "stream"] }
owo-colors = { version = "4.1", features = ["supports-colors"] } serde = { version = "1.0.213", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "stream"] } serde_json = "1.0.132"
serde = { version = "1.0", features = ["derive"] } thiserror = "1.0.68"
serde_json = "1.0" tokio = { version = "1.41.0", features = ["fs", "macros", "rt-multi-thread"] }
thiserror = "1.0" tokio-util = { version = "0.7.12", features = ["codec"] }
tokio = { version = "1.41", features = ["fs", "macros", "rt-multi-thread"] } toml = "0.8.19"
tokio-tungstenite = { version = "0.26.2", features = ["native-tls"] } uuid = { version = "1.11.0", features = ["serde", "v4"] }
tokio-util = { version = "0.7", features = ["codec"] }
toml = "0.8"
url = { version = "2.5.4", features = ["serde"] }
uuid = { version = "1.11", features = ["serde", "v4"] }

View file

@ -1,17 +1,13 @@
use std::{error::Error, fs, io::{self, Read, Write}, path::{Path, PathBuf}}; use std::{error::Error, fs, io::{self, Read, Write}, os::unix::fs::MetadataExt, 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::File, io::{AsyncReadExt, AsyncWriteExt}, join, task::JoinSet}; use tokio::{fs::{create_dir, File}, io::{AsyncReadExt, AsyncWriteExt}, 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};
@ -57,7 +53,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 (assumes https://) /// Set the URL of the server to connect to
#[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
@ -86,16 +82,17 @@ async fn main() -> Result<()> {
match &cli.command { match &cli.command {
Commands::Upload { files, duration } => { Commands::Upload { files, duration } => {
let Some(url) = config.url.clone() else { if config.url.is_empty() {
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}")),
@ -128,7 +125,8 @@ async fn main() -> Result<()> {
let response = upload_file( let response = upload_file(
name.into_owned(), name.into_owned(),
&path, &path,
&url, &client,
&config.url,
duration, duration,
&config.login &config.login
).await.with_context(|| "Failed to upload").unwrap(); ).await.with_context(|| "Failed to upload").unwrap();
@ -143,19 +141,11 @@ 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(), (url.to_string() + "/f/" + &response.mmid.0).underline() "URL:".truecolor(174,196,223).bold(), (config.url.clone() + "/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 {
@ -177,6 +167,7 @@ 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()) {
@ -212,10 +203,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}", url)) client.get(format!("{}/f/{mmid}", config.url))
.basic_auth(&login.user, Some(&login.pass)) .basic_auth(&login.user, Some(&login.pass))
} else { } else {
client.get(format!("{}/f/{mmid}", url)) client.get(format!("{}/f/{mmid}", config.url))
} }
.send() .send()
.await .await
@ -314,14 +305,7 @@ async fn main() -> Result<()> {
url url
}; };
let new_url = if !url.starts_with("https://") && !url.starts_with("http://") { config.url = url.to_string();
("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}\"");
} }
@ -367,7 +351,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}")]
WebSocketFailed(String), InvalidRequest(String),
#[error("error on reqwest transaction: {0}")] #[error("error on reqwest transaction: {0}")]
Reqwest(#[from] reqwest::Error), Reqwest(#[from] reqwest::Error),
@ -376,101 +360,104 @@ enum UploadError {
async fn upload_file<P: AsRef<Path>>( async fn upload_file<P: AsRef<Path>>(
name: String, name: String,
path: &P, path: &P,
url: &Url, client: &Client,
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 file_size = file.metadata().await.unwrap().len(); let size = file.metadata().await.unwrap().size() as u64;
// Construct the URL let ChunkedResponse {status, message, uuid, chunk_size} = {
let mut url = url.clone(); client.post(format!("{url}/upload/chunked/"))
if url.scheme() == "http" { .json(
url.set_scheme("ws").unwrap(); &ChunkedInfo {
} else if url.scheme() == "https" { name: name.clone(),
url.set_scheme("wss").unwrap(); size,
expire_duration: duration.num_seconds() as u64,
} }
)
url.set_path("/upload/websocket"); .basic_auth(&login.as_ref().unwrap().user, login.as_ref().unwrap().pass.clone().into())
url.set_query(Some(&format!("name={}&size={}&duration={}", name, file_size, duration.num_seconds()))); .send()
.await?
let mut request = url.to_string().into_client_request().unwrap(); .json()
.await?
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
}; };
if !status {
return Err(UploadError::InvalidRequest(message));
}
let mut i = 0;
let post_url = format!("{url}/upload/chunked/{}", uuid.unwrap());
let mut request_set = JoinSet::new();
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();
// Get the progress of the file upload request_set.spawn({
let progress_task = async move { let post_url = post_url.clone();
let final_json = loop { let user = login.as_ref().unwrap().user.clone();
let Some(p) = read.next().await else { let pass = login.as_ref().unwrap().pass.clone();
break String::new() // Reuse the client for all the threads
}; let client = Client::clone(client);
let p = p.unwrap(); async move {
client.post(&post_url)
.query(&[("chunk", i)])
.basic_auth(&user, pass.into())
.body(chunk)
.send()
.await
}
});
// Got the final json information, return that i += 1;
if p.is_text() {
break p.into_text().unwrap().to_string() // Limit the number of concurrent uploads to 5
if request_set.len() >= 5 {
bar.set_message("");
request_set.join_next().await;
bar.set_message("");
} }
// Get the progress information let percent = f64::trunc(((i as f64 * chunk_size.unwrap() as f64) / size as f64) * 100.0);
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. { if percent <= 100. {
bar.set_position(percent as u64); bar.set_position(percent as u64);
} }
};
(read, final_json, bar)
};
// Wait for both of the tasks to finish
let (read, write) = join!(progress_task, upload_task);
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(file_info) Ok(
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<()> {
@ -490,13 +477,7 @@ 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 Some(url) = config.url.clone() else { let url = config.url.clone();
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"));
@ -534,11 +515,9 @@ 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)
@ -612,7 +591,7 @@ struct Login {
#[derive(Deserialize, Serialize, Debug, Default)] #[derive(Deserialize, Serialize, Debug, Default)]
#[serde(default)] #[serde(default)]
struct Config { struct Config {
url: Option<Url>, url: String,
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>>,
@ -627,7 +606,7 @@ impl Config {
str str
} else { } else {
let c = Config { let c = Config {
url: None, url: String::new(),
login: None, login: None,
info_fetch: None, info_fetch: None,
info: None, info: None,
@ -660,7 +639,7 @@ impl Config {
if buf.is_empty() { if buf.is_empty() {
let c = Config { let c = Config {
url: None, url: String::new(),
login: None, login: None,
info: None, info: None,
info_fetch: None, info_fetch: None,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB