Compare commits

...

5 Commits

Author SHA1 Message Date
sigoden
f148817c52 chore(release): version v0.26.0 2022-07-11 08:54:29 +08:00
sigoden
00ae36d486 chore: improve readme 2022-07-08 22:36:16 +08:00
sigoden
4e823e8bba feat: make --path-prefix works on serving single file (#102) 2022-07-08 19:30:05 +08:00
sigoden
4e84e6c532 fix: cors headers (#100) 2022-07-08 16:18:10 +08:00
sigoden
f49b590a56 chore: update description of --path-prefix 2022-07-07 15:44:25 +08:00
9 changed files with 148 additions and 72 deletions

View File

@@ -2,6 +2,16 @@
All notable changes to this project will be documented in this file.
## [0.26.0] - 2022-07-08
### Bug Fixes
- Cors headers ([#100](https://github.com/sigoden/dufs/issues/100))
### Features
- Make --path-prefix works on serving single file ([#102](https://github.com/sigoden/dufs/issues/102))
## [0.25.0] - 2022-07-06
### Features

2
Cargo.lock generated
View File

@@ -379,7 +379,7 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "dufs"
version = "0.25.0"
version = "0.26.0"
dependencies = [
"assert_cmd",
"assert_fs",

View File

@@ -1,6 +1,6 @@
[package]
name = "dufs"
version = "0.25.0"
version = "0.26.0"
edition = "2021"
authors = ["sigoden <sigoden@gmail.com>"]
description = "Dufs is a distinctive utility file server"

View File

@@ -30,7 +30,7 @@ cargo install dufs
### With docker
```
docker run -v `pwd`:/data -p 5000:5000 --rm -it sigoden/dufs /data
docker run -v `pwd`:/data -p 5000:5000 --rm -it sigoden/dufs /data -A
```
### Binaries on macOS, Linux, Windows
@@ -51,7 +51,7 @@ ARGS:
OPTIONS:
-b, --bind <addr>... Specify bind address
-p, --port <port> Specify port to listen on [default: 5000]
--path-prefix <path> Specify an path prefix
--path-prefix <path> Specify a path prefix
--hidden <value> Hide directories from directory listings, separated by `,`
-a, --auth <rule>... Add auth for path
--auth-method <value> Select auth method [default: digest] [possible values: basic, digest]
@@ -79,7 +79,7 @@ Serve current working directory
dufs
```
Explicitly allow all operations including download/upload/delete
Allow all operations like upload/delete/search...
```
dufs -A
@@ -97,24 +97,24 @@ Serve a specific directory
dufs Downloads
```
Serve a specific file
Serve a single file
```
dufs linux-distro.iso
```
Serve index.html when requesting a directory
```
dufs --render-index
```
Serve single-page application like react
Serve a single-page application like react/vue
```
dufs --render-spa
```
Serve a static website with index.html
```
dufs --render-index
```
Require username/password
```
@@ -141,6 +141,12 @@ dufs --tls-cert my.crt --tls-key my.key
## API
Upload a file
```
curl -T path-to-file http://127.0.0.1:5000/new-path/path-to-file
```
Download a file
```
curl http://127.0.0.1:5000/path-to-file
@@ -152,16 +158,10 @@ Download a folder as zip file
curl -o path-to-folder.zip http://127.0.0.1:5000/path-to-folder?zip
```
Upload a file
```
curl --upload-file path-to-file http://127.0.0.1:5000/path-to-file
```
Delete a file/folder
```
curl -X DELETE http://127.0.0.1:5000/path-to-file
curl -X DELETE http://127.0.0.1:5000/path-to-file-or-folder
```
## Access Control

View File

@@ -10,6 +10,7 @@ use crate::auth::AccessControl;
use crate::auth::AuthMethod;
#[cfg(feature = "tls")]
use crate::tls::{load_certs, load_private_key};
use crate::utils::encode_uri;
use crate::BoxResult;
pub fn build_cli() -> Command<'static> {
@@ -49,7 +50,7 @@ pub fn build_cli() -> Command<'static> {
Arg::new("path-prefix")
.long("path-prefix")
.value_name("path")
.help("Specify an path prefix"),
.help("Specify a path prefix"),
)
.arg(
Arg::new("hidden")
@@ -196,7 +197,7 @@ impl Args {
let uri_prefix = if path_prefix.is_empty() {
"/".to_owned()
} else {
format!("/{}/", &path_prefix)
format!("/{}/", &encode_uri(&path_prefix))
};
let hidden: String = matches
.value_of("hidden")

View File

@@ -9,13 +9,13 @@ use async_zip::Compression;
use chrono::{TimeZone, Utc};
use futures::TryStreamExt;
use headers::{
AcceptRanges, AccessControlAllowCredentials, AccessControlAllowHeaders,
AccessControlAllowOrigin, Connection, ContentLength, ContentType, ETag, HeaderMap,
HeaderMapExt, IfModifiedSince, IfNoneMatch, IfRange, LastModified, Range,
AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, Connection,
ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfModifiedSince, IfNoneMatch,
IfRange, LastModified, Range,
};
use hyper::header::{
HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE,
CONTENT_TYPE, ORIGIN, RANGE, WWW_AUTHENTICATE,
HeaderValue, AUTHORIZATION, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE,
RANGE, WWW_AUTHENTICATE,
};
use hyper::{Body, Method, StatusCode, Uri};
use serde::Serialize;
@@ -45,15 +45,30 @@ const BUF_SIZE: usize = 65536;
pub struct Server {
args: Arc<Args>,
assets_prefix: String,
single_file_req_paths: Vec<String>,
running: Arc<AtomicBool>,
}
impl Server {
pub fn new(args: Arc<Args>, running: Arc<AtomicBool>) -> Self {
let assets_prefix = format!("{}__dufs_v{}_", args.uri_prefix, env!("CARGO_PKG_VERSION"));
let single_file_req_paths = if args.path_is_file {
vec![
args.uri_prefix.to_string(),
args.uri_prefix[0..args.uri_prefix.len() - 1].to_string(),
encode_uri(&format!(
"{}{}",
&args.uri_prefix,
get_file_name(&args.path)
)),
]
} else {
vec![]
};
Self {
args,
running,
single_file_req_paths,
assets_prefix,
}
}
@@ -118,8 +133,16 @@ impl Server {
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?;
if self
.single_file_req_paths
.iter()
.any(|v| v.as_str() == req_path)
{
self.handle_send_file(&self.args.path, headers, head_only, &mut res)
.await?;
} else {
status_not_found(&mut res);
}
return Ok(res);
}
@@ -1008,11 +1031,19 @@ fn add_cors(res: &mut Response) {
.typed_insert(AccessControlAllowOrigin::ANY);
res.headers_mut()
.typed_insert(AccessControlAllowCredentials);
res.headers_mut().typed_insert(
vec![RANGE, CONTENT_TYPE, ACCEPT, ORIGIN, WWW_AUTHENTICATE]
.into_iter()
.collect::<AccessControlAllowHeaders>(),
res.headers_mut().insert(
"Access-Control-Allow-Methods",
HeaderValue::from_static("GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"),
);
res.headers_mut().insert(
"Access-Control-Allow-Headers",
HeaderValue::from_static("Authorization,Destination,Range"),
);
res.headers_mut().insert(
"Access-Control-Expose-Headers",
HeaderValue::from_static(
"WWW-Authenticate,Content-Range,Accept-Ranges,Content-Disposition",
),
);
}

View File

@@ -3,11 +3,8 @@
mod fixtures;
mod utils;
use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir;
use fixtures::{port, server, tmpdir, wait_for_port, Error, TestServer};
use fixtures::{server, Error, TestServer};
use rstest::rstest;
use std::process::{Command, Stdio};
#[rstest]
fn path_prefix_index(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
@@ -33,22 +30,3 @@ fn path_prefix_propfind(
assert!(text.contains("<D:href>/xyz/</D:href>"));
Ok(())
}
#[rstest]
#[case("index.html")]
fn serve_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")?
.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

@@ -7,31 +7,27 @@ use rstest::rstest;
#[rstest]
fn cors(#[with(&["--enable-cors"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"*"
);
assert_eq!(
resp.headers().get("access-control-allow-headers").unwrap(),
"range, content-type, accept, origin, www-authenticate"
resp.headers()
.get("access-control-allow-credentials")
.unwrap(),
"true"
);
Ok(())
}
#[rstest]
fn cors_options(#[with(&["--enable-cors"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"OPTIONS", server.url()).send()?;
assert_eq!(
resp.headers().get("access-control-allow-origin").unwrap(),
"*"
resp.headers().get("access-control-allow-methods").unwrap(),
"GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"
);
assert_eq!(
resp.headers().get("access-control-allow-headers").unwrap(),
"range, content-type, accept, origin, www-authenticate"
"Authorization,Destination,Range"
);
assert_eq!(
resp.headers().get("access-control-expose-headers").unwrap(),
"WWW-Authenticate,Content-Range,Accept-Ranges,Content-Disposition"
);
Ok(())
}

60
tests/single_file.rs Normal file
View File

@@ -0,0 +1,60 @@
//! Run file server with different args
mod fixtures;
mod utils;
use assert_cmd::prelude::*;
use assert_fs::fixture::TempDir;
use fixtures::{port, tmpdir, wait_for_port, Error};
use rstest::rstest;
use std::process::{Command, Stdio};
#[rstest]
#[case("index.html")]
fn single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")?
.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:{}", port))?;
assert_eq!(resp.text()?, "This is index.html");
let resp = reqwest::blocking::get(format!("http://localhost:{}/", port))?;
assert_eq!(resp.text()?, "This is index.html");
let resp = reqwest::blocking::get(format!("http://localhost:{}/index.html", port))?;
assert_eq!(resp.text()?, "This is index.html");
child.kill()?;
Ok(())
}
#[rstest]
#[case("index.html")]
fn path_prefix_single_file(tmpdir: TempDir, port: u16, #[case] file: &str) -> Result<(), Error> {
let mut child = Command::cargo_bin("dufs")?
.arg(tmpdir.path().join(file))
.arg("-p")
.arg(port.to_string())
.arg("--path-prefix")
.arg("xyz")
.stdout(Stdio::piped())
.spawn()?;
wait_for_port(port);
let resp = reqwest::blocking::get(format!("http://localhost:{}/xyz", port))?;
assert_eq!(resp.text()?, "This is index.html");
let resp = reqwest::blocking::get(format!("http://localhost:{}/xyz/", port))?;
assert_eq!(resp.text()?, "This is index.html");
let resp = reqwest::blocking::get(format!("http://localhost:{}/xyz/index.html", port))?;
assert_eq!(resp.text()?, "This is index.html");
let resp = reqwest::blocking::get(format!("http://localhost:{}", port))?;
assert_eq!(resp.status(), 404);
child.kill()?;
Ok(())
}