#![deny(missing_docs, clippy::print_stderr, clippy::print_stdout)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::result_large_err)]
use bytes::Buf;
use futures_core::Stream;
use http::header::{HeaderMap, HeaderValue};
use std::error::Error;
use std::fmt::Display;
use std::fs::File;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::io::Error as IOError;
use std::io::ErrorKind;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::time::SystemTime;
macro_rules! unsafe_fmt_ascii_val {
($max_len:expr, $fmt:expr, $($arg:tt)+) => {{
let mut buf = bytes::BytesMut::with_capacity($max_len);
use std::fmt::Write;
write!(buf, $fmt, $($arg)*).expect("fmt_val fits within provided max len");
unsafe {
http::header::HeaderValue::from_maybe_shared_unchecked(buf.freeze())
}
}}
}
fn as_u64(len: usize) -> u64 {
const {
assert!(std::mem::size_of::<usize>() <= std::mem::size_of::<u64>());
};
len as u64
}
#[derive(Debug)]
pub enum SerdirError {
ConfigError(String),
IsDirectory(PathBuf),
NotFound(Option<FileEntity>),
CompressionError(String, IOError),
InvalidPath(String),
IOError(IOError),
}
impl Display for SerdirError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SerdirError::ConfigError(msg) => write!(f, "{msg}"),
SerdirError::IsDirectory(path) => {
write!(f, "Path is a directory: {}", path.display())
}
SerdirError::NotFound(_) => write!(f, "File not found"),
SerdirError::InvalidPath(msg) => write!(f, "Invalid path: {msg}"),
SerdirError::IOError(err) => write!(f, "I/O error: {err}"),
SerdirError::CompressionError(msg, err) => {
write!(f, "Brotli compression error: {msg} (I/O error: {err})")
}
}
}
}
impl Error for SerdirError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
SerdirError::IOError(err) => Some(err),
SerdirError::CompressionError(_, err) => Some(err),
_ => None,
}
}
}
impl From<IOError> for SerdirError {
fn from(err: IOError) -> Self {
if err.kind() == ErrorKind::NotFound {
SerdirError::NotFound(None)
} else {
SerdirError::IOError(err)
}
}
}
mod body;
mod served_dir;
#[cfg(any(feature = "tower", feature = "hyper"))]
pub mod integration;
pub mod compression;
mod etag;
mod file;
mod platform;
mod range;
mod serving;
pub use crate::body::Body;
pub use crate::etag::{ETag, FileHasher};
pub use crate::file::FileEntity;
pub use crate::served_dir::{ServedDir, ServedDirBuilder};
#[cfg(feature = "runtime-compression")]
mod brotli_cache;
#[derive(Hash, Eq, PartialEq, Clone, Copy)]
pub(crate) struct FileInfo {
path_hash: u64,
len: u64,
mtime: SystemTime,
}
impl FileInfo {
pub(crate) fn open_file(path: &Path, file: &File) -> Result<Self, SerdirError> {
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
let path_hash: u64 = hasher.finish();
let metadata = file.metadata()?;
let file_type = metadata.file_type();
if file_type.is_dir() {
return Err(SerdirError::IsDirectory(path.to_path_buf()));
}
if !file_type.is_file() {
return Err(SerdirError::NotFound(None));
}
Ok(Self {
path_hash,
len: metadata.len(),
mtime: metadata.modified()?,
})
}
pub(crate) fn len(&self) -> u64 {
self.len
}
pub(crate) fn mtime(&self) -> SystemTime {
self.mtime
}
#[cfg(feature = "runtime-compression")]
pub(crate) fn for_path(path: &Path) -> Result<Self, SerdirError> {
let file = File::open(path)?;
Self::open_file(path, &file)
}
#[cfg(feature = "runtime-compression")]
pub(crate) fn get_hash(&self) -> u64 {
use std::hash::Hash;
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
}
pub(crate) trait Entity: 'static + Send + Sync {
type Error: 'static + Send + Sync;
type Data: 'static + Buf + From<Vec<u8>> + From<&'static [u8]>;
fn len(&self) -> u64;
#[allow(dead_code)]
fn is_empty(&self) -> bool {
self.len() == 0
}
#[allow(clippy::type_complexity)]
fn get_range(
&self,
range: Range<u64>,
) -> Pin<Box<dyn Stream<Item = Result<Self::Data, Self::Error>> + Send + Sync>>;
fn add_headers(&self, _: &mut HeaderMap);
fn etag(&self) -> Option<HeaderValue>;
fn last_modified(&self) -> Option<SystemTime>;
}