feat(serve): implement list directory

This commit is contained in:
xfy
2025-06-19 22:52:38 +08:00
parent 0c925f289b
commit f73fc1db67
3 changed files with 213 additions and 13 deletions

100
Cargo.lock generated
View File

@ -41,6 +41,21 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.18"
@ -378,6 +393,7 @@ dependencies = [
"axum-extra",
"axum-server",
"bytes",
"chrono",
"clap",
"const_format",
"dashmap",
@ -431,6 +447,20 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
@ -1019,6 +1049,30 @@ dependencies = [
"windows-registry",
]
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
@ -2519,6 +2573,41 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
@ -2532,7 +2621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result",
"windows-strings",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
@ -2554,6 +2643,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"

View File

@ -48,6 +48,7 @@ clap = { version = "4.5.39", features = ["derive"] }
serde_repr = "0.1.20"
serde_json = "1.0.140"
mime_guess = "2.0.5"
chrono = "0.4.41"
[dev-dependencies]
tempfile = "3.20.0"

View File

@ -1,8 +1,4 @@
use std::{
path::PathBuf,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use std::{path::PathBuf, str::FromStr, time::UNIX_EPOCH};
use anyhow::{Context, anyhow};
use axum::{
@ -13,7 +9,7 @@ use axum::{
use axum_extra::extract::Host;
use dashmap::mapref::one::Ref;
use http::{
HeaderValue, StatusCode, Uri,
HeaderMap, HeaderValue, StatusCode, Uri,
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
};
use mime_guess::from_path;
@ -162,8 +158,10 @@ pub async fn serve(
if host_route.auto_index {
let root_path = PathBuf::from(root);
let list = list_dir(&root_path).await?;
debug!("list: {:?}", list);
todo!()
let list_html = render_list_html(list);
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("text/html"));
return Ok((headers, list_html).into_response());
} else {
debug!("No valid file found in path candidates");
return custom_not_found!(host_route, request, false).await;
@ -216,7 +214,7 @@ async fn stream_file(
path: PathBuf,
request: Request,
status: Option<StatusCode>,
) -> RouteResult<impl IntoResponse> {
) -> RouteResult<Response<Body>> {
let file = File::open(path.clone())
.await
.with_context(|| "open file failed")?;
@ -233,8 +231,6 @@ async fn stream_file(
.with_context(|| "parse if-none-match failed")?
== etag
{
// let empty_stream = stream::empty::<u8>();
// let body = Some(StreamBody::new(empty_stream));
response = response.status(StatusCode::NOT_MODIFIED);
not_modified = true;
};
@ -332,16 +328,107 @@ pub fn resolve_parent_path(uri: &Uri, path: Option<&Path<String>>) -> String {
}
}
fn render_list_html(list: Vec<DirList>) -> String {
let body_rows = list
.iter()
.map(|dist| {
format!(
r#"<tr><td><a href="{}">{}</a></td><td>{}</td><td>{}</td></tr>"#,
dist.path.display(),
dist.name,
dist.size,
dist.last_modified,
// dist.is_dir
)
})
.collect::<Vec<String>>()
.join("");
let list_html = format!(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Index of /</title>
<style>
body {{
font-family: Arial, sans-serif;
margin: 20px;
background-color: #ffffff;
color: #000000;
}}
h1 {{
font-size: 1.5em;
margin-bottom: 20px;
text-align: left;
}}
table {{
width: 100%;
border-collapse: collapse;
border: 1px solid #dddddd;
}}
th, td {{
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid #dddddd;
}}
th {{
background-color: #f0f0f0;
font-weight: bold;
}}
tr:nth-child(even) {{
background-color: #f9f9f9;
}}
tr:hover {{
background-color: #f0f0f0;
}}
.dir {{
color: #0066cc;
font-weight: bold;
}}
.file {{
color: #000000;
}}
a {{
text-decoration: none;
color: inherit;
}}
a:hover {{
text-decoration: underline;
}}
</style>
</head>
<body>
<h1>Index of /</h1>
<table>
<tr>
<th>Name</th>
<th>Last Modified</th>
<th>Size</th>
</tr>
<tbody id="directory-content">
{body_rows}
</tbody>
</table>
</body>
</html>
"#
);
list_html
}
#[derive(Debug, Clone)]
pub struct DirList {
pub name: String,
pub path: PathBuf,
pub is_dir: bool,
pub size: u64,
pub last_modified: SystemTime,
pub last_modified: String,
}
async fn list_dir(path: &PathBuf) -> anyhow::Result<Vec<DirList>> {
use chrono::{Local, TimeZone};
let mut list = vec![];
let mut entries = fs::read_dir(path)
.await
@ -358,9 +445,23 @@ async fn list_dir(path: &PathBuf) -> anyhow::Result<Vec<DirList>> {
.metadata()
.await
.with_context(|| "get file metadata failed")?;
// convert last modified to string
let last_modified = metadata
.modified()
.with_context(|| "get file modified failed")?;
let last_modified = last_modified
.duration_since(UNIX_EPOCH)
.with_context(|| "calculate unix timestamp failed")?;
let datetime = match Local
.timestamp_opt(last_modified.as_secs() as i64, last_modified.subsec_nanos())
{
chrono::LocalResult::Ambiguous(earlier, later) => {
tracing::warn!("发现歧义时间: {} 和 {}", earlier, later);
earlier // 选择较早的时间
}
_ => Local::now(),
};
let last_modified = datetime.format("%Y-%m-%d %H:%M:%S").to_string();
let size = metadata.len();
let is_dir = metadata.is_dir();
let name = entry.file_name().to_string_lossy().to_string();