Begin port to actix-web 3.0
* actix-files is quite easy to use now * uses futures 0.3 now, and async/await. the streaming archiver is not ported yet. 0.11.x versions will be dev versions until the port is done
This commit is contained in:
parent
ec93e016bf
commit
cce8560bd5
5 changed files with 1443 additions and 1236 deletions
2315
Cargo.lock
generated
2315
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "http-server"
|
name = "http-server"
|
||||||
version = "0.10.2"
|
version = "0.11.0"
|
||||||
authors = ["Damjan Georgievski <gdamjan@gmail.com>"]
|
authors = ["Damjan Georgievski <gdamjan@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -8,13 +8,13 @@ homepage = "https://github.com/gdamjan/http-server-rs"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "1.0"
|
actix-web = "3.0"
|
||||||
actix-files = "0.1.1"
|
actix-files = "0.3.0"
|
||||||
bytes = "0.4"
|
bytes = "0.4"
|
||||||
clap = "2"
|
clap = "2"
|
||||||
env_logger = "*"
|
env_logger = "*"
|
||||||
log = "*"
|
log = "*"
|
||||||
futures = "0.1"
|
futures = "0.3"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
percent-encoding = "2.0"
|
percent-encoding = "2.0"
|
||||||
v_htmlescape = "0.4"
|
v_htmlescape = "0.4"
|
||||||
|
|
178
src/listing.rs
Normal file
178
src/listing.rs
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
use actix_files::Directory;
|
||||||
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
|
use actix_web::dev::ServiceResponse;
|
||||||
|
use std::path::Path;
|
||||||
|
use percent_encoding::{utf8_percent_encode, CONTROLS}; // NON_ALPHANUMERIC
|
||||||
|
use v_htmlescape::escape as escape_html_entity;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
macro_rules! encode_file_url {
|
||||||
|
($path:ident) => {
|
||||||
|
utf8_percent_encode(&$path, CONTROLS)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||||
|
macro_rules! encode_file_name {
|
||||||
|
($entry:ident) => {
|
||||||
|
escape_html_entity(&$entry.file_name().to_string_lossy())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn directory_listing(
|
||||||
|
dir: &Directory,
|
||||||
|
req: &HttpRequest,
|
||||||
|
) -> Result<ServiceResponse, std::io::Error> {
|
||||||
|
let index_of = format!("Index of {}", req.path());
|
||||||
|
let mut body = String::new();
|
||||||
|
let base = Path::new(req.path());
|
||||||
|
|
||||||
|
for entry in dir.path.read_dir()? {
|
||||||
|
if dir.is_visible(&entry) {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
let p = match entry.path().strip_prefix(&dir.path) {
|
||||||
|
Ok(p) if cfg!(windows) => {
|
||||||
|
base.join(p).to_string_lossy().replace("\\", "/")
|
||||||
|
}
|
||||||
|
Ok(p) => base.join(p).to_string_lossy().into_owned(),
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// if file is a directory, add '/' to the end of the name
|
||||||
|
if let Ok(metadata) = entry.metadata() {
|
||||||
|
if metadata.is_dir() {
|
||||||
|
let _ = write!(
|
||||||
|
body,
|
||||||
|
"<li><a href=\"{}\">{}/</a></li>",
|
||||||
|
encode_file_url!(p),
|
||||||
|
encode_file_name!(entry),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = write!(
|
||||||
|
body,
|
||||||
|
"<li><a href=\"{}\">{}</a></li>",
|
||||||
|
encode_file_url!(p),
|
||||||
|
encode_file_name!(entry),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = format!(
|
||||||
|
"<html>\
|
||||||
|
<head><title>{}</title></head>\
|
||||||
|
<body><h1>{}</h1>\
|
||||||
|
<ul>\
|
||||||
|
{}\
|
||||||
|
</ul></body>\n</html>",
|
||||||
|
index_of, index_of, body
|
||||||
|
);
|
||||||
|
Ok(ServiceResponse::new(
|
||||||
|
req.clone(),
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("text/html; charset=utf-8")
|
||||||
|
.body(html),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn handle_directory(
|
||||||
|
// dir: &fs::Directory,
|
||||||
|
// req: &HttpRequest,
|
||||||
|
// ) -> Result<ServiceResponse, std::io::Error> {
|
||||||
|
// let rd = std::fs::read_dir(&dir.path)?;
|
||||||
|
|
||||||
|
// fn optimistic_is_dir(entry: &std::fs::DirEntry) -> bool {
|
||||||
|
// // consider it non directory if metadata reading fails, better than an unwrap() panic
|
||||||
|
// entry
|
||||||
|
// .metadata()
|
||||||
|
// .map(|m| m.file_type().is_dir())
|
||||||
|
// .unwrap_or(false)
|
||||||
|
// }
|
||||||
|
// let mut paths: Vec<_> = rd
|
||||||
|
// .filter_map(|entry| {
|
||||||
|
// if dir.is_visible(&entry) {
|
||||||
|
// entry.ok()
|
||||||
|
// } else {
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .collect();
|
||||||
|
// paths.sort_by_key(|entry| (!optimistic_is_dir(entry), entry.file_name()));
|
||||||
|
|
||||||
|
// let tar_url = req.path().trim_end_matches('/'); // this is already encoded
|
||||||
|
|
||||||
|
// let mut body = String::new();
|
||||||
|
// writeln!(body, "<h1>Index of {}</h1>", req.path()).unwrap(); // FIXME: decode from url, escape for html
|
||||||
|
// writeln!(
|
||||||
|
// body,
|
||||||
|
// r#"<small>[<a href="{}.tar">.tar</a> of whole directory]</small>"#,
|
||||||
|
// tar_url
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
// writeln!(body, "<table>").unwrap();
|
||||||
|
// writeln!(
|
||||||
|
// body,
|
||||||
|
// "<tr><td>📁 <a href='../'>../</a></td><td>Size</td></tr>"
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
|
||||||
|
// for entry in paths {
|
||||||
|
// let meta = entry.metadata()?;
|
||||||
|
// let file_url =
|
||||||
|
// utf8_percent_encode(&entry.file_name().to_string_lossy(), NON_ALPHANUMERIC).to_string();
|
||||||
|
// let file_name = escape_html_entity(&entry.file_name().to_string_lossy()).to_string();
|
||||||
|
// let size = meta.len();
|
||||||
|
|
||||||
|
// write!(body, "<tr>").unwrap();
|
||||||
|
// if meta.file_type().is_dir() {
|
||||||
|
// writeln!(
|
||||||
|
// body,
|
||||||
|
// r#"<td>📂 <a href="{}/">{}/</a></td>"#,
|
||||||
|
// file_url, file_name
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
// write!(
|
||||||
|
// body,
|
||||||
|
// r#" <td><small>[<a href="{}.tar">.tar</a>]</small></td>"#,
|
||||||
|
// file_url
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
// } else {
|
||||||
|
// writeln!(
|
||||||
|
// body,
|
||||||
|
// r#"<td>🗎 <a href="{}">{}</a></td>"#,
|
||||||
|
// file_url, file_name
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
// write!(body, " <td>{}</td>", size).unwrap();
|
||||||
|
// }
|
||||||
|
// writeln!(body, "</tr>").unwrap();
|
||||||
|
// }
|
||||||
|
// writeln!(body, "</table>").unwrap();
|
||||||
|
// writeln!(
|
||||||
|
// body,
|
||||||
|
// r#"<footer><a href="{}">{} {}</a></footer>"#,
|
||||||
|
// env!("CARGO_PKG_HOMEPAGE"),
|
||||||
|
// env!("CARGO_PKG_NAME"),
|
||||||
|
// env!("CARGO_PKG_VERSION")
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
|
||||||
|
// let mut html = String::new();
|
||||||
|
// writeln!(html, "<!DOCTYPE html>").unwrap();
|
||||||
|
// writeln!(html, "<html><head>").unwrap();
|
||||||
|
// writeln!(html, "<title>Index of {}</title>", req.path()).unwrap();
|
||||||
|
// writeln!(html, "<style>\n{}</style>", include_str!("style.css")).unwrap();
|
||||||
|
// writeln!(html, "</head>").unwrap();
|
||||||
|
// writeln!(html, "<body>\n{}</body>", body).unwrap();
|
||||||
|
// writeln!(html, "</html>").unwrap();
|
||||||
|
|
||||||
|
// let resp = HttpResponse::Ok()
|
||||||
|
// .content_type("text/html; charset=utf-8")
|
||||||
|
// .body(html);
|
||||||
|
|
||||||
|
// Ok(ServiceResponse::new(req.clone(), resp))
|
||||||
|
// }
|
|
@ -1,7 +1,10 @@
|
||||||
mod threaded_archiver;
|
// mod threaded_archiver;
|
||||||
|
mod listing;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
let app = clap::App::new(clap::crate_name!())
|
let app = clap::App::new(clap::crate_name!())
|
||||||
.author(clap::crate_authors!("\n"))
|
.author(clap::crate_authors!("\n"))
|
||||||
.version(clap::crate_version!())
|
.version(clap::crate_version!())
|
||||||
|
@ -47,5 +50,5 @@ fn main() -> std::io::Result<()> {
|
||||||
let root = std::path::PathBuf::from(chdir).canonicalize()?;
|
let root = std::path::PathBuf::from(chdir).canonicalize()?;
|
||||||
std::env::set_current_dir(&root)?;
|
std::env::set_current_dir(&root)?;
|
||||||
|
|
||||||
web::run(&bind_addr, &root)
|
web::run(&bind_addr, &root).await
|
||||||
}
|
}
|
||||||
|
|
169
src/web.rs
169
src/web.rs
|
@ -1,155 +1,58 @@
|
||||||
use actix_files as fs;
|
use actix_files::Files;
|
||||||
use actix_web::dev::ServiceResponse;
|
use actix_web::{get, middleware, web, App, HttpServer, HttpResponse, Responder};
|
||||||
use actix_web::{error, middleware, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
|
|
||||||
use futures::Stream;
|
|
||||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
|
||||||
use v_htmlescape::escape as escape_html_entity;
|
|
||||||
|
|
||||||
use crate::threaded_archiver;
|
|
||||||
|
|
||||||
use std::fmt::Write;
|
// use crate::threaded_archiver;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn run(bind_addr: &str, root: &PathBuf) -> std::io::Result<()> {
|
|
||||||
let root = root.clone();
|
|
||||||
HttpServer::new(move || {
|
|
||||||
log::info!("Serving files from {:?}", &root);
|
|
||||||
|
|
||||||
let static_files = fs::Files::new("/", &root)
|
pub async fn run(bind_addr: &str, root: &PathBuf) -> std::io::Result<()> {
|
||||||
.show_files_listing()
|
let root_ = root.clone();
|
||||||
.files_listing_renderer(handle_directory);
|
let s = HttpServer::new(move || {
|
||||||
|
|
||||||
|
let static_files = Files::new("/", &root_)
|
||||||
|
.show_files_listing()
|
||||||
|
.redirect_to_slash_directory()
|
||||||
|
.files_listing_renderer(crate::listing::directory_listing);
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.data(root.clone())
|
.app_data(root_.clone())
|
||||||
.wrap(middleware::Logger::default())
|
.wrap(middleware::Logger::default())
|
||||||
.service(web::resource(r"/{tail:.*}.tar").to(handle_tar))
|
.service(favicon_ico)
|
||||||
.service(web::resource(r"/favicon.ico").to(favicon_ico))
|
.service(handle_tar)
|
||||||
.service(static_files)
|
.service(static_files)
|
||||||
})
|
})
|
||||||
.bind(bind_addr)?
|
.bind(bind_addr)?
|
||||||
.workers(1)
|
.run();
|
||||||
.run()
|
|
||||||
|
log::info!("Serving files from {:?}", &root);
|
||||||
|
s.await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_directory(
|
#[get("/{tail:.*}.tar")]
|
||||||
dir: &fs::Directory,
|
async fn handle_tar(_root: web::Data<PathBuf>, web::Path(_tail): web::Path<String>) -> impl Responder {
|
||||||
req: &HttpRequest,
|
// let relpath = PathBuf::from(tail.trim_end_matches('/'));
|
||||||
) -> Result<ServiceResponse, std::io::Error> {
|
// let fullpath = root.join(&relpath).canonicalize()?;
|
||||||
let rd = std::fs::read_dir(&dir.path)?;
|
|
||||||
|
|
||||||
fn optimistic_is_dir(entry: &std::fs::DirEntry) -> bool {
|
// if !(fullpath.is_dir()) {
|
||||||
// consider it non directory if metadata reading fails, better than an unwrap() panic
|
// return Err(error::ErrorBadRequest("not a directory"));
|
||||||
entry
|
// }
|
||||||
.metadata()
|
|
||||||
.map(|m| m.file_type().is_dir())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
let mut paths: Vec<_> = rd
|
|
||||||
.filter_map(|entry| {
|
|
||||||
if dir.is_visible(&entry) {
|
|
||||||
entry.ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
paths.sort_by_key(|entry| (!optimistic_is_dir(entry), entry.file_name()));
|
|
||||||
|
|
||||||
let tar_url = req.path().trim_end_matches('/'); // this is already encoded
|
// let stream = threaded_archiver::stream_tar_in_thread(fullpath);
|
||||||
|
// let resp = HttpResponse::Ok()
|
||||||
let mut body = String::new();
|
// .content_type("application/x-tar")
|
||||||
writeln!(body, "<h1>Index of {}</h1>", req.path()).unwrap(); // FIXME: decode from url, escape for html
|
// .streaming(stream.map_err(|_e| error::ErrorBadRequest("stream error")));
|
||||||
writeln!(
|
// Ok(resp)
|
||||||
body,
|
HttpResponse::Ok()
|
||||||
r#"<small>[<a href="{}.tar">.tar</a> of whole directory]</small>"#,
|
|
||||||
tar_url
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
writeln!(body, "<table>").unwrap();
|
|
||||||
writeln!(
|
|
||||||
body,
|
|
||||||
"<tr><td>📁 <a href='../'>../</a></td><td>Size</td></tr>"
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for entry in paths {
|
|
||||||
let meta = entry.metadata()?;
|
|
||||||
let file_url =
|
|
||||||
utf8_percent_encode(&entry.file_name().to_string_lossy(), NON_ALPHANUMERIC).to_string();
|
|
||||||
let file_name = escape_html_entity(&entry.file_name().to_string_lossy()).to_string();
|
|
||||||
let size = meta.len();
|
|
||||||
|
|
||||||
write!(body, "<tr>").unwrap();
|
|
||||||
if meta.file_type().is_dir() {
|
|
||||||
writeln!(
|
|
||||||
body,
|
|
||||||
r#"<td>📂 <a href="{}/">{}/</a></td>"#,
|
|
||||||
file_url, file_name
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
write!(
|
|
||||||
body,
|
|
||||||
r#" <td><small>[<a href="{}.tar">.tar</a>]</small></td>"#,
|
|
||||||
file_url
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(
|
|
||||||
body,
|
|
||||||
r#"<td>🗎 <a href="{}">{}</a></td>"#,
|
|
||||||
file_url, file_name
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
write!(body, " <td>{}</td>", size).unwrap();
|
|
||||||
}
|
|
||||||
writeln!(body, "</tr>").unwrap();
|
|
||||||
}
|
|
||||||
writeln!(body, "</table>").unwrap();
|
|
||||||
writeln!(
|
|
||||||
body,
|
|
||||||
r#"<footer><a href="{}">{} {}</a></footer>"#,
|
|
||||||
env!("CARGO_PKG_HOMEPAGE"),
|
|
||||||
env!("CARGO_PKG_NAME"),
|
|
||||||
env!("CARGO_PKG_VERSION")
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut html = String::new();
|
|
||||||
writeln!(html, "<!DOCTYPE html>").unwrap();
|
|
||||||
writeln!(html, "<html><head>").unwrap();
|
|
||||||
writeln!(html, "<title>Index of {}</title>", req.path()).unwrap();
|
|
||||||
writeln!(html, "<style>\n{}</style>", include_str!("style.css")).unwrap();
|
|
||||||
writeln!(html, "</head>").unwrap();
|
|
||||||
writeln!(html, "<body>\n{}</body>", body).unwrap();
|
|
||||||
writeln!(html, "</html>").unwrap();
|
|
||||||
|
|
||||||
let resp = HttpResponse::Ok()
|
|
||||||
.content_type("text/html; charset=utf-8")
|
|
||||||
.body(html);
|
|
||||||
|
|
||||||
Ok(ServiceResponse::new(req.clone(), resp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_tar(req: HttpRequest) -> impl Responder {
|
const FAVICON_ICO: &'static [u8] = include_bytes!("favicon.png");
|
||||||
let root = req.app_data::<PathBuf>().unwrap();
|
|
||||||
let tail = req.match_info().query("tail");
|
|
||||||
let relpath = PathBuf::from(tail.trim_end_matches('/'));
|
|
||||||
let fullpath = root.join(&relpath).canonicalize()?;
|
|
||||||
|
|
||||||
if !(fullpath.is_dir()) {
|
#[get("/favicon.ico")]
|
||||||
return Err(error::ErrorBadRequest("not a directory"));
|
async fn favicon_ico() -> impl Responder {
|
||||||
}
|
|
||||||
|
|
||||||
let stream = threaded_archiver::stream_tar_in_thread(fullpath);
|
|
||||||
let resp = HttpResponse::Ok()
|
|
||||||
.content_type("application/x-tar")
|
|
||||||
.streaming(stream.map_err(|_e| error::ErrorBadRequest("stream error")));
|
|
||||||
Ok(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn favicon_ico() -> impl Responder {
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type("image/png")
|
.content_type("image/png")
|
||||||
.header("Cache-Control", "only-if-cached, max-age=86400")
|
.header("Cache-Control", "only-if-cached, max-age=86400")
|
||||||
.body(bytes::Bytes::from_static(include_bytes!("favicon.png")))
|
.body(FAVICON_ICO)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue