diff --git a/Cargo.toml b/Cargo.toml index fefb8d8..f3e05ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ "confetti-box", - "confetti-cli", + "imu", ] [workspace.package] diff --git a/confetti-box/src/endpoints.rs b/confetti-box/src/endpoints.rs index 70edec1..e7c587e 100644 --- a/confetti-box/src/endpoints.rs +++ b/confetti-box/src/endpoints.rs @@ -8,10 +8,11 @@ use maud::{html, Markup, DOCTYPE}; use rocket::{ get, http::ContentType, response::{self, Redirect, Responder, Response}, serde::{self, json::Json}, tokio::{self, fs::File}, uri, Request, State }; +use ::serde::Deserialize; use serde::Serialize; use crate::{ - database::{Mmid, MochiFile, Mochibase}, settings::Settings, strings::{to_pretty_size, to_pretty_time, BreakStyle, TimeGranularity} + database::{Mmid, MochiFile, Mochibase}, settings::Settings, strings::{to_pretty_size, pretty_time, BreakStyle, TimeGranularity} }; /// An endpoint to obtain information about the server's capabilities @@ -56,7 +57,7 @@ pub async fn file_info_opengraph( let size = to_pretty_size(file.metadata().await.ok()?.len()); let seconds_till_expiry = entry.expiry().signed_duration_since(Utc::now()).num_seconds(); - let expiry = to_pretty_time(seconds_till_expiry as u32, BreakStyle::Space, TimeGranularity::Minutes); + let expiry = pretty_time(seconds_till_expiry, BreakStyle::Space, TimeGranularity::Minutes); let title = entry.name().clone() + " - " + &size + " - " + &expiry; @@ -82,14 +83,14 @@ pub async fn file_info_opengraph( }) } -#[derive(Serialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] #[serde(crate = "rocket::serde")] pub struct ServerInfo { - max_filesize: u64, - max_duration: u32, - default_duration: u32, + pub max_filesize: u64, + pub max_duration: u32, + pub default_duration: u32, #[serde(skip_serializing_if = "Vec::is_empty")] - allowed_durations: Vec, + pub allowed_durations: Vec, } #[get("/f/")] diff --git a/confetti-box/src/lib.rs b/confetti-box/src/lib.rs index 62dbf97..dfaadb4 100644 --- a/confetti-box/src/lib.rs +++ b/confetti-box/src/lib.rs @@ -14,7 +14,7 @@ use std::{ use crate::{ pages::{footer, head}, settings::Settings, - strings::to_pretty_time, + strings::pretty_time, }; use chrono::{TimeDelta, Utc}; use database::{Chunkbase, ChunkedInfo, Mmid, MochiFile, Mochibase}; @@ -48,7 +48,7 @@ pub fn home(settings: &State) -> Markup { button.button.{@if settings.duration.default == *d { "selected" }} data-duration-seconds=(d.num_seconds()) { - (PreEscaped(to_pretty_time(d.num_seconds() as u32, BreakStyle::Break, TimeGranularity::Seconds))) + (PreEscaped(pretty_time(d.num_seconds() as u32, BreakStyle::Break, TimeGranularity::Seconds))) } } } diff --git a/confetti-box/src/strings.rs b/confetti-box/src/strings.rs index 4ba9417..40220b6 100644 --- a/confetti-box/src/strings.rs +++ b/confetti-box/src/strings.rs @@ -49,7 +49,23 @@ pub enum TimeGranularity { Seconds, } -pub fn to_pretty_time(seconds: u32, breaks: BreakStyle, granularity: TimeGranularity) -> String { +pub fn pretty_time_short(seconds: i64) -> String { + let days = (seconds as f32 / 86400.0).floor(); + let hour = ((seconds as f32 - (days * 86400.0)) / 3600.0).floor(); + let mins = ((seconds as f32 - (hour * 3600.0) - (days * 86400.0)) / 60.0).floor(); + let secs = seconds as f32 - (hour * 3600.0) - (mins * 60.0) - (days * 86400.0); + + let days = if days > 0. {days.to_string() + "d"} else { "".into() }; + let hour = if hour > 0. {hour.to_string() + "h"} else { "".into() }; + let mins = if mins > 0. {mins.to_string() + "m"} else { "".into() }; + let secs = if secs > 0. {secs.to_string() + "s"} else { "".into() }; + + (days + " " + &hour + " " + &mins + " " + &secs) + .trim() + .to_string() +} + +pub fn pretty_time(seconds: i64, breaks: BreakStyle, granularity: TimeGranularity) -> String { let days = (seconds as f32 / 86400.0).floor(); let hour = ((seconds as f32 - (days * 86400.0)) / 3600.0).floor(); let mins = ((seconds as f32 - (hour * 3600.0) - (days * 86400.0)) / 60.0).floor(); diff --git a/confetti-cli/Cargo.toml b/imu/Cargo.toml similarity index 84% rename from confetti-cli/Cargo.toml rename to imu/Cargo.toml index a3579bd..4ae740a 100644 --- a/confetti-cli/Cargo.toml +++ b/imu/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "confetti_cli" +name = "imu" version = "0.1.1" description = "A simple command line interface to interact with a Confetti-Box instance." repository = "https://github.com/Dangoware/confetti-box" @@ -9,10 +9,6 @@ authors.workspace = true license = "AGPL-3.0-or-later" edition = "2024" -[[bin]] -name = "imu" -path = "src/main.rs" - [lints] workspace = true @@ -22,7 +18,7 @@ base64 = "0.22.1" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.5", features = ["derive", "unicode"] } directories = "6.0" -futures-util = "0.3.31" +futures-util = "0.3" indicatif = { version = "0.17", features = ["improved_unicode"] } owo-colors = { version = "4.1", features = ["supports-colors"] } reqwest = { version = "0.12", features = ["json", "stream"] } @@ -30,8 +26,10 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" tokio = { version = "1.41", features = ["fs", "macros", "rt-multi-thread"] } -tokio-tungstenite = { version = "0.26.2", features = ["native-tls"] } +tokio-tungstenite = { version = "0.26", features = ["native-tls"] } tokio-util = { version = "0.7", features = ["codec"] } toml = "0.8" -url = { version = "2.5.4", features = ["serde"] } +url = { version = "2.5", features = ["serde"] } uuid = { version = "1.11", features = ["serde", "v4"] } + +confetti_box = { path = "../confetti-box" } diff --git a/confetti-cli/LICENSE b/imu/LICENSE similarity index 100% rename from confetti-cli/LICENSE rename to imu/LICENSE diff --git a/confetti-cli/src/main.rs b/imu/src/main.rs similarity index 81% rename from confetti-cli/src/main.rs rename to imu/src/main.rs index 8036b91..080d23e 100644 --- a/confetti-cli/src/main.rs +++ b/imu/src/main.rs @@ -1,18 +1,18 @@ -use std::{error::Error, fs, io::{self, Read, Write}, path::{Path, PathBuf}}; +use std::{error::Error, fs, io::{Read, Write}, path::{Path, PathBuf}}; use base64::{prelude::BASE64_URL_SAFE, Engine}; use chrono::{DateTime, Datelike, Local, Month, TimeDelta, Timelike, Utc}; +use confetti_box::{database::MochiFile, endpoints::ServerInfo, strings::{pretty_time, pretty_time_short, BreakStyle, TimeGranularity}}; use futures_util::{stream::FusedStream as _, SinkExt as _, StreamExt as _}; use indicatif::{ProgressBar, ProgressStyle}; use owo_colors::OwoColorize; use reqwest::Client; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}, join, task::JoinSet}; +use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt}, join}; use tokio_tungstenite::{connect_async, tungstenite::{client::IntoClientRequest as _, Message}}; use url::Url; -use uuid::Uuid; use clap::{arg, builder::{styling::RgbColor, Styles}, Parser, Subcommand}; use anyhow::{anyhow, bail, Context as _, Result}; @@ -101,13 +101,13 @@ async fn main() -> Result<()> { Err(e) => return Err(anyhow!("Invalid duration: {e}")), }; - if !config.info.as_ref().unwrap().allowed_durations.contains(&duration.num_seconds()) { + if !config.info.as_ref().unwrap().allowed_durations.contains(&(duration.num_seconds() as u32)) { let pretty_durations: Vec = config.info.as_ref() .unwrap() .allowed_durations .clone() .iter() - .map(|d| pretty_time_short(*d)) + .map(|d| pretty_time_short(*d as i64)) .collect(); exit_error( @@ -133,7 +133,7 @@ async fn main() -> Result<()> { &config.login ).await.with_context(|| "Failed to upload").unwrap(); - let datetime: DateTime = DateTime::from(response.expiry_datetime); + let datetime: DateTime = DateTime::from(response.expiry()); let date = format!( "{} {}", Month::try_from(u8::try_from(datetime.month()).unwrap()).unwrap().name(), @@ -142,8 +142,11 @@ async fn main() -> Result<()> { let time = format!("{:02}:{:02}", datetime.hour(), datetime.minute()); println!( "{:>8} {}, {} (in {})\n{:>8} {}", - "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() + "Expires:".truecolor(174,196,223).bold(), + date, + time, + pretty_time(duration.num_seconds(), BreakStyle::Space, TimeGranularity::Seconds), + "URL:".truecolor(174,196,223).bold(), (url.to_string() + "/f/" + &response.mmid().to_string()).underline() ); } } @@ -221,7 +224,7 @@ async fn main() -> Result<()> { .await .unwrap(); - let out_directory = out_directory.join(info.name); + let out_directory = out_directory.join(info.name()); let mut out_file: File = tokio::fs::OpenOptions::new() .create(true) .append(true) @@ -527,82 +530,6 @@ async fn get_info(config: &Config) -> Result { Ok(info) } -/// Attempts to fill a buffer completely from a stream, but if it cannot do so, -/// it will only fill what it can read. If it has reached the end of a file, 0 -/// bytes will be read into the buffer. -async fn fill_buffer(buffer: &mut [u8], mut stream: S) -> Result { - let mut bytes_read = 0; - while bytes_read < buffer.len() { - let len = stream.read(&mut buffer[bytes_read..]).await?; - - if len == 0 { - break; - } - - bytes_read += len; - } - Ok(bytes_read) -} - -#[derive(Debug)] -struct Upload { - file: File, - name: String, - duration: i64, -} - -#[derive(Deserialize, Serialize, Debug)] -struct ServerInfo { - max_filesize: u64, - max_duration: i64, - default_duration: i64, - allowed_durations: Vec, -} - -#[derive(Serialize, Debug)] -pub struct ChunkedInfo { - pub name: String, - pub size: u64, - pub expire_duration: u64, -} - -#[derive(Serialize, Deserialize, Default, Debug)] -pub struct ChunkedResponse { - status: bool, - message: String, - - /// UUID used for associating the chunk with the final file - uuid: Option, - - /// Valid max chunk size in bytes - chunk_size: Option, -} - -#[derive(Deserialize, Debug)] -pub struct MochiFile { - /// A unique identifier describing this file - mmid: Mmid, - - /// The original name of the file - name: String, - - /// The MIME type of the file - mime_type: String, - - /// The Blake3 hash of the file - hash: String, - - /// The datetime when the file was uploaded - upload_datetime: DateTime, - - /// The datetime when the file is set to expire - expiry_datetime: DateTime, -} - -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -#[derive(Deserialize, Serialize)] -pub struct Mmid(String); - #[derive(Deserialize, Serialize, Debug, Clone)] struct Login { user: String, @@ -706,98 +633,6 @@ impl Config { } } -fn parse_time_string(string: &str) -> Result> { - if string.len() > 7 { - return Err("Not valid time string".into()); - } - - let unit = string.chars().last(); - let multiplier = if let Some(u) = unit { - if !u.is_ascii_alphabetic() { - return Err("Not valid time string".into()); - } - - match u { - 'D' | 'd' => TimeDelta::days(1), - 'H' | 'h' => TimeDelta::hours(1), - 'M' | 'm' => TimeDelta::minutes(1), - 'S' | 's' => TimeDelta::seconds(1), - _ => return Err("Not valid time string".into()), - } - } else { - return Err("Not valid time string".into()); - }; - - let time = if let Ok(n) = string[..string.len() - 1].parse::() { - n - } else { - return Err("Not valid time string".into()); - }; - - let final_time = multiplier * time; - - Ok(final_time) -} - -fn pretty_time_short(seconds: i64) -> String { - let days = (seconds as f32 / 86400.0).floor(); - let hour = ((seconds as f32 - (days * 86400.0)) / 3600.0).floor(); - let mins = ((seconds as f32 - (hour * 3600.0) - (days * 86400.0)) / 60.0).floor(); - let secs = seconds as f32 - (hour * 3600.0) - (mins * 60.0) - (days * 86400.0); - - let days = if days > 0. {days.to_string() + "d"} else { "".into() }; - let hour = if hour > 0. {hour.to_string() + "h"} else { "".into() }; - let mins = if mins > 0. {mins.to_string() + "m"} else { "".into() }; - let secs = if secs > 0. {secs.to_string() + "s"} else { "".into() }; - - (days + " " + &hour + " " + &mins + " " + &secs) - .trim() - .to_string() -} - -fn pretty_time_long(seconds: i64) -> String { - let days = (seconds as f32 / 86400.0).floor(); - let hour = ((seconds as f32 - (days * 86400.0)) / 3600.0).floor(); - let mins = ((seconds as f32 - (hour * 3600.0) - (days * 86400.0)) / 60.0).floor(); - let secs = seconds as f32 - (hour * 3600.0) - (mins * 60.0) - (days * 86400.0); - - let days = if days == 0.0 { - "".to_string() - } else if days == 1.0 { - days.to_string() + " day" - } else { - days.to_string() + " days" - }; - - let hour = if hour == 0.0 { - "".to_string() - } else if hour == 1.0 { - hour.to_string() + " hour" - } else { - hour.to_string() + " hours" - }; - - let mins = if mins == 0.0 { - "".to_string() - } else if mins == 1.0 { - mins.to_string() + " minute" - } else { - mins.to_string() + " minutes" - }; - - let secs = if secs == 0.0 { - "".to_string() - } else if secs == 1.0 { - secs.to_string() + " second" - } else { - secs.to_string() + " seconds" - }; - - (days + " " + &hour + " " + &mins + " " + &secs) - .trim() - .to_string() -} - fn exit_error(main_message: String, fix: Option, fix_values: Option>) -> ! { print_error_line(main_message);