Compare commits

...

16 Commits

Author SHA1 Message Date
sigoden
6510ae8be9 chore: release v0.36.0 2023-08-24 18:46:30 +08:00
sigoden
9545fb6e37 fix: ui readonly if no write perm (#258) 2023-08-24 18:32:34 +08:00
sigoden
0fd0f11298 chore: update deps 2023-08-24 16:46:38 +08:00
figsoda
46aa8fcc02 test: remove dependency on native tls (#255) 2023-08-15 11:01:25 +08:00
sigoden
09bb738866 chore: update changelog 2023-08-15 07:29:02 +08:00
sigoden
3612ef10d1 chore: release 0.35.0 (#254)
* chore: release 0.35.0

* update release profile
2023-08-15 07:24:22 +08:00
sigoden
7ac2039a36 chore: update deps 2023-08-14 17:31:52 +08:00
sigoden
7f83de765a fix: typo __ASSERTS_PREFIX__ (#252) 2023-08-13 15:05:45 +08:00
sigoden
9b3779b13a chore: update readme
close #247
2023-07-20 06:33:17 +08:00
sigoden
11a52f29c4 chore: fix release ci (#244) 2023-07-15 16:34:22 +08:00
sigoden
10204c723f chore: fix clippy (#245) 2023-07-15 16:27:13 +08:00
sigoden
204421643d chore: update ci (#242) 2023-07-04 10:25:49 +08:00
sigoden
d9706d75ef feat: sort by type first, then sort by name/mtime/size (#241) 2023-07-04 10:10:48 +08:00
sigoden
40df0bd2f9 chore: update readme 2023-06-18 08:55:42 +08:00
sigoden
a53411b4d6 fix: search should ignore entry path (#235) 2023-06-15 08:28:21 +08:00
ElmTran
609017b2f5 chore: Update README.md (#233)
update examples on new auth.
2023-06-13 08:23:05 +08:00
12 changed files with 528 additions and 556 deletions

View File

@@ -29,16 +29,12 @@ jobs:
RUSTFLAGS: --deny warnings
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install Rust Toolchain Components
uses: actions-rs/toolchain@v1
with:
components: clippy, rustfmt
override: true
toolchain: stable
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v1
- uses: Swatinem/rust-cache@v2
- name: Test
run: cargo test --all

View File

@@ -71,30 +71,40 @@ jobs:
use-cross: true
cargo-flags: "--no-default-features"
runs-on: ${{matrix.os}}
env:
BUILD_CMD: cargo
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Check Tag
id: check-tag
shell: bash
run: |
tag=${GITHUB_REF##*/}
echo "::set-output name=version::$tag"
if [[ "$tag" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
echo "::set-output name=rc::false"
ver=${GITHUB_REF##*/}
echo "version=$ver" >> $GITHUB_OUTPUT
if [[ "$ver" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
echo "rc=false" >> $GITHUB_OUTPUT
else
echo "::set-output name=rc::true"
echo "rc=true" >> $GITHUB_OUTPUT
fi
- name: Install Rust Toolchain Components
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@stable
with:
override: true
target: ${{ matrix.target }}
toolchain: stable
profile: minimal # minimal component installation (ie, no documentation)
targets: ${{ matrix.target }}
- name: Install cross
if: matrix.use-cross
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Overwrite build command env variable
if: matrix.use-cross
shell: bash
run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
- name: Show Version Information (Rust, cargo, GCC)
shell: bash
@@ -107,11 +117,8 @@ jobs:
rustc -V
- name: Build
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.use-cross }}
command: build
args: --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
shell: bash
run: $BUILD_CMD build --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
- name: Build Archive
shell: bash
@@ -123,8 +130,7 @@ jobs:
set -euxo pipefail
bin=${GITHUB_REPOSITORY##*/}
src=`pwd`
dist=$src/dist
dist_dir=`pwd`/dist
name=$bin-$version-$target
executable=target/$target/release/$bin
@@ -132,22 +138,22 @@ jobs:
executable=$executable.exe
fi
mkdir $dist
cp $executable $dist
cd $dist
mkdir $dist_dir
cp $executable $dist_dir
cd $dist_dir
if [[ "$RUNNER_OS" == "Windows" ]]; then
archive=$dist/$name.zip
archive=$dist_dir/$name.zip
7z a $archive *
echo "::set-output name=archive::`pwd -W`/$name.zip"
echo "archive=dist/$name.zip" >> $GITHUB_OUTPUT
else
archive=$dist/$name.tar.gz
tar czf $archive *
echo "::set-output name=archive::$archive"
archive=$dist_dir/$name.tar.gz
tar -czf $archive *
echo "archive=dist/$name.tar.gz" >> $GITHUB_OUTPUT
fi
- name: Publish Archive
uses: softprops/action-gh-release@v0.1.5
uses: softprops/action-gh-release@v1
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
draft: false
@@ -163,16 +169,16 @@ jobs:
needs: release
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
build-args: |
REPO=${{ github.repository }}
@@ -191,13 +197,11 @@ jobs:
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Publish
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }}
run: cargo publish

View File

@@ -2,6 +2,27 @@
All notable changes to this project will be documented in this file.
## [0.36.0] - 2023-08-24
### Bug Fixes
- Ui readonly if no write perm ([#258](https://github.com/sigoden/dufs/issues/258))
### Testing
- Remove dependency on native tls ([#255](https://github.com/sigoden/dufs/issues/255))
## [0.35.0] - 2023-08-14
### Bug Fixes
- Search should ignore entry path ([#235](https://github.com/sigoden/dufs/issues/235))
- Typo __ASSERTS_PREFIX__ ([#252](https://github.com/sigoden/dufs/issues/252))
### Features
- Sort by type first, then sort by name/mtime/size ([#241](https://github.com/sigoden/dufs/issues/241))
## [0.34.2] - 2023-06-05
### Bug Fixes

804
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "dufs"
version = "0.34.2"
version = "0.36.0"
edition = "2021"
authors = ["sigoden <sigoden@gmail.com>"]
description = "Dufs is a distinctive utility file server"
@@ -13,11 +13,11 @@ keywords = ["static", "file", "server", "webdav", "cli"]
[dependencies]
clap = { version = "4", features = ["wrap_help", "env"] }
clap_complete = "4"
chrono = "0.4"
chrono = { version = "0.4", default-features = false, features = ["clock"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"]}
tokio-util = { version = "0.7", features = ["io-util", "compat"] }
hyper = { version = "0.14", features = ["http1", "server", "tcp", "stream"] }
percent-encoding = "2.1"
percent-encoding = "2.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
futures = "0.3"
@@ -31,20 +31,20 @@ rustls-pemfile = { version = "1", optional = true }
tokio-rustls = { version = "0.24", optional = true }
md5 = "0.7"
lazy_static = "1.4"
uuid = { version = "1.1", features = ["v4", "fast-rng"] }
uuid = { version = "1.4", features = ["v4", "fast-rng"] }
urlencoding = "2.1"
xml-rs = "0.8"
log = "0.4"
socket2 = "0.5"
async-stream = "0.3"
walkdir = "2.3"
form_urlencoded = "1.0"
form_urlencoded = "1.2"
alphanumeric-sort = "1.4"
content_inspector = "0.2"
anyhow = "1.0"
chardetng = "0.1"
glob = "0.3.1"
indexmap = "1.9"
indexmap = "2.0"
[features]
default = ["tls"]
@@ -55,13 +55,15 @@ assert_cmd = "2"
reqwest = { version = "0.11", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
assert_fs = "1"
port_check = "0.1"
rstest = "0.17"
rstest = "0.18"
regex = "1"
url = "2"
diqwest = { version = "1", features = ["blocking"] }
diqwest = { version = "1", features = ["blocking", "rustls-tls"], default-features = false }
predicates = "3"
[profile.release]
opt-level = 3
lto = true
strip = true
opt-level = "z"
codegen-units = 1
panic = "abort"
strip = "symbols"

View File

@@ -126,7 +126,7 @@ dufs --render-index
Require username/password
```
dufs -a /@admin:123
dufs -a admin:123@/:rw
```
Listen on specific host:ip
@@ -208,10 +208,11 @@ Dufs supports account based access control. You can control who can do what on w
```
dufs -a [user:pass]@path[:rw][,path[:rw]...][|...]
```
1: Multiple rules are separated by "|"
2: User and pass are the account name and password, if omitted, it is an anonymous user
3: One rule can set multiple paths, separated by ","
4: Add `:rw` after the path to indicate that the path has read and write permissions, otherwise the path has readonly permissions.
1. Multiple rules are separated by "|"
2. User and pass are the account name and password, if omitted, it is an anonymous user
3. One rule can set multiple paths, separated by ","
4. Add `:rw` after the path to indicate that the path has read and write permissions, otherwise the path has readonly permissions.
```
dufs -A -a admin:admin@/:rw
@@ -307,10 +308,10 @@ dufs --log-format '$remote_addr $remote_user "$request" $status' -a /@admin:admi
All options can be set using environment variables prefixed with `DUFS_`.
```
[ROOT_DIR] DUFS_ROOT_DIR=/dir
[SERVE_PATH] DUFS_SERVE_PATH=/dir
-b, --bind <addrs> DUFS_BIND=0.0.0.0
-p, --port <port> DUFS_PORT=5000
--path-prefix <path> DUFS_PATH_RREFIX=/path
--path-prefix <path> DUFS_PATH_PREFIX=/path
--hidden <value> DUFS_HIDDEN=*.log
-a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/"
--auth-method <value> DUFS_AUTH_METHOD=basic
@@ -343,7 +344,7 @@ Your assets folder must contains a `index.html` file.
`index.html` can use the following placeholder variables to retrieve internal data.
- `__INDEX_DATA__`: directory listing data
- `__ASSERTS_PREFIX__`: assets url prefix
- `__ASSETS_PREFIX__`: assets url prefix
</details>

View File

@@ -4,12 +4,12 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="__ASSERTS_PREFIX__favicon.ico">
<link rel="stylesheet" href="__ASSERTS_PREFIX__index.css">
<link rel="icon" type="image/x-icon" href="__ASSETS_PREFIX__favicon.ico">
<link rel="stylesheet" href="__ASSETS_PREFIX__index.css">
<script>
DATA = __INDEX_DATA__
</script>
<script src="__ASSERTS_PREFIX__index.js"></script>
<script src="__ASSETS_PREFIX__index.js"></script>
</head>
<body>

View File

@@ -26,12 +26,13 @@ use hyper::header::{
use hyper::{Body, Method, StatusCode, Uri};
use serde::Serialize;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fs::Metadata;
use std::io::SeekFrom;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{self, AtomicBool};
use std::sync::Arc;
use std::time::SystemTime;
use tokio::fs::File;
@@ -459,7 +460,7 @@ impl Server {
) -> Result<()> {
let mut paths = vec![];
if exist {
paths = match self.list_dir(path, path, access_paths).await {
paths = match self.list_dir(path, path, access_paths.clone()).await {
Ok(paths) => paths,
Err(_) => {
status_forbid(res);
@@ -467,7 +468,16 @@ impl Server {
}
}
};
self.send_index(path, paths, exist, query_params, head_only, user, res)
self.send_index(
path,
paths,
exist,
query_params,
head_only,
user,
access_paths,
res,
)
}
async fn handle_search_dir(
@@ -489,12 +499,14 @@ impl Server {
let hidden = Arc::new(self.args.hidden.to_vec());
let hidden = hidden.clone();
let running = self.running.clone();
let access_paths = access_paths.clone();
let search_paths = tokio::task::spawn_blocking(move || {
let mut paths: Vec<PathBuf> = vec![];
for dir in access_paths.leaf_paths(&path_buf) {
let mut it = WalkDir::new(&dir).into_iter();
it.next();
while let Some(Ok(entry)) = it.next() {
if !running.load(Ordering::SeqCst) {
if !running.load(atomic::Ordering::SeqCst) {
break;
}
let entry_path = entry.path();
@@ -532,7 +544,16 @@ impl Server {
}
}
}
self.send_index(path, paths, true, query_params, head_only, user, res)
self.send_index(
path,
paths,
true,
query_params,
head_only,
user,
access_paths,
res,
)
}
async fn handle_zip_dir(
@@ -773,7 +794,7 @@ impl Server {
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
let output = self
.html
.replace("__ASSERTS_PREFIX__", &self.assets_prefix)
.replace("__ASSETS_PREFIX__", &self.assets_prefix)
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?);
res.headers_mut()
.typed_insert(ContentLength(output.as_bytes().len() as u64));
@@ -926,17 +947,33 @@ impl Server {
query_params: &HashMap<String, String>,
head_only: bool,
user: Option<String>,
access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
if let Some(sort) = query_params.get("sort") {
if sort == "name" {
paths.sort_by(|v1, v2| {
alphanumeric_sort::compare_str(v1.name.to_lowercase(), v2.name.to_lowercase())
paths.sort_by(|v1, v2| match v1.path_type.cmp(&v2.path_type) {
Ordering::Equal => {
alphanumeric_sort::compare_str(v1.name.clone(), v2.name.clone())
}
v => v,
})
} else if sort == "mtime" {
paths.sort_by(|v1, v2| v1.mtime.cmp(&v2.mtime))
paths.sort_by(|v1, v2| match v1.path_type.cmp(&v2.path_type) {
Ordering::Equal => v1.mtime.cmp(&v2.mtime),
v => v,
})
} else if sort == "size" {
paths.sort_by(|v1, v2| v1.size.unwrap_or(0).cmp(&v2.size.unwrap_or(0)))
paths.sort_by(|v1, v2| match v1.path_type.cmp(&v2.path_type) {
Ordering::Equal => {
if v1.is_dir() {
alphanumeric_sort::compare_str(v1.name.clone(), v2.name.clone())
} else {
v1.size.unwrap_or(0).cmp(&v2.size.unwrap_or(0))
}
}
v => v,
})
}
if query_params
.get("order")
@@ -971,12 +1008,13 @@ impl Server {
return Ok(());
}
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
let readwrite = access_paths.perm().readwrite();
let data = IndexData {
kind: DataKind::Index,
href,
uri_prefix: self.args.uri_prefix.clone(),
allow_upload: self.args.allow_upload,
allow_delete: self.args.allow_delete,
allow_upload: self.args.allow_upload && readwrite,
allow_delete: self.args.allow_delete && readwrite,
allow_search: self.args.allow_search,
allow_archive: self.args.allow_archive,
dir_exists: exist,
@@ -992,7 +1030,7 @@ impl Server {
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
self.html
.replace("__ASSERTS_PREFIX__", &self.assets_prefix)
.replace("__ASSETS_PREFIX__", &self.assets_prefix)
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?)
};
res.headers_mut()
@@ -1253,7 +1291,7 @@ impl PathItem {
}
}
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
#[derive(Debug, Serialize, Eq, PartialEq)]
enum PathType {
Dir,
SymlinkDir,
@@ -1261,6 +1299,24 @@ enum PathType {
SymlinkFile,
}
impl Ord for PathType {
fn cmp(&self, other: &Self) -> Ordering {
let to_value = |t: &Self| -> u8 {
if matches!(t, Self::Dir | Self::SymlinkDir) {
0
} else {
1
}
};
to_value(self).cmp(&to_value(other))
}
}
impl PartialOrd for PathType {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn to_timestamp(time: &SystemTime) -> u64 {
time.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
@@ -1333,8 +1389,9 @@ async fn zip_dir<W: AsyncWrite + Unpin>(
let mut paths: Vec<PathBuf> = vec![];
for dir in access_paths.leaf_paths(&dir_clone) {
let mut it = WalkDir::new(&dir).into_iter();
it.next();
while let Some(Ok(entry)) = it.next() {
if !running.load(Ordering::SeqCst) {
if !running.load(atomic::Ordering::SeqCst) {
break;
}
let entry_path = entry.path();

View File

@@ -213,3 +213,22 @@ fn no_auth_propfind_dir(
assert!(body.contains("<D:href>/dir1/</D:href>"));
Ok(())
}
#[rstest]
fn auth_data(
#[with(&["--auth", "user:pass@/:rw|@/", "-A", "--auth-method", "basic"])] server: TestServer,
) -> Result<(), Error> {
let resp = reqwest::blocking::get(server.url())?;
let content = resp.text()?;
let json = utils::retrive_json(&content).unwrap();
assert_eq!(json["allow_delete"], serde_json::Value::Bool(false));
assert_eq!(json["allow_upload"], serde_json::Value::Bool(false));
let resp = fetch!(b"GET", server.url())
.basic_auth("user", Some("pass"))
.send()?;
let content = resp.text()?;
let json = utils::retrive_json(&content).unwrap();
assert_eq!(json["allow_delete"], serde_json::Value::Bool(true));
assert_eq!(json["allow_upload"], serde_json::Value::Bool(true));
Ok(())
}

View File

@@ -46,7 +46,7 @@ pub fn tmpdir() -> TempDir {
let tmpdir = assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests");
for file in FILES {
if *file == BIN_FILE {
tmpdir.child(file).write_binary(b"bin\0\0123").unwrap();
tmpdir.child(file).write_binary(b"bin\0\x00123").unwrap();
} else {
tmpdir
.child(file)
@@ -58,7 +58,7 @@ pub fn tmpdir() -> TempDir {
if *directory == DIR_ASSETS {
tmpdir
.child(format!("{}{}", directory, "index.html"))
.write_str("__ASSERTS_PREFIX__index.js;DATA = __INDEX_DATA__")
.write_str("__ASSETS_PREFIX__index.js;DATA = __INDEX_DATA__")
.unwrap();
} else {
for file in FILES {
@@ -68,7 +68,7 @@ pub fn tmpdir() -> TempDir {
if *file == BIN_FILE {
tmpdir
.child(format!("{directory}{file}"))
.write_binary(b"bin\0\0123")
.write_binary(b"bin\0\x00123")
.unwrap();
} else {
tmpdir

View File

@@ -123,6 +123,15 @@ fn get_dir_search3(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
Ok(())
}
#[rstest]
fn get_dir_search4(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}dir1?q=dir1&simple", server.url()))?;
assert_eq!(resp.status(), 200);
let text = resp.text().unwrap();
assert!(text.is_empty());
Ok(())
}
#[rstest]
fn head_dir_search(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}?q={}", server.url(), "test.html")).send()?;

View File

@@ -59,7 +59,8 @@ pub fn encode_uri(v: &str) -> String {
parts.join("/")
}
fn retrive_json(content: &str) -> Option<Value> {
#[allow(dead_code)]
pub fn retrive_json(content: &str) -> Option<Value> {
let lines: Vec<&str> = content.lines().collect();
let line = lines.iter().find(|v| v.contains("DATA ="))?;
let line_col = line.find("DATA =").unwrap() + 6;