From 8eeb4dbcf3df2bd5d92c73e3174eeb341740400d Mon Sep 17 00:00:00 2001 From: BBaoVanC Date: Wed, 7 Aug 2024 02:59:45 -0500 Subject: [PATCH] Replace duration_str with simpler regex implementation Updating duration_str to 0.11 causes weird compile errors, and it's not really necessary to have such a heavy dependency. --- Cargo.lock | 29 +--------- bobashare-web/Cargo.toml | 2 +- bobashare-web/src/api/v1/upload.rs | 8 +-- bobashare-web/src/lib.rs | 90 +++++++++++++++++++++++++++++- bobashare-web/src/main.rs | 8 +-- 5 files changed, 98 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0d07777..11582823 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,12 +96,6 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "askama" version = "0.12.1" @@ -363,7 +357,6 @@ dependencies = [ "clap", "config", "displaydoc", - "duration-str", "futures-util", "headers", "hex", @@ -371,6 +364,7 @@ dependencies = [ "hyper", "mime", "pulldown-cmark", + "regex", "rust-embed", "serde", "serde-error", @@ -560,17 +554,6 @@ dependencies = [ "syn", ] -[[package]] -name = "duration-str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8bb6a301a95ba86fa0ebaf71d49ae4838c51f8b84cb88ed140dfb66452bb3c4" -dependencies = [ - "nom", - "rust_decimal", - "thiserror", -] - [[package]] name = "encoding_rs" version = "0.8.34" @@ -1380,16 +1363,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rust_decimal" -version = "1.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" -dependencies = [ - "arrayvec", - "num-traits", -] - [[package]] name = "rustc-demangle" version = "0.1.24" diff --git a/bobashare-web/Cargo.toml b/bobashare-web/Cargo.toml index d14ebaa0..0510072a 100644 --- a/bobashare-web/Cargo.toml +++ b/bobashare-web/Cargo.toml @@ -22,7 +22,6 @@ chrono = { version = "0.4.22", features = ["serde"] } clap = { version = "4.0.12", features = ["derive"] } config = { version = "0.14.0", default-features = false, features = ["toml"] } displaydoc = "0.2.3" -duration-str = { version = "0.7.0", default-features = false } futures-util = "0.3.24" headers = "0.4.0" hex = "0.4.3" @@ -30,6 +29,7 @@ humansize = "2.1.0" hyper = "1.2.0" mime = "0.3.16" pulldown-cmark = "0.11.0" +regex = "1.10.6" rust-embed = { version = "8.0.0", features = ["mime-guess"] } serde = "1.0.145" serde-error = "0.1.2" diff --git a/bobashare-web/src/api/v1/upload.rs b/bobashare-web/src/api/v1/upload.rs index e80872f3..5b8f38e3 100644 --- a/bobashare-web/src/api/v1/upload.rs +++ b/bobashare-web/src/api/v1/upload.rs @@ -22,7 +22,7 @@ use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufWriter}; use tracing::{event, instrument, Instrument, Level}; use super::ApiErrorExt; -use crate::{clamp_expiry, AppState}; +use crate::{clamp_expiry, str_to_duration, AppState}; /// The JSON API response after uploading a file #[derive(Debug, Clone, Serialize)] @@ -106,14 +106,12 @@ impl IntoResponse for UploadError { /// - `Bobashare-Expiry` (optional) -- number -- duration until the upload /// should expire /// - specify `0` for no expiry -/// - examples (see [`duration_str`] for more information): +/// - examples (see [`str_to_duration`] for more information): /// - `1d` -- 1 day /// - `1h` -- 1 hour /// - `1m` -- 1 minute /// - `1s` -- 1 second /// -/// [`duration_str`]: https://crates.io/crates/duration_str -/// /// - `Bobashare-Delete-Key` (optional) -- string -- custom key to use for /// deleting the file later; if not provided, one will be randomly generated /// @@ -185,7 +183,7 @@ pub async fn put( None } else { Some( - Duration::from_std(duration_str::parse(expiry).map_err(|e| { + Duration::from_std(str_to_duration(expiry).map_err(|e| { UploadError::ParseHeader { name: String::from("Bobashare-Expiry"), source: anyhow::Error::new(e).context("error parsing duration string"), diff --git a/bobashare-web/src/lib.rs b/bobashare-web/src/lib.rs index c0b2db96..6ca6dabb 100644 --- a/bobashare-web/src/lib.rs +++ b/bobashare-web/src/lib.rs @@ -1,12 +1,15 @@ //! Webserver written with [`axum`] which provides a frontend and REST API for //! [`bobashare`] -use std::time::Duration as StdDuration; +use std::{num::ParseIntError, sync::LazyLock, time::Duration as StdDuration}; use bobashare::storage::file::FileBackend; use chrono::Duration; +use displaydoc::Display; use pulldown_cmark::Options; +use regex::Regex; use syntect::{html::ClassStyle, parsing::SyntaxSet}; +use thiserror::Error; use tokio::sync::broadcast; use url::Url; @@ -14,6 +17,10 @@ pub mod api; pub mod static_routes; pub mod views; +/// Regex used for duration string parsing in [`str_to_duration`] +static DURATION_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^([0-9]+)(m|h|d|w|mon|y)$").unwrap()); + /// Prefix for CSS classes used for [`syntect`] highlighting pub const HIGHLIGHT_CLASS_PREFIX: &str = "hl-"; /// [`ClassStyle`] used for [`syntect`] highlighting @@ -109,10 +116,12 @@ pub struct AppState { /// ``` /// # use chrono::Duration; /// let max_expiry = Some(Duration::days(7)); +/// println!("a"); /// assert_eq!( /// bobashare_web::clamp_expiry(max_expiry, Some(Duration::days(30))), /// max_expiry, /// ); +/// println!("b"); /// ``` pub fn clamp_expiry(max_expiry: Option, other: Option) -> Option { match other { @@ -125,3 +134,82 @@ pub fn clamp_expiry(max_expiry: Option, other: Option) -> Op }, } } + +#[derive(Debug, Error, Display)] +pub enum StrToDurationError { + /// string does not match duration format (try: 15d) + Invalid, + + /// could not parse number in duration, is it too large? + NumberParse(ParseIntError), +} + +/// Take a string with a simple duration format (single number followed by unit) +/// and output a [`StdDuration`]. Accepts durations in minutes (m), hours +/// (h), days (d), weeks (w), months (mon), or years (y). +/// +/// A month is equivalent to 30 days. A year is equivalent to 365 days. +/// +/// # Examples +/// +/// Basic (small numbers that fit within the unit) +/// +/// ``` +/// use chrono::TimeDelta; +/// +/// assert_eq!( +/// TimeDelta::from_std(bobashare_web::str_to_duration("17m").unwrap()).unwrap(), +/// TimeDelta::minutes(17) +/// ); +/// assert_eq!( +/// TimeDelta::from_std(bobashare_web::str_to_duration("14h").unwrap()).unwrap(), +/// TimeDelta::hours(14) +/// ); +/// assert_eq!( +/// TimeDelta::from_std(bobashare_web::str_to_duration("26d").unwrap()).unwrap(), +/// TimeDelta::days(26) +/// ); +/// assert_eq!( +/// TimeDelta::from_std(bobashare_web::str_to_duration("2w").unwrap()).unwrap(), +/// TimeDelta::weeks(2) +/// ); +/// assert_eq!( +/// TimeDelta::from_std(bobashare_web::str_to_duration("4mon").unwrap()).unwrap(), +/// TimeDelta::days(30 * 4) +/// ); +/// assert_eq!( +/// TimeDelta::from_std(bobashare_web::str_to_duration("7y").unwrap()).unwrap(), +/// TimeDelta::days(365 * 7) +/// ); +/// ``` +/// +/// Demonstrate the day values of months and years +/// +/// ``` +/// # use chrono::TimeDelta; +/// assert_eq!( +/// TimeDelta::from_std(bobashare_web::str_to_duration("1mon").unwrap()).unwrap(), +/// TimeDelta::days(30) +/// ); +/// assert_eq!( +/// TimeDelta::from_std(bobashare_web::str_to_duration("1y").unwrap()).unwrap(), +/// TimeDelta::days(365) +/// ); +/// ``` +pub fn str_to_duration>(s: S) -> Result { + let caps = DURATION_REGEX + .captures(s.as_ref()) + .ok_or(StrToDurationError::Invalid)?; + let count = caps[1] + .parse::() + .map_err(StrToDurationError::NumberParse)?; + Ok(match &caps[2] { + "m" => StdDuration::from_secs(count * 60), + "h" => StdDuration::from_secs(count * 60 * 60), + "d" => StdDuration::from_secs(count * 60 * 60 * 24), + "w" => StdDuration::from_secs(count * 60 * 60 * 24 * 7), + "mon" => StdDuration::from_secs(count * 60 * 60 * 24 * 30), + "y" => StdDuration::from_secs(count * 60 * 60 * 24 * 365), + _ => panic!("invalid duration unit received from regex"), + }) +} diff --git a/bobashare-web/src/main.rs b/bobashare-web/src/main.rs index 8ba21663..ab3a919a 100644 --- a/bobashare-web/src/main.rs +++ b/bobashare-web/src/main.rs @@ -9,7 +9,7 @@ use anyhow::Context; use axum::{self, routing::get, Router}; use bobashare::storage::file::FileBackend; use bobashare_web::{ - api, static_routes, + api, static_routes, str_to_duration, views::{self, ErrorResponse, ErrorTemplate}, AppState, }; @@ -116,7 +116,7 @@ async fn main() -> anyhow::Result<()> { let backend = FileBackend::new(PathBuf::from(config.get_string("backend_path").unwrap())).await?; - let cleanup_interval = duration_str::parse(config.get_string("cleanup_interval").unwrap()) + let cleanup_interval = str_to_duration(config.get_string("cleanup_interval").unwrap()) .context("error parsing `cleanup_interval`")?; let base_url: Url = config .get_string("base_url") @@ -126,13 +126,13 @@ async fn main() -> anyhow::Result<()> { let raw_url = base_url.join("raw/").unwrap(); let id_length = config.get_int("id_length").unwrap().try_into().unwrap(); let default_expiry = TimeDelta::from_std( - duration_str::parse(config.get_string("default_expiry").unwrap()) + str_to_duration(config.get_string("default_expiry").unwrap()) .context("error parsing `default_expiry`")?, ) .unwrap(); let max_expiry = match config.get_string("max_expiry").unwrap().as_str() { "never" => None, - exp => Some(duration_str::parse(exp).context("error parsing `max_expiry`")?), + exp => Some(str_to_duration(exp).context("error parsing `max_expiry`")?), } .map(|d| TimeDelta::from_std(d).unwrap()); let max_file_size = config.get_int("max_file_size").unwrap().try_into().unwrap();