Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ bytes = "1"
bcrypt = "0.17"
cookie = "0.18"
chacha20poly1305 = "0.10"
chardetng = "0.1"
chrono = "0.4"
compact_str = { version = "0.9", features = ["serde"] }
content_inspector = "0.2"
encoding_rs = "0.8"
email_address = "0.2"
enumflags2 = "0.7"
Expand Down
2 changes: 2 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ anyhow = { workspace = true, optional = true }
async-trait = { workspace = true }
base64 = { workspace = true }
bytes = { workspace = true }
chardetng = { workspace = true }
content_inspector = { workspace = true }
cookie = { workspace = true, features = ["percent-encode", "private", "signed"], optional = true }
encoding_rs = { workspace = true, optional = true }
enumflags2 = { workspace = true }
Expand Down
40 changes: 25 additions & 15 deletions crates/core/src/fs/named_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ use std::os::unix::fs::MetadataExt;
use enumflags2::{BitFlags, bitflags};
use headers::*;
use tokio::fs::File;
use tokio::io::AsyncReadExt;

use super::{ChunkedFile, ChunkedState};
use crate::http::header::{
CONTENT_DISPOSITION, CONTENT_ENCODING, CONTENT_TYPE, IF_NONE_MATCH, RANGE,
};
use crate::http::{HttpRange, Mime, Request, Response, StatusCode, StatusError};
use crate::http::{HttpRange, Mime, Request, Response, StatusCode, StatusError, detect_text_mime};
use crate::{Depot, Error, Result, Writer, async_trait};

const CHUNK_SIZE: u64 = 1024 * 1024;
Expand Down Expand Up @@ -171,21 +172,30 @@ impl NamedFileBuilder {
} = 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()
{
//TODO: auto detect charset
format!("{ct}; charset=utf-8")
.parse::<mime::Mime>()
.unwrap_or(ct)
let content_type =
if let Some(mime) = content_type.or_else(|| mime_infer::from_path(&path).first()) {
if mime == mime::TEXT_PLAIN {
let mut buffer: Vec<u8> = vec![];
let _ = file.take(1024).read(&mut buffer).await;
if let Some(mime) = detect_text_mime(&buffer) {
mime
} else {
mime
}
} else {
mime
}
} else {
ct
}
});
let mut buffer: Vec<u8> = vec![];
let _ = file.take(1024).read(&mut buffer).await;
if let Some(mime) = detect_text_mime(&buffer) {
mime
} else {
mime::APPLICATION_OCTET_STREAM
}
};

let file = File::open(&path).await?;
let metadata = file.metadata().await?;
let modified = metadata.modified().ok();
let content_encoding = match content_encoding {
Expand Down
22 changes: 22 additions & 0 deletions crates/core/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ pub fn guess_accept_mime(req: &Request, default_type: Option<Mime>) -> Mime {
.unwrap_or(dmime)
}

#[doc(hidden)]
#[inline]
#[must_use]
pub fn detect_text_mime(buffer: &[u8]) -> Option<mime::Mime> {
let info = content_inspector::inspect(buffer);
if info.is_text() {
let mut detector = chardetng::EncodingDetector::new();
detector.feed(buffer, buffer.len() < 1024);

let (encoding, _) = detector.guess_assess(None, true);
if encoding.name().eq_ignore_ascii_case("utf-8") {
Some(mime::TEXT_PLAIN_UTF_8)
} else {
format!("text/plain; charset={}", encoding.name())
.parse::<mime::Mime>()
.ok()
}
} else {
None
}
}

#[cfg(test)]
mod tests {
use super::header::*;
Expand Down
5 changes: 1 addition & 4 deletions crates/serve-static/src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,7 @@ impl Handler for StaticDir {
};

let builder = {
let mut builder = NamedFile::builder(named_path).content_type(
mime_infer::from_ext(ext.as_deref().unwrap_or_default())
.first_or_octet_stream(),
);
let mut builder = NamedFile::builder(named_path);
if let Some(content_encoding) = content_encoding {
builder = builder.content_encoding(content_encoding);
}
Expand Down
27 changes: 22 additions & 5 deletions crates/serve-static/src/embed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use salvo_core::http::header::{
ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_TYPE, ETAG, IF_NONE_MATCH, RANGE,
};
use salvo_core::http::headers::{ContentLength, ContentRange, HeaderMapExt};
use salvo_core::http::{HeaderValue, HttpRange, Mime, Request, Response, StatusCode};
use salvo_core::http::{
HeaderValue, HttpRange, Mime, Request, Response, StatusCode, detect_text_mime,
};
use salvo_core::{Depot, FlowCtrl, IntoVecString, async_trait};

use super::{decode_url_path_safely, format_url_path_safely, join_path, redirect_to_dir_url};
Expand Down Expand Up @@ -63,14 +65,29 @@ fn render_embedded_data(
metadata: &Metadata,
req: &Request,
res: &mut Response,
mime_override: Option<Mime>,
mime: Option<Mime>,
) {
// Determine Content-Type once
let effective_mime = mime_override
.unwrap_or_else(|| mime_infer::from_path(req.uri().path()).first_or_octet_stream());
let content_type =
if let Some(mime) = mime.or_else(|| mime_infer::from_path(req.uri().path()).first()) {
if mime == mime::TEXT_PLAIN {
if let Some(mime) = detect_text_mime(&data) {
mime
} else {
mime
}
} else {
mime
}
} else if let Some(mime) = detect_text_mime(&data) {
mime
} else {
mime::APPLICATION_OCTET_STREAM
};

res.headers_mut().insert(
CONTENT_TYPE,
effective_mime
content_type
.as_ref()
.parse()
.unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
Expand Down
8 changes: 7 additions & 1 deletion examples/static-dir-list/static/boy/work_ansi.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@ Let me take it down

An elephant said to a mouse ,"no doubt that you are the smallest znd most useless thing that Ihave e ver seen ."

"Pless ,say it again .Let me take it down ."the mouse said ."I will tell a flea what I know.
"Pless ,say it again .Let me take it down ."the mouse said ."I will tell a flea what I know.

让我把它记下来

一头大象对一只老鼠说:“毫无疑问,你是我见过的最小、最没用的东西。”

“请再说一遍。让我把它记下来。”老鼠说,“我会把我知道的告诉跳蚤。”
2 changes: 2 additions & 0 deletions examples/static-dir-list/static/boy/新文件.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
浣犲ソ鏈嬪弸
abc
3 changes: 3 additions & 0 deletions examples/static-embed-files/static/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<html>
<body>
Index page

<p><a href="test1.txt">test1.txt</a></p>
<p><a href="test2.txt">test2.txt</a></p>
</body>
</html>
4 changes: 3 additions & 1 deletion examples/static-embed-files/static/test1.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
copy1
copy1

浣犲ソ鏈嬪弸
Loading