momo的首次提交!
This commit is contained in:
11
src/error.rs
Normal file
11
src/error.rs
Normal file
@ -0,0 +1,11 @@
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("无法获取目录")]
|
||||
CannotGetItemList(#[from] std::io::Error),
|
||||
#[error("路径穿越")]
|
||||
PathTraversal,
|
||||
#[error("路径不合法")]
|
||||
PathNotValid,
|
||||
#[error("无法获取工作目录")]
|
||||
CannotGetWorkDir
|
||||
}
|
||||
86
src/filetype.rs
Normal file
86
src/filetype.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
pub const IMAGE_FILE_EXTENSION: [&str; 16] = [
|
||||
"apng", "avif", "gif", "jpg", "jpeg", "jfif", "pjpeg", "pjp", "png", "svg", "webp", "bmp",
|
||||
"ico", "cur", "tif", "tiff",
|
||||
];
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum FileType {
|
||||
Image(ImageType),
|
||||
Other,
|
||||
//TODO 完成其他类型
|
||||
}
|
||||
|
||||
impl ToContentType for FileType {
|
||||
fn get_content_type(&self) -> impl std::fmt::Display {
|
||||
match self {
|
||||
FileType::Image(image) => image.get_content_type().to_string(),
|
||||
FileType::Other => "application/octet-stream".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ImageType {
|
||||
Apng,
|
||||
Avif,
|
||||
Gif,
|
||||
Jpeg,
|
||||
Png,
|
||||
Svg,
|
||||
WebP,
|
||||
Bmp,
|
||||
Ico,
|
||||
Tiff,
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Display for ImageType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
ImageType::Apng => "apng",
|
||||
ImageType::Avif => "avif",
|
||||
ImageType::Gif => "gif",
|
||||
ImageType::Jpeg => "jpeg",
|
||||
ImageType::Png => "png",
|
||||
ImageType::Svg => "svg",
|
||||
ImageType::WebP => "webp",
|
||||
ImageType::Bmp => "bmp",
|
||||
ImageType::Ico => "ico",
|
||||
ImageType::Tiff => "tiff",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageType {
|
||||
//全转小写,方便比对
|
||||
pub fn is_image_file(extension: &str) -> bool {
|
||||
IMAGE_FILE_EXTENSION.contains(&extension.to_lowercase().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToContentType for ImageType {
|
||||
fn get_content_type(&self) -> impl Display {
|
||||
match self {
|
||||
ImageType::Apng => "image/apng",
|
||||
ImageType::Avif => "image/avif",
|
||||
ImageType::Gif => "image/gif",
|
||||
ImageType::Jpeg => "image/jpeg",
|
||||
ImageType::Png => "image/png",
|
||||
ImageType::Svg => "image/svg+xml",
|
||||
ImageType::WebP => "image/webp",
|
||||
ImageType::Bmp => "image/bmp",
|
||||
ImageType::Ico => "image/x-icon",
|
||||
ImageType::Tiff => "image/tiff",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 实现此trait后可以通过结构体获取Content-Type
|
||||
pub trait ToContentType {
|
||||
fn get_content_type(&self) -> impl Display;
|
||||
}
|
||||
296
src/handlers.rs
Normal file
296
src/handlers.rs
Normal file
@ -0,0 +1,296 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
http::{StatusCode, header},
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tokio::fs::{self};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::{
|
||||
CONFIG,
|
||||
error::Error,
|
||||
filetype::{self, FileType, IMAGE_FILE_EXTENSION, ImageType, ToContentType},
|
||||
template::{ImageFallTemplate, Imgs},
|
||||
};
|
||||
|
||||
pub async fn handler(uri: Option<axum::extract::Path<String>>) -> impl IntoResponse {
|
||||
let request_path = match uri {
|
||||
Some(u) => PathBuf::from(u.0),
|
||||
None => PathBuf::from("/"),
|
||||
};
|
||||
|
||||
let base_path = match std::env::current_dir() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("无法获取工作目录。\n原因:{e}"),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let uri = Uri::frompath(&request_path);
|
||||
|
||||
let path = match uri.0.as_str() {
|
||||
//看看是否为请求网站更目录目录(即工作目录根目录)
|
||||
"/" => base_path,
|
||||
_ => format_path(base_path.join(&request_path)),
|
||||
};
|
||||
|
||||
let request_uri = UriBox::new(path, uri);
|
||||
match request_uri.path.try_exists() {
|
||||
// 尝试判断是否存在,避免硬件错误
|
||||
Ok(status) => {
|
||||
//判断是否存在
|
||||
if status {
|
||||
//存在
|
||||
if request_uri.path.is_file() {
|
||||
file_handler(request_uri).await.into_response()
|
||||
} else if request_uri.path.is_dir() {
|
||||
dir_handler(request_uri).await.into_response()
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "不是文件也不是目录").into_response()
|
||||
}
|
||||
} else {
|
||||
//不存在
|
||||
(StatusCode::NOT_FOUND, "目标不存在").into_response()
|
||||
}
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", e)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dir_handler(dir: UriBox) -> impl IntoResponse {
|
||||
//获取请求目录的列表
|
||||
let items = match get_item(dir) {
|
||||
Ok(o) => o,
|
||||
Err(e) => match e {
|
||||
Error::CannotGetItemList(error) => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
|
||||
}
|
||||
Error::PathTraversal => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
|
||||
}
|
||||
Error::PathNotValid => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
|
||||
}
|
||||
Error::CannotGetWorkDir => {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut imgs = Vec::new();
|
||||
for i in items {
|
||||
if let Some(ext) = i.path.extension() {
|
||||
let ext = match ext.to_str() {
|
||||
Some(e) => e,
|
||||
None => panic!(),
|
||||
};
|
||||
if filetype::ImageType::is_image_file(ext) {
|
||||
// info!("{}",i.uri.0);
|
||||
imgs.push(Imgs::new(i.uri.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if imgs.len() == 0 {
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Html(include_bytes!("../templates/NoResources.html")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// 返回模板数据
|
||||
let html = match (ImageFallTemplate {
|
||||
imgs,
|
||||
column_width: CONFIG.width,
|
||||
})
|
||||
.render()
|
||||
{
|
||||
Ok(h) => h,
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, ("无法返回模板文件")).into_response(),
|
||||
};
|
||||
(StatusCode::OK, Html(html)).into_response()
|
||||
}
|
||||
|
||||
/// # 读取文件并且设置CONTENT_TYPE
|
||||
/// ### 需要确定文件存在!
|
||||
pub async fn file_handler(file: UriBox) -> impl IntoResponse {
|
||||
let extension = file.path.extension();
|
||||
let header = match extension {
|
||||
//如果有后缀
|
||||
Some(e) => {
|
||||
let extension = e.to_string_lossy().to_string().to_lowercase(); //全部转小写,避免出错
|
||||
|
||||
let content_type = extension_handler(extension).get_content_type().to_string();
|
||||
[(header::CONTENT_TYPE, content_type)]
|
||||
} //如果没后缀
|
||||
None => [(
|
||||
header::CONTENT_TYPE,
|
||||
FileType::Other.get_content_type().to_string(),
|
||||
)],
|
||||
};
|
||||
// 异步读取文件
|
||||
match fs::read(&file.path).await {
|
||||
Ok(data) => (header, Bytes::from(data)).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("文件读取出错, 原因:{e}"),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn extension_handler(ext: String) -> FileType {
|
||||
if IMAGE_FILE_EXTENSION.contains(&ext.to_lowercase().as_str())
|
||||
//如果为图片格式
|
||||
{
|
||||
FileType::Image(image_extension_handler(ext))
|
||||
} else {
|
||||
FileType::Other
|
||||
}
|
||||
}
|
||||
// 全部自动转小写
|
||||
fn image_extension_handler(extension: impl ToString) -> ImageType {
|
||||
match &extension.to_string().to_lowercase()[..] {
|
||||
"apng" => ImageType::Apng,
|
||||
"jpeg" | "jpg" | "jfif" | "pjpeg" | "pjp" => ImageType::Jpeg,
|
||||
"png" => ImageType::Png,
|
||||
"svg" => ImageType::Svg,
|
||||
"webp" => ImageType::WebP,
|
||||
"bmp" => ImageType::Bmp,
|
||||
"ico" | "cur" => ImageType::Ico,
|
||||
"tif" | "tiff" => ImageType::Tiff,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_path(ori: PathBuf) -> PathBuf {
|
||||
let mut new_path = PathBuf::new();
|
||||
for p in ori.components() {
|
||||
new_path.push(p);
|
||||
}
|
||||
new_path
|
||||
}
|
||||
|
||||
fn get_item(p: UriBox) -> Result<Vec<UriBox>, Error> {
|
||||
let mut items = Vec::new();
|
||||
let base_path = match std::env::current_dir() {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return Err(Error::CannotGetWorkDir);
|
||||
}
|
||||
};
|
||||
for entry in std::fs::read_dir(&p.path)? {
|
||||
let entry = entry?;
|
||||
if entry.file_type()?.is_file() {
|
||||
match reslove_relative_path(&base_path, entry.path()) {
|
||||
Ok(uri) => items.push(UriBox {
|
||||
path: entry.path(),
|
||||
uri,
|
||||
}),
|
||||
Err(e) => return Err(e),
|
||||
}; //计算相对路径
|
||||
}
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Uri(String);
|
||||
|
||||
impl Uri {
|
||||
//直接转化为Uri
|
||||
fn new(s: impl Into<String>) -> Self {
|
||||
Self(s.into())
|
||||
}
|
||||
fn frompath(url: impl AsRef<Path>) -> Self {
|
||||
if url.as_ref() == PathBuf::from("/") {
|
||||
Self("/".into())
|
||||
} else {
|
||||
let mut new_url = String::new();
|
||||
for i in url.as_ref().components() {
|
||||
new_url.push('/');
|
||||
new_url.push_str(&format!("{}", i.as_os_str().to_string_lossy()));
|
||||
}
|
||||
Self(new_url)
|
||||
}
|
||||
}
|
||||
|
||||
//将迭代器转为Uri
|
||||
fn from_iter<T: Into<String>>(slice: impl Iterator<Item = T>) -> Self {
|
||||
let mut uri = String::new();
|
||||
for i in slice {
|
||||
uri.push('/');
|
||||
uri.push_str(&i.into());
|
||||
}
|
||||
Self::new(uri)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Uri {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
//处理url和本地目录的关系
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UriBox {
|
||||
path: PathBuf,
|
||||
uri: Uri,
|
||||
}
|
||||
|
||||
impl UriBox {
|
||||
fn new(path: PathBuf, uri: Uri) -> Self {
|
||||
Self { path, uri }
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算相对路径
|
||||
fn reslove_relative_path(
|
||||
base_path: impl AsRef<Path>,
|
||||
relative_path: impl AsRef<Path>,
|
||||
) -> Result<Uri, Error> {
|
||||
let mut base_path_components: Vec<String> = match base_path.as_ref().canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return Err(Error::PathNotValid),
|
||||
}
|
||||
.components()
|
||||
.map(|x| x.as_os_str().to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
let relative_path_canonicalize = match relative_path.as_ref().canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return Err(Error::PathNotValid),
|
||||
};
|
||||
let mut relative_path_components: Vec<String> = relative_path_canonicalize
|
||||
.components()
|
||||
.map(|x| x.as_os_str().to_string_lossy().to_string())
|
||||
.collect();
|
||||
base_path_components.retain(|x| x != "\\");
|
||||
relative_path_components.retain(|x| x != "\\");
|
||||
|
||||
if relative_path_components.len() < base_path_components.len() {
|
||||
//如果请求路径短于基础路径那肯定是非法的
|
||||
warn!("路径穿越攻击!");
|
||||
return Err(Error::PathTraversal);
|
||||
}
|
||||
|
||||
for i in 0..(base_path_components.len()) {
|
||||
if base_path_components[i] != relative_path_components[i] {
|
||||
return Err(Error::PathTraversal);
|
||||
}
|
||||
}
|
||||
let result: Vec<String> = relative_path_components
|
||||
.drain((base_path_components.len())..)
|
||||
.collect();
|
||||
Ok(Uri::from_iter(result.iter()))
|
||||
}
|
||||
50
src/main.rs
Normal file
50
src/main.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use clap::Parser;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::info;
|
||||
|
||||
mod error;
|
||||
mod filetype;
|
||||
mod handlers;
|
||||
mod myclap;
|
||||
mod template;
|
||||
|
||||
use handlers::handler;
|
||||
use myclap::Cli;
|
||||
|
||||
use crate::myclap::Config;
|
||||
|
||||
pub static CONFIG: LazyLock<Config> =
|
||||
LazyLock::new(|| Config::from_parser(Cli::parse()).expect("无法读取配置"));
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
//日志
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(CONFIG.loglevel)
|
||||
.init();
|
||||
|
||||
info!("Log Level: {}",CONFIG.loglevel.to_string());
|
||||
info!("Image Width: {}px",CONFIG.width);
|
||||
|
||||
info!("Working Directory: [{}]", CONFIG.directory.display());
|
||||
// 设置工作目录
|
||||
std::env::set_current_dir(CONFIG.directory.clone()).expect("无法设置工作目录");
|
||||
|
||||
// 路由
|
||||
let app = Router::new()
|
||||
.route("/", get(handler))
|
||||
.route("/{*filename}", get(handler))
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", CONFIG.port))
|
||||
.await
|
||||
.unwrap();
|
||||
info!("Listen on 127.0.0.1:{}", CONFIG.port);
|
||||
info!("Listen on 0.0.0.0:{}", CONFIG.port);
|
||||
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
54
src/myclap.rs
Normal file
54
src/myclap.rs
Normal file
@ -0,0 +1,54 @@
|
||||
use std::path::PathBuf;
|
||||
use clap::Parser;
|
||||
use tracing::Level;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
/// 设置工作目录
|
||||
#[arg(short, long, value_name = "directory")]
|
||||
pub directory: Option<PathBuf>,
|
||||
|
||||
/// 监听的端口号
|
||||
#[arg(short, long, value_name = "port",default_value_t = 3000)]
|
||||
pub port: u16,
|
||||
|
||||
/// 启用调试信息
|
||||
#[arg(long,short, action = clap::ArgAction::Count)]
|
||||
pub loglevel: u8,
|
||||
|
||||
/// 设置图片的宽度,该设置决定了瀑布流的宽度
|
||||
#[arg(long,short,default_value_t = 350)]
|
||||
pub width: usize,
|
||||
|
||||
}
|
||||
|
||||
pub struct Config{
|
||||
pub directory:PathBuf,
|
||||
pub port: u16,
|
||||
pub loglevel: Level,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_parser(cli:Cli)->Result<Self,Error>{
|
||||
let port = cli.port;
|
||||
let workdir = match cli.directory {
|
||||
Some(d) => d,
|
||||
None => match std::env::current_dir() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return Err(Error::CannotGetWorkDir),
|
||||
},
|
||||
};
|
||||
let loglevel = match cli.loglevel {
|
||||
1 => tracing::Level::ERROR,
|
||||
2 => tracing::Level::WARN,
|
||||
0|3 => tracing::Level::INFO,
|
||||
4 => tracing::Level::DEBUG,
|
||||
5.. => tracing::Level::TRACE,
|
||||
};
|
||||
Ok(Self { directory: workdir, port, loglevel: loglevel, width: cli.width })
|
||||
}
|
||||
}
|
||||
18
src/template.rs
Normal file
18
src/template.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use askama::Template;
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.2.html")]
|
||||
pub struct ImageFallTemplate {
|
||||
pub imgs: Vec<Imgs>,
|
||||
pub column_width:usize,
|
||||
}
|
||||
pub struct Imgs {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl Imgs {
|
||||
pub fn new(path: String) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user