From fd46bfb55a11e5660960b5d95f0ca289dc8fd0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=98=D0=B0=D0=BD=20=D0=93=D0=B5=D0=BE?= =?UTF-8?q?=D1=80=D0=B3=D0=B8=D0=B5=D0=B2=D1=81=D0=BA=D0=B8?= Date: Thu, 5 Jul 2018 17:48:40 +0200 Subject: [PATCH] refactor: split code in multiple files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … main.rs is now small enough to add cli args --- Cargo.toml | 2 +- README.md | 6 ++- src/channel.rs | 56 ++++++++++++++++++++++++++ src/main.rs | 106 ++++--------------------------------------------- src/web.rs | 70 ++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 101 deletions(-) create mode 100644 src/channel.rs create mode 100644 src/web.rs diff --git a/Cargo.toml b/Cargo.toml index a80a49f..a455416 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http-server" -version = "0.1.0" +version = "0.2.0" authors = ["Damjan Georgievski "] license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index 6031d2c..19bda43 100644 --- a/README.md +++ b/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. diff --git a/src/channel.rs b/src/channel.rs new file mode 100644 index 0000000..c14c28d --- /dev/null +++ b/src/channel.rs @@ -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 { + 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 +} + +impl MpscWriter { + fn new() -> (Self, futures::sync::mpsc::UnboundedReceiver) { + let (tx, rx) = futures::sync::mpsc::unbounded(); + (MpscWriter{tx:tx}, rx) + } +} + +impl io::Write for MpscWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.tx.unbounded_send(bytes::Bytes::from(buf)); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index d9150fa..dbf3ce5 100644 --- a/src/main.rs +++ b/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 { - - 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(" - \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(""); - if meta.file_type().is_dir() { - t.push_str(&format!("", file_name=file_name, file_url=file_url)); - t.push_str(&format!("\n", file_url=file_url)); - } else { - t.push_str(&format!("", file_name=file_name, file_url=file_url)); - t.push_str(&format!("", size=size)); - } - t.push_str("\n"); - } - t.push_str("
📁 ../Size
📂 {file_name}/[.tar]🗎 {file_name}{size}
"); - let mut body = String::from(format!(" - - Index of {index} - - - -

Index of {index}


\n", index=req.path())); - body.push_str(t.as_str()); - body.push_str("
\n"); - Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(body)) -} - -fn handle_tar(req: &HttpRequest) -> Result { - 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 -} - -impl MpscWriter { - fn new() -> (Self, futures::sync::mpsc::UnboundedReceiver) { - 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 { - self.tx.unbounded_send(bytes::Bytes::from(buf)); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} diff --git a/src/web.rs b/src/web.rs new file mode 100644 index 0000000..bdec225 --- /dev/null +++ b/src/web.rs @@ -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 { + + 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(" + \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(""); + if meta.file_type().is_dir() { + t.push_str(&format!("", file_name=file_name, file_url=file_url)); + t.push_str(&format!("\n", file_url=file_url)); + } else { + t.push_str(&format!("", file_name=file_name, file_url=file_url)); + t.push_str(&format!("", size=size)); + } + t.push_str("\n"); + } + t.push_str("
📁 ../Size
📂 {file_name}/[.tar]🗎 {file_name}{size}
"); + let mut body = String::from(format!(" + + Index of {index} + + + +

Index of {index}


\n", index=req.path())); + body.push_str(t.as_str()); + body.push_str("
\n"); + Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(body)) +} + +fn handle_tar(req: &HttpRequest) -> Result { + 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) +}