refactor: split code in multiple files
… main.rs is now small enough to add cli args
This commit is contained in:
parent
b8f4607dba
commit
fd46bfb55a
5 changed files with 139 additions and 101 deletions
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "http-server"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
authors = ["Damjan Georgievski <gdamjan@gmail.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
|
|
|
@ -7,7 +7,7 @@ a simple http server like `python -m http.server` but:
|
|||
* maybe announce itself on mDNS (avahi)
|
||||
* maybe compress
|
||||
|
||||
Usage:
|
||||
Usage [TODO]:
|
||||
|
||||
```
|
||||
http-server [--bind ADDRESS] [--chdir DIRECTORY] [port]
|
||||
|
@ -16,3 +16,7 @@ http-server [--bind ADDRESS] [--chdir DIRECTORY] [port]
|
|||
--bind ADDRESS Specify alternate bind address [default: all interfaces]
|
||||
--chdir DIRECTORY Specify directory to server [default: current directory]
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
* Q: why .tar and not .zip? A: you can't stream a zip file efficiently, it needs to write back in a file.
|
||||
|
|
56
src/channel.rs
Normal file
56
src/channel.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use futures;
|
||||
use bytes;
|
||||
use tar;
|
||||
|
||||
use std::thread;
|
||||
use std::path::PathBuf;
|
||||
use std::io;
|
||||
|
||||
pub fn run_tar_in_thread(path: PathBuf) -> futures::sync::mpsc::UnboundedReceiver<bytes::Bytes> {
|
||||
let (writer, stream) = MpscWriter::new();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut a = tar::Builder::new(writer);
|
||||
a.mode(tar::HeaderMode::Deterministic);
|
||||
a.append_dir_all(path.clone(), path);
|
||||
a.finish();
|
||||
});
|
||||
stream
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* TODO:
|
||||
*
|
||||
* there are 2 features important about futures::sync::mpsc
|
||||
* - it works with tokio (and so with actix), so the stream is async friendly
|
||||
* -
|
||||
* cons:
|
||||
* futures::sync::mpsc::unbounded() is unbounded, which means the tar thread will
|
||||
* just push everything in memory as fast as it can (as cpu allows).
|
||||
* a better implementation would use a bounded channel, so that the thread would block
|
||||
* if the async core can't send data from the stream fast enough, and wouldn't fill up
|
||||
* GBs of memory. Alas, there doesn't seem to be a bounded channel compatible
|
||||
* with futures at this time (05-07-2018, but pending work on futures 0.3 might help).
|
||||
*/
|
||||
struct MpscWriter {
|
||||
tx: futures::sync::mpsc::UnboundedSender<bytes::Bytes>
|
||||
}
|
||||
|
||||
impl MpscWriter {
|
||||
fn new() -> (Self, futures::sync::mpsc::UnboundedReceiver<bytes::Bytes>) {
|
||||
let (tx, rx) = futures::sync::mpsc::unbounded();
|
||||
(MpscWriter{tx:tx}, rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Write for MpscWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.tx.unbounded_send(bytes::Bytes::from(buf));
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
106
src/main.rs
106
src/main.rs
|
@ -6,24 +6,20 @@ extern crate tar;
|
|||
extern crate htmlescape;
|
||||
extern crate percent_encoding;
|
||||
|
||||
use actix_web::{server, error, fs, App, HttpRequest, HttpResponse, Error, Result, http::Method};
|
||||
use futures::Stream;
|
||||
mod channel;
|
||||
mod web;
|
||||
|
||||
use actix_web::server;
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::io;
|
||||
|
||||
// TODO cli args
|
||||
fn main() -> Result<(), Error> {
|
||||
fn main() -> Result<(), io::Error> {
|
||||
let bind_addr = env::var("HTTP_ADDR").unwrap_or(String::from("0.0.0.0:8000"));
|
||||
let sys = actix::System::new("static_index");
|
||||
|
||||
server::new(|| {
|
||||
let s = fs::StaticFiles::new(".").show_files_listing().files_listing_renderer(handle_directory);
|
||||
App::new()
|
||||
.resource(r"/{tail:.*}.tar", |r| r.method(Method::GET).f(handle_tar))
|
||||
.handler("/", s)
|
||||
})
|
||||
server::new(web::create_app)
|
||||
.bind(&bind_addr)
|
||||
.expect(&format!("Can't listen on {} ", bind_addr))
|
||||
.start();
|
||||
|
@ -32,91 +28,3 @@ fn main() -> Result<(), Error> {
|
|||
let _ = sys.run();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
|
||||
use htmlescape::encode_minimal as escape_html_entity;
|
||||
|
||||
fn handle_directory<'a, 'b>(
|
||||
dir: &'a fs::Directory,
|
||||
req: &'b HttpRequest,
|
||||
) -> std::io::Result<HttpResponse> {
|
||||
|
||||
let mut paths: Vec<_> = std::fs::read_dir(&dir.path).unwrap()
|
||||
.filter(|r| dir.is_visible(r))
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
paths.sort_by_key(|r| (!r.metadata().unwrap().file_type().is_dir(), r.file_name()));
|
||||
let mut t = String::from("<table>
|
||||
<tr><td>📁 <a href='../'>../</a></td><td>Size</td></tr>\n");
|
||||
for entry in paths {
|
||||
let meta = entry.metadata()?;
|
||||
let file_url = utf8_percent_encode(&entry.file_name().to_string_lossy(), DEFAULT_ENCODE_SET).to_string();
|
||||
let file_name = escape_html_entity(&entry.file_name().to_string_lossy());
|
||||
let size = meta.len();
|
||||
|
||||
t.push_str("<tr>");
|
||||
if meta.file_type().is_dir() {
|
||||
t.push_str(&format!("<td>📂 <a href=\"{file_url}/\">{file_name}/</a></td>", file_name=file_name, file_url=file_url));
|
||||
t.push_str(&format!("<td><small>[<a href=\"{file_url}.tar\">.tar</a>]</small></td>\n", file_url=file_url));
|
||||
} else {
|
||||
t.push_str(&format!("<td>🗎 <a href=\"{file_url}\">{file_name}</a></td>", file_name=file_name, file_url=file_url));
|
||||
t.push_str(&format!("<td>{size}</td>", size=size));
|
||||
}
|
||||
t.push_str("</tr>\n");
|
||||
}
|
||||
t.push_str("</table>");
|
||||
let mut body = String::from(format!("<html>
|
||||
<head>
|
||||
<title>Index of {index}</title>
|
||||
<style>table {{width:100%}} table td:nth-child(2) {{text-align:right}}</style>
|
||||
</head>
|
||||
<body bgcolor='white'>
|
||||
<h1>Index of {index}</h1><hr>\n", index=req.path()));
|
||||
body.push_str(t.as_str());
|
||||
body.push_str("<hr></body></html>\n");
|
||||
Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(body))
|
||||
}
|
||||
|
||||
fn handle_tar(req: &HttpRequest) -> Result<HttpResponse> {
|
||||
let path: PathBuf = req.match_info().query("tail")?;
|
||||
if !(path.is_dir()) {
|
||||
return Err(error::ErrorBadRequest("not a directory"));
|
||||
}
|
||||
let (writer, stream) = MpscWriter::new();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut a = tar::Builder::new(writer);
|
||||
a.mode(tar::HeaderMode::Deterministic);
|
||||
a.append_dir_all(path.clone(), path);
|
||||
a.finish();
|
||||
});
|
||||
|
||||
let resp = HttpResponse::Ok()
|
||||
.content_type("application/x-tar")
|
||||
.streaming(stream.map_err(|_e| error::ErrorBadRequest("bad request")));
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
|
||||
// TODO: see about a bounded channel, that work with tokio
|
||||
struct MpscWriter {
|
||||
tx: futures::sync::mpsc::UnboundedSender<bytes::Bytes>
|
||||
}
|
||||
|
||||
impl MpscWriter {
|
||||
fn new() -> (Self, futures::sync::mpsc::UnboundedReceiver<bytes::Bytes>) {
|
||||
let (tx, rx) = futures::sync::mpsc::unbounded();
|
||||
(MpscWriter{tx:tx}, rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for MpscWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.tx.unbounded_send(bytes::Bytes::from(buf));
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
70
src/web.rs
Normal file
70
src/web.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use actix_web::{error, fs, App, HttpRequest, HttpResponse, Result, http::Method};
|
||||
use futures::Stream;
|
||||
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET};
|
||||
use htmlescape::encode_minimal as escape_html_entity;
|
||||
|
||||
use channel;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std;
|
||||
|
||||
pub fn create_app() -> App {
|
||||
let s = fs::StaticFiles::new(".").show_files_listing().files_listing_renderer(handle_directory);
|
||||
App::new()
|
||||
.resource(r"/{tail:.*}.tar", |r| r.method(Method::GET).f(handle_tar))
|
||||
.handler("/", s)
|
||||
}
|
||||
|
||||
fn handle_directory<'a, 'b>(
|
||||
dir: &'a fs::Directory,
|
||||
req: &'b HttpRequest,
|
||||
) -> std::io::Result<HttpResponse> {
|
||||
|
||||
let mut paths: Vec<_> = std::fs::read_dir(&dir.path).unwrap()
|
||||
.filter(|r| dir.is_visible(r))
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
paths.sort_by_key(|r| (!r.metadata().unwrap().file_type().is_dir(), r.file_name()));
|
||||
let mut t = String::from("<table>
|
||||
<tr><td>📁 <a href='../'>../</a></td><td>Size</td></tr>\n");
|
||||
for entry in paths {
|
||||
let meta = entry.metadata()?;
|
||||
let file_url = utf8_percent_encode(&entry.file_name().to_string_lossy(), DEFAULT_ENCODE_SET).to_string();
|
||||
let file_name = escape_html_entity(&entry.file_name().to_string_lossy());
|
||||
let size = meta.len();
|
||||
|
||||
t.push_str("<tr>");
|
||||
if meta.file_type().is_dir() {
|
||||
t.push_str(&format!("<td>📂 <a href=\"{file_url}/\">{file_name}/</a></td>", file_name=file_name, file_url=file_url));
|
||||
t.push_str(&format!("<td><small>[<a href=\"{file_url}.tar\">.tar</a>]</small></td>\n", file_url=file_url));
|
||||
} else {
|
||||
t.push_str(&format!("<td>🗎 <a href=\"{file_url}\">{file_name}</a></td>", file_name=file_name, file_url=file_url));
|
||||
t.push_str(&format!("<td>{size}</td>", size=size));
|
||||
}
|
||||
t.push_str("</tr>\n");
|
||||
}
|
||||
t.push_str("</table>");
|
||||
let mut body = String::from(format!("<html>
|
||||
<head>
|
||||
<title>Index of {index}</title>
|
||||
<style>table {{width:100%}} table td:nth-child(2) {{text-align:right}}</style>
|
||||
</head>
|
||||
<body bgcolor='white'>
|
||||
<h1>Index of {index}</h1><hr>\n", index=req.path()));
|
||||
body.push_str(t.as_str());
|
||||
body.push_str("<hr></body></html>\n");
|
||||
Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(body))
|
||||
}
|
||||
|
||||
fn handle_tar(req: &HttpRequest) -> Result<HttpResponse> {
|
||||
let path: PathBuf = req.match_info().query("tail")?;
|
||||
if !(path.is_dir()) {
|
||||
return Err(error::ErrorBadRequest("not a directory"));
|
||||
}
|
||||
|
||||
let stream = channel::run_tar_in_thread(path);
|
||||
let resp = HttpResponse::Ok()
|
||||
.content_type("application/x-tar")
|
||||
.streaming(stream.map_err(|_e| error::ErrorBadRequest("bad request")));
|
||||
Ok(resp)
|
||||
}
|
Loading…
Add table
Reference in a new issue