use std::convert::AsRef;
use std::fs::File;
use std::io::{self, BufReader, Read, Seek, SeekFrom};
use std::path::Path;
use std::pin::Pin;
use std::sync::Mutex;
use std::task::Poll;
use bytes::BytesMut;
use futures::Stream;
use ignore::WalkBuilder;
use serde::Serialize;
use tera::{Context, Tera};
use zip::ZipWriter;
use crate::extensions::PathExt;
use crate::server::PathType;
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
struct Item {
path_type: PathType,
name: String,
path: String,
}
#[derive(Debug, Serialize)]
struct Breadcrumb<'a> {
name: &'a str,
path: String,
}
fn get_dir_contents<P: AsRef<Path>>(
dir_path: P,
with_ignore: bool,
show_all: bool,
depth: Option<usize>,
) -> ignore::Walk {
WalkBuilder::new(dir_path)
.standard_filters(false) .git_ignore(with_ignore)
.hidden(!show_all) .max_depth(depth) .build()
}
pub fn send_dir<P1: AsRef<Path>, P2: AsRef<Path>>(
dir_path: P1,
base_path: P2,
show_all: bool,
with_ignore: bool,
path_prefix: Option<&str>,
) -> io::Result<(Vec<u8>, usize)> {
let base_path = base_path.as_ref();
let dir_path = dir_path.as_ref();
let prefix = path_prefix.unwrap_or("");
let breadcrumbs = create_breadcrumbs(dir_path, base_path, prefix);
let files_iter = get_dir_contents(dir_path, with_ignore, show_all, Some(1))
.filter_map(|entry| entry.ok())
.filter(|entry| dir_path != entry.path()) .map(|entry| {
let abs_path = entry.path();
let rel_path = abs_path.strip_prefix(base_path).unwrap();
let rel_path_ref = rel_path.to_str().unwrap_or_default();
Item {
path_type: abs_path.type_(),
name: rel_path.filename_str().to_owned(),
path: format!(
"{}/{}",
prefix,
if cfg!(windows) {
rel_path_ref.replace("\\", "/")
} else {
rel_path_ref.to_string()
}
),
}
});
let mut files = if base_path == dir_path {
files_iter.collect::<Vec<_>>()
} else {
let path = format!(
"{}/{}",
prefix,
dir_path
.parent()
.unwrap()
.strip_prefix(base_path)
.unwrap()
.to_str()
.unwrap()
);
vec![Item {
name: "..".to_owned(),
path,
path_type: PathType::Dir,
}]
.into_iter()
.chain(files_iter)
.collect::<Vec<_>>()
};
files.sort_unstable();
let content = render(dir_path.filename_str(), &files, &breadcrumbs).into_bytes();
let size = content.len();
Ok((content, size))
}
#[derive(Debug)]
pub struct FileStream<T> {
reader: Mutex<T>,
}
impl<T: Read> Stream for FileStream<T> {
type Item = io::Result<hyper::body::Bytes>;
fn poll_next(self: Pin<&mut Self>, _: &mut std::task::Context<'_>) -> Poll<Option<Self::Item>> {
let mut r = match self.reader.lock() {
Ok(r) => r,
Err(e) => {
eprintln!("{e:?}");
let e = io::Error::new(io::ErrorKind::Other, "Failed to read file");
return Poll::Ready(Some(Err(e)));
}
};
let mut buf = BytesMut::zeroed(4_096);
match r.read(&mut buf[..]) {
Ok(bytes) => {
if bytes == 0 {
Poll::Ready(None)
} else {
buf.truncate(bytes);
Poll::Ready(Some(Ok(buf.freeze())))
}
}
Err(e) => Poll::Ready(Some(Err(e))),
}
}
}
pub fn send_file<P: AsRef<Path>>(file_path: P) -> io::Result<(FileStream<BufReader<File>>, u64)> {
let file = File::open(file_path)?;
let size = file.metadata()?.len();
let reader = Mutex::new(BufReader::new(file));
Ok((FileStream { reader }, size))
}
pub fn send_dir_as_zip<P: AsRef<Path>>(
dir_path: P,
show_all: bool,
with_ignore: bool,
) -> io::Result<(FileStream<BufReader<File>>, u64)> {
let dir_path = dir_path.as_ref();
let zip_file = tempfile::tempfile()?;
let mut zip_writer = ZipWriter::new(zip_file);
let zip_options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.unix_permissions(0o755);
let files_iter = get_dir_contents(dir_path, with_ignore, show_all, None)
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path() != dir_path);
for dir_entry in files_iter {
let file_path = dir_entry.path();
let name = file_path.strip_prefix(dir_path).unwrap().to_str().unwrap();
if file_path.is_dir() {
zip_writer
.add_directory(name, zip_options)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
} else {
zip_writer
.start_file(name, zip_options)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut file = File::open(file_path)?;
std::io::copy(&mut file, &mut zip_writer)?;
}
}
let mut zip = zip_writer
.finish()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
zip.seek(SeekFrom::Start(0))?;
let size = zip.metadata()?.len();
let reader = Mutex::new(BufReader::new(zip));
Ok((FileStream { reader }, size))
}
pub fn send_file_with_range<P: AsRef<Path>>(
file_path: P,
range: (u64, u64),
) -> io::Result<(FileStream<std::io::Take<BufReader<File>>>, u64)> {
let (start, end) = range; if end < start {
return Err(io::Error::from(io::ErrorKind::InvalidInput));
}
let mut f = File::open(file_path)?;
let max_end = f.metadata()?.len() - 1;
f.seek(SeekFrom::Start(start))?;
let reader = Mutex::new(BufReader::new(f).take(end - start + 1));
let size = if start > max_end {
0
} else {
std::cmp::min(end, max_end) - start + 1
};
Ok((FileStream { reader }, size))
}
fn create_breadcrumbs<'a>(
dir_path: &'a Path,
base_path: &'a Path,
prefix: &str,
) -> Vec<Breadcrumb<'a>> {
let base_breadcrumb = Breadcrumb {
name: base_path.filename_str(),
path: format!("{}/", prefix),
};
vec![base_breadcrumb]
.into_iter()
.chain(
dir_path
.strip_prefix(base_path)
.unwrap()
.iter()
.map(|s| s.to_str().unwrap())
.scan(prefix.to_string(), |path, name| {
path.push('/');
path.push_str(name);
Some(Breadcrumb {
name,
path: path.clone(),
})
}),
)
.collect::<Vec<_>>()
}
fn render(dir_name: &str, files: &[Item], breadcrumbs: &[Breadcrumb]) -> String {
let mut ctx = Context::new();
ctx.insert("dir_name", dir_name);
ctx.insert("files", files);
ctx.insert("breadcrumbs", breadcrumbs);
ctx.insert("style", include_str!("style.css"));
Tera::one_off(include_str!("index.html"), &ctx, true)
.unwrap_or_else(|e| format!("500 Internal server error: {}", e))
}
#[cfg(test)]
mod t {
use super::*;
#[test]
fn render_successfully() {
let page = render("", &vec![], &vec![]);
assert!(page.starts_with("<!DOCTYPE html>"))
}
#[test]
fn breadcrumbs() {
let base_path = Path::new("/a");
let dir_path = Path::new("/a");
let breadcrumbs = create_breadcrumbs(dir_path, base_path, "");
assert_eq!(breadcrumbs.len(), 1);
assert_eq!(breadcrumbs[0].name, "a");
assert_eq!(breadcrumbs[0].path, "/");
let base_path = Path::new("/a");
let dir_path = Path::new("/a/b");
let breadcrumbs = create_breadcrumbs(dir_path, base_path, "");
assert_eq!(breadcrumbs.len(), 2);
assert_eq!(breadcrumbs[0].name, "a");
assert_eq!(breadcrumbs[0].path, "/");
assert_eq!(breadcrumbs[1].name, "b");
assert_eq!(breadcrumbs[1].path, "/b");
let base_path = Path::new("/a");
let dir_path = Path::new("/a/b/c/d");
let breadcrumbs = create_breadcrumbs(dir_path, base_path, "");
assert_eq!(breadcrumbs.len(), 4);
assert_eq!(breadcrumbs[0].name, "a");
assert_eq!(breadcrumbs[0].path, "/");
assert_eq!(breadcrumbs[1].name, "b");
assert_eq!(breadcrumbs[1].path, "/b");
assert_eq!(breadcrumbs[2].name, "c");
assert_eq!(breadcrumbs[2].path, "/b/c");
assert_eq!(breadcrumbs[3].name, "d");
assert_eq!(breadcrumbs[3].path, "/b/c/d");
}
#[test]
fn breadcrumbs_with_slashes() {
let base_path = Path::new("////a/b");
let dir_path = Path::new("////////a//////b///c////////////");
let breadcrumbs = create_breadcrumbs(dir_path, base_path, "");
assert_eq!(breadcrumbs.len(), 2);
assert_eq!(breadcrumbs[0].name, "b");
assert_eq!(breadcrumbs[0].path, "/");
assert_eq!(breadcrumbs[1].name, "c");
assert_eq!(breadcrumbs[1].path, "/c");
}
#[test]
fn prefixed_breadcrumbs() {
let base_path = Path::new("/a");
let dir_path = Path::new("/a/b/c");
let breadcrumbs = create_breadcrumbs(dir_path, base_path, "/xdd~帥//");
assert_eq!(breadcrumbs.len(), 3);
assert_eq!(breadcrumbs[0].name, "a");
assert_eq!(breadcrumbs[0].path, "/xdd~帥///");
assert_eq!(breadcrumbs[1].name, "b");
assert_eq!(breadcrumbs[1].path, "/xdd~帥///b");
assert_eq!(breadcrumbs[2].name, "c");
assert_eq!(breadcrumbs[2].path, "/xdd~帥///b/c");
}
#[test]
fn breadcrumbs_from_root() {
let base_path = Path::new("/");
let dir_path = Path::new("/a/b");
let breadcrumbs = create_breadcrumbs(dir_path, base_path, "");
assert_eq!(breadcrumbs.len(), 3);
assert_eq!(breadcrumbs[0].name, "");
assert_eq!(breadcrumbs[0].path, "/");
assert_eq!(breadcrumbs[1].name, "a");
assert_eq!(breadcrumbs[1].path, "/a");
assert_eq!(breadcrumbs[2].name, "b");
assert_eq!(breadcrumbs[2].path, "/a/b");
}
}
#[cfg(test)]
mod t_send {
use futures::StreamExt;
use super::*;
fn file_txt_path() -> std::path::PathBuf {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("./tests/file.txt");
path
}
fn dir_with_sub_dir_path() -> std::path::PathBuf {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("./tests/dir_with_sub_dirs/");
path
}
fn missing_file_path() -> std::path::PathBuf {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("./missing/file");
path
}
#[ignore]
#[test]
fn t_send_dir() {}
async fn stream_to_vec<T: Read + std::marker::Unpin>(mut s: FileStream<T>) -> Vec<u8> {
let mut buf = vec![];
while let Some(r) = s.next().await {
if let Ok(b) = r {
buf.extend_from_slice(&b);
}
}
buf
}
#[tokio::test]
async fn t_send_file_success() {
let (s, size) = send_file(file_txt_path()).unwrap();
assert!(size > 0);
let buf = stream_to_vec(s).await;
assert_eq!(&buf, b"01234567");
}
#[test]
fn t_send_file_not_found() {
let buf = send_file(missing_file_path());
assert_eq!(buf.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[tokio::test]
async fn t_send_file_with_range_one_byte() {
for i in 0..=7 {
let (s, size) = send_file_with_range(file_txt_path(), (i, i)).unwrap();
let buf = stream_to_vec(s).await;
assert_eq!(buf, i.to_string().as_bytes());
assert_eq!(size, 1);
}
}
#[tokio::test]
async fn t_send_file_with_range_multiple_bytes() {
let (s, size) = send_file_with_range(file_txt_path(), (0, 1)).unwrap();
let buf = stream_to_vec(s).await;
assert_eq!(buf, b"01");
assert_eq!(size, 2);
let (s, size) = send_file_with_range(file_txt_path(), (1, 2)).unwrap();
let buf = stream_to_vec(s).await;
assert_eq!(buf, b"12");
assert_eq!(size, 2);
let (s, size) = send_file_with_range(file_txt_path(), (1, 4)).unwrap();
let buf = stream_to_vec(s).await;
assert_eq!(buf, b"1234");
assert_eq!(size, 4);
let (s, size) = send_file_with_range(file_txt_path(), (7, 65535)).unwrap();
let buf = stream_to_vec(s).await;
assert_eq!(buf, b"7");
assert_eq!(size, 1);
let (s, size) = send_file_with_range(file_txt_path(), (8, 8)).unwrap();
let buf = stream_to_vec(s).await;
assert_eq!(buf, b"");
assert_eq!(size, 0);
}
#[test]
fn t_send_file_with_range_not_found() {
let buf = send_file_with_range(missing_file_path(), (0, 0));
assert_eq!(buf.unwrap_err().kind(), std::io::ErrorKind::NotFound);
}
#[test]
fn t_send_file_with_range_invalid_range() {
let buf = send_file_with_range(file_txt_path(), (1, 0));
assert_eq!(buf.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
}
#[tokio::test]
async fn t_send_dir_as_zip() {
let s = send_dir_as_zip(dir_with_sub_dir_path(), true, false);
assert!(s.is_ok());
let (s, size) = s.unwrap();
assert!(size > 0);
let v = stream_to_vec(s).await;
assert!(v.len() > 0);
assert_eq!(&v[0..4], &[0x50, 0x4b, 0x03, 0x04]);
}
}