use axum::{
Json,
body::Body,
extract::{Query, State},
http::header,
response::IntoResponse,
};
use tokio_util::io::ReaderStream;
use crate::error::FsvError;
use crate::types::{AppState, FileInfo, FileParams};
use crate::util::{get_file_info, resolve_safe_path};
pub async fn list(
State(state): State<AppState>,
Query(params): Query<FileParams>,
) -> Result<Json<Vec<FileInfo>>, FsvError> {
let canonical_root = state.root_path.canonicalize().map_err(|e| {
FsvError::PathError(format!("Failed to canonicalize root path: {}", e))
})?;
if canonical_root.is_file() {
return Ok(Json(vec![get_file_info(&canonical_root, &canonical_root)?]));
}
let target = resolve_safe_path(&state.root_path, params.path.as_deref())?;
if target.is_file() {
return Ok(Json(vec![get_file_info(&canonical_root, &target)?]));
}
let mut entries = Vec::new();
let mut dir = tokio::fs::read_dir(&target).await?;
while let Some(entry) = dir.next_entry().await? {
if let Ok(info) = get_file_info(&canonical_root, &entry.path()) {
entries.push(info);
}
}
entries.sort_by(|a, b| {
b.is_dir
.cmp(&a.is_dir)
.then_with(|| a.name.cmp(&b.name))
});
Ok(Json(entries))
}
pub async fn file(
State(state): State<AppState>,
Query(params): Query<FileParams>,
) -> Result<impl IntoResponse, FsvError> {
let canonical_root = state.root_path.canonicalize().map_err(|e| {
FsvError::PathError(format!("Failed to canonicalize root path: {}", e))
})?;
let target = if canonical_root.is_file() {
canonical_root
} else {
resolve_safe_path(&state.root_path, params.path.as_deref())?
};
if !target.is_file() {
return Err(FsvError::NotAFile);
}
let file = tokio::fs::File::open(&target).await?;
let file_len = file.metadata().await?.len();
let file_name = target
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "download".into());
let mut headers = axum::http::HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
axum::http::HeaderValue::from_static("application/octet-stream"),
);
if let Ok(v) = axum::http::HeaderValue::from_str(&format!(
"attachment; filename=\"{}\"",
file_name
)) {
headers.insert(header::CONTENT_DISPOSITION, v);
}
if let Ok(v) = axum::http::HeaderValue::from_str(&file_len.to_string()) {
headers.insert(header::CONTENT_LENGTH, v);
}
Ok((headers, Body::from_stream(ReaderStream::new(file))))
}
pub async fn index() -> axum::response::Html<&'static str> {
axum::response::Html(include_str!("../dist/index.html"))
}