feat: serve single file (#54)

close #53
This commit is contained in:
sigoden 2022-06-19 14:23:10 +08:00 committed by GitHub
parent 9c2e9d1503
commit c3ac2a21c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 56 additions and 11 deletions

View file

@ -120,6 +120,7 @@ pub struct Args {
pub addrs: Vec<IpAddr>, pub addrs: Vec<IpAddr>,
pub port: u16, pub port: u16,
pub path: PathBuf, pub path: PathBuf,
pub path_is_file: bool,
pub path_prefix: String, pub path_prefix: String,
pub uri_prefix: String, pub uri_prefix: String,
pub auth: AccessControl, pub auth: AccessControl,
@ -146,6 +147,7 @@ impl Args {
.unwrap_or_else(|| vec!["0.0.0.0", "::"]); .unwrap_or_else(|| vec!["0.0.0.0", "::"]);
let addrs: Vec<IpAddr> = Args::parse_addrs(&addrs)?; let addrs: Vec<IpAddr> = Args::parse_addrs(&addrs)?;
let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?; let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
let path_is_file = path.metadata()?.is_file();
let path_prefix = matches let path_prefix = matches
.value_of("path-prefix") .value_of("path-prefix")
.map(|v| v.trim_matches('/').to_owned()) .map(|v| v.trim_matches('/').to_owned())
@ -180,6 +182,7 @@ impl Args {
addrs, addrs,
port, port,
path, path,
path_is_file,
path_prefix, path_prefix,
uri_prefix, uri_prefix,
auth, auth,

View file

@ -90,19 +90,26 @@ impl Server {
let headers = req.headers(); let headers = req.headers();
let method = req.method().clone(); let method = req.method().clone();
let authorization = headers.get(AUTHORIZATION);
let guard_type = self.args.auth.guard(req_path, &method, authorization);
if req_path == "/favicon.ico" && method == Method::GET { if req_path == "/favicon.ico" && method == Method::GET {
self.handle_send_favicon(headers, &mut res).await?; self.handle_send_favicon(headers, &mut res).await?;
return Ok(res); return Ok(res);
} }
let authorization = headers.get(AUTHORIZATION);
let guard_type = self.args.auth.guard(req_path, &method, authorization);
if guard_type.is_reject() { if guard_type.is_reject() {
self.auth_reject(&mut res); self.auth_reject(&mut res);
return Ok(res); return Ok(res);
} }
let head_only = method == Method::HEAD;
if self.args.path_is_file {
self.handle_send_file(&self.args.path, headers, head_only, &mut res)
.await?;
return Ok(res);
}
let path = match self.extract_path(req_path) { let path = match self.extract_path(req_path) {
Some(v) => v, Some(v) => v,
None => { None => {
@ -133,7 +140,6 @@ impl Server {
match method { match method {
Method::GET | Method::HEAD => { Method::GET | Method::HEAD => {
let head_only = method == Method::HEAD;
if is_dir { if is_dir {
if render_try_index && query == "zip" { if render_try_index && query == "zip" {
self.handle_zip_dir(path, head_only, &mut res).await?; self.handle_zip_dir(path, head_only, &mut res).await?;
@ -340,10 +346,7 @@ impl Server {
res: &mut Response, res: &mut Response,
) -> BoxResult<()> { ) -> BoxResult<()> {
let (mut writer, reader) = tokio::io::duplex(BUF_SIZE); let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
let filename = path let filename = get_file_name(path)?;
.file_name()
.and_then(|v| v.to_str())
.ok_or_else(|| format!("Failed to get name of `{}`", path.display()))?;
res.headers_mut().insert( res.headers_mut().insert(
CONTENT_DISPOSITION, CONTENT_DISPOSITION,
HeaderValue::from_str(&format!( HeaderValue::from_str(&format!(
@ -482,6 +485,13 @@ impl Server {
); );
} }
let filename = get_file_name(path)?;
res.headers_mut().insert(
CONTENT_DISPOSITION,
HeaderValue::from_str(&format!("inline; filename=\"{}\"", encode_uri(filename),))
.unwrap(),
);
res.headers_mut().typed_insert(AcceptRanges::bytes()); res.headers_mut().typed_insert(AcceptRanges::bytes());
let size = meta.len(); let size = meta.len();
@ -1022,6 +1032,12 @@ fn status_no_content(res: &mut Response) {
*res.status_mut() = StatusCode::NO_CONTENT; *res.status_mut() = StatusCode::NO_CONTENT;
} }
fn get_file_name(path: &Path) -> BoxResult<&str> {
path.file_name()
.and_then(|v| v.to_str())
.ok_or_else(|| format!("Failed to get file name of `{}`", path.display()).into())
}
fn set_webdav_headers(res: &mut Response) { fn set_webdav_headers(res: &mut Response) {
res.headers_mut().insert( res.headers_mut().insert(
"Allow", "Allow",

View file

@ -1,8 +1,11 @@
mod fixtures; mod fixtures;
mod utils; mod utils;
use fixtures::{server, Error, TestServer}; use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir;
use fixtures::{port, server, tmpdir, wait_for_port, Error, TestServer};
use rstest::rstest; use rstest::rstest;
use std::process::{Command, Stdio};
#[rstest] #[rstest]
fn path_prefix_index(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> { fn path_prefix_index(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
@ -28,3 +31,23 @@ fn path_prefix_propfind(
assert!(text.contains("<D:href>/xyz/</D:href>")); assert!(text.contains("<D:href>/xyz/</D:href>"));
Ok(()) Ok(())
} }
#[rstest]
#[case("index.html")]
fn serve_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> {
let mut child = Command::cargo_bin("duf")?
.env("RUST_LOG", "false")
.arg(tmpdir.path().join(file))
.arg("-p")
.arg(port.to_string())
.stdout(Stdio::piped())
.spawn()?;
wait_for_port(port);
let resp = reqwest::blocking::get(format!("http://localhost:{}/index.html", port))?;
assert_eq!(resp.text()?, "This is index.html");
child.kill()?;
Ok(())
}

View file

@ -1,6 +1,6 @@
mod fixtures; mod fixtures;
use fixtures::{port, server, tmpdir, Error, TestServer}; use fixtures::{port, server, tmpdir, wait_for_port, Error, TestServer};
use assert_cmd::prelude::*; use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir; use assert_fs::fixture::TempDir;
@ -59,6 +59,8 @@ fn validate_printed_urls(tmpdir: TempDir, port: u16, #[case] args: &[&str]) -> R
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn()?; .spawn()?;
wait_for_port(port);
// WARN assumes urls list is terminated by an empty line // WARN assumes urls list is terminated by an empty line
let url_lines = BufReader::new(child.stdout.take().unwrap()) let url_lines = BufReader::new(child.stdout.take().unwrap())
.lines() .lines()

View file

@ -142,7 +142,7 @@ where
} }
/// Wait a max of 1s for the port to become available. /// Wait a max of 1s for the port to become available.
fn wait_for_port(port: u16) { pub fn wait_for_port(port: u16) {
let start_wait = Instant::now(); let start_wait = Instant::now();
while !port_check::is_port_reachable(format!("localhost:{}", port)) { while !port_check::is_port_reachable(format!("localhost:{}", port)) {

View file

@ -105,6 +105,7 @@ fn head_file(server: TestServer) -> Result<(), Error> {
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
assert_eq!(resp.headers().get("content-type").unwrap(), "text/html"); assert_eq!(resp.headers().get("content-type").unwrap(), "text/html");
assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes"); assert_eq!(resp.headers().get("accept-ranges").unwrap(), "bytes");
assert!(resp.headers().contains_key("content-disposition"));
assert!(resp.headers().contains_key("etag")); assert!(resp.headers().contains_key("etag"));
assert!(resp.headers().contains_key("last-modified")); assert!(resp.headers().contains_key("last-modified"));
assert!(resp.headers().contains_key("content-length")); assert!(resp.headers().contains_key("content-length"));