From 6fc3fd8aaffc5e1499868dd478c27632c4b8ef4f Mon Sep 17 00:00:00 2001 From: Eduardo Trujillo <ed@chromabits.com> Date: Sun, 19 May 2024 21:25:22 +0000 Subject: [PATCH] refactor: Migrate from snafu to thiserror --- Cargo.lock | 29 +------------- Cargo.toml | 2 +- src/bundle/mod.rs | 79 +++++++++++++++++++++++++++--------- src/bundle/packager.rs | 15 +++++-- src/bundle/poller.rs | 18 +++++++-- src/bundle/s3/packager.rs | 49 ++++++++++++----------- src/bundle/s3/poller.rs | 28 ++++++++----- src/cli/bundle.rs | 2 +- src/cli/serve.rs | 49 ++++++++++++++--------- src/config.rs | 22 ++++++---- src/files/path.rs | 29 +++++++++----- src/files/path_context.rs | 55 +++++++++++++++---------- src/files/pathbuf.rs | 70 ++++++++++++++++---------------- src/files/service.rs | 84 +++++++++++++++++++++++---------------- src/main.rs | 38 ++++++++++++------ src/server.rs | 32 ++++++++++----- src/stats.rs | 43 ++++++++++++++------ 17 files changed, 390 insertions(+), 254 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf1e401..4a76f70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,12 +932,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "either" version = "1.12.0" @@ -1032,7 +1026,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "snafu", + "thiserror", "tokio", "tokio-stream", "tokio-tar", @@ -2511,27 +2505,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "snafu" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "socket2" version = "0.5.7" diff --git a/Cargo.toml b/Cargo.toml index 03e9fac..111461f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ rusoto_s3 = "0.48.0" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" -snafu = "0.6.8" +thiserror = "1.0" tokio-stream = "0.1" toml = "0.5" url = "2.5" diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs index 191aa17..8e909f1 100644 --- a/src/bundle/mod.rs +++ b/src/bundle/mod.rs @@ -2,8 +2,8 @@ use crate::config::Config; use collective::rundir::{self, RunDir}; use s3::packager::S3BundlePackager; use serde::Serialize; -use snafu::{ResultExt, Snafu}; use std::{path::PathBuf, sync::Arc}; +use thiserror::Error; use tokio::{ sync::RwLock, time::{interval, Duration}, @@ -16,19 +16,42 @@ pub mod packager; pub mod poller; pub mod s3; -#[derive(Snafu, Debug)] -pub enum Error { - InitRunDir { source: rundir::RunDirError }, - DeinitRunDir { source: rundir::RunDirError }, +#[derive(Debug, Error)] +pub enum BundleError { + #[error("Unable to initialzie rundir")] + InitRunDir { + #[source] + source: rundir::RunDirError, + }, + #[error("Unable to deinitialize rundir")] + DeinitRunDir { + #[source] + source: rundir::RunDirError, + }, + #[error("Unable to read lock")] LockRead, + #[error("Unable to write lock")] LockWrite, - AttachSignalHook { source: std::io::Error }, + #[error("Unable to attach signal hook")] + AttachSignalHook { + #[source] + source: std::io::Error, + }, + #[error("Got missing eTag")] MissingETag, - PollError { source: poller::Error }, - SubDirError { source: rundir::RunDirError }, + #[error(transparent)] + PollError { + #[from] + source: poller::PollerError, + }, + #[error("Unable to set up subdir")] + SubDirError { + #[source] + source: rundir::RunDirError, + }, } -type Result<T, E = Error> = std::result::Result<T, E>; +type Result<T, E = BundleError> = std::result::Result<T, E>; #[derive(Copy, Clone, Debug, Serialize, PartialEq)] pub enum UnbundlerStatus { @@ -79,7 +102,7 @@ impl Bundler { Bundler { packager } } - pub async fn package(&self, path: PathBuf) -> Result<(), packager::Error> { + pub async fn package(&self, path: PathBuf) -> Result<(), packager::PackagerError> { log::info!("Packaging bundle at {}", path.display()); self.packager.generate(path).await @@ -188,8 +211,16 @@ impl Unbundler { let mut state = self.state.write().await; - state.rundir.initialize().context(InitRunDir)?; - state.temp_dir = Some(state.rundir.create_subdir("temp").context(InitRunDir)?); + state + .rundir + .initialize() + .map_err(|source| BundleError::InitRunDir { source })?; + state.temp_dir = Some( + state + .rundir + .create_subdir("temp") + .map_err(|source| BundleError::InitRunDir { source })?, + ); state.status = UnbundlerStatus::Initialized; @@ -231,8 +262,7 @@ impl Unbundler { Err(err) } res => res, - } - .context(PollError)?; + }?; match result { poller::PollResult::Skip => { @@ -261,7 +291,11 @@ impl Unbundler { poller::PollResult::UpdateReady { etag } => { let mut state = self.state.write().await; - if state.rundir.subdir_exists(&etag).context(SubDirError)? { + if state + .rundir + .subdir_exists(&etag) + .map_err(|source| BundleError::SubDirError { source })? + { log::warn!("Unbundler: Skipping update. Subdir already exists."); return Ok(()); @@ -271,7 +305,10 @@ impl Unbundler { etag: Some(etag.clone()), }); - let newdir = state.rundir.create_subdir(&etag).context(SubDirError)?; + let newdir = state + .rundir + .create_subdir(&etag) + .map_err(|source| BundleError::SubDirError { source })?; let result = self.poller.retrieve(&etag, newdir.clone()).await; @@ -281,7 +318,10 @@ impl Unbundler { state.staging_bundle = None; state.status = UnbundlerStatus::Ready; - state.rundir.remove_subdir_all(&etag).context(SubDirError)?; + state + .rundir + .remove_subdir_all(&etag) + .map_err(|source| BundleError::SubDirError { source })?; return Ok(()); } @@ -304,7 +344,10 @@ impl Unbundler { let mut state = self.state.write().await; state.status = UnbundlerStatus::Idle; - state.rundir.cleanup().context(DeinitRunDir)?; + state + .rundir + .cleanup() + .map_err(|source| BundleError::DeinitRunDir { source })?; Ok(()) } diff --git a/src/bundle/packager.rs b/src/bundle/packager.rs index 6ee1aed..9e8fb84 100644 --- a/src/bundle/packager.rs +++ b/src/bundle/packager.rs @@ -1,15 +1,22 @@ use async_trait::async_trait; -use snafu::Snafu; use std::path::PathBuf; +use thiserror::Error; -#[derive(Snafu, Debug)] -pub enum Error { +#[derive(Debug, Error)] +pub enum PackagerError { + #[error("Internal packager error")] InternalBundlePackagerError { + #[from] source: Box<dyn std::error::Error + Sync + Send>, }, + #[error(transparent)] + IOError { + #[from] + source: std::io::Error, + }, } -pub type Result<T, E = Error> = std::result::Result<T, E>; +pub type Result<T, E = PackagerError> = std::result::Result<T, E>; #[async_trait(?Send)] pub trait BundlePackager { diff --git a/src/bundle/poller.rs b/src/bundle/poller.rs index e4f5d6f..861df3e 100644 --- a/src/bundle/poller.rs +++ b/src/bundle/poller.rs @@ -1,24 +1,34 @@ use super::Bundle; use async_trait::async_trait; -use snafu::Snafu; use std::path::PathBuf; +use thiserror::Error; -#[derive(Snafu, Debug)] -pub enum Error { +#[derive(Debug, Error)] +pub enum PollerError { + #[error("Bundle ID is missing")] MissingBundleID, + #[error("Internal poller error")] InternalPollerError { + #[from] source: Box<dyn std::error::Error + Sync + Send>, }, + #[error( + "Original Bundle ID {} does not match Current ID {}", + original_id, + current_id + )] MismatchedBundleID { original_id: String, current_id: String, }, + #[error("Unable to unpack bundle")] UnpackError { + #[source] source: std::io::Error, }, } -pub type Result<T, E = Error> = std::result::Result<T, E>; +pub type Result<T, E = PollerError> = std::result::Result<T, E>; pub enum PollResult { Skip, diff --git a/src/bundle/s3/packager.rs b/src/bundle/s3/packager.rs index 248127a..7526f4e 100644 --- a/src/bundle/s3/packager.rs +++ b/src/bundle/s3/packager.rs @@ -1,11 +1,11 @@ -use crate::bundle::packager::{BundlePackager, Error, Result}; +use crate::bundle::packager::{BundlePackager, PackagerError, Result}; use async_compression::tokio::write::GzipEncoder; use async_trait::async_trait; use futures_util::TryStreamExt; use rusoto_core::RusotoError; use rusoto_s3::{PutObjectError, PutObjectRequest, S3Client, StreamingBody, S3}; -use snafu::{ResultExt, Snafu}; use std::path::PathBuf; +use thiserror::Error; use tokio::{ fs::{File, OpenOptions}, io::AsyncWriteExt, @@ -13,15 +13,23 @@ use tokio::{ use tokio_tar::Builder as TarBuilder; use tokio_util::codec::{BytesCodec, FramedRead}; -#[derive(Snafu, Debug)] +#[derive(Debug, Error)] pub enum InternalError { - S3PutObjectError { source: RusotoError<PutObjectError> }, - IOError { source: std::io::Error }, + #[error(transparent)] + S3PutObjectError { + #[from] + source: RusotoError<PutObjectError>, + }, + #[error(transparent)] + IOError { + #[from] + source: std::io::Error, + }, } -impl From<InternalError> for Error { +impl From<InternalError> for PackagerError { fn from(err: InternalError) -> Self { - Error::InternalBundlePackagerError { + PackagerError::InternalBundlePackagerError { source: Box::new(err), } } @@ -66,24 +74,21 @@ impl BundlePackager for S3BundlePackager { .write(true) .read(true) .open("bundle.tar.gz") - .await - .context(IOError)?; - let tar_gz_file = File::create("bundle.tar").await.context(IOError)?; + .await?; + let tar_gz_file = File::create("bundle.tar").await?; let mut builder = TarBuilder::new(tar_file); - builder.append_dir_all(".", path).await.context(IOError)?; - let mut tar_file = builder.into_inner().await.context(IOError)?; + builder.append_dir_all(".", path).await?; + let mut tar_file = builder.into_inner().await?; let mut encoder = GzipEncoder::new(tar_gz_file); - tokio::io::copy(&mut tar_file, &mut encoder) - .await - .context(IOError)?; + tokio::io::copy(&mut tar_file, &mut encoder).await?; - encoder.shutdown().await.context(IOError)?; + encoder.shutdown().await?; - let tar_gz_file = File::open("bundle.tar.gz").await.context(IOError)?; + let tar_gz_file = File::open("bundle.tar.gz").await?; let final_stream_of_bytes = FramedRead::new(tar_gz_file, BytesCodec::new()); @@ -106,7 +111,7 @@ impl BundlePackager for S3BundlePackager { .client .put_object(request) .await - .context(S3PutObjectError)?; + .map_err(|source| InternalError::S3PutObjectError { source })?; log::info!( "S3BundlePackager: Bundle uploaded. ETag: {}", @@ -115,12 +120,8 @@ impl BundlePackager for S3BundlePackager { log::info!("S3BundlePackager: Removing temporary files..."); - tokio::fs::remove_file("bundle.tar") - .await - .context(IOError)?; - tokio::fs::remove_file("bundle.tar.gz") - .await - .context(IOError)?; + tokio::fs::remove_file("bundle.tar").await?; + tokio::fs::remove_file("bundle.tar.gz").await?; Ok(()) } diff --git a/src/bundle/s3/poller.rs b/src/bundle/s3/poller.rs index 8a2ef57..d52fafa 100644 --- a/src/bundle/s3/poller.rs +++ b/src/bundle/s3/poller.rs @@ -1,5 +1,5 @@ use crate::bundle::{ - poller::{BundlePoller, Error, PollResult, Result}, + poller::{BundlePoller, PollResult, PollerError, Result}, Bundle, }; @@ -8,25 +8,31 @@ use rusoto_core::RusotoError; use rusoto_s3::{ GetObjectError, GetObjectRequest, HeadObjectError, HeadObjectRequest, S3Client, S3, }; -use snafu::{ResultExt, Snafu}; use std::path::PathBuf; +use thiserror::Error; use tokio_tar::Archive; -#[derive(Snafu, Debug)] +#[derive(Debug, Error)] pub enum InternalError { + #[error(transparent)] S3HeadObjectError { + #[from] source: RusotoError<HeadObjectError>, }, + #[error(transparent)] S3GetObjectError { + #[from] source: RusotoError<GetObjectError>, }, + #[error("Missing body")] S3MissingBody, + #[error("Missing eTag")] S3MissingETag, } -impl From<InternalError> for Error { +impl From<InternalError> for PollerError { fn from(err: InternalError) -> Self { - Error::InternalPollerError { + PollerError::InternalPollerError { source: Box::new(err), } } @@ -78,7 +84,7 @@ impl BundlePoller for S3BundlePoller { .client .head_object(head_request) .await - .context(S3HeadObjectError)?; + .map_err(|source| InternalError::S3HeadObjectError { source })?; if head_response.e_tag == active_bundle.etag { log::info!( @@ -97,7 +103,7 @@ impl BundlePoller for S3BundlePoller { } else { log::error!("S3BundlePoller: Expected the active bundle to have a valid ETag."); - return Err(Error::MissingBundleID); + return Err(PollerError::MissingBundleID); } } @@ -112,7 +118,7 @@ impl BundlePoller for S3BundlePoller { .client .head_object(head_request) .await - .context(S3HeadObjectError)?; + .map_err(|source| InternalError::S3HeadObjectError { source })?; Ok(PollResult::UpdateReady { etag: head_response.e_tag.ok_or(InternalError::S3MissingETag)?, @@ -132,14 +138,14 @@ impl BundlePoller for S3BundlePoller { .client .get_object(get_object_request) .await - .context(S3GetObjectError)?; + .map_err(|source| InternalError::S3GetObjectError { source })?; let current_bundle_id = get_object_response .e_tag .ok_or(InternalError::S3MissingETag)?; if current_bundle_id != bundle_id { - return Err(Error::MismatchedBundleID { + return Err(PollerError::MismatchedBundleID { original_id: String::from(bundle_id), current_id: current_bundle_id, }); @@ -158,7 +164,7 @@ impl BundlePoller for S3BundlePoller { archive .unpack(path) .await - .map_err(|err| Error::UnpackError { source: err })?; + .map_err(|err| PollerError::UnpackError { source: err })?; Ok(()) } diff --git a/src/cli/bundle.rs b/src/cli/bundle.rs index 0ede3af..0ec32e5 100644 --- a/src/cli/bundle.rs +++ b/src/cli/bundle.rs @@ -7,7 +7,7 @@ use espresso::{ use super::args::BundleOpts; -pub async fn bundle(config: Arc<Config>, opts: BundleOpts) -> Result<(), packager::Error> { +pub async fn bundle(config: Arc<Config>, opts: BundleOpts) -> Result<(), packager::PackagerError> { let bundler = Bundler::new(config); bundler.package(opts.source_path).await.unwrap(); diff --git a/src/cli/serve.rs b/src/cli/serve.rs index d494e6f..417cdd1 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -10,30 +10,47 @@ use espresso::{ stats::{self, StatsServer}, }; use lazy_static::lazy_static; -use snafu::{ResultExt, Snafu}; use std::{ collections::HashSet, sync::{mpsc, Arc}, }; +use thiserror::Error; use tokio::sync::RwLock; lazy_static! { static ref MONITOR: ThreadMonitor = ThreadMonitor::new(); } -#[derive(Snafu, Debug)] -pub enum Error { - Unbundle { source: bundle::Error }, - ServeError { source: Box<server::Error> }, - ServeStats { source: stats::Error }, - MonitorError { source: monitor::Error }, +#[derive(Debug, Error)] +pub enum ServeError { + #[error(transparent)] + Unbundle { + #[from] + source: bundle::BundleError, + }, + #[error(transparent)] + ServeError { + #[from] + source: Box<server::ServerError>, + }, + #[error(transparent)] + ServeStats { + #[from] + source: stats::StatsError, + }, + #[error(transparent)] + MonitorError { + #[from] + source: monitor::Error, + }, + #[error("Issue with receiving notification")] RecvNotify, } -type Result<T, E = Error> = std::result::Result<T, E>; +type Result<T, E = ServeError> = std::result::Result<T, E>; pub async fn serve(config: Arc<Config>) -> Result<()> { - MONITOR.init().context(MonitorError)?; + MONITOR.init()?; // Set up a channel for receiving thread notifications. let (monitor_tx, monitor_rx) = mpsc::channel(); @@ -52,7 +69,7 @@ pub async fn serve(config: Arc<Config>) -> Result<()> { server .spawn(monitor_tx.clone()) .await - .map_err(|err| Error::ServeError { + .map_err(|err| ServeError::ServeError { source: Box::new(err), })?; @@ -65,10 +82,8 @@ pub async fn serve(config: Arc<Config>) -> Result<()> { Some(stats_config) => { let stats_server = StatsServer::new(stats_config.clone(), unbundler.clone()); - let (stats_server_handle, stats_thread_handle) = stats_server - .spawn(monitor_tx.clone()) - .await - .context(ServeStats)?; + let (stats_server_handle, stats_thread_handle) = + stats_server.spawn(monitor_tx.clone()).await?; maybe_stats_server_handle = Some(stats_server_handle); server_thread_ids.insert(stats_thread_handle.thread().id()); @@ -79,9 +94,7 @@ pub async fn serve(config: Arc<Config>) -> Result<()> { let unbundler_thread_handle = thread::handle::spawn(monitor_tx.clone(), move || { let sys = System::new(); - let result = sys - .block_on(async move { unbundler.enter().await }) - .context(Unbundle); + let result = sys.block_on(async move { unbundler.enter().await }); if let Err(e) = result { log::error!("Unbundler failed: {:?}", e); @@ -105,7 +118,7 @@ pub async fn serve(config: Arc<Config>) -> Result<()> { // Wait for a thread to finish. loop { - monitor_rx.recv().map_err(|_| Error::RecvNotify)?; + monitor_rx.recv().map_err(|_| ServeError::RecvNotify)?; if Ok(true) == monitor_thread_handle.get_end_handle().has_ended() { log::info!("Stopping servers due to a panic."); diff --git a/src/config.rs b/src/config.rs index ef5f1fa..630c1b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,36 +2,42 @@ use crate::files::directory::{index::IndexStrategy, listing::default_listing_renderer}; use serde_derive::{Deserialize, Serialize}; -use snafu::Snafu; use std::{ collections::{HashMap, HashSet}, net::SocketAddr, path::PathBuf, sync::Arc, }; +use thiserror::Error; -#[derive(Snafu, Debug)] -pub enum Error { +#[derive(Debug, Error)] +pub enum ConfigError { /// The configuration file could not be found or read. - #[snafu(display("Could not open config from {}: {}", path.display(), source))] + #[error("Could not open config from {}: {}", path.display(), source)] OpenConfig { path: PathBuf, + #[source] source: std::io::Error, }, /// The configuration file could not be parsed or deserialized. - #[snafu(display("Could not deserialize config from {}: {}", path.display(), source))] + #[error("Could not deserialize config from {}: {}", path.display(), source)] DeserializeConfig { path: PathBuf, + #[source] source: toml::de::Error, }, /// The current directory could not be determined. - GetCurrentDir { source: std::io::Error }, + #[error("Unable to get current directory")] + GetCurrentDir { + #[source] + source: std::io::Error, + }, /// None of the provided paths were valid configuration files. - #[snafu(display("None of the provided paths were valid configuration files."))] + #[error("None of the provided paths were valid configuration files.")] NoValidPath, } -pub type Result<T, E = Error> = std::result::Result<T, E>; +pub type Result<T, E = ConfigError> = std::result::Result<T, E>; /// Root configuration struct for Espresso servers. #[derive(Serialize, Deserialize, PartialEq, Debug)] diff --git a/src/files/path.rs b/src/files/path.rs index c5a8d20..0328037 100644 --- a/src/files/path.rs +++ b/src/files/path.rs @@ -1,18 +1,24 @@ -use snafu::{ResultExt, Snafu}; use std::path::{Path, PathBuf}; +use thiserror::Error; -#[derive(Snafu, Debug)] -pub enum Error { +#[derive(Debug, Error)] +pub enum PathError { + #[error(transparent)] StripPrefix { + #[from] source: std::path::StripPrefixError, }, + #[error("Path has no parent")] NoParent, + #[error("Path is relative")] RelativePath, - #[snafu(display("Unable to canonicalize path (Path: {}, Source: {})", path.display(), source))] + #[error("Unable to canonicalize path (Path: {}, Source: {})", path.display(), source)] Canonicalize { path: PathBuf, + #[source] source: std::io::Error, }, + #[error("Path is out of bounds")] PathOutOfBounds, } @@ -24,19 +30,22 @@ pub fn resolve_path_within_tree( base_path: &Path, current_path: &Path, path: &Path, -) -> Result<PathBuf, Error> { +) -> Result<PathBuf, PathError> { let new_path = if path.is_absolute() { - base_path.join(path.strip_prefix("/").context(StripPrefix)?) + base_path.join(path.strip_prefix("/")?) } else if current_path.is_dir() { current_path.join(path) } else { current_path .parent() .map(|x| x.join(path)) - .ok_or(Error::NoParent)? + .ok_or(PathError::NoParent)? }; - new_path.canonicalize().context(Canonicalize { - path: new_path.clone(), - }) + new_path + .canonicalize() + .map_err(|source| PathError::Canonicalize { + path: new_path.clone(), + source, + }) } diff --git a/src/files/path_context.rs b/src/files/path_context.rs index 05c3c64..91b79c3 100644 --- a/src/files/path_context.rs +++ b/src/files/path_context.rs @@ -2,36 +2,42 @@ use super::directory::index::IndexStrategy; use crate::config::{ContentDispositionConfig, PathConfig, PathMatcherConfig, ServerConfig}; use actix_web::http::header::{self, DispositionType, HeaderMap}; use regex::Regex; -use snafu::{ResultExt, Snafu}; use std::{ collections::HashMap, convert::{TryFrom, TryInto}, fmt::Debug, path::{Path, PathBuf}, }; - -#[derive(Snafu, Debug)] -pub enum Error { - #[snafu(display("Failed to parse the Mime type."))] - ParseMime { source: mime::FromStrError }, - #[snafu(display("Failed to parse the header name: {}", name))] +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum PathContextError { + #[error("Failed to parse the Mime type.")] + ParseMime { + #[from] + source: mime::FromStrError, + }, + #[error("Failed to parse the header name: {}", name)] ParseHeaderName { + #[source] source: header::InvalidHeaderName, name: String, }, - #[snafu(display("Failed to parse the header value: {}", value))] + #[error("Failed to parse the header value: {}", value)] ParseHeaderValue { + #[source] source: header::InvalidHeaderValue, value: String, }, - #[snafu(display("Failed to compile the Regex pattern: {}", pattern))] + #[error("Failed to compile the Regex pattern: {}", pattern)] InvalidRegexPattern { pattern: String, + #[source] source: regex::Error, }, } -type Result<T, E = Error> = std::result::Result<T, E>; +type Result<T, E = PathContextError> = std::result::Result<T, E>; #[derive(Clone, Debug)] enum PathMatcher { @@ -56,7 +62,7 @@ impl PathMatcher { } impl TryFrom<&PathMatcherConfig> for PathMatcher { - type Error = Error; + type Error = PathContextError; fn try_from(config: &PathMatcherConfig) -> Result<Self, Self::Error> { Ok(match config { @@ -67,8 +73,9 @@ impl TryFrom<&PathMatcherConfig> for PathMatcher { prefix: prefix.clone(), }, PathMatcherConfig::RegexMatcher { pattern } => PathMatcher::Regex { - regex: Regex::new(pattern).context(InvalidRegexPattern { + regex: Regex::new(pattern).map_err(|source| PathContextError::InvalidRegexPattern { pattern: pattern.clone(), + source, })?, }, }) @@ -166,9 +173,9 @@ impl Debug for PathContext { } impl TryFrom<&PathConfig> for PathContext { - type Error = Error; + type Error = PathContextError; - fn try_from(config: &PathConfig) -> Result<Self, Error> { + fn try_from(config: &PathConfig) -> Result<Self, PathContextError> { let mime_disposition = try_get_mime_disposition(&config.mime_disposition)?; let headers = try_get_headers(&config.headers)?; @@ -187,9 +194,9 @@ impl TryFrom<&PathConfig> for PathContext { } impl TryFrom<&ServerConfig> for PathContext { - type Error = Error; + type Error = PathContextError; - fn try_from(config: &ServerConfig) -> Result<Self, Error> { + fn try_from(config: &ServerConfig) -> Result<Self, PathContextError> { let mime_disposition = try_get_mime_disposition(&config.mime_disposition)?; let headers = try_get_headers(&config.headers)?; @@ -213,7 +220,7 @@ fn try_get_mime_disposition( for (key, value) in mime_disposition.iter() { new_mime_disposition.insert( - key.clone().parse().context(ParseMime)?, + key.clone().parse()?, match value { ContentDispositionConfig::Inline => DispositionType::Inline, ContentDispositionConfig::Attachment => DispositionType::Attachment, @@ -235,10 +242,16 @@ fn try_get_headers(headers: &Option<HashMap<String, String>>) -> Result<Option<H new_headers.insert( name .parse() - .context(ParseHeaderName { name: name.clone() })?, - value.parse().context(ParseHeaderValue { - value: value.clone(), - })?, + .map_err(|source| PathContextError::ParseHeaderName { + name: name.clone(), + source, + })?, + value + .parse() + .map_err(|source| PathContextError::ParseHeaderValue { + value: value.clone(), + source, + })?, ); } diff --git a/src/files/pathbuf.rs b/src/files/pathbuf.rs index 429834e..11a324e 100644 --- a/src/files/pathbuf.rs +++ b/src/files/pathbuf.rs @@ -5,41 +5,41 @@ use actix_web::{ }; use futures_util::future::{ready, Ready}; use percent_encoding::percent_decode_str; -use snafu::Snafu; use std::{ convert::{TryFrom, TryInto}, ops::Deref, path::{Component, Path, PathBuf}, str::FromStr, }; +use thiserror::Error; -#[derive(Snafu, Debug, PartialEq)] -pub enum Error { +#[derive(Debug, Error, PartialEq)] +pub enum PathBufError { /// The segment started with the wrapped invalid character. - #[snafu(display("The segment started with the wrapped invalid character."))] + #[error("The segment started with the wrapped invalid character: {}", char)] BadStart { char: char }, /// The segment contained the wrapped invalid character. - #[snafu(display("The segment contained the wrapped invalid character."))] + #[error("The segment contained the wrapped invalid character: {}", char)] BadChar { char: char }, /// The segment ended with the wrapped invalid character. - #[snafu(display("The segment ended with the wrapped invalid character."))] + #[error("The segment ended with the wrapped invalid character: {}", char)] BadEnd { char: char }, /// The path is not a valid UTF-8 string after percent-decoding - #[snafu(display("The path is not a valid UTF-8 string after percent-decoding."))] + #[error("The path is not a valid UTF-8 string after percent-decoding.")] NotValidUtf8, /// The path has invalid or unexpected components. - #[snafu(display("The path has invalid or unexpected components."))] + #[error("The path has invalid or unexpected components.")] InvalidComponents, } -/// Return `BadRequest` for `Error`. -impl ResponseError for Error { +/// Return `BadRequest` for `PathBufError`. +impl ResponseError for PathBufError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } -type Result<T, E = Error> = std::result::Result<T, E>; +type Result<T, E = PathBufError> = std::result::Result<T, E>; #[derive(Debug)] pub struct UriPathBuf(PathBuf); @@ -52,10 +52,10 @@ impl UriPathBuf { let decoded_path = percent_decode_str(raw_path) .decode_utf8() - .map_err(|_| Error::NotValidUtf8)?; + .map_err(|_| PathBufError::NotValidUtf8)?; if segment_count != decoded_path.matches('/').count() + 1 { - return Err(Error::BadChar { char: '/' }); + return Err(PathBufError::BadChar { char: '/' }); } for segment in decoded_path.split('/') { @@ -63,22 +63,22 @@ impl UriPathBuf { segment_count -= 1; path.pop(); } else if segment.starts_with('.') { - return Err(Error::BadStart { char: '.' }); + return Err(PathBufError::BadStart { char: '.' }); } else if segment.starts_with('*') { - return Err(Error::BadStart { char: '*' }); + return Err(PathBufError::BadStart { char: '*' }); } else if segment.ends_with(':') { - return Err(Error::BadEnd { char: ':' }); + return Err(PathBufError::BadEnd { char: ':' }); } else if segment.ends_with('>') { - return Err(Error::BadEnd { char: '>' }); + return Err(PathBufError::BadEnd { char: '>' }); } else if segment.ends_with('<') { - return Err(Error::BadEnd { char: '<' }); + return Err(PathBufError::BadEnd { char: '<' }); } else if segment.is_empty() { segment_count -= 1; continue; } else if cfg!(windows) && segment.contains('\\') { - return Err(Error::BadChar { char: '\\' }); + return Err(PathBufError::BadChar { char: '\\' }); } else if cfg!(windows) && segment.contains(':') { - return Err(Error::BadChar { char: ':' }); + return Err(PathBufError::BadChar { char: ':' }); } else { path.push(segment) } @@ -86,7 +86,7 @@ impl UriPathBuf { for (i, component) in path.components().enumerate() { if !matches!(component, Component::Normal(_)) || i >= segment_count { - return Err(Error::InvalidComponents); + return Err(PathBufError::InvalidComponents); } } @@ -95,7 +95,7 @@ impl UriPathBuf { } impl FromStr for UriPathBuf { - type Err = Error; + type Err = PathBufError; fn from_str(raw_path: &str) -> Result<Self, Self::Err> { Self::new(raw_path) @@ -103,7 +103,7 @@ impl FromStr for UriPathBuf { } impl FromRequest for UriPathBuf { - type Error = Error; + type Error = PathBufError; type Future = Ready<Result<Self, Self::Error>>; fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future { @@ -112,17 +112,17 @@ impl FromRequest for UriPathBuf { } impl TryFrom<&HttpRequest> for UriPathBuf { - type Error = Error; + type Error = PathBufError; - fn try_from(request: &HttpRequest) -> Result<Self, Error> { + fn try_from(request: &HttpRequest) -> Result<Self, PathBufError> { request.match_info().unprocessed().parse() } } impl TryFrom<&ServiceRequest> for UriPathBuf { - type Error = Error; + type Error = PathBufError; - fn try_from(request: &ServiceRequest) -> Result<Self, Error> { + fn try_from(request: &ServiceRequest) -> Result<Self, PathBufError> { request.match_info().unprocessed().parse() } } @@ -141,17 +141,17 @@ impl AsRef<Path> for UriPathBuf { #[cfg(test)] mod tests { - use super::{Error, UriPathBuf}; + use super::{PathBufError, UriPathBuf}; use std::{iter::FromIterator, path::PathBuf}; #[actix_rt::test] async fn test_path_buf() { - let cases: &[(&str, Result<PathBuf, Error>)] = &[ - ("/test/.tt", Err(Error::BadStart { char: '.' })), - ("/test/*tt", Err(Error::BadStart { char: '*' })), - ("/test/tt:", Err(Error::BadEnd { char: ':' })), - ("/test/tt<", Err(Error::BadEnd { char: '<' })), - ("/test/tt>", Err(Error::BadEnd { char: '>' })), + let cases: &[(&str, Result<PathBuf, PathBufError>)] = &[ + ("/test/.tt", Err(PathBufError::BadStart { char: '.' })), + ("/test/*tt", Err(PathBufError::BadStart { char: '*' })), + ("/test/tt:", Err(PathBufError::BadEnd { char: ':' })), + ("/test/tt<", Err(PathBufError::BadEnd { char: '<' })), + ("/test/tt>", Err(PathBufError::BadEnd { char: '>' })), ("hello%20world", Ok(PathBuf::from_iter(vec!["hello world"]))), ( "/testing%21/hello%20world", @@ -165,7 +165,7 @@ mod tests { ), ( "/../../../..%2F../dev/null", - Err(Error::BadChar { char: '/' }), + Err(PathBufError::BadChar { char: '/' }), ), ]; diff --git a/src/files/service.rs b/src/files/service.rs index 33ae2d0..1599a6e 100644 --- a/src/files/service.rs +++ b/src/files/service.rs @@ -18,8 +18,6 @@ use actix_web::{ }; use async_recursion::async_recursion; use futures_util::future::LocalBoxFuture; -use snafu::ResultExt; -use snafu::Snafu; use std::{ convert::TryInto, io, @@ -29,60 +27,68 @@ use std::{ sync::Arc, task::{Context, Poll}, }; +use thiserror::Error; use tokio::sync::RwLock; /// Errors which can occur when serving static files. -#[derive(Snafu, Debug)] -pub enum Error { +#[derive(Debug, Error)] +pub enum ServiceError { /// Path is not a directory - #[snafu(display("Path is not a directory. Unable to serve static files"))] + #[error("Path is not a directory. Unable to serve static files")] IsNotDirectory, /// Cannot render directory - #[snafu(display("Unable to render directory without index file"))] + #[error("Unable to render directory without index file")] IsDirectory, - #[snafu(display("Serve directory is not ready."))] + #[error("Serve directory is not ready.")] ServeDirNotReady, - #[snafu(display("Unable to obtain read lock for serve dir."))] + #[error("Unable to obtain read lock for serve dir.")] ServerDirReadLockFail, - #[snafu(display("Unable to find or open the specified file."))] + #[error("Unable to find or open the specified file.")] OpenFile { + #[source] source: io::Error, }, + #[error("Unable to canonicalize path")] CanonicalizePath { + #[source] source: io::Error, }, - MethodNotAllowed { - method: Method, - }, + #[error("Method {} is not allowed", method)] + MethodNotAllowed { method: Method }, + #[error(transparent)] BuildUriPath { - source: super::pathbuf::Error, + #[from] + source: super::pathbuf::PathBufError, }, - RenderListing { - source: io::Error, - }, + #[error("Unable to render listing")] + RenderListing { source: io::Error }, + #[error("Unable to reconstruct request")] ReconstructRequest, + #[error("Unable to build file response")] BuildFileResponse { + #[source] source: ActixError, }, + #[error("Custom path not found")] ServeCustomNotFoundPath, } -/// Return `NotFound` for `Error` -impl ResponseError for Error { +/// Return `NotFound` for `ServiceError` +impl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { match self { - Error::MethodNotAllowed { .. } => HttpResponse::MethodNotAllowed() + ServiceError::MethodNotAllowed { .. } => HttpResponse::MethodNotAllowed() .append_header((header::CONTENT_TYPE, "text/plain")) .body("Request did not meet this resource's requirements."), _ => HttpResponse::new(StatusCode::NOT_FOUND), @@ -144,7 +150,7 @@ impl FilesServiceInner { #[async_recursion(?Send)] async fn handle_err( &self, - e: Error, + e: ServiceError, req: ServiceRequest, request_context: Option<RequestContext>, ) -> Result<ServiceResponse, ActixError> { @@ -167,9 +173,9 @@ impl FilesServiceInner { async fn get_request_context_for_request( &self, req: &ServiceRequest, - ) -> Result<RequestContext, Error> { + ) -> Result<RequestContext, ServiceError> { let serve_dir = self.get_serve_dir().await?; - let path_from_request: UriPathBuf = req.try_into().context(BuildUriPath)?; + let path_from_request: UriPathBuf = req.try_into()?; Ok(RequestContext { path: serve_dir.join(&path_from_request), @@ -180,7 +186,7 @@ impl FilesServiceInner { async fn handle_early_err( &self, - e: Error, + e: ServiceError, req: ServiceRequest, ) -> Result<ServiceResponse, ActixError> { let request_context = self.get_request_context_for_request(&req).await; @@ -235,7 +241,7 @@ impl FilesServiceInner { ), Err(_) => { self - .handle_err(Error::ServeCustomNotFoundPath, req, None) + .handle_err(ServiceError::ServeCustomNotFoundPath, req, None) .await } } @@ -277,7 +283,11 @@ impl FilesServiceInner { let req = ServiceRequest::from_parts(http_req, payload); self - .handle_err(Error::RenderListing { source }, req, Some(request_context)) + .handle_err( + ServiceError::RenderListing { source }, + req, + Some(request_context), + ) .await } } @@ -313,7 +323,11 @@ impl FilesServiceInner { let req = ServiceRequest::from_parts(http_req, payload); self - .handle_err(Error::RenderListing { source }, req, Some(request_context)) + .handle_err( + ServiceError::RenderListing { source }, + req, + Some(request_context), + ) .await } } @@ -337,12 +351,12 @@ impl FilesServiceInner { } self - .handle_err(Error::IsDirectory, req, Some(request_context)) + .handle_err(ServiceError::IsDirectory, req, Some(request_context)) .await } IndexStrategy::NoIndex => { self - .handle_err(Error::IsDirectory, req, Some(request_context)) + .handle_err(ServiceError::IsDirectory, req, Some(request_context)) .await } }; @@ -358,7 +372,7 @@ impl FilesServiceInner { path, path_context, .. } = &request_context; - match NamedFile::open(path).context(OpenFile) { + match NamedFile::open(path).map_err(|source| ServiceError::OpenFile { source }) { Ok(named_file) => { let (http_req, _payload) = req.into_parts(); @@ -386,28 +400,28 @@ impl FilesServiceInner { } } - async fn get_serve_dir(&self) -> Result<PathBuf, Error> { + async fn get_serve_dir(&self) -> Result<PathBuf, ServiceError> { let maybe_dir = self.directory.read().await.clone(); match maybe_dir { Some(dir) => Ok(dir), - None => Err(Error::ServeDirNotReady), + None => Err(ServiceError::ServeDirNotReady), } } async fn get_canonical_path_from_request( &self, req: &ServiceRequest, - ) -> Result<(PathBuf, PathBuf, UriPathBuf), Error> { + ) -> Result<(PathBuf, PathBuf, UriPathBuf), ServiceError> { let serve_dir = self.get_serve_dir().await?; - let path_from_request: UriPathBuf = req.try_into().context(BuildUriPath)?; + let path_from_request: UriPathBuf = req.try_into()?; // Obtain the normalized full file path to use. let canonical_path = serve_dir .join(&path_from_request) .canonicalize() - .context(CanonicalizePath)?; + .map_err(|source| ServiceError::CanonicalizePath { source })?; Ok((canonical_path, serve_dir, path_from_request)) } @@ -456,7 +470,7 @@ impl Service<ServiceRequest> for FilesService { if !is_method_valid { return this .handle_early_err( - Error::MethodNotAllowed { + ServiceError::MethodNotAllowed { method: req.method().clone(), }, req, diff --git a/src/main.rs b/src/main.rs index 100c803..10f604c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,19 +8,31 @@ use cli::{ }; use collective::cli::ConfigurableAppOpts; use espresso::bundle::packager; -use snafu::{ResultExt, Snafu}; use std::sync::Arc; +use thiserror::Error; pub mod cli; -#[derive(Snafu, Debug)] -pub enum Error { - ServeError { source: serve::Error }, - BundleError { source: packager::Error }, - CliError { source: collective::cli::CliError }, +#[derive(Error, Debug)] +pub enum MainError { + #[error(transparent)] + ServeError { + #[from] + source: serve::ServeError, + }, + #[error(transparent)] + BundleError { + #[from] + source: packager::PackagerError, + }, + #[error(transparent)] + CliError { + #[from] + source: collective::cli::CliError, + }, } -type Result<T, E = Error> = std::result::Result<T, E>; +type Result<T, E = MainError> = std::result::Result<T, E>; #[actix_rt::main] async fn main() -> Result<()> { @@ -29,7 +41,7 @@ async fn main() -> Result<()> { let result = inner_main().await; - if let Err(Error::CliError { + if let Err(MainError::CliError { source: collective::cli::CliError::ArgParse(inner), }) = &result { @@ -40,12 +52,14 @@ async fn main() -> Result<()> { } async fn inner_main() -> Result<()> { - let (opts, config) = Opts::try_init_with_config().context(CliError)?; + let (opts, config) = Opts::try_init_with_config()?; let config = Arc::new(config); match opts.subcmd { - SubCommand::Serve => serve::serve(config).await.context(ServeError), - SubCommand::Bundle(opts) => bundle::bundle(config, opts).await.context(BundleError), - } + SubCommand::Serve => serve::serve(config).await?, + SubCommand::Bundle(opts) => bundle::bundle(config, opts).await?, + }; + + Ok(()) } diff --git a/src/server.rs b/src/server.rs index a7f142e..177fcae 100644 --- a/src/server.rs +++ b/src/server.rs @@ -10,7 +10,6 @@ use actix_web::{ App, HttpServer, }; use collective::thread::{self, handle::ThreadHandle}; -use snafu::{ResultExt, Snafu}; use std::{ convert::{TryFrom, TryInto}, path::PathBuf, @@ -20,28 +19,39 @@ use std::{ }, thread::Thread, }; +use thiserror::Error; use tokio::sync::RwLock; -#[derive(Debug, Snafu)] -pub enum Error { +#[derive(Debug, Error)] +pub enum ServerError { + #[error(transparent)] ChannelReceive { + #[from] source: RecvError, }, + #[error("Unable to bind")] Bind { + #[source] source: std::io::Error, }, + #[error("Unable to start runtime")] SystemRun { + #[source] source: std::io::Error, }, + #[error("Unable to parse redirect URI")] RedirectUrlParseError { + #[from] source: InvalidUri, }, + #[error("Unable to create path context")] CreatePathContext { - source: crate::files::path_context::Error, + #[from] + source: crate::files::path_context::PathContextError, }, } -pub type Result<T, E = Error> = std::result::Result<T, E>; +pub type Result<T, E = ServerError> = std::result::Result<T, E>; pub struct Server { config: ServerConfig, @@ -70,13 +80,12 @@ impl Server { Some(CompressionConfig::Auto) | None ); - let root_path_context = - Arc::new(PathContext::try_from(&self.config).context(CreatePathContext)?); + let root_path_context = Arc::new(PathContext::try_from(&self.config)?); let mut path_contexts: Vec<PathContext> = Vec::new(); if let Some(path_configs) = &self.config.path_configs { for path_config in path_configs { - path_contexts.push(path_config.try_into().context(CreatePathContext)?); + path_contexts.push(path_config.try_into()?); } } @@ -106,17 +115,18 @@ impl Server { .default_service(files_service) }) .bind(server_address) - .context(Bind)? + .map_err(|source| ServerError::Bind { source })? .shutdown_timeout(60) .run(); let _ = tx.send(srv.handle()); - rt.block_on(async { srv.await }).context(SystemRun)?; + rt.block_on(async { srv.await }) + .map_err(|source| ServerError::SystemRun { source })?; Ok(()) }); - Ok((rx.recv().context(ChannelReceive)?, thread_handle)) + Ok((rx.recv()?, thread_handle)) } } diff --git a/src/stats.rs b/src/stats.rs index 0c513f2..3692414 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -9,22 +9,38 @@ use actix_web::{middleware::Logger, App, HttpResponse, HttpServer, Responder}; use collective::thread::{self, handle::ThreadHandle}; use mpsc::{RecvError, SendError, Sender}; use serde::Serialize; -use snafu::{ResultExt, Snafu}; use std::{ path::PathBuf, sync::{mpsc, Arc}, thread::Thread, }; - -#[derive(Debug, Snafu)] -pub enum Error { - ChannelSend { source: SendError<ServerHandle> }, - ChannelReceive { source: RecvError }, - Bind { source: std::io::Error }, - SystemRun { source: std::io::Error }, +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum StatsError { + #[error(transparent)] + ChannelSend { + #[from] + source: SendError<ServerHandle>, + }, + #[error(transparent)] + ChannelReceive { + #[from] + source: RecvError, + }, + #[error("Unable to bind")] + Bind { + #[source] + source: std::io::Error, + }, + #[error("Unable to start runtime")] + SystemRun { + #[source] + source: std::io::Error, + }, } -pub type Result<T, E = Error> = std::result::Result<T, E>; +pub type Result<T, E = StatsError> = std::result::Result<T, E>; struct State { unbundler: Arc<Unbundler>, @@ -62,18 +78,19 @@ impl StatsServer { })) }) .bind(server_address) - .context(Bind)? + .map_err(|source| StatsError::Bind { source })? .shutdown_timeout(60) .run(); - tx.send(srv.handle()).context(ChannelSend)?; + tx.send(srv.handle())?; - rt.block_on(async { srv.await }).context(SystemRun)?; + rt.block_on(async { srv.await }) + .map_err(|source| StatsError::SystemRun { source })?; Ok(()) }); - Ok((rx.recv().context(ChannelReceive)?, thread_handle)) + Ok((rx.recv()?, thread_handle)) } } -- GitLab