mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 09:09:03 +03:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f148817c52 | ||
|
|
00ae36d486 | ||
|
|
4e823e8bba | ||
|
|
4e84e6c532 | ||
|
|
f49b590a56 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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
|
## [0.25.0] - 2022-07-06
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -379,7 +379,7 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dufs"
|
name = "dufs"
|
||||||
version = "0.25.0"
|
version = "0.26.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"assert_fs",
|
"assert_fs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dufs"
|
name = "dufs"
|
||||||
version = "0.25.0"
|
version = "0.26.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["sigoden <sigoden@gmail.com>"]
|
authors = ["sigoden <sigoden@gmail.com>"]
|
||||||
description = "Dufs is a distinctive utility file server"
|
description = "Dufs is a distinctive utility file server"
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -30,7 +30,7 @@ cargo install dufs
|
|||||||
### With docker
|
### 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
|
### Binaries on macOS, Linux, Windows
|
||||||
@@ -51,7 +51,7 @@ ARGS:
|
|||||||
OPTIONS:
|
OPTIONS:
|
||||||
-b, --bind <addr>... Specify bind address
|
-b, --bind <addr>... Specify bind address
|
||||||
-p, --port <port> Specify port to listen on [default: 5000]
|
-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 `,`
|
--hidden <value> Hide directories from directory listings, separated by `,`
|
||||||
-a, --auth <rule>... Add auth for path
|
-a, --auth <rule>... Add auth for path
|
||||||
--auth-method <value> Select auth method [default: digest] [possible values: basic, digest]
|
--auth-method <value> Select auth method [default: digest] [possible values: basic, digest]
|
||||||
@@ -79,7 +79,7 @@ Serve current working directory
|
|||||||
dufs
|
dufs
|
||||||
```
|
```
|
||||||
|
|
||||||
Explicitly allow all operations including download/upload/delete
|
Allow all operations like upload/delete/search...
|
||||||
|
|
||||||
```
|
```
|
||||||
dufs -A
|
dufs -A
|
||||||
@@ -97,24 +97,24 @@ Serve a specific directory
|
|||||||
dufs Downloads
|
dufs Downloads
|
||||||
```
|
```
|
||||||
|
|
||||||
Serve a specific file
|
Serve a single file
|
||||||
|
|
||||||
```
|
```
|
||||||
dufs linux-distro.iso
|
dufs linux-distro.iso
|
||||||
```
|
```
|
||||||
|
|
||||||
Serve index.html when requesting a directory
|
Serve a single-page application like react/vue
|
||||||
|
|
||||||
```
|
|
||||||
dufs --render-index
|
|
||||||
```
|
|
||||||
|
|
||||||
Serve single-page application like react
|
|
||||||
|
|
||||||
```
|
```
|
||||||
dufs --render-spa
|
dufs --render-spa
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Serve a static website with index.html
|
||||||
|
|
||||||
|
```
|
||||||
|
dufs --render-index
|
||||||
|
```
|
||||||
|
|
||||||
Require username/password
|
Require username/password
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -141,6 +141,12 @@ dufs --tls-cert my.crt --tls-key my.key
|
|||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
|
Upload a file
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -T path-to-file http://127.0.0.1:5000/new-path/path-to-file
|
||||||
|
```
|
||||||
|
|
||||||
Download a file
|
Download a file
|
||||||
```
|
```
|
||||||
curl http://127.0.0.1:5000/path-to-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
|
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
|
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
|
## Access Control
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use crate::auth::AccessControl;
|
|||||||
use crate::auth::AuthMethod;
|
use crate::auth::AuthMethod;
|
||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
use crate::tls::{load_certs, load_private_key};
|
use crate::tls::{load_certs, load_private_key};
|
||||||
|
use crate::utils::encode_uri;
|
||||||
use crate::BoxResult;
|
use crate::BoxResult;
|
||||||
|
|
||||||
pub fn build_cli() -> Command<'static> {
|
pub fn build_cli() -> Command<'static> {
|
||||||
@@ -49,7 +50,7 @@ pub fn build_cli() -> Command<'static> {
|
|||||||
Arg::new("path-prefix")
|
Arg::new("path-prefix")
|
||||||
.long("path-prefix")
|
.long("path-prefix")
|
||||||
.value_name("path")
|
.value_name("path")
|
||||||
.help("Specify an path prefix"),
|
.help("Specify a path prefix"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("hidden")
|
Arg::new("hidden")
|
||||||
@@ -196,7 +197,7 @@ impl Args {
|
|||||||
let uri_prefix = if path_prefix.is_empty() {
|
let uri_prefix = if path_prefix.is_empty() {
|
||||||
"/".to_owned()
|
"/".to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("/{}/", &path_prefix)
|
format!("/{}/", &encode_uri(&path_prefix))
|
||||||
};
|
};
|
||||||
let hidden: String = matches
|
let hidden: String = matches
|
||||||
.value_of("hidden")
|
.value_of("hidden")
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ use async_zip::Compression;
|
|||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use headers::{
|
use headers::{
|
||||||
AcceptRanges, AccessControlAllowCredentials, AccessControlAllowHeaders,
|
AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, Connection,
|
||||||
AccessControlAllowOrigin, Connection, ContentLength, ContentType, ETag, HeaderMap,
|
ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfModifiedSince, IfNoneMatch,
|
||||||
HeaderMapExt, IfModifiedSince, IfNoneMatch, IfRange, LastModified, Range,
|
IfRange, LastModified, Range,
|
||||||
};
|
};
|
||||||
use hyper::header::{
|
use hyper::header::{
|
||||||
HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE,
|
HeaderValue, AUTHORIZATION, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE,
|
||||||
CONTENT_TYPE, ORIGIN, RANGE, WWW_AUTHENTICATE,
|
RANGE, WWW_AUTHENTICATE,
|
||||||
};
|
};
|
||||||
use hyper::{Body, Method, StatusCode, Uri};
|
use hyper::{Body, Method, StatusCode, Uri};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -45,15 +45,30 @@ const BUF_SIZE: usize = 65536;
|
|||||||
pub struct Server {
|
pub struct Server {
|
||||||
args: Arc<Args>,
|
args: Arc<Args>,
|
||||||
assets_prefix: String,
|
assets_prefix: String,
|
||||||
|
single_file_req_paths: Vec<String>,
|
||||||
running: Arc<AtomicBool>,
|
running: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
pub fn new(args: Arc<Args>, running: Arc<AtomicBool>) -> Self {
|
pub fn new(args: Arc<Args>, running: Arc<AtomicBool>) -> Self {
|
||||||
let assets_prefix = format!("{}__dufs_v{}_", args.uri_prefix, env!("CARGO_PKG_VERSION"));
|
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 {
|
Self {
|
||||||
args,
|
args,
|
||||||
running,
|
running,
|
||||||
|
single_file_req_paths,
|
||||||
assets_prefix,
|
assets_prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,8 +133,16 @@ impl Server {
|
|||||||
let head_only = method == Method::HEAD;
|
let head_only = method == Method::HEAD;
|
||||||
|
|
||||||
if self.args.path_is_file {
|
if self.args.path_is_file {
|
||||||
|
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)
|
self.handle_send_file(&self.args.path, headers, head_only, &mut res)
|
||||||
.await?;
|
.await?;
|
||||||
|
} else {
|
||||||
|
status_not_found(&mut res);
|
||||||
|
}
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1008,11 +1031,19 @@ fn add_cors(res: &mut Response) {
|
|||||||
.typed_insert(AccessControlAllowOrigin::ANY);
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
||||||
res.headers_mut()
|
res.headers_mut()
|
||||||
.typed_insert(AccessControlAllowCredentials);
|
.typed_insert(AccessControlAllowCredentials);
|
||||||
|
res.headers_mut().insert(
|
||||||
res.headers_mut().typed_insert(
|
"Access-Control-Allow-Methods",
|
||||||
vec![RANGE, CONTENT_TYPE, ACCEPT, ORIGIN, WWW_AUTHENTICATE]
|
HeaderValue::from_static("GET,HEAD,PUT,OPTIONS,DELETE,PROPFIND,COPY,MOVE"),
|
||||||
.into_iter()
|
);
|
||||||
.collect::<AccessControlAllowHeaders>(),
|
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",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,8 @@
|
|||||||
mod fixtures;
|
mod fixtures;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use assert_cmd::prelude::*;
|
use fixtures::{server, Error, TestServer};
|
||||||
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> {
|
||||||
@@ -33,22 +30,3 @@ 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("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(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,31 +7,27 @@ use rstest::rstest;
|
|||||||
#[rstest]
|
#[rstest]
|
||||||
fn cors(#[with(&["--enable-cors"])] server: TestServer) -> Result<(), Error> {
|
fn cors(#[with(&["--enable-cors"])] server: TestServer) -> Result<(), Error> {
|
||||||
let resp = reqwest::blocking::get(server.url())?;
|
let resp = reqwest::blocking::get(server.url())?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get("access-control-allow-origin").unwrap(),
|
resp.headers().get("access-control-allow-origin").unwrap(),
|
||||||
"*"
|
"*"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resp.headers().get("access-control-allow-headers").unwrap(),
|
resp.headers()
|
||||||
"range, content-type, accept, origin, www-authenticate"
|
.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!(
|
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!(
|
assert_eq!(
|
||||||
resp.headers().get("access-control-allow-headers").unwrap(),
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
60
tests/single_file.rs
Normal file
60
tests/single_file.rs
Normal 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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user