mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 17:13:02 +03:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
835438fc2a | ||
|
|
d445b78f96 | ||
|
|
881a67e1a4 | ||
|
|
bb5a5564b4 | ||
|
|
2cf6d39032 | ||
|
|
c500ce7acc | ||
|
|
f87c52fda2 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
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.43.0] - 2024-11-04
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Auth failed if password contains `:` ([#449](https://github.com/sigoden/dufs/issues/449))
|
||||||
|
- Resolve speed bottleneck in 10G network ([#451](https://github.com/sigoden/dufs/issues/451))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Webui displays subdirectory items ([#457](https://github.com/sigoden/dufs/issues/457))
|
||||||
|
- Support binding abstract unix socket ([#468](https://github.com/sigoden/dufs/issues/468))
|
||||||
|
- Provide healthcheck API ([#474](https://github.com/sigoden/dufs/issues/474))
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Do not show size for Dir ([#447](https://github.com/sigoden/dufs/issues/447))
|
||||||
|
|
||||||
## [0.42.0] - 2024-09-01
|
## [0.42.0] - 2024-09-01
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
526
Cargo.lock
generated
526
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dufs"
|
name = "dufs"
|
||||||
version = "0.42.0"
|
version = "0.43.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"
|
||||||
@@ -63,7 +63,7 @@ assert_cmd = "2"
|
|||||||
reqwest = { version = "0.12", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
|
reqwest = { version = "0.12", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
|
||||||
assert_fs = "1"
|
assert_fs = "1"
|
||||||
port_check = "0.2"
|
port_check = "0.2"
|
||||||
rstest = "0.22"
|
rstest = "0.23"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
url = "2"
|
url = "2"
|
||||||
predicates = "3"
|
predicates = "3"
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -216,8 +216,14 @@ dd skip=$upload_offset if=file status=none ibs=1 | \
|
|||||||
curl -X PATCH -H "X-Update-Range: append" --data-binary @- http://127.0.0.1:5000/file
|
curl -X PATCH -H "X-Update-Range: append" --data-binary @- http://127.0.0.1:5000/file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Health checks
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl http://127.0.0.1:5000/__dufs__/health
|
||||||
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><h2>Advanced topics</h2></summary>
|
<summary><h2>Advanced Topics</h2></summary>
|
||||||
|
|
||||||
### Access Control
|
### Access Control
|
||||||
|
|
||||||
@@ -247,8 +253,7 @@ DUFS supports the use of sha-512 hashed password.
|
|||||||
Create hashed password
|
Create hashed password
|
||||||
|
|
||||||
```
|
```
|
||||||
$ mkpasswd -m sha-512 -s
|
$ mkpasswd -m sha-512 123456
|
||||||
Password: 123456
|
|
||||||
$6$tWMB51u6Kb2ui3wd$5gVHP92V9kZcMwQeKTjyTRgySsYJu471Jb1I6iHQ8iZ6s07GgCIO69KcPBRuwPE5tDq05xMAzye0NxVKuJdYs/
|
$6$tWMB51u6Kb2ui3wd$5gVHP92V9kZcMwQeKTjyTRgySsYJu471Jb1I6iHQ8iZ6s07GgCIO69KcPBRuwPE5tDq05xMAzye0NxVKuJdYs/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -396,6 +401,8 @@ Dufs allows users to customize the UI with your own assets.
|
|||||||
dufs --assets my-assets-dir/
|
dufs --assets my-assets-dir/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> If you only need to make slight adjustments to the current UI, you copy dufs's [assets](https://github.com/sigoden/dufs/tree/main/assets) directory and modify it accordingly. The current UI doesn't use any frameworks, just plain HTML/JS/CSS. As long as you have some basic knowledge of web development, it shouldn't be difficult to modify.
|
||||||
|
|
||||||
Your assets folder must contains a `index.html` file.
|
Your assets folder must contains a `index.html` file.
|
||||||
|
|
||||||
`index.html` can use the following placeholder variables to retrieve internal data.
|
`index.html` can use the following placeholder variables to retrieve internal data.
|
||||||
|
|||||||
@@ -478,6 +478,8 @@ function addPath(file, index) {
|
|||||||
${actionEdit}
|
${actionEdit}
|
||||||
</td>`;
|
</td>`;
|
||||||
|
|
||||||
|
let sizeDisplay = isDir ? `${file.size} ${file.size === 1 ? "item" : "items"}` : formatSize(file.size).join(" ");
|
||||||
|
|
||||||
$pathsTableBody.insertAdjacentHTML("beforeend", `
|
$pathsTableBody.insertAdjacentHTML("beforeend", `
|
||||||
<tr id="addPath${index}">
|
<tr id="addPath${index}">
|
||||||
<td class="path cell-icon">
|
<td class="path cell-icon">
|
||||||
@@ -487,7 +489,7 @@ function addPath(file, index) {
|
|||||||
<a href="${url}" ${isDir ? "" : `target="_blank"`}>${encodedName}</a>
|
<a href="${url}" ${isDir ? "" : `target="_blank"`}>${encodedName}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="cell-mtime">${formatMtime(file.mtime)}</td>
|
<td class="cell-mtime">${formatMtime(file.mtime)}</td>
|
||||||
<td class="cell-size">${formatSize(file.size).join(" ")}</td>
|
<td class="cell-size">${sizeDisplay}</td>
|
||||||
${actionCell}
|
${actionCell}
|
||||||
</tr>`);
|
</tr>`);
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/args.rs
22
src/args.rs
@@ -461,28 +461,30 @@ impl Args {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum BindAddr {
|
pub enum BindAddr {
|
||||||
Address(IpAddr),
|
IpAddr(IpAddr),
|
||||||
Path(PathBuf),
|
#[cfg(unix)]
|
||||||
|
SocketPath(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BindAddr {
|
impl BindAddr {
|
||||||
fn parse_addrs(addrs: &[&str]) -> Result<Vec<Self>> {
|
fn parse_addrs(addrs: &[&str]) -> Result<Vec<Self>> {
|
||||||
let mut bind_addrs = vec![];
|
let mut bind_addrs = vec![];
|
||||||
|
#[cfg(not(unix))]
|
||||||
let mut invalid_addrs = vec![];
|
let mut invalid_addrs = vec![];
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
match addr.parse::<IpAddr>() {
|
match addr.parse::<IpAddr>() {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
bind_addrs.push(BindAddr::Address(v));
|
bind_addrs.push(BindAddr::IpAddr(v));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
if cfg!(unix) {
|
#[cfg(unix)]
|
||||||
bind_addrs.push(BindAddr::Path(PathBuf::from(addr)));
|
bind_addrs.push(BindAddr::SocketPath(addr.to_string()));
|
||||||
} else {
|
#[cfg(not(unix))]
|
||||||
invalid_addrs.push(*addr);
|
invalid_addrs.push(*addr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
#[cfg(not(unix))]
|
||||||
if !invalid_addrs.is_empty() {
|
if !invalid_addrs.is_empty() {
|
||||||
bail!("Invalid bind address `{}`", invalid_addrs.join(","));
|
bail!("Invalid bind address `{}`", invalid_addrs.join(","));
|
||||||
}
|
}
|
||||||
@@ -710,7 +712,7 @@ hidden: tmp,*.log,*.lock
|
|||||||
assert_eq!(args.serve_path, Args::sanitize_path(&tmpdir).unwrap());
|
assert_eq!(args.serve_path, Args::sanitize_path(&tmpdir).unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
args.addrs,
|
args.addrs,
|
||||||
vec![BindAddr::Address("0.0.0.0".parse().unwrap())]
|
vec![BindAddr::IpAddr("0.0.0.0".parse().unwrap())]
|
||||||
);
|
);
|
||||||
assert_eq!(args.hidden, ["tmp", "*.log", "*.lock"]);
|
assert_eq!(args.hidden, ["tmp", "*.log", "*.lock"]);
|
||||||
assert_eq!(args.port, 3000);
|
assert_eq!(args.port, 3000);
|
||||||
@@ -740,8 +742,8 @@ hidden:
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
args.addrs,
|
args.addrs,
|
||||||
vec![
|
vec![
|
||||||
BindAddr::Address("127.0.0.1".parse().unwrap()),
|
BindAddr::IpAddr("127.0.0.1".parse().unwrap()),
|
||||||
BindAddr::Address("192.168.8.10".parse().unwrap())
|
BindAddr::IpAddr("192.168.8.10".parse().unwrap())
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
assert_eq!(args.hidden, ["tmp", "*.log", "*.lock"]);
|
assert_eq!(args.hidden, ["tmp", "*.log", "*.lock"]);
|
||||||
|
|||||||
@@ -307,17 +307,17 @@ pub fn check_auth(
|
|||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
|
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
|
||||||
let value: Vec<u8> = STANDARD.decode(value).ok()?;
|
let value: Vec<u8> = STANDARD.decode(value).ok()?;
|
||||||
let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
|
let (user, pass) = std::str::from_utf8(&value).ok()?.split_once(':')?;
|
||||||
|
|
||||||
if parts[0] != auth_user {
|
if user != auth_user {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth_pass.starts_with("$6$") {
|
if auth_pass.starts_with("$6$") {
|
||||||
if let Ok(()) = sha_crypt::sha512_check(parts[1], auth_pass) {
|
if let Ok(()) = sha_crypt::sha512_check(pass, auth_pass) {
|
||||||
return Some(());
|
return Some(());
|
||||||
}
|
}
|
||||||
} else if parts[1] == auth_pass {
|
} else if pass == auth_pass {
|
||||||
return Some(());
|
return Some(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
src/main.rs
34
src/main.rs
@@ -78,7 +78,7 @@ fn serve(args: Args, running: Arc<AtomicBool>) -> Result<Vec<JoinHandle<()>>> {
|
|||||||
for bind_addr in addrs.iter() {
|
for bind_addr in addrs.iter() {
|
||||||
let server_handle = server_handle.clone();
|
let server_handle = server_handle.clone();
|
||||||
match bind_addr {
|
match bind_addr {
|
||||||
BindAddr::Address(ip) => {
|
BindAddr::IpAddr(ip) => {
|
||||||
let listener = create_listener(SocketAddr::new(*ip, port))
|
let listener = create_listener(SocketAddr::new(*ip, port))
|
||||||
.with_context(|| format!("Failed to bind `{ip}:{port}`"))?;
|
.with_context(|| format!("Failed to bind `{ip}:{port}`"))?;
|
||||||
|
|
||||||
@@ -140,14 +140,21 @@ fn serve(args: Args, running: Arc<AtomicBool>) -> Result<Vec<JoinHandle<()>>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
BindAddr::Path(path) => {
|
|
||||||
if path.exists() {
|
|
||||||
std::fs::remove_file(path)?;
|
|
||||||
}
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
BindAddr::SocketPath(path) => {
|
||||||
|
let socket_path = if path.starts_with("@")
|
||||||
|
&& cfg!(any(target_os = "linux", target_os = "android"))
|
||||||
{
|
{
|
||||||
let listener = tokio::net::UnixListener::bind(path)
|
let mut path_buf = path.as_bytes().to_vec();
|
||||||
.with_context(|| format!("Failed to bind `{}`", path.display()))?;
|
path_buf[0] = b'\0';
|
||||||
|
unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(&path_buf) }
|
||||||
|
.to_os_string()
|
||||||
|
} else {
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
path.into()
|
||||||
|
};
|
||||||
|
let listener = tokio::net::UnixListener::bind(socket_path)
|
||||||
|
.with_context(|| format!("Failed to bind `{}`", path))?;
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let Ok((stream, _addr)) = listener.accept().await else {
|
let Ok((stream, _addr)) = listener.accept().await else {
|
||||||
@@ -162,7 +169,6 @@ fn serve(args: Args, running: Arc<AtomicBool>) -> Result<Vec<JoinHandle<()>>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(handles)
|
Ok(handles)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +213,7 @@ fn check_addrs(args: &Args) -> Result<(Vec<BindAddr>, Vec<BindAddr>)> {
|
|||||||
let (ipv4_addrs, ipv6_addrs) = interface_addrs()?;
|
let (ipv4_addrs, ipv6_addrs) = interface_addrs()?;
|
||||||
for bind_addr in args.addrs.iter() {
|
for bind_addr in args.addrs.iter() {
|
||||||
match bind_addr {
|
match bind_addr {
|
||||||
BindAddr::Address(ip) => match &ip {
|
BindAddr::IpAddr(ip) => match &ip {
|
||||||
IpAddr::V4(_) => {
|
IpAddr::V4(_) => {
|
||||||
if !ipv4_addrs.is_empty() {
|
if !ipv4_addrs.is_empty() {
|
||||||
new_addrs.push(bind_addr.clone());
|
new_addrs.push(bind_addr.clone());
|
||||||
@@ -229,6 +235,7 @@ fn check_addrs(args: &Args) -> Result<(Vec<BindAddr>, Vec<BindAddr>)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
#[cfg(unix)]
|
||||||
_ => {
|
_ => {
|
||||||
new_addrs.push(bind_addr.clone());
|
new_addrs.push(bind_addr.clone());
|
||||||
print_addrs.push(bind_addr.clone())
|
print_addrs.push(bind_addr.clone())
|
||||||
@@ -246,10 +253,10 @@ fn interface_addrs() -> Result<(Vec<BindAddr>, Vec<BindAddr>)> {
|
|||||||
for iface in ifaces.into_iter() {
|
for iface in ifaces.into_iter() {
|
||||||
let ip = iface.ip();
|
let ip = iface.ip();
|
||||||
if ip.is_ipv4() {
|
if ip.is_ipv4() {
|
||||||
ipv4_addrs.push(BindAddr::Address(ip))
|
ipv4_addrs.push(BindAddr::IpAddr(ip))
|
||||||
}
|
}
|
||||||
if ip.is_ipv6() {
|
if ip.is_ipv6() {
|
||||||
ipv6_addrs.push(BindAddr::Address(ip))
|
ipv6_addrs.push(BindAddr::IpAddr(ip))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((ipv4_addrs, ipv6_addrs))
|
Ok((ipv4_addrs, ipv6_addrs))
|
||||||
@@ -260,7 +267,7 @@ fn print_listening(args: &Args, print_addrs: &[BindAddr]) -> Result<String> {
|
|||||||
let urls = print_addrs
|
let urls = print_addrs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|bind_addr| match bind_addr {
|
.map(|bind_addr| match bind_addr {
|
||||||
BindAddr::Address(addr) => {
|
BindAddr::IpAddr(addr) => {
|
||||||
let addr = match addr {
|
let addr = match addr {
|
||||||
IpAddr::V4(_) => format!("{}:{}", addr, args.port),
|
IpAddr::V4(_) => format!("{}:{}", addr, args.port),
|
||||||
IpAddr::V6(_) => format!("[{}]:{}", addr, args.port),
|
IpAddr::V6(_) => format!("[{}]:{}", addr, args.port),
|
||||||
@@ -272,7 +279,8 @@ fn print_listening(args: &Args, print_addrs: &[BindAddr]) -> Result<String> {
|
|||||||
};
|
};
|
||||||
format!("{}://{}{}", protocol, addr, args.uri_prefix)
|
format!("{}://{}{}", protocol, addr, args.uri_prefix)
|
||||||
}
|
}
|
||||||
BindAddr::Path(path) => path.display().to_string(),
|
#[cfg(unix)]
|
||||||
|
BindAddr::SocketPath(path) => path.to_string(),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const INDEX_NAME: &str = "index.html";
|
|||||||
const BUF_SIZE: usize = 65536;
|
const BUF_SIZE: usize = 65536;
|
||||||
const EDITABLE_TEXT_MAX_SIZE: u64 = 4194304; // 4M
|
const EDITABLE_TEXT_MAX_SIZE: u64 = 4194304; // 4M
|
||||||
const RESUMABLE_UPLOAD_MIN_SIZE: u64 = 20971520; // 20M
|
const RESUMABLE_UPLOAD_MIN_SIZE: u64 = 20971520; // 20M
|
||||||
|
const HEALTH_CHECK_PATH: &str = "__dufs__/health";
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
args: Args,
|
args: Args,
|
||||||
@@ -171,7 +172,7 @@ impl Server {
|
|||||||
|
|
||||||
if method == Method::GET
|
if method == Method::GET
|
||||||
&& self
|
&& self
|
||||||
.handle_assets(&relative_path, headers, &mut res)
|
.handle_internal(&relative_path, headers, &mut res)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
@@ -682,7 +683,7 @@ impl Server {
|
|||||||
error!("Failed to zip {}, {}", path.display(), e);
|
error!("Failed to zip {}, {}", path.display(), e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let reader_stream = ReaderStream::new(reader);
|
let reader_stream = ReaderStream::with_capacity(reader, BUF_SIZE);
|
||||||
let stream_body = StreamBody::new(
|
let stream_body = StreamBody::new(
|
||||||
reader_stream
|
reader_stream
|
||||||
.map_ok(Frame::data)
|
.map_ok(Frame::data)
|
||||||
@@ -738,7 +739,7 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_assets(
|
async fn handle_internal(
|
||||||
&self,
|
&self,
|
||||||
req_path: &str,
|
req_path: &str,
|
||||||
headers: &HeaderMap<HeaderValue>,
|
headers: &HeaderMap<HeaderValue>,
|
||||||
@@ -789,6 +790,12 @@ impl Server {
|
|||||||
HeaderValue::from_static("nosniff"),
|
HeaderValue::from_static("nosniff"),
|
||||||
);
|
);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
} else if req_path == HEALTH_CHECK_PATH {
|
||||||
|
res.headers_mut()
|
||||||
|
.typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON));
|
||||||
|
|
||||||
|
*res.body_mut() = body_full(r#"{"status":"OK"}"#);
|
||||||
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
@@ -899,7 +906,7 @@ impl Server {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let reader_stream = ReaderStream::new(file);
|
let reader_stream = ReaderStream::with_capacity(file, BUF_SIZE);
|
||||||
let stream_body = StreamBody::new(
|
let stream_body = StreamBody::new(
|
||||||
reader_stream
|
reader_stream
|
||||||
.map_ok(Frame::data)
|
.map_ok(Frame::data)
|
||||||
@@ -1363,8 +1370,15 @@ impl Server {
|
|||||||
};
|
};
|
||||||
let mtime = to_timestamp(&meta.modified()?);
|
let mtime = to_timestamp(&meta.modified()?);
|
||||||
let size = match path_type {
|
let size = match path_type {
|
||||||
PathType::Dir | PathType::SymlinkDir => None,
|
PathType::Dir | PathType::SymlinkDir => {
|
||||||
PathType::File | PathType::SymlinkFile => Some(meta.len()),
|
let mut count = 0;
|
||||||
|
let mut entries = tokio::fs::read_dir(&path).await?;
|
||||||
|
while entries.next_entry().await?.is_some() {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
PathType::File | PathType::SymlinkFile => meta.len(),
|
||||||
};
|
};
|
||||||
let rel_path = path.strip_prefix(base_path)?;
|
let rel_path = path.strip_prefix(base_path)?;
|
||||||
let name = normalize_path(rel_path);
|
let name = normalize_path(rel_path);
|
||||||
@@ -1416,7 +1430,7 @@ struct PathItem {
|
|||||||
path_type: PathType,
|
path_type: PathType,
|
||||||
name: String,
|
name: String,
|
||||||
mtime: u64,
|
mtime: u64,
|
||||||
size: Option<u64>,
|
size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PathItem {
|
impl PathItem {
|
||||||
@@ -1450,21 +1464,18 @@ impl PathItem {
|
|||||||
),
|
),
|
||||||
PathType::File | PathType::SymlinkFile => format!(
|
PathType::File | PathType::SymlinkFile => format!(
|
||||||
r#"<D:response>
|
r#"<D:response>
|
||||||
<D:href>{}</D:href>
|
<D:href>{href}</D:href>
|
||||||
<D:propstat>
|
<D:propstat>
|
||||||
<D:prop>
|
<D:prop>
|
||||||
<D:displayname>{}</D:displayname>
|
<D:displayname>{displayname}</D:displayname>
|
||||||
<D:getcontentlength>{}</D:getcontentlength>
|
<D:getcontentlength>{}</D:getcontentlength>
|
||||||
<D:getlastmodified>{}</D:getlastmodified>
|
<D:getlastmodified>{mtime}</D:getlastmodified>
|
||||||
<D:resourcetype></D:resourcetype>
|
<D:resourcetype></D:resourcetype>
|
||||||
</D:prop>
|
</D:prop>
|
||||||
<D:status>HTTP/1.1 200 OK</D:status>
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
</D:propstat>
|
</D:propstat>
|
||||||
</D:response>"#,
|
</D:response>"#,
|
||||||
href,
|
self.size
|
||||||
displayname,
|
|
||||||
self.size.unwrap_or_default(),
|
|
||||||
mtime
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1491,16 +1502,7 @@ impl PathItem {
|
|||||||
|
|
||||||
pub fn sort_by_size(&self, other: &Self) -> Ordering {
|
pub fn sort_by_size(&self, other: &Self) -> Ordering {
|
||||||
match self.path_type.cmp(&other.path_type) {
|
match self.path_type.cmp(&other.path_type) {
|
||||||
Ordering::Equal => {
|
Ordering::Equal => self.size.cmp(&other.size),
|
||||||
if self.is_dir() {
|
|
||||||
alphanumeric_sort::compare_str(
|
|
||||||
self.name.to_lowercase(),
|
|
||||||
other.name.to_lowercase(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
self.size.unwrap_or(0).cmp(&other.size.unwrap_or(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v => v,
|
v => v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,17 +57,18 @@ fn invalid_auth(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
const HASHED_PASSWORD_AUTH: &str = "user:$6$gQxZwKyWn/ZmWEA2$4uV7KKMnSUnET2BtWTj/9T5.Jq3h/MdkOlnIl5hdlTxDZ4MZKmJ.kl6C.NL9xnNPqC4lVHC1vuI0E5cLpTJX81@/:rw"; // user:pass
|
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
|
#[case(server(&["--auth", "user:$6$gQxZwKyWn/ZmWEA2$4uV7KKMnSUnET2BtWTj/9T5.Jq3h/MdkOlnIl5hdlTxDZ4MZKmJ.kl6C.NL9xnNPqC4lVHC1vuI0E5cLpTJX81@/:rw", "-A"]), "user", "pass")]
|
||||||
|
#[case(server(&["--auth", "user:$6$YV1J6OHZAAgbzCbS$V55ZEgvJ6JFdz1nLO4AD696PRHAJYhfQf.Gy2HafrCz5itnbgNTtTgfUSqZrt4BJ7FcpRfSt/QZzAan68pido0@/:rw", "-A"]), "user", "pa:ss@1")]
|
||||||
fn auth_hashed_password(
|
fn auth_hashed_password(
|
||||||
#[with(&["--auth", HASHED_PASSWORD_AUTH, "-A"])] server: TestServer,
|
#[case] server: TestServer,
|
||||||
|
#[case] user: &str,
|
||||||
|
#[case] pass: &str,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let url = format!("{}file1", server.url());
|
let url = format!("{}file1", server.url());
|
||||||
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
|
let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?;
|
||||||
assert_eq!(resp.status(), 401);
|
assert_eq!(resp.status(), 401);
|
||||||
if let Err(err) =
|
if let Err(err) = send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), user, pass)
|
||||||
send_with_digest_auth(fetch!(b"PUT", &url).body(b"abc".to_vec()), "user", "pass")
|
|
||||||
{
|
{
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
@@ -76,7 +77,7 @@ fn auth_hashed_password(
|
|||||||
}
|
}
|
||||||
let resp = fetch!(b"PUT", &url)
|
let resp = fetch!(b"PUT", &url)
|
||||||
.body(b"abc".to_vec())
|
.body(b"abc".to_vec())
|
||||||
.basic_auth("user", Some("pass"))
|
.basic_auth(user, Some(pass))
|
||||||
.send()?;
|
.send()?;
|
||||||
assert_eq!(resp.status(), 201);
|
assert_eq!(resp.status(), 201);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
31
tests/health.rs
Normal file
31
tests/health.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
mod fixtures;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use fixtures::{server, Error, TestServer};
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
|
const HEALTH_CHECK_PATH: &str = "__dufs__/health";
|
||||||
|
const HEALTH_CHECK_RESPONSE: &str = r#"{"status":"OK"}"#;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn normal_health(server: TestServer) -> Result<(), Error> {
|
||||||
|
let resp = reqwest::blocking::get(format!("{}{HEALTH_CHECK_PATH}", server.url()))?;
|
||||||
|
assert_eq!(resp.text()?, HEALTH_CHECK_RESPONSE);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn auth_health(
|
||||||
|
#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let resp = reqwest::blocking::get(format!("{}{HEALTH_CHECK_PATH}", server.url()))?;
|
||||||
|
assert_eq!(resp.text()?, HEALTH_CHECK_RESPONSE);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn path_prefix_health(#[with(&["--path-prefix", "xyz"])] server: TestServer) -> Result<(), Error> {
|
||||||
|
let resp = reqwest::blocking::get(format!("{}xyz/{HEALTH_CHECK_PATH}", server.url()))?;
|
||||||
|
assert_eq!(resp.text()?, HEALTH_CHECK_RESPONSE);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user