Merge pull request #33 from joseluisq/feature/windows_support
feat: windows support
Diff
.drone.yml | 1 +-
Cargo.lock | 9 +-
Cargo.toml | 1 +-
src/helpers.rs | 16 ++-
src/lib.rs | 3 +-
src/server.rs | 2 +-
src/staticfile_middleware/staticfile.rs | 225 ++++++++++++++-------------------
src/staticfiles.rs | 28 +---
8 files changed, 135 insertions(+), 150 deletions(-)
@@ -48,6 +48,7 @@ trigger:
- feature/*
- bugfix/*
- hotfix/*
- 1.x
@@ -501,6 +501,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "phf"
version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -872,6 +878,7 @@ dependencies = [
"mime_guess",
"nix",
"openssl",
"percent-encoding 2.1.0",
"signal",
"structopt",
"tempdir",
@@ -1055,7 +1062,7 @@ checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
dependencies = [
"idna",
"matches",
"percent-encoding",
"percent-encoding 1.0.1",
]
[[package]]
@@ -38,6 +38,7 @@ mime_guess = "1.8"
structopt = { version = "0.3", default-features = false }
time = "0.1"
url = "1.4"
percent-encoding = "2.1"
[target.'cfg(not(windows))'.dependencies.nix]
version = "0.14"
@@ -33,3 +33,19 @@ where
))),
}
}
#[cfg(not(windows))]
pub fn adjust_canonicalization<P: AsRef<std::path::Path>>(p: P) -> String {
p.as_ref().display().to_string()
}
#[cfg(windows)]
pub fn adjust_canonicalization<P: AsRef<std::path::Path>>(p: P) -> String {
const VERBATIM_PREFIX: &str = r#"\\?\"#;
let p = p.as_ref().display().to_string();
if p.starts_with(VERBATIM_PREFIX) {
p[VERBATIM_PREFIX.len()..].to_string()
} else {
p
}
}
@@ -13,7 +13,10 @@ pub mod gzip;
pub mod helpers;
pub mod logger;
pub mod server;
#[cfg(not(windows))]
pub mod signal_manager;
pub mod staticfile_middleware;
pub mod staticfiles;
@@ -109,5 +109,5 @@ fn handle_signals() {
#[cfg(windows)]
fn handle_signals() {
println!("TODO: Windows signals...")
}
@@ -1,56 +1,57 @@
use humansize::{file_size_opts, FileSize};
use iron::headers::{
AcceptEncoding, AcceptRanges, ContentEncoding, ContentLength, Encoding, HttpDate,
IfModifiedSince, LastModified, Range, RangeUnit,
AcceptRanges, ContentEncoding, ContentLength, Encoding, HttpDate, IfModifiedSince,
LastModified, Range, RangeUnit,
};
use iron::method::Method;
use iron::middleware::Handler;
use iron::modifiers::Header;
use iron::prelude::*;
use iron::status;
use percent_encoding::percent_decode_str;
use std::fs::{File, Metadata};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{error, io};
use std::{ffi::OsString, time::SystemTime};
use crate::staticfile_middleware::helpers;
use crate::helpers;
use crate::staticfile_middleware::partial_file::PartialFile;
pub struct Staticfile {
root: PathBuf,
assets: PathBuf,
dir_list: bool,
dir_listing: bool,
}
impl Staticfile {
pub fn new<P>(root: P, assets: P, dir_list: bool) -> io::Result<Staticfile>
pub fn new<P: AsRef<Path>>(
root_dir: P,
assets_dir: P,
dir_listing: bool,
) -> io::Result<Staticfile>
where
P: AsRef<Path>,
PathBuf: From<P>,
{
let root = root.as_ref().canonicalize()?;
let assets = assets.as_ref().canonicalize()?;
Ok(Staticfile {
root,
assets,
dir_list,
root: root_dir.into(),
assets: assets_dir.into(),
dir_listing,
})
}
fn resolve_path(&self, path: &[&str]) -> Result<PathBuf, Box<dyn error::Error>> {
let path_dirname = path[0];
let current_dirname = percent_decode_str(path[0]).decode_utf8()?;
let asserts_dirname = self.assets.iter().last().unwrap().to_str().unwrap();
let mut is_assets = false;
let resolved = if path_dirname == asserts_dirname {
let path_resolved = if current_dirname.as_ref() == asserts_dirname {
is_assets = true;
let mut res = self.assets.clone();
for component in path.iter().skip(1) {
res.push(component);
res.push(percent_decode_str(component).decode_utf8()?.as_ref());
}
res
@@ -58,21 +59,26 @@ impl Staticfile {
let mut res = self.root.clone();
for component in path {
res.push(component);
res.push(percent_decode_str(component).decode_utf8()?.as_ref());
}
res
};
let resolved = resolved.canonicalize()?;
let path = if is_assets { &self.assets } else { &self.root };
let base_path = if is_assets { &self.assets } else { &self.root };
let path_resolved = PathBuf::from(helpers::adjust_canonicalization(
path_resolved.canonicalize()?,
));
if !resolved.starts_with(&path) {
return Result::Err(From::from(format!("Cannot leave {:?} path", &path)));
if !path_resolved.starts_with(&base_path) {
return Err(From::from(format!(
"Cannot leave {:?} base path",
&base_path
)));
}
Ok(resolved)
Ok(path_resolved)
}
}
@@ -84,17 +90,20 @@ impl Handler for Staticfile {
}
let file_path = match self.resolve_path(&req.url.path()) {
let path_resolved = match self.resolve_path(&req.url.path()) {
Ok(file_path) => file_path,
Err(_) => return Ok(Response::with(status::NotFound)),
Err(e) => {
trace!("{}", e);
return Ok(Response::with(status::NotFound));
}
};
if self.dir_list && file_path.is_dir() && !file_path.join("index.html").exists() {
let encoding = Encoding::Identity;
let readir = match std::fs::read_dir(file_path) {
if self.dir_listing && path_resolved.is_dir() && !path_resolved.join("index.html").exists()
{
let read_dir = match std::fs::read_dir(path_resolved) {
Ok(dir) => dir,
Err(err) => {
error!("{}", err);
@@ -129,7 +138,7 @@ impl Handler for Staticfile {
entries_str =
String::from("<tr><td colspan=\"3\"><a href=\"../\">../</a></td></tr>");
}
for entry in readir {
for entry in read_dir {
let entry = entry.unwrap();
let meta = entry.metadata().unwrap();
let mut filesize = meta.len().file_size(file_size_opts::DECIMAL).unwrap();
@@ -138,8 +147,9 @@ impl Handler for Staticfile {
name = format!("{}/", name);
filesize = String::from("-")
}
let uri = format!("{}{}", current_path, name);
let modified = get_last_modified(meta.modified().unwrap()).unwrap();
let modified = parse_last_modified(meta.modified().unwrap()).unwrap();
entries_str = format!(
"{}<tr><td><a href=\"{}\" title=\"{}\">{}</a></td><td style=\"width: 160px;\">{}</td><td align=\"right\" style=\"width: 140px;\">{}</td></tr>",
@@ -152,12 +162,16 @@ impl Handler for Staticfile {
);
}
let page = format!(
"<html><head><title>Index of {}</title></head><body><h1>Index of {}</h1><table style=\"min-width:680px;\"><tr><th colspan=\"3\"><hr></th></tr>{}<tr><th colspan=\"3\"><hr></th></tr></table></body></html>", current_path, current_path, entries_str
let current_path = percent_decode_str(¤t_path)
.decode_utf8()
.unwrap()
.to_string();
let page_str = format!(
"<html><head><meta charset=\"utf-8\"><title>Index of {}</title></head><body><h1>Index of {}</h1><table style=\"min-width:680px;\"><tr><th colspan=\"3\"><hr></th></tr>{}<tr><th colspan=\"3\"><hr></th></tr></table></body></html>", current_path, current_path, entries_str
);
let len = page.len() as u64;
let content_encoding = ContentEncoding(vec![encoding]);
let mut resp = Response::with((status::Ok, Header(content_encoding), page));
let len = page_str.len() as u64;
let content_encoding = ContentEncoding(vec![Encoding::Identity]);
let mut resp = Response::with((status::Ok, Header(content_encoding), page_str));
@@ -170,53 +184,39 @@ impl Handler for Staticfile {
return Ok(resp);
}
let accept_gz = helpers::accept_gzip(req.headers.get::<AcceptEncoding>());
let file = match StaticFileWithMetadata::search(&file_path, accept_gz) {
let static_file = match StaticFileWithMeta::search(path_resolved.clone()) {
Ok(f) => f,
Err(_) => return Ok(Response::with(status::NotFound)),
Err(e) => {
trace!("{}", e);
return Ok(Response::with(status::NotFound));
}
};
let client_last_modified = req.headers.get::<IfModifiedSince>();
let last_modified = file.last_modified().ok().map(HttpDate);
let client_last_mod = req.headers.get::<IfModifiedSince>();
let last_mod = static_file.last_modified().ok().map(HttpDate);
if let (Some(client_last_modified), Some(last_modified)) =
(client_last_modified, last_modified)
{
if let (Some(client_last_mod), Some(last_mod)) = (client_last_mod, last_mod) {
trace!(
"Comparing {} (file) <= {} (req)",
last_modified,
client_last_modified.0
last_mod,
client_last_mod.0
);
if last_modified <= client_last_modified.0 {
if last_mod <= client_last_mod.0 {
return Ok(Response::with(status::NotModified));
}
}
let encoding = if file.is_gz {
Encoding::Gzip
} else {
Encoding::Identity
};
let encoding = ContentEncoding(vec![encoding]);
let mut resp = match last_modified {
Some(last_modified) => {
let last_modified = LastModified(last_modified);
Response::with((
status::Ok,
Header(last_modified),
Header(encoding),
file.file,
))
let mut resp = match last_mod {
Some(last_mod) => {
Response::with((status::Ok, Header(LastModified(last_mod)), static_file.file))
}
None => Response::with((status::Ok, Header(encoding), file.file)),
None => Response::with((status::Ok, static_file.file)),
};
@@ -224,7 +224,7 @@ impl Handler for Staticfile {
if req.method == Method::Head {
resp.set_mut(vec![]);
resp.set_mut(Header(ContentLength(file.metadata.len())));
resp.set_mut(Header(ContentLength(static_file.meta.len())));
return Ok(resp);
}
@@ -236,7 +236,7 @@ impl Handler for Staticfile {
None => resp,
Some(Range::Bytes(v)) => {
if let Ok(partial_file) = PartialFile::from_path(&file_path, v) {
if let Ok(partial_file) = PartialFile::from_path(&path_resolved, v) {
Response::with((
status::Ok,
partial_file,
@@ -253,79 +253,46 @@ impl Handler for Staticfile {
}
}
struct StaticFileWithMetadata {
struct StaticFileWithMeta {
file: File,
metadata: Metadata,
is_gz: bool,
meta: Metadata,
}
impl StaticFileWithMetadata {
pub fn search<P>(
path: P,
allow_gz: bool,
) -> Result<StaticFileWithMetadata, Box<dyn error::Error>>
where
P: Into<PathBuf>,
{
let mut file_path = path.into();
trace!("Opening {}", file_path.display());
let mut file = StaticFileWithMetadata::open(&file_path)?;
if file.metadata.is_dir() {
file_path.push("index.html");
trace!("Redirecting to index {}", file_path.display());
file = StaticFileWithMetadata::open(&file_path)?;
impl StaticFileWithMeta {
pub fn search(mut src: PathBuf) -> Result<StaticFileWithMeta, Box<dyn error::Error>> {
trace!("Opening {}", src.display());
let mut auto_index = false;
let meta = std::fs::metadata(&src)?;
if meta.is_dir() {
src.push("index.html");
auto_index = true;
trace!("Redirecting to index {}", src.display());
}
if file.metadata.is_file() {
if allow_gz {
let mut side_by_side_path: OsString = file_path.into();
side_by_side_path.push(".gz");
file_path = side_by_side_path.into();
trace!("Attempting to find side-by-side GZ {}", file_path.display());
match StaticFileWithMetadata::open(&file_path) {
Ok(mut gz_file) => {
if gz_file.metadata.is_file() {
gz_file.is_gz = true;
Ok(gz_file)
} else {
Ok(file)
}
}
Err(_) => Ok(file),
}
} else {
Ok(file)
}
let file = File::open(src)?;
let meta = if auto_index { file.metadata()? } else { meta };
if meta.is_file() {
Ok(StaticFileWithMeta { file, meta })
} else {
Err(From::from("Requested path was not a regular file"))
}
}
fn open<P>(path: P) -> Result<StaticFileWithMetadata, Box<dyn error::Error>>
where
P: AsRef<Path>,
{
let file = File::open(path)?;
let metadata = file.metadata()?;
Ok(StaticFileWithMetadata {
file,
metadata,
is_gz: false,
})
}
pub fn last_modified(&self) -> Result<time::Tm, Box<dyn error::Error>> {
get_last_modified(self.metadata.modified()?)
parse_last_modified(self.meta.modified()?)
}
}
fn get_last_modified(modified: SystemTime) -> Result<time::Tm, Box<dyn error::Error>> {
fn parse_last_modified(modified: SystemTime) -> Result<time::Tm, Box<dyn error::Error>> {
let since_epoch = modified.duration_since(UNIX_EPOCH)?;
@@ -1,8 +1,8 @@
use iron::mime;
use iron::prelude::*;
use iron_cors::CorsMiddleware;
use std::collections::HashSet;
use std::time::Duration;
use std::{collections::HashSet, path::PathBuf};
use crate::error_page::ErrorPage;
use crate::gzip::GzipMiddleware;
@@ -32,26 +32,16 @@ impl StaticFiles {
pub fn handle(&self) -> Chain {
let root_dir = &match helpers::get_valid_dirpath(&self.opts.root_dir) {
Err(e) => {
error!("{}", e);
std::process::exit(1)
}
Ok(v) => v,
};
let p = PathBuf::from(&self.opts.root_dir).canonicalize().unwrap();
let root_dir = PathBuf::from(helpers::adjust_canonicalization(p));
let assets_dir = &match helpers::get_valid_dirpath(&self.opts.assets_dir) {
Err(e) => {
error!("{}", e);
std::process::exit(1)
}
Ok(v) => v,
};
let p = PathBuf::from(&self.opts.assets_dir).canonicalize().unwrap();
let assets_dir = PathBuf::from(helpers::adjust_canonicalization(p));
let assets_dirname = &match helpers::get_dirname(assets_dir) {
let assets_dirname = &match helpers::get_dirname(&assets_dir) {
Err(e) => {
error!("{}", e);
std::process::exit(1)