use std::borrow::Cow;
use std::cmp;
use std::fs::Metadata;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use enumflags2::{bitflags, BitFlags};
use headers::*;
use tokio::fs::File;
use super::{ChunkedFile, ChunkedState};
use crate::http::header::{CONTENT_DISPOSITION, CONTENT_ENCODING, IF_NONE_MATCH, RANGE};
use crate::http::{HttpRange, Mime, Request, Response, StatusCode, StatusError};
use crate::{async_trait, Depot, Error, Result, Writer};
const CHUNK_SIZE: u64 = 1024 * 1024;
#[bitflags(default = Etag | LastModified | ContentDisposition)]
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum Flag {
Etag = 0b0001,
LastModified = 0b0010,
ContentDisposition = 0b0100,
}
#[derive(Debug)]
pub struct NamedFile {
path: PathBuf,
file: File,
modified: Option<SystemTime>,
buffer_size: u64,
metadata: Metadata,
flags: BitFlags<Flag>,
content_type: mime::Mime,
content_disposition: Option<HeaderValue>,
content_encoding: Option<HeaderValue>,
}
#[derive(Clone)]
pub struct NamedFileBuilder {
path: PathBuf,
attached_name: Option<String>,
disposition_type: Option<String>,
content_type: Option<mime::Mime>,
content_encoding: Option<String>,
buffer_size: Option<u64>,
flags: BitFlags<Flag>,
}
impl NamedFileBuilder {
#[inline]
pub fn attached_name<T: Into<String>>(mut self, attached_name: T) -> Self {
self.attached_name = Some(attached_name.into());
self.flags.insert(Flag::ContentDisposition);
self
}
#[inline]
pub fn disposition_type<T: Into<String>>(mut self, disposition_type: T) -> Self {
self.disposition_type = Some(disposition_type.into());
self.flags.insert(Flag::ContentDisposition);
self
}
#[inline]
pub fn disable_content_disposition(&mut self) {
self.flags.remove(Flag::ContentDisposition);
}
#[inline]
pub fn content_type(mut self, content_type: mime::Mime) -> Self {
self.content_type = Some(content_type);
self
}
#[inline]
pub fn content_encoding<T: Into<String>>(mut self, content_encoding: T) -> Self {
self.content_encoding = Some(content_encoding.into());
self
}
#[inline]
pub fn buffer_size(mut self, buffer_size: u64) -> Self {
self.buffer_size = Some(buffer_size);
self
}
#[inline]
pub fn use_etag(mut self, value: bool) -> Self {
if value {
self.flags.insert(Flag::Etag);
} else {
self.flags.remove(Flag::Etag);
}
self
}
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self {
if value {
self.flags.insert(Flag::LastModified);
} else {
self.flags.remove(Flag::LastModified);
}
self
}
pub async fn send(self, req_headers: &HeaderMap, res: &mut Response) {
if !self.path.exists() {
res.render(StatusError::not_found());
} else {
match self.build().await {
Ok(file) => file.send(req_headers, res).await,
Err(_) => res.render(StatusError::internal_server_error()),
}
}
}
pub async fn build(self) -> Result<NamedFile> {
let NamedFileBuilder {
path,
content_type,
content_encoding,
buffer_size,
disposition_type,
attached_name,
flags,
} = self;
let file = File::open(&path).await?;
let content_type = content_type.unwrap_or_else(|| {
let ct = mime_infer::from_path(&path).first_or_octet_stream();
let ftype = ct.type_();
let stype = ct.subtype();
if (ftype == mime::TEXT || stype == mime::JSON || stype == mime::JAVASCRIPT)
&& ct.get_param(mime::CHARSET).is_none()
{
format!("{ct}; charset=utf-8").parse::<mime::Mime>().unwrap_or(ct)
} else {
ct
}
});
let metadata = file.metadata().await?;
let modified = metadata.modified().ok();
let content_encoding = match content_encoding {
Some(content_encoding) => Some(content_encoding.parse::<HeaderValue>().map_err(Error::other)?),
None => None,
};
let mut content_disposition = None;
if attached_name.is_some() || disposition_type.is_some() {
content_disposition = Some(build_content_disposition(
&path,
&content_type,
disposition_type.as_deref(),
attached_name.as_deref(),
)?);
}
Ok(NamedFile {
path,
file,
content_type,
content_disposition,
metadata,
modified,
content_encoding,
buffer_size: buffer_size.unwrap_or(CHUNK_SIZE),
flags,
})
}
}
fn build_content_disposition(
file_path: impl AsRef<Path>,
content_type: &Mime,
disposition_type: Option<&str>,
attached_name: Option<&str>,
) -> Result<HeaderValue> {
let disposition_type = disposition_type.unwrap_or_else(|| {
if attached_name.is_some() {
"attachment"
} else {
match (content_type.type_(), content_type.subtype()) {
(mime::IMAGE | mime::TEXT | mime::VIDEO | mime::AUDIO, _) | (_, mime::JAVASCRIPT | mime::JSON) => {
"inline"
}
_ => "attachment",
}
}
});
let content_disposition = if disposition_type == "attachment" {
let attached_name = match attached_name {
Some(attached_name) => Cow::Borrowed(attached_name),
None => file_path
.as_ref()
.file_name()
.map(|file_name| file_name.to_string_lossy().to_string())
.unwrap_or_else(|| "file".into())
.into(),
};
format!("attachment; filename={attached_name}")
.parse::<HeaderValue>()
.map_err(Error::other)?
} else {
disposition_type.parse::<HeaderValue>().map_err(Error::other)?
};
Ok(content_disposition)
}
impl NamedFile {
#[inline]
pub fn builder(path: impl Into<PathBuf>) -> NamedFileBuilder {
NamedFileBuilder {
path: path.into(),
attached_name: None,
disposition_type: None,
content_type: None,
content_encoding: None,
buffer_size: None,
flags: BitFlags::default(),
}
}
#[inline]
pub async fn open<P>(path: P) -> Result<NamedFile>
where
P: Into<PathBuf> + Send,
{
Self::builder(path).build().await
}
#[inline]
pub fn file(&self) -> &File {
&self.file
}
#[inline]
pub fn path(&self) -> &Path {
self.path.as_path()
}
#[inline]
pub fn content_type(&self) -> &mime::Mime {
&self.content_type
}
#[inline]
pub fn set_content_type(&mut self, content_type: mime::Mime) {
self.content_type = content_type;
}
#[inline]
pub fn content_disposition(&self) -> Option<&HeaderValue> {
self.content_disposition.as_ref()
}
#[inline]
pub fn set_content_disposition(&mut self, content_disposition: HeaderValue) {
self.content_disposition = Some(content_disposition);
self.flags.insert(Flag::ContentDisposition);
}
#[inline]
pub fn disable_content_disposition(&mut self) {
self.flags.remove(Flag::ContentDisposition);
}
#[inline]
pub fn content_encoding(&self) -> Option<&HeaderValue> {
self.content_encoding.as_ref()
}
#[inline]
pub fn set_content_encoding(&mut self, content_encoding: HeaderValue) {
self.content_encoding = Some(content_encoding);
}
pub fn etag(&self) -> Option<ETag> {
self.modified.as_ref().and_then(|mtime| {
let ino = {
#[cfg(unix)]
{
self.metadata.ino()
}
#[cfg(not(unix))]
{
0
}
};
let dur = mtime
.duration_since(UNIX_EPOCH)
.expect("modification time must be after epoch");
let etag_str = format!(
"\"{:x}-{:x}-{:x}-{:x}\"",
ino,
self.metadata.len(),
dur.as_secs(),
dur.subsec_nanos()
);
match etag_str.parse::<ETag>() {
Ok(etag) => Some(etag),
Err(e) => {
tracing::error!(error = ?e, etag = %etag_str, "set file's etag failed");
None
}
}
})
}
#[inline]
pub fn use_etag(&mut self, value: bool) {
if value {
self.flags.insert(Flag::Etag);
} else {
self.flags.remove(Flag::Etag);
}
}
#[inline]
pub fn last_modified(&self) -> Option<SystemTime> {
self.modified
}
#[inline]
pub fn use_last_modified(&mut self, value: bool) {
if value {
self.flags.insert(Flag::LastModified);
} else {
self.flags.remove(Flag::LastModified);
}
}
pub async fn send(mut self, req_headers: &HeaderMap, res: &mut Response) {
let etag = if self.flags.contains(Flag::Etag) {
self.etag()
} else {
None
};
let last_modified = if self.flags.contains(Flag::LastModified) {
self.last_modified()
} else {
None
};
let precondition_failed = if !any_match(etag.as_ref(), req_headers) {
true
} else if let (Some(last_modified), Some(since)) =
(&last_modified, req_headers.typed_get::<IfUnmodifiedSince>())
{
!since.precondition_passes(*last_modified)
} else {
false
};
let not_modified = if !none_match(etag.as_ref(), req_headers) {
true
} else if req_headers.contains_key(IF_NONE_MATCH) {
false
} else if let (Some(last_modified), Some(since)) = (&last_modified, req_headers.typed_get::<IfModifiedSince>())
{
!since.is_modified(*last_modified)
} else {
false
};
if self.flags.contains(Flag::ContentDisposition) {
if let Some(content_disposition) = self.content_disposition.take() {
res.headers_mut().insert(CONTENT_DISPOSITION, content_disposition);
} else if !res.headers().contains_key(CONTENT_DISPOSITION) {
match build_content_disposition(&self.path, &self.content_type, None, None) {
Ok(content_disposition) => {
res.headers_mut().insert(CONTENT_DISPOSITION, content_disposition);
}
Err(e) => {
tracing::error!(error = ?e, "build file's content disposition failed");
}
}
}
}
res.headers_mut()
.typed_insert(ContentType::from(self.content_type.clone()));
if let Some(lm) = last_modified {
res.headers_mut().typed_insert(LastModified::from(lm));
}
if let Some(etag) = self.etag() {
res.headers_mut().typed_insert(etag);
}
res.headers_mut().typed_insert(AcceptRanges::bytes());
let mut length = self.metadata.len();
if let Some(content_encoding) = &self.content_encoding {
res.headers_mut().insert(CONTENT_ENCODING, content_encoding.clone());
}
let mut offset = 0;
let range = req_headers.get(RANGE);
if let Some(range) = range {
if let Ok(range) = range.to_str() {
if let Ok(range) = HttpRange::parse(range, length) {
length = range[0].length;
offset = range[0].start;
} else {
res.headers_mut().typed_insert(ContentRange::unsatisfied_bytes(length));
res.status_code(StatusCode::RANGE_NOT_SATISFIABLE);
return;
};
} else {
res.status_code(StatusCode::BAD_REQUEST);
return;
};
}
if precondition_failed {
res.status_code(StatusCode::PRECONDITION_FAILED);
return;
} else if not_modified {
res.status_code(StatusCode::NOT_MODIFIED);
return;
}
if offset != 0 || length != self.metadata.len() || range.is_some() {
res.status_code(StatusCode::PARTIAL_CONTENT);
match ContentRange::bytes(offset..offset + length - 1, self.metadata.len()) {
Ok(content_range) => {
res.headers_mut().typed_insert(content_range);
}
Err(e) => {
tracing::error!(error = ?e, "set file's content ranage failed");
}
}
let reader = ChunkedFile {
offset,
total_size: cmp::min(length, self.metadata.len()),
read_size: 0,
state: ChunkedState::File(Some(self.file.into_std().await)),
buffer_size: self.buffer_size,
};
res.headers_mut().typed_insert(ContentLength(reader.total_size));
res.stream(reader);
} else {
res.status_code(StatusCode::OK);
let reader = ChunkedFile {
offset,
state: ChunkedState::File(Some(self.file.into_std().await)),
total_size: length,
read_size: 0,
buffer_size: self.buffer_size,
};
res.headers_mut().typed_insert(ContentLength(length));
res.stream(reader);
}
}
}
#[async_trait]
impl Writer for NamedFile {
async fn write(self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
self.send(req.headers(), res).await;
}
}
impl Deref for NamedFile {
type Target = File;
fn deref(&self) -> &File {
&self.file
}
}
impl DerefMut for NamedFile {
fn deref_mut(&mut self) -> &mut File {
&mut self.file
}
}
fn any_match(etag: Option<&ETag>, req_headers: &HeaderMap) -> bool {
match req_headers.typed_get::<IfMatch>() {
None => true,
Some(if_match) => {
if if_match == IfMatch::any() {
true
} else if let Some(etag) = etag {
if_match.precondition_passes(etag)
} else {
false
}
}
}
}
fn none_match(etag: Option<&ETag>, req_headers: &HeaderMap) -> bool {
match req_headers.typed_get::<IfNoneMatch>() {
None => true,
Some(if_none_match) => {
if if_none_match == IfNoneMatch::any() {
false
} else if let Some(etag) = etag {
if_none_match.precondition_passes(etag)
} else {
true
}
}
}
}