Add toolbox
This commit is contained in:
31
crates/toolbox/Cargo.toml
Normal file
31
crates/toolbox/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[package]
|
||||||
|
name = "toolbox"
|
||||||
|
version = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
num = { workspace = true }
|
||||||
|
|
||||||
|
clap = { workspace = true, optional = true }
|
||||||
|
anstyle = { workspace = true, optional = true }
|
||||||
|
tracing-subscriber = { workspace = true, optional = true }
|
||||||
|
anyhow = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
tracing-loki = { workspace = true, optional = true }
|
||||||
|
tokio = { workspace = true, optional = true }
|
||||||
|
url = { workspace = true, optional = true }
|
||||||
|
base64 = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
cli = ["dep:clap", "dep:anstyle", "dep:tracing-subscriber", "dep:anyhow"]
|
||||||
|
loki = ["cli", "dep:tracing-loki", "dep:base64", "dep:tokio", "dep:url"]
|
||||||
37
crates/toolbox/src/cli.rs
Normal file
37
crates/toolbox/src/cli.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use anstyle::{AnsiColor, Color, Style};
|
||||||
|
|
||||||
|
pub fn clap_styles() -> clap::builder::Styles {
|
||||||
|
clap::builder::Styles::styled()
|
||||||
|
.usage(
|
||||||
|
Style::new()
|
||||||
|
.bold()
|
||||||
|
.fg_color(Some(Color::Ansi(AnsiColor::Blue))),
|
||||||
|
)
|
||||||
|
.header(
|
||||||
|
Style::new()
|
||||||
|
.bold()
|
||||||
|
.fg_color(Some(Color::Ansi(AnsiColor::Blue))),
|
||||||
|
)
|
||||||
|
.literal(
|
||||||
|
Style::new()
|
||||||
|
.bold()
|
||||||
|
.fg_color(Some(Color::Ansi(AnsiColor::BrightBlack))),
|
||||||
|
)
|
||||||
|
.invalid(
|
||||||
|
Style::new()
|
||||||
|
.bold()
|
||||||
|
.fg_color(Some(Color::Ansi(AnsiColor::Red))),
|
||||||
|
)
|
||||||
|
.error(
|
||||||
|
Style::new()
|
||||||
|
.bold()
|
||||||
|
.fg_color(Some(Color::Ansi(AnsiColor::Red))),
|
||||||
|
)
|
||||||
|
.valid(
|
||||||
|
Style::new()
|
||||||
|
.bold()
|
||||||
|
.underline()
|
||||||
|
.fg_color(Some(Color::Ansi(AnsiColor::Green))),
|
||||||
|
)
|
||||||
|
.placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White))))
|
||||||
|
}
|
||||||
11
crates/toolbox/src/lib.rs
Normal file
11
crates/toolbox/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//! This crate contains various bits of useful code that don't fit anywhere else.
|
||||||
|
|
||||||
|
pub mod mime;
|
||||||
|
pub mod misc;
|
||||||
|
pub mod strings;
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
pub mod cli;
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
pub mod logging;
|
||||||
448
crates/toolbox/src/logging.rs
Normal file
448
crates/toolbox/src/logging.rs
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: loglevel
|
||||||
|
//
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, ValueEnum)]
|
||||||
|
pub enum LogLevel {
|
||||||
|
Trace,
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogLevel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for LogLevel {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Trace => write!(f, "trace"),
|
||||||
|
Self::Debug => write!(f, "debug"),
|
||||||
|
Self::Info => write!(f, "info"),
|
||||||
|
Self::Warn => write!(f, "warn"),
|
||||||
|
Self::Error => write!(f, "error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: logconfig
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Configures log levels for known sources
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
|
||||||
|
pub struct LoggingConfig {
|
||||||
|
pub other: LogLevel,
|
||||||
|
pub silence: LogLevel,
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
pub libservice: LogLevel,
|
||||||
|
pub toolbox: LogLevel,
|
||||||
|
|
||||||
|
// Bins
|
||||||
|
pub service: LogLevel,
|
||||||
|
pub webpage: LogLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LoggingConfig> for EnvFilter {
|
||||||
|
fn from(conf: LoggingConfig) -> Self {
|
||||||
|
// Should never fail
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
EnvFilter::from_str(
|
||||||
|
&[
|
||||||
|
//
|
||||||
|
// Silence
|
||||||
|
//
|
||||||
|
// http
|
||||||
|
format!("hyper_util={}", conf.silence),
|
||||||
|
format!("h2={}", conf.silence),
|
||||||
|
format!("rustls={}", conf.silence),
|
||||||
|
format!("tower={}", conf.silence),
|
||||||
|
//
|
||||||
|
// Libs
|
||||||
|
//
|
||||||
|
format!("toolbox={}", conf.toolbox),
|
||||||
|
format!("libservice={}", conf.libservice),
|
||||||
|
//
|
||||||
|
// Bins
|
||||||
|
//
|
||||||
|
format!("service_webpage={}", conf.service),
|
||||||
|
format!("webpage={}", conf.webpage),
|
||||||
|
conf.other.to_string(),
|
||||||
|
]
|
||||||
|
.join(","),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: LogCliVQ
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Provides global -v and -q cli arguments.
|
||||||
|
/// Use with `#[arg(flatten)]`.
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
pub struct LogCliVQ {
|
||||||
|
/// Increase verbosity (can be repeated)
|
||||||
|
#[arg(default_value = "0")]
|
||||||
|
#[arg(short, action = clap::ArgAction::Count,global = true)]
|
||||||
|
v: u8,
|
||||||
|
|
||||||
|
/// Decrease verbosity (can be repeated)
|
||||||
|
#[arg(default_value = "0")]
|
||||||
|
#[arg(short, action = clap::ArgAction::Count, global = true)]
|
||||||
|
q: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogCliVQ {
|
||||||
|
pub fn into_preset(&self) -> LogFilterPreset {
|
||||||
|
let level_i: i16 = self.v as i16 - self.q as i16;
|
||||||
|
|
||||||
|
let preset;
|
||||||
|
if level_i <= -2 {
|
||||||
|
preset = LogFilterPreset::Error
|
||||||
|
} else if level_i == -1 {
|
||||||
|
preset = LogFilterPreset::Warn
|
||||||
|
} else if level_i == 0 {
|
||||||
|
preset = LogFilterPreset::Info
|
||||||
|
} else if level_i == 1 {
|
||||||
|
preset = LogFilterPreset::Debug
|
||||||
|
} else if level_i >= 2 {
|
||||||
|
preset = LogFilterPreset::Trace
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: logpreset
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Provides preset configurations of [LoggingConfig]
|
||||||
|
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||||
|
pub enum LogFilterPreset {
|
||||||
|
/// Standard "error" log level
|
||||||
|
Error,
|
||||||
|
|
||||||
|
/// Standard "warn" log level
|
||||||
|
Warn,
|
||||||
|
|
||||||
|
/// Standard "info" log level.
|
||||||
|
/// This is the default.
|
||||||
|
Info,
|
||||||
|
|
||||||
|
/// Standard "debug" log level
|
||||||
|
Debug,
|
||||||
|
|
||||||
|
/// Standard "trace" log level
|
||||||
|
Trace,
|
||||||
|
|
||||||
|
/// Filter for loki subscriber.
|
||||||
|
///
|
||||||
|
/// This is similar to `Trace`,
|
||||||
|
/// but excludes particulary spammy sources.
|
||||||
|
Loki,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogFilterPreset {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self::Info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LogFilterPreset> for LoggingConfig {
|
||||||
|
fn from(val: LogFilterPreset) -> Self {
|
||||||
|
val.get_config()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogFilterPreset {
|
||||||
|
pub fn get_config(&self) -> LoggingConfig {
|
||||||
|
match self {
|
||||||
|
Self::Error => LoggingConfig {
|
||||||
|
other: LogLevel::Error,
|
||||||
|
silence: LogLevel::Error,
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
libservice: LogLevel::Error,
|
||||||
|
toolbox: LogLevel::Error,
|
||||||
|
|
||||||
|
// Bins
|
||||||
|
webpage: LogLevel::Error,
|
||||||
|
service: LogLevel::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
Self::Warn => LoggingConfig {
|
||||||
|
other: LogLevel::Warn,
|
||||||
|
silence: LogLevel::Warn,
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
libservice: LogLevel::Warn,
|
||||||
|
toolbox: LogLevel::Warn,
|
||||||
|
|
||||||
|
// Bins
|
||||||
|
webpage: LogLevel::Warn,
|
||||||
|
service: LogLevel::Warn,
|
||||||
|
},
|
||||||
|
|
||||||
|
Self::Info => LoggingConfig {
|
||||||
|
other: LogLevel::Warn,
|
||||||
|
silence: LogLevel::Warn,
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
libservice: LogLevel::Info,
|
||||||
|
toolbox: LogLevel::Info,
|
||||||
|
|
||||||
|
// Bins
|
||||||
|
webpage: LogLevel::Info,
|
||||||
|
service: LogLevel::Info,
|
||||||
|
},
|
||||||
|
|
||||||
|
Self::Debug => LoggingConfig {
|
||||||
|
other: LogLevel::Warn,
|
||||||
|
silence: LogLevel::Warn,
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
libservice: LogLevel::Debug,
|
||||||
|
toolbox: LogLevel::Debug,
|
||||||
|
|
||||||
|
// Bins
|
||||||
|
webpage: LogLevel::Debug,
|
||||||
|
service: LogLevel::Debug,
|
||||||
|
},
|
||||||
|
|
||||||
|
Self::Trace => LoggingConfig {
|
||||||
|
other: LogLevel::Trace,
|
||||||
|
silence: LogLevel::Warn,
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
libservice: LogLevel::Trace,
|
||||||
|
toolbox: LogLevel::Trace,
|
||||||
|
|
||||||
|
// Bins
|
||||||
|
webpage: LogLevel::Trace,
|
||||||
|
service: LogLevel::Trace,
|
||||||
|
},
|
||||||
|
|
||||||
|
Self::Loki => LoggingConfig {
|
||||||
|
other: LogLevel::Trace,
|
||||||
|
silence: LogLevel::Warn,
|
||||||
|
|
||||||
|
// Libs
|
||||||
|
libservice: LogLevel::Trace,
|
||||||
|
toolbox: LogLevel::Trace,
|
||||||
|
|
||||||
|
// Bins
|
||||||
|
webpage: LogLevel::Trace,
|
||||||
|
service: LogLevel::Trace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: initializer
|
||||||
|
//
|
||||||
|
|
||||||
|
#[cfg(feature = "loki")]
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct LokiConfig {
|
||||||
|
pub loki_host: url::Url,
|
||||||
|
pub loki_user: String,
|
||||||
|
pub loki_pass: String,
|
||||||
|
pub loki_node_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where to print logs
|
||||||
|
pub enum LoggingTarget {
|
||||||
|
/// Send logs to stdout
|
||||||
|
Stdout { format: LoggingFormat },
|
||||||
|
|
||||||
|
/// Send logs to stderr
|
||||||
|
Stderr { format: LoggingFormat },
|
||||||
|
/*
|
||||||
|
/// Send logs to an IndicatifWriter.
|
||||||
|
///
|
||||||
|
/// This is the same as Stderr { format: Ansi {color:true} },
|
||||||
|
/// but uses an indicatifwriter with the given multiprogress.
|
||||||
|
Indicatif(MultiProgress),
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How to print logs
|
||||||
|
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||||
|
pub enum LoggingFormat {
|
||||||
|
Ansi,
|
||||||
|
AnsiNoColor,
|
||||||
|
Json,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LoggingFormat {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Ansi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoggingInitializer {
|
||||||
|
pub app_name: &'static str,
|
||||||
|
|
||||||
|
/// If `Some`, send logs to the given loki server
|
||||||
|
#[cfg(feature = "loki")]
|
||||||
|
pub loki: Option<LokiConfig>,
|
||||||
|
|
||||||
|
/// Log filter for printed logs
|
||||||
|
pub preset: LogFilterPreset,
|
||||||
|
|
||||||
|
/// Where to print logs
|
||||||
|
pub target: LoggingTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoggingInitializer {
|
||||||
|
pub fn initialize(self) -> Result<()> {
|
||||||
|
let mut stderr_ansi_layer = None;
|
||||||
|
let mut stderr_json_layer = None;
|
||||||
|
let mut stdout_ansi_layer = None;
|
||||||
|
let mut stdout_json_layer = None;
|
||||||
|
match self.target {
|
||||||
|
LoggingTarget::Stderr {
|
||||||
|
format: LoggingFormat::Ansi,
|
||||||
|
} => {
|
||||||
|
stderr_ansi_layer = Some(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.without_time()
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingTarget::Stderr {
|
||||||
|
format: LoggingFormat::AnsiNoColor,
|
||||||
|
} => {
|
||||||
|
stderr_ansi_layer = Some(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.without_time()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingTarget::Stderr {
|
||||||
|
format: LoggingFormat::Json,
|
||||||
|
} => {
|
||||||
|
stderr_json_layer = Some(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.without_time()
|
||||||
|
.with_ansi(false)
|
||||||
|
.json()
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingTarget::Stdout {
|
||||||
|
format: LoggingFormat::Ansi,
|
||||||
|
} => {
|
||||||
|
stdout_ansi_layer = Some(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.without_time()
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_writer(std::io::stdout)
|
||||||
|
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingTarget::Stdout {
|
||||||
|
format: LoggingFormat::AnsiNoColor,
|
||||||
|
} => {
|
||||||
|
stdout_ansi_layer = Some(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.without_time()
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_writer(std::io::stdout)
|
||||||
|
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggingTarget::Stdout {
|
||||||
|
format: LoggingFormat::Json,
|
||||||
|
} => {
|
||||||
|
stdout_json_layer = Some(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.without_time()
|
||||||
|
.with_ansi(false)
|
||||||
|
.json()
|
||||||
|
.with_writer(std::io::stdout)
|
||||||
|
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||||
|
)
|
||||||
|
} /*
|
||||||
|
LoggingTarget::Indicatif(mp) => {
|
||||||
|
let writer: IndicatifWriter<tracing_indicatif::writer::Stderr> =
|
||||||
|
IndicatifWriter::new(mp);
|
||||||
|
|
||||||
|
indicatif_layer = Some(
|
||||||
|
tracing_subscriber::fmt::Layer::default()
|
||||||
|
.without_time()
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_writer(writer.make_writer())
|
||||||
|
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
let loki_layer = {
|
||||||
|
#[cfg(feature = "loki")]
|
||||||
|
if let Some(cfg) = self.loki {
|
||||||
|
use anyhow::Context;
|
||||||
|
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||||
|
|
||||||
|
let basic_auth = format!("{}:{}", cfg.loki_user, cfg.loki_pass);
|
||||||
|
let encoded_basic_auth = BASE64_STANDARD.encode(basic_auth.as_bytes());
|
||||||
|
|
||||||
|
let (layer, task) = tracing_loki::builder()
|
||||||
|
.label("node_name", cfg.loki_node_name)
|
||||||
|
.context("while building loki node_name label")?
|
||||||
|
.label("app", self.app_name)
|
||||||
|
.context("while building loki app label")?
|
||||||
|
.http_header("Authorization", format!("Basic {encoded_basic_auth}"))
|
||||||
|
.context("while building loki header")?
|
||||||
|
.build_url(cfg.loki_host)
|
||||||
|
.context("while building loki layer")?;
|
||||||
|
|
||||||
|
tokio::spawn(task);
|
||||||
|
Some(layer.with_filter::<EnvFilter>(LogFilterPreset::Loki.get_config().into()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "loki"))]
|
||||||
|
None::<Box<dyn Layer<_> + Send + Sync>>
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(loki_layer)
|
||||||
|
.with(stdout_ansi_layer)
|
||||||
|
.with(stdout_json_layer)
|
||||||
|
.with(stderr_ansi_layer)
|
||||||
|
.with(stderr_json_layer)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
668
crates/toolbox/src/mime.rs
Normal file
668
crates/toolbox/src/mime.rs
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
/// A media type, conveniently parsed
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum MimeType {
|
||||||
|
/// A mimetype we didn't recognize
|
||||||
|
Other(String),
|
||||||
|
|
||||||
|
/// An unstructured binary blob
|
||||||
|
/// Use this whenever a mime type is unknown
|
||||||
|
Blob,
|
||||||
|
|
||||||
|
// MARK: Audio
|
||||||
|
/// AAC audio file (audio/aac)
|
||||||
|
Aac,
|
||||||
|
/// FLAC audio file (audio/flac)
|
||||||
|
Flac,
|
||||||
|
/// MIDI audio file (audio/midi)
|
||||||
|
Midi,
|
||||||
|
/// MP3 audio file (audio/mpeg)
|
||||||
|
Mp3,
|
||||||
|
/// OGG audio file (audio/ogg)
|
||||||
|
Oga,
|
||||||
|
/// Opus audio file in Ogg container (audio/ogg)
|
||||||
|
Opus,
|
||||||
|
/// Waveform Audio Format (audio/wav)
|
||||||
|
Wav,
|
||||||
|
/// WEBM audio file (audio/webm)
|
||||||
|
Weba,
|
||||||
|
|
||||||
|
// MARK: Video
|
||||||
|
/// AVI: Audio Video Interleave (video/x-msvideo)
|
||||||
|
Avi,
|
||||||
|
/// MP4 video file (video/mp4)
|
||||||
|
Mp4,
|
||||||
|
/// MPEG video file (video/mpeg)
|
||||||
|
Mpeg,
|
||||||
|
/// OGG video file (video/ogg)
|
||||||
|
Ogv,
|
||||||
|
/// MPEG transport stream (video/mp2t)
|
||||||
|
Ts,
|
||||||
|
/// WEBM video file (video/webm)
|
||||||
|
WebmVideo,
|
||||||
|
/// 3GPP audio/video container (video/3gpp)
|
||||||
|
ThreeGp,
|
||||||
|
/// 3GPP2 audio/video container (video/3gpp2)
|
||||||
|
ThreeG2,
|
||||||
|
|
||||||
|
// MARK: Images
|
||||||
|
/// Animated Portable Network Graphics (image/apng)
|
||||||
|
Apng,
|
||||||
|
/// AVIF image (image/avif)
|
||||||
|
Avif,
|
||||||
|
/// Windows OS/2 Bitmap Graphics (image/bmp)
|
||||||
|
Bmp,
|
||||||
|
/// Graphics Interchange Format (image/gif)
|
||||||
|
Gif,
|
||||||
|
/// Icon format (image/vnd.microsoft.icon)
|
||||||
|
Ico,
|
||||||
|
/// JPEG image (image/jpeg)
|
||||||
|
Jpg,
|
||||||
|
/// Portable Network Graphics (image/png)
|
||||||
|
Png,
|
||||||
|
/// Scalable Vector Graphics (image/svg+xml)
|
||||||
|
Svg,
|
||||||
|
/// Tagged Image File Format (image/tiff)
|
||||||
|
Tiff,
|
||||||
|
/// WEBP image (image/webp)
|
||||||
|
Webp,
|
||||||
|
|
||||||
|
// MARK: Text
|
||||||
|
/// Plain text (text/plain)
|
||||||
|
Text,
|
||||||
|
/// Cascading Style Sheets (text/css)
|
||||||
|
Css,
|
||||||
|
/// Comma-separated values (text/csv)
|
||||||
|
Csv,
|
||||||
|
/// HyperText Markup Language (text/html)
|
||||||
|
Html,
|
||||||
|
/// JavaScript (text/javascript)
|
||||||
|
Javascript,
|
||||||
|
/// JSON format (application/json)
|
||||||
|
Json,
|
||||||
|
/// JSON-LD format (application/ld+json)
|
||||||
|
JsonLd,
|
||||||
|
/// XML (application/xml)
|
||||||
|
Xml,
|
||||||
|
|
||||||
|
// MARK: Documents
|
||||||
|
/// Adobe Portable Document Format (application/pdf)
|
||||||
|
Pdf,
|
||||||
|
/// Rich Text Format (application/rtf)
|
||||||
|
Rtf,
|
||||||
|
|
||||||
|
// MARK: Archives
|
||||||
|
/// Archive document, multiple files embedded (application/x-freearc)
|
||||||
|
Arc,
|
||||||
|
/// BZip archive (application/x-bzip)
|
||||||
|
Bz,
|
||||||
|
/// BZip2 archive (application/x-bzip2)
|
||||||
|
Bz2,
|
||||||
|
/// GZip Compressed Archive (application/gzip)
|
||||||
|
Gz,
|
||||||
|
/// Java Archive (application/java-archive)
|
||||||
|
Jar,
|
||||||
|
/// OGG (application/ogg)
|
||||||
|
Ogg,
|
||||||
|
/// RAR archive (application/vnd.rar)
|
||||||
|
Rar,
|
||||||
|
/// 7-zip archive (application/x-7z-compressed)
|
||||||
|
SevenZ,
|
||||||
|
/// Tape Archive (application/x-tar)
|
||||||
|
Tar,
|
||||||
|
/// ZIP archive (application/zip)
|
||||||
|
Zip,
|
||||||
|
|
||||||
|
// MARK: Fonts
|
||||||
|
/// MS Embedded OpenType fonts (application/vnd.ms-fontobject)
|
||||||
|
Eot,
|
||||||
|
/// OpenType font (font/otf)
|
||||||
|
Otf,
|
||||||
|
/// TrueType Font (font/ttf)
|
||||||
|
Ttf,
|
||||||
|
/// Web Open Font Format (font/woff)
|
||||||
|
Woff,
|
||||||
|
/// Web Open Font Format 2 (font/woff2)
|
||||||
|
Woff2,
|
||||||
|
|
||||||
|
// MARK: Applications
|
||||||
|
/// AbiWord document (application/x-abiword)
|
||||||
|
Abiword,
|
||||||
|
/// Amazon Kindle eBook format (application/vnd.amazon.ebook)
|
||||||
|
Azw,
|
||||||
|
/// CD audio (application/x-cdf)
|
||||||
|
Cda,
|
||||||
|
/// C-Shell script (application/x-csh)
|
||||||
|
Csh,
|
||||||
|
/// Microsoft Word (application/msword)
|
||||||
|
Doc,
|
||||||
|
/// Microsoft Word OpenXML (application/vnd.openxmlformats-officedocument.wordprocessingml.document)
|
||||||
|
Docx,
|
||||||
|
/// Electronic publication (application/epub+zip)
|
||||||
|
Epub,
|
||||||
|
/// iCalendar format (text/calendar)
|
||||||
|
Ics,
|
||||||
|
/// Apple Installer Package (application/vnd.apple.installer+xml)
|
||||||
|
Mpkg,
|
||||||
|
/// OpenDocument presentation (application/vnd.oasis.opendocument.presentation)
|
||||||
|
Odp,
|
||||||
|
/// OpenDocument spreadsheet (application/vnd.oasis.opendocument.spreadsheet)
|
||||||
|
Ods,
|
||||||
|
/// OpenDocument text document (application/vnd.oasis.opendocument.text)
|
||||||
|
Odt,
|
||||||
|
/// Hypertext Preprocessor (application/x-httpd-php)
|
||||||
|
Php,
|
||||||
|
/// Microsoft PowerPoint (application/vnd.ms-powerpoint)
|
||||||
|
Ppt,
|
||||||
|
/// Microsoft PowerPoint OpenXML (application/vnd.openxmlformats-officedocument.presentationml.presentation)
|
||||||
|
Pptx,
|
||||||
|
/// Bourne shell script (application/x-sh)
|
||||||
|
Sh,
|
||||||
|
/// Microsoft Visio (application/vnd.visio)
|
||||||
|
Vsd,
|
||||||
|
/// XHTML (application/xhtml+xml)
|
||||||
|
Xhtml,
|
||||||
|
/// Microsoft Excel (application/vnd.ms-excel)
|
||||||
|
Xls,
|
||||||
|
/// Microsoft Excel OpenXML (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
|
||||||
|
Xlsx,
|
||||||
|
/// XUL (application/vnd.mozilla.xul+xml)
|
||||||
|
Xul,
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: ser/de
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl utoipa::ToSchema for MimeType {
|
||||||
|
fn name() -> std::borrow::Cow<'static, str> {
|
||||||
|
std::borrow::Cow::Borrowed("MimeType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl utoipa::PartialSchema for MimeType {
|
||||||
|
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
|
||||||
|
utoipa::openapi::Schema::Object(
|
||||||
|
utoipa::openapi::schema::ObjectBuilder::new()
|
||||||
|
.schema_type(utoipa::openapi::schema::SchemaType::Type(Type::String))
|
||||||
|
.description(Some(
|
||||||
|
"A media type string (e.g., 'application/json', 'text/plain')",
|
||||||
|
))
|
||||||
|
.examples(Some("application/json"))
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
impl Serialize for MimeType {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for MimeType {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
Ok(MimeType::from_str(&s).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: misc
|
||||||
|
|
||||||
|
impl Default for MimeType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Blob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for MimeType {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self::from_str(&value).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for MimeType {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self::from_str(value).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&MimeType> for String {
|
||||||
|
fn from(value: &MimeType) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MimeType {
|
||||||
|
type Err = std::convert::Infallible;
|
||||||
|
|
||||||
|
// Must match `display` below, but may provide other alternatives.
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(match s {
|
||||||
|
"application/octet-stream" => Self::Blob,
|
||||||
|
|
||||||
|
// MARK: Audio
|
||||||
|
"audio/aac" => Self::Aac,
|
||||||
|
"audio/flac" => Self::Flac,
|
||||||
|
"audio/midi" | "audio/x-midi" => Self::Midi,
|
||||||
|
"audio/mpeg" => Self::Mp3,
|
||||||
|
"audio/ogg" => Self::Oga,
|
||||||
|
"audio/wav" => Self::Wav,
|
||||||
|
"audio/webm" => Self::Weba,
|
||||||
|
|
||||||
|
// MARK: Video
|
||||||
|
"video/x-msvideo" => Self::Avi,
|
||||||
|
"video/mp4" => Self::Mp4,
|
||||||
|
"video/mpeg" => Self::Mpeg,
|
||||||
|
"video/ogg" => Self::Ogv,
|
||||||
|
"video/mp2t" => Self::Ts,
|
||||||
|
"video/webm" => Self::WebmVideo,
|
||||||
|
"video/3gpp" => Self::ThreeGp,
|
||||||
|
"video/3gpp2" => Self::ThreeG2,
|
||||||
|
|
||||||
|
// MARK: Images
|
||||||
|
"image/apng" => Self::Apng,
|
||||||
|
"image/avif" => Self::Avif,
|
||||||
|
"image/bmp" => Self::Bmp,
|
||||||
|
"image/gif" => Self::Gif,
|
||||||
|
"image/vnd.microsoft.icon" => Self::Ico,
|
||||||
|
"image/jpeg" | "image/jpg" => Self::Jpg,
|
||||||
|
"image/png" => Self::Png,
|
||||||
|
"image/svg+xml" => Self::Svg,
|
||||||
|
"image/tiff" => Self::Tiff,
|
||||||
|
"image/webp" => Self::Webp,
|
||||||
|
|
||||||
|
// MARK: Text
|
||||||
|
"text/plain" => Self::Text,
|
||||||
|
"text/css" => Self::Css,
|
||||||
|
"text/csv" => Self::Csv,
|
||||||
|
"text/html" => Self::Html,
|
||||||
|
"text/javascript" => Self::Javascript,
|
||||||
|
"application/json" => Self::Json,
|
||||||
|
"application/ld+json" => Self::JsonLd,
|
||||||
|
"application/xml" | "text/xml" => Self::Xml,
|
||||||
|
|
||||||
|
// MARK: Documents
|
||||||
|
"application/pdf" => Self::Pdf,
|
||||||
|
"application/rtf" => Self::Rtf,
|
||||||
|
|
||||||
|
// MARK: Archives
|
||||||
|
"application/x-freearc" => Self::Arc,
|
||||||
|
"application/x-bzip" => Self::Bz,
|
||||||
|
"application/x-bzip2" => Self::Bz2,
|
||||||
|
"application/gzip" | "application/x-gzip" => Self::Gz,
|
||||||
|
"application/java-archive" => Self::Jar,
|
||||||
|
"application/ogg" => Self::Ogg,
|
||||||
|
"application/vnd.rar" => Self::Rar,
|
||||||
|
"application/x-7z-compressed" => Self::SevenZ,
|
||||||
|
"application/x-tar" => Self::Tar,
|
||||||
|
"application/zip" | "application/x-zip-compressed" => Self::Zip,
|
||||||
|
|
||||||
|
// MARK: Fonts
|
||||||
|
"application/vnd.ms-fontobject" => Self::Eot,
|
||||||
|
"font/otf" => Self::Otf,
|
||||||
|
"font/ttf" => Self::Ttf,
|
||||||
|
"font/woff" => Self::Woff,
|
||||||
|
"font/woff2" => Self::Woff2,
|
||||||
|
|
||||||
|
// MARK: Applications
|
||||||
|
"application/x-abiword" => Self::Abiword,
|
||||||
|
"application/vnd.amazon.ebook" => Self::Azw,
|
||||||
|
"application/x-cdf" => Self::Cda,
|
||||||
|
"application/x-csh" => Self::Csh,
|
||||||
|
"application/msword" => Self::Doc,
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" => Self::Docx,
|
||||||
|
"application/epub+zip" => Self::Epub,
|
||||||
|
"text/calendar" => Self::Ics,
|
||||||
|
"application/vnd.apple.installer+xml" => Self::Mpkg,
|
||||||
|
"application/vnd.oasis.opendocument.presentation" => Self::Odp,
|
||||||
|
"application/vnd.oasis.opendocument.spreadsheet" => Self::Ods,
|
||||||
|
"application/vnd.oasis.opendocument.text" => Self::Odt,
|
||||||
|
"application/x-httpd-php" => Self::Php,
|
||||||
|
"application/vnd.ms-powerpoint" => Self::Ppt,
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation" => {
|
||||||
|
Self::Pptx
|
||||||
|
}
|
||||||
|
"application/x-sh" => Self::Sh,
|
||||||
|
"application/vnd.visio" => Self::Vsd,
|
||||||
|
"application/xhtml+xml" => Self::Xhtml,
|
||||||
|
"application/vnd.ms-excel" => Self::Xls,
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => Self::Xlsx,
|
||||||
|
"application/vnd.mozilla.xul+xml" => Self::Xul,
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
debug!(message = "Encountered unknown mimetype", mime_string = s);
|
||||||
|
Self::Other(s.into())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MimeType {
|
||||||
|
/// Get a string representation of this mimetype.
|
||||||
|
///
|
||||||
|
/// The following always holds:
|
||||||
|
/// ```rust
|
||||||
|
/// # use toolbox::mime::MimeType;
|
||||||
|
/// # let x = MimeType::Blob;
|
||||||
|
/// assert_eq!(MimeType::from(x.to_string()), x);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The following might not hold:
|
||||||
|
/// ```rust
|
||||||
|
/// # use toolbox::mime::MimeType;
|
||||||
|
/// # let y = "application/custom";
|
||||||
|
/// // MimeType::from(y).to_string() may not equal y
|
||||||
|
/// ```
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Blob => write!(f, "application/octet-stream"),
|
||||||
|
|
||||||
|
// MARK: Audio
|
||||||
|
Self::Aac => write!(f, "audio/aac"),
|
||||||
|
Self::Flac => write!(f, "audio/flac"),
|
||||||
|
Self::Midi => write!(f, "audio/midi"),
|
||||||
|
Self::Mp3 => write!(f, "audio/mpeg"),
|
||||||
|
Self::Oga => write!(f, "audio/ogg"),
|
||||||
|
Self::Opus => write!(f, "audio/ogg"),
|
||||||
|
Self::Wav => write!(f, "audio/wav"),
|
||||||
|
Self::Weba => write!(f, "audio/webm"),
|
||||||
|
|
||||||
|
// MARK: Video
|
||||||
|
Self::Avi => write!(f, "video/x-msvideo"),
|
||||||
|
Self::Mp4 => write!(f, "video/mp4"),
|
||||||
|
Self::Mpeg => write!(f, "video/mpeg"),
|
||||||
|
Self::Ogv => write!(f, "video/ogg"),
|
||||||
|
Self::Ts => write!(f, "video/mp2t"),
|
||||||
|
Self::WebmVideo => write!(f, "video/webm"),
|
||||||
|
Self::ThreeGp => write!(f, "video/3gpp"),
|
||||||
|
Self::ThreeG2 => write!(f, "video/3gpp2"),
|
||||||
|
|
||||||
|
// MARK: Images
|
||||||
|
Self::Apng => write!(f, "image/apng"),
|
||||||
|
Self::Avif => write!(f, "image/avif"),
|
||||||
|
Self::Bmp => write!(f, "image/bmp"),
|
||||||
|
Self::Gif => write!(f, "image/gif"),
|
||||||
|
Self::Ico => write!(f, "image/vnd.microsoft.icon"),
|
||||||
|
Self::Jpg => write!(f, "image/jpeg"),
|
||||||
|
Self::Png => write!(f, "image/png"),
|
||||||
|
Self::Svg => write!(f, "image/svg+xml"),
|
||||||
|
Self::Tiff => write!(f, "image/tiff"),
|
||||||
|
Self::Webp => write!(f, "image/webp"),
|
||||||
|
|
||||||
|
// MARK: Text
|
||||||
|
Self::Text => write!(f, "text/plain"),
|
||||||
|
Self::Css => write!(f, "text/css"),
|
||||||
|
Self::Csv => write!(f, "text/csv"),
|
||||||
|
Self::Html => write!(f, "text/html"),
|
||||||
|
Self::Javascript => write!(f, "text/javascript"),
|
||||||
|
Self::Json => write!(f, "application/json"),
|
||||||
|
Self::JsonLd => write!(f, "application/ld+json"),
|
||||||
|
Self::Xml => write!(f, "application/xml"),
|
||||||
|
|
||||||
|
// MARK: Documents
|
||||||
|
Self::Pdf => write!(f, "application/pdf"),
|
||||||
|
Self::Rtf => write!(f, "application/rtf"),
|
||||||
|
|
||||||
|
// MARK: Archives
|
||||||
|
Self::Arc => write!(f, "application/x-freearc"),
|
||||||
|
Self::Bz => write!(f, "application/x-bzip"),
|
||||||
|
Self::Bz2 => write!(f, "application/x-bzip2"),
|
||||||
|
Self::Gz => write!(f, "application/gzip"),
|
||||||
|
Self::Jar => write!(f, "application/java-archive"),
|
||||||
|
Self::Ogg => write!(f, "application/ogg"),
|
||||||
|
Self::Rar => write!(f, "application/vnd.rar"),
|
||||||
|
Self::SevenZ => write!(f, "application/x-7z-compressed"),
|
||||||
|
Self::Tar => write!(f, "application/x-tar"),
|
||||||
|
Self::Zip => write!(f, "application/zip"),
|
||||||
|
|
||||||
|
// MARK: Fonts
|
||||||
|
Self::Eot => write!(f, "application/vnd.ms-fontobject"),
|
||||||
|
Self::Otf => write!(f, "font/otf"),
|
||||||
|
Self::Ttf => write!(f, "font/ttf"),
|
||||||
|
Self::Woff => write!(f, "font/woff"),
|
||||||
|
Self::Woff2 => write!(f, "font/woff2"),
|
||||||
|
|
||||||
|
// MARK: Applications
|
||||||
|
Self::Abiword => write!(f, "application/x-abiword"),
|
||||||
|
Self::Azw => write!(f, "application/vnd.amazon.ebook"),
|
||||||
|
Self::Cda => write!(f, "application/x-cdf"),
|
||||||
|
Self::Csh => write!(f, "application/x-csh"),
|
||||||
|
Self::Doc => write!(f, "application/msword"),
|
||||||
|
Self::Docx => write!(
|
||||||
|
f,
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
),
|
||||||
|
Self::Epub => write!(f, "application/epub+zip"),
|
||||||
|
Self::Ics => write!(f, "text/calendar"),
|
||||||
|
Self::Mpkg => write!(f, "application/vnd.apple.installer+xml"),
|
||||||
|
Self::Odp => write!(f, "application/vnd.oasis.opendocument.presentation"),
|
||||||
|
Self::Ods => write!(f, "application/vnd.oasis.opendocument.spreadsheet"),
|
||||||
|
Self::Odt => write!(f, "application/vnd.oasis.opendocument.text"),
|
||||||
|
Self::Php => write!(f, "application/x-httpd-php"),
|
||||||
|
Self::Ppt => write!(f, "application/vnd.ms-powerpoint"),
|
||||||
|
Self::Pptx => write!(
|
||||||
|
f,
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||||
|
),
|
||||||
|
Self::Sh => write!(f, "application/x-sh"),
|
||||||
|
Self::Vsd => write!(f, "application/vnd.visio"),
|
||||||
|
Self::Xhtml => write!(f, "application/xhtml+xml"),
|
||||||
|
Self::Xls => write!(f, "application/vnd.ms-excel"),
|
||||||
|
Self::Xlsx => write!(
|
||||||
|
f,
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
),
|
||||||
|
Self::Xul => write!(f, "application/vnd.mozilla.xul+xml"),
|
||||||
|
|
||||||
|
Self::Other(x) => write!(f, "{x}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MimeType {
|
||||||
|
// Must match `From<String>` above
|
||||||
|
|
||||||
|
/// Try to guess a file's mime type from its extension.
|
||||||
|
/// `ext` should NOT start with a dot.
|
||||||
|
pub fn from_extension(ext: &str) -> Option<Self> {
|
||||||
|
Some(match ext {
|
||||||
|
// MARK: Audio
|
||||||
|
"aac" => Self::Aac,
|
||||||
|
"flac" => Self::Flac,
|
||||||
|
"mid" | "midi" => Self::Midi,
|
||||||
|
"mp3" => Self::Mp3,
|
||||||
|
"oga" => Self::Oga,
|
||||||
|
"opus" => Self::Opus,
|
||||||
|
"wav" => Self::Wav,
|
||||||
|
"weba" => Self::Weba,
|
||||||
|
|
||||||
|
// MARK: Video
|
||||||
|
"avi" => Self::Avi,
|
||||||
|
"mp4" => Self::Mp4,
|
||||||
|
"mpeg" => Self::Mpeg,
|
||||||
|
"ogv" => Self::Ogv,
|
||||||
|
"ts" => Self::Ts,
|
||||||
|
"webm" => Self::WebmVideo,
|
||||||
|
"3gp" => Self::ThreeGp,
|
||||||
|
"3g2" => Self::ThreeG2,
|
||||||
|
|
||||||
|
// MARK: Images
|
||||||
|
"apng" => Self::Apng,
|
||||||
|
"avif" => Self::Avif,
|
||||||
|
"bmp" => Self::Bmp,
|
||||||
|
"gif" => Self::Gif,
|
||||||
|
"ico" => Self::Ico,
|
||||||
|
"jpg" | "jpeg" => Self::Jpg,
|
||||||
|
"png" => Self::Png,
|
||||||
|
"svg" => Self::Svg,
|
||||||
|
"tif" | "tiff" => Self::Tiff,
|
||||||
|
"webp" => Self::Webp,
|
||||||
|
|
||||||
|
// MARK: Text
|
||||||
|
"txt" => Self::Text,
|
||||||
|
"css" => Self::Css,
|
||||||
|
"csv" => Self::Csv,
|
||||||
|
"htm" | "html" => Self::Html,
|
||||||
|
"js" | "mjs" => Self::Javascript,
|
||||||
|
"json" => Self::Json,
|
||||||
|
"jsonld" => Self::JsonLd,
|
||||||
|
"xml" => Self::Xml,
|
||||||
|
|
||||||
|
// MARK: Documents
|
||||||
|
"pdf" => Self::Pdf,
|
||||||
|
"rtf" => Self::Rtf,
|
||||||
|
|
||||||
|
// MARK: Archives
|
||||||
|
"arc" => Self::Arc,
|
||||||
|
"bz" => Self::Bz,
|
||||||
|
"bz2" => Self::Bz2,
|
||||||
|
"gz" => Self::Gz,
|
||||||
|
"jar" => Self::Jar,
|
||||||
|
"ogx" => Self::Ogg,
|
||||||
|
"rar" => Self::Rar,
|
||||||
|
"7z" => Self::SevenZ,
|
||||||
|
"tar" => Self::Tar,
|
||||||
|
"zip" => Self::Zip,
|
||||||
|
|
||||||
|
// MARK: Fonts
|
||||||
|
"eot" => Self::Eot,
|
||||||
|
"otf" => Self::Otf,
|
||||||
|
"ttf" => Self::Ttf,
|
||||||
|
"woff" => Self::Woff,
|
||||||
|
"woff2" => Self::Woff2,
|
||||||
|
|
||||||
|
// MARK: Applications
|
||||||
|
"abw" => Self::Abiword,
|
||||||
|
"azw" => Self::Azw,
|
||||||
|
"cda" => Self::Cda,
|
||||||
|
"csh" => Self::Csh,
|
||||||
|
"doc" => Self::Doc,
|
||||||
|
"docx" => Self::Docx,
|
||||||
|
"epub" => Self::Epub,
|
||||||
|
"ics" => Self::Ics,
|
||||||
|
"mpkg" => Self::Mpkg,
|
||||||
|
"odp" => Self::Odp,
|
||||||
|
"ods" => Self::Ods,
|
||||||
|
"odt" => Self::Odt,
|
||||||
|
"php" => Self::Php,
|
||||||
|
"ppt" => Self::Ppt,
|
||||||
|
"pptx" => Self::Pptx,
|
||||||
|
"sh" => Self::Sh,
|
||||||
|
"vsd" => Self::Vsd,
|
||||||
|
"xhtml" => Self::Xhtml,
|
||||||
|
"xls" => Self::Xls,
|
||||||
|
"xlsx" => Self::Xlsx,
|
||||||
|
"xul" => Self::Xul,
|
||||||
|
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the extension we use for files with this type.
|
||||||
|
/// Includes a dot. Might be the empty string.
|
||||||
|
pub fn extension(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Blob => "",
|
||||||
|
Self::Other(_) => "",
|
||||||
|
|
||||||
|
// MARK: Audio
|
||||||
|
Self::Aac => ".aac",
|
||||||
|
Self::Flac => ".flac",
|
||||||
|
Self::Midi => ".midi",
|
||||||
|
Self::Mp3 => ".mp3",
|
||||||
|
Self::Oga => ".oga",
|
||||||
|
Self::Opus => ".opus",
|
||||||
|
Self::Wav => ".wav",
|
||||||
|
Self::Weba => ".weba",
|
||||||
|
|
||||||
|
// MARK: Video
|
||||||
|
Self::Avi => ".avi",
|
||||||
|
Self::Mp4 => ".mp4",
|
||||||
|
Self::Mpeg => ".mpeg",
|
||||||
|
Self::Ogv => ".ogv",
|
||||||
|
Self::Ts => ".ts",
|
||||||
|
Self::WebmVideo => ".webm",
|
||||||
|
Self::ThreeGp => ".3gp",
|
||||||
|
Self::ThreeG2 => ".3g2",
|
||||||
|
|
||||||
|
// MARK: Images
|
||||||
|
Self::Apng => ".apng",
|
||||||
|
Self::Avif => ".avif",
|
||||||
|
Self::Bmp => ".bmp",
|
||||||
|
Self::Gif => ".gif",
|
||||||
|
Self::Ico => ".ico",
|
||||||
|
Self::Jpg => ".jpg",
|
||||||
|
Self::Png => ".png",
|
||||||
|
Self::Svg => ".svg",
|
||||||
|
Self::Tiff => ".tiff",
|
||||||
|
Self::Webp => ".webp",
|
||||||
|
|
||||||
|
// MARK: Text
|
||||||
|
Self::Text => ".txt",
|
||||||
|
Self::Css => ".css",
|
||||||
|
Self::Csv => ".csv",
|
||||||
|
Self::Html => ".html",
|
||||||
|
Self::Javascript => ".js",
|
||||||
|
Self::Json => ".json",
|
||||||
|
Self::JsonLd => ".jsonld",
|
||||||
|
Self::Xml => ".xml",
|
||||||
|
|
||||||
|
// MARK: Documents
|
||||||
|
Self::Pdf => ".pdf",
|
||||||
|
Self::Rtf => ".rtf",
|
||||||
|
|
||||||
|
// MARK: Archives
|
||||||
|
Self::Arc => ".arc",
|
||||||
|
Self::Bz => ".bz",
|
||||||
|
Self::Bz2 => ".bz2",
|
||||||
|
Self::Gz => ".gz",
|
||||||
|
Self::Jar => ".jar",
|
||||||
|
Self::Ogg => ".ogx",
|
||||||
|
Self::Rar => ".rar",
|
||||||
|
Self::SevenZ => ".7z",
|
||||||
|
Self::Tar => ".tar",
|
||||||
|
Self::Zip => ".zip",
|
||||||
|
|
||||||
|
// MARK: Fonts
|
||||||
|
Self::Eot => ".eot",
|
||||||
|
Self::Otf => ".otf",
|
||||||
|
Self::Ttf => ".ttf",
|
||||||
|
Self::Woff => ".woff",
|
||||||
|
Self::Woff2 => ".woff2",
|
||||||
|
|
||||||
|
// MARK: Applications
|
||||||
|
Self::Abiword => ".abw",
|
||||||
|
Self::Azw => ".azw",
|
||||||
|
Self::Cda => ".cda",
|
||||||
|
Self::Csh => ".csh",
|
||||||
|
Self::Doc => ".doc",
|
||||||
|
Self::Docx => ".docx",
|
||||||
|
Self::Epub => ".epub",
|
||||||
|
Self::Ics => ".ics",
|
||||||
|
Self::Mpkg => ".mpkg",
|
||||||
|
Self::Odp => ".odp",
|
||||||
|
Self::Ods => ".ods",
|
||||||
|
Self::Odt => ".odt",
|
||||||
|
Self::Php => ".php",
|
||||||
|
Self::Ppt => ".ppt",
|
||||||
|
Self::Pptx => ".pptx",
|
||||||
|
Self::Sh => ".sh",
|
||||||
|
Self::Vsd => ".vsd",
|
||||||
|
Self::Xhtml => ".xhtml",
|
||||||
|
Self::Xls => ".xls",
|
||||||
|
Self::Xlsx => ".xlsx",
|
||||||
|
Self::Xul => ".xul",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
crates/toolbox/src/misc.rs
Normal file
36
crates/toolbox/src/misc.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/// Normalize a domain. This does the following:
|
||||||
|
/// - removes protocol prefixes
|
||||||
|
/// - removes leading `www`
|
||||||
|
/// - removes query params and path segments.
|
||||||
|
///
|
||||||
|
/// This function is for roach, and should exactly match the ts implementation.
|
||||||
|
///
|
||||||
|
/// ## Examples:
|
||||||
|
/// ```
|
||||||
|
/// # use toolbox::misc::normalize_domain;
|
||||||
|
/// assert_eq!("domain.com", normalize_domain("domain.com"));
|
||||||
|
/// assert_eq!("domain.com", normalize_domain("domain.com/"));
|
||||||
|
/// assert_eq!("domain.com", normalize_domain("domain.com/en/us"));
|
||||||
|
/// assert_eq!("domain.com", normalize_domain("domain.com/?key=val"));
|
||||||
|
/// assert_eq!("domain.com", normalize_domain("www.domain.com"));
|
||||||
|
/// assert_eq!("domain.com", normalize_domain("https://www.domain.com"));
|
||||||
|
/// assert_eq!("us.domain.com", normalize_domain("us.domain.com"));
|
||||||
|
/// ```
|
||||||
|
pub fn normalize_domain(domain: &str) -> &str {
|
||||||
|
let mut domain = domain.strip_prefix("http://").unwrap_or(domain);
|
||||||
|
domain = domain.strip_prefix("https://").unwrap_or(domain);
|
||||||
|
domain = domain.strip_prefix("www.").unwrap_or(domain);
|
||||||
|
domain = domain.find("/").map_or(domain, |x| &domain[0..x]);
|
||||||
|
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn random_string(length: usize) -> String {
|
||||||
|
rand::rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(length)
|
||||||
|
.map(char::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
*/
|
||||||
136
crates/toolbox/src/strings.rs
Normal file
136
crates/toolbox/src/strings.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use num::{Integer, Zero, cast::AsPrimitive};
|
||||||
|
|
||||||
|
/// Pretty-print a quantity of bytes
|
||||||
|
pub fn pp_bytes_si<T: Integer + Display + Zero + Clone + AsPrimitive<f64>>(bytes: T) -> String {
|
||||||
|
const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||||
|
const THRESHOLD: f64 = 1000.0;
|
||||||
|
|
||||||
|
if bytes.is_zero() {
|
||||||
|
return "0 B".to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes_f: f64 = bytes.as_();
|
||||||
|
let unit_index = (bytes_f.log10() / THRESHOLD.log10()).floor() as usize;
|
||||||
|
let unit_index = unit_index.min(UNITS.len() - 1);
|
||||||
|
|
||||||
|
let value = bytes_f / THRESHOLD.powi(unit_index as i32);
|
||||||
|
|
||||||
|
if unit_index == 0 {
|
||||||
|
format!("{} {}", bytes, UNITS[unit_index])
|
||||||
|
} else if value >= 100.0 {
|
||||||
|
format!("{:.0} {}", value, UNITS[unit_index])
|
||||||
|
} else if value >= 10.0 {
|
||||||
|
format!("{:.1} {}", value, UNITS[unit_index])
|
||||||
|
} else {
|
||||||
|
format!("{:.2} {}", value, UNITS[unit_index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pretty-print a quantity of bytes
|
||||||
|
pub fn pp_bytes_b2<T: Integer + Display + Zero + Clone + AsPrimitive<f64>>(bytes: T) -> String {
|
||||||
|
const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
||||||
|
const THRESHOLD: f64 = 1024.0;
|
||||||
|
|
||||||
|
if bytes.is_zero() {
|
||||||
|
return "0 B".to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes_f: f64 = bytes.as_();
|
||||||
|
let unit_index = (bytes_f.log2() / THRESHOLD.log2()).floor() as usize;
|
||||||
|
let unit_index = unit_index.min(UNITS.len() - 1);
|
||||||
|
|
||||||
|
let value = bytes_f / THRESHOLD.powi(unit_index as i32);
|
||||||
|
|
||||||
|
if unit_index == 0 {
|
||||||
|
format!("{} {}", bytes, UNITS[unit_index])
|
||||||
|
} else if value >= 100.0 {
|
||||||
|
format!("{:.0} {}", value, UNITS[unit_index])
|
||||||
|
} else if value >= 10.0 {
|
||||||
|
format!("{:.1} {}", value, UNITS[unit_index])
|
||||||
|
} else {
|
||||||
|
format!("{:.2} {}", value, UNITS[unit_index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pretty-print an integer with comma separators
|
||||||
|
pub fn pp_int_commas<T: Integer + Display>(value: T) -> String {
|
||||||
|
let s = value.to_string();
|
||||||
|
let mut result = String::new();
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
|
||||||
|
for (i, &byte) in bytes.iter().enumerate() {
|
||||||
|
if i > 0 && (bytes.len() - i).is_multiple_of(3) {
|
||||||
|
result.push(',');
|
||||||
|
}
|
||||||
|
result.push(byte as char);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert bytes to a hex string
|
||||||
|
#[inline]
|
||||||
|
pub fn to_hex(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().map(|b| format!("{:02x}", b)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a hex string as bytes
|
||||||
|
pub fn from_hex(hex_str: &str) -> Result<Vec<u8>, &'static str> {
|
||||||
|
if !hex_str.len().is_multiple_of(2) {
|
||||||
|
return Err("hex string must have even length");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bytes = Vec::with_capacity(hex_str.len() / 2);
|
||||||
|
|
||||||
|
for chunk in hex_str.as_bytes().chunks(2) {
|
||||||
|
let hex_byte = std::str::from_utf8(chunk).map_err(|_err| "invalid UTF-8 in hex string")?;
|
||||||
|
let byte = u8::from_str_radix(hex_byte, 16).map_err(|_err| "invalid hex character")?;
|
||||||
|
bytes.push(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncate a string from the left, replacing the first character with "…" if truncated
|
||||||
|
pub fn truncate_left(length: usize, string: &str) -> String {
|
||||||
|
if string.chars().count() <= length {
|
||||||
|
return string.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 1 {
|
||||||
|
return "…".to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let chars: Vec<char> = string.chars().collect();
|
||||||
|
let start_index = chars.len() - (length - 1);
|
||||||
|
let mut result = String::from("…");
|
||||||
|
result.extend(&chars[start_index..]);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncate a string from the right, replacing the last character with "…" if truncated
|
||||||
|
pub fn truncate_right(length: usize, string: &str) -> String {
|
||||||
|
if string.chars().count() <= length {
|
||||||
|
return string.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 1 {
|
||||||
|
return "…".to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
let chars: Vec<char> = string.chars().collect();
|
||||||
|
let mut result = String::new();
|
||||||
|
result.extend(&chars[0..length - 1]);
|
||||||
|
result.push('…');
|
||||||
|
result
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user