Skip to content

Commit

Permalink
Replace duration_str with simpler regex implementation
Browse files Browse the repository at this point in the history
Updating duration_str to 0.11 causes weird compile errors, and it's not
really necessary to have such a heavy dependency.
  • Loading branch information
BBaoVanC committed Aug 7, 2024
1 parent 5faabbd commit 8eeb4db
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 39 deletions.
29 changes: 1 addition & 28 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bobashare-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ 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"
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"
Expand Down
8 changes: 3 additions & 5 deletions bobashare-web/src/api/v1/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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
///
Expand Down Expand Up @@ -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"),
Expand Down
90 changes: 89 additions & 1 deletion bobashare-web/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
//! 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;

pub mod api;
pub mod static_routes;
pub mod views;

/// Regex used for duration string parsing in [`str_to_duration`]
static DURATION_REGEX: LazyLock<Regex> =
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
Expand Down Expand Up @@ -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<Duration>, other: Option<Duration>) -> Option<Duration> {
match other {
Expand All @@ -125,3 +134,82 @@ pub fn clamp_expiry(max_expiry: Option<Duration>, other: Option<Duration>) -> 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: AsRef<str>>(s: S) -> Result<StdDuration, StrToDurationError> {
let caps = DURATION_REGEX
.captures(s.as_ref())
.ok_or(StrToDurationError::Invalid)?;
let count = caps[1]
.parse::<u64>()
.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"),
})
}
8 changes: 4 additions & 4 deletions bobashare-web/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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")
Expand All @@ -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();
Expand Down

0 comments on commit 8eeb4db

Please sign in to comment.