Files
zosbuilder/fl-server/src/serve_flists.rs
Jan De Landtsheer 9790ef4dac Squashed 'components/rfs/' content from commit 9808a5e
git-subtree-dir: components/rfs
git-subtree-split: 9808a5e9fc768edc7d8b1dfa5b91b3f018dff0cb
2025-08-16 21:12:45 +02:00

175 lines
5.6 KiB
Rust

use axum::extract::State;
use std::{io::Error, path::PathBuf, sync::Arc};
use tokio::io;
use tower::util::ServiceExt;
use tower_http::services::ServeDir;
use axum::{
body::Body,
http::{Request, StatusCode},
response::IntoResponse,
};
use axum_macros::debug_handler;
use percent_encoding::percent_decode;
use crate::{
config,
handlers::Filter,
response::{
DirListTemplate, DirLister, ErrorTemplate, FileInfo, ResponseError, ResponseResult,
TemplateErr,
},
};
#[debug_handler]
pub async fn serve_flists(
State(state): State<Arc<config::AppState>>,
req: Request<Body>,
) -> impl IntoResponse {
let path = req.uri().path().to_string();
return match ServeDir::new("").oneshot(req).await {
Ok(res) => {
let status = res.status();
match status {
StatusCode::NOT_FOUND => {
let full_path = match validate_path(&path) {
Ok(p) => p,
Err(_) => {
return Err(ResponseError::TemplateError(ErrorTemplate {
err: TemplateErr::BadRequest("invalid path".to_string()),
cur_path: path.to_string(),
message: "invalid path".to_owned(),
}));
}
};
let cur_path = std::path::Path::new(&full_path);
match cur_path.is_dir() {
true => {
let rs = visit_dir_one_level(&full_path, &state, None).await;
match rs {
Ok(files) => Ok(ResponseResult::DirTemplate(DirListTemplate {
lister: DirLister { files },
cur_path: path.to_string(),
})),
Err(e) => Err(ResponseError::TemplateError(ErrorTemplate {
err: TemplateErr::InternalServerError(e.to_string()),
cur_path: path.to_string(),
message: e.to_string(),
})),
}
}
false => Err(ResponseError::TemplateError(ErrorTemplate {
err: TemplateErr::NotFound("file not found".to_string()),
cur_path: path.to_string(),
message: "file not found".to_owned(),
})),
}
}
_ => Ok(ResponseResult::Res(res)),
}
}
Err(err) => Err(ResponseError::TemplateError(ErrorTemplate {
err: TemplateErr::InternalServerError(format!("Unhandled error: {}", err)),
cur_path: path.to_string(),
message: format!("Unhandled error: {}", err),
})),
};
}
fn validate_path(path: &str) -> io::Result<PathBuf> {
let path = path.trim_start_matches('/');
let path = percent_decode(path.as_ref()).decode_utf8_lossy();
let mut full_path = PathBuf::new();
// validate
for seg in path.split('/') {
if seg.starts_with("..") || seg.contains('\\') {
return Err(Error::other("invalid path"));
}
full_path.push(seg);
}
Ok(full_path)
}
pub async fn visit_dir_one_level<P: AsRef<std::path::Path>>(
path: P,
state: &Arc<config::AppState>,
filter: Option<Filter>,
) -> io::Result<Vec<FileInfo>> {
let path = path.as_ref();
let mut dir = tokio::fs::read_dir(path).await?;
let mut files: Vec<FileInfo> = Vec::new();
while let Some(child) = dir.next_entry().await? {
let path_uri = child.path().to_string_lossy().to_string();
let is_file = child.file_type().await?.is_file();
let name = child.file_name().to_string_lossy().to_string();
let size = child.metadata().await?.len();
let mut progress = 0.0;
if is_file {
match state
.flists_progress
.lock()
.expect("failed to lock state")
.get(&path.join(&name).to_path_buf())
{
Some(p) => progress = *p,
None => progress = 100.0,
}
let ext = child
.path()
.extension()
.expect("failed to get path extension")
.to_string_lossy()
.to_string();
if ext != "fl" {
continue;
}
}
if let Some(ref filter_files) = filter {
if let Some(ref filter_name) = filter_files.name {
if filter_name.clone() != name {
continue;
}
}
if let Some(ref filter_max_size) = filter_files.max_size {
if filter_max_size.clone() < size as usize {
continue;
}
}
if let Some(ref filter_min_size) = filter_files.min_size {
if filter_min_size.clone() > size as usize {
continue;
}
}
}
files.push(FileInfo {
name,
path_uri,
is_file,
size: size,
last_modified: child
.metadata()
.await?
.modified()?
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("failed to get duration")
.as_secs() as i64,
progress,
});
}
Ok(files)
}