mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 17:13:02 +03:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51cedf2f8a | ||
|
|
48c3c7ded6 | ||
|
|
4491a74b34 | ||
|
|
7c2449cb1a | ||
|
|
0a3d9c391f | ||
|
|
07f4e7d0f2 | ||
|
|
c50f97925c | ||
|
|
ecb3984edc | ||
|
|
24f885164a |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -2,6 +2,31 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.10.0] - 2022-06-02
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Remove unzip file even failed to unzip
|
||||
- Rename --no-auth-read to --no-auth-access
|
||||
- Broken ui
|
||||
|
||||
### Documentation
|
||||
|
||||
- Refactor readme
|
||||
|
||||
### Features
|
||||
|
||||
- Change auth logic/options
|
||||
- Improve ui
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Insert cli output
|
||||
|
||||
### Refactor
|
||||
|
||||
- Small improvement
|
||||
|
||||
## [0.9.0] - 2022-06-02
|
||||
|
||||
### Documentation
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -286,7 +286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "duf"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"async-walkdir",
|
||||
"async_zip",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "duf"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0"
|
||||
edition = "2021"
|
||||
authors = ["sigoden <sigoden@gmail.com>"]
|
||||
description = "Duf is a fully functional file server."
|
||||
|
||||
37
README.md
37
README.md
@@ -16,7 +16,8 @@ Duf is a fully functional file server.
|
||||
- Delete files
|
||||
- Basic authentication
|
||||
- Upload zip file then unzip
|
||||
- Serve through https
|
||||
- Partial responses (Parallel/Resume download)
|
||||
- Support https/tls
|
||||
- Easy to use with curl
|
||||
|
||||
## Install
|
||||
@@ -31,7 +32,37 @@ cargo install duf
|
||||
|
||||
Download from [Github Releases](https://github.com/sigoden/duf/releases), unzip and add duf to your $PATH.
|
||||
|
||||
## Usage
|
||||
## CLI
|
||||
|
||||
```
|
||||
Duf is a fully functional file server.
|
||||
|
||||
USAGE:
|
||||
duf [OPTIONS] [path]
|
||||
|
||||
ARGS:
|
||||
<path> Path to a root directory for serving files [default: .]
|
||||
|
||||
OPTIONS:
|
||||
-a, --auth <user:pass> Use HTTP authentication
|
||||
--no-auth-access Not required auth when access static files
|
||||
-A, --allow-all Allow all operations
|
||||
--allow-delete Allow delete files/folders
|
||||
--allow-symlink Allow symlink to files/folders outside root directory
|
||||
--allow-upload Allow upload files/folders
|
||||
-b, --bind <address> Specify bind address [default: 127.0.0.1]
|
||||
--cors Enable CORS, sets `Access-Control-Allow-Origin: *`
|
||||
-h, --help Print help information
|
||||
-p, --port <port> Specify port to listen on [default: 5000]
|
||||
--path-prefix <path> Specify an url path prefix
|
||||
--render-index Render index.html when requesting a directory
|
||||
--render-spa Render for single-page application
|
||||
--tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
|
||||
--tls-key <path> Path to the SSL/TLS certificate's private key
|
||||
-V, --version Print version information
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
You can run this command to start serving your current working directory on 127.0.0.1:5000 by default.
|
||||
|
||||
@@ -77,7 +108,7 @@ Serve https
|
||||
duf --tls-cert my.crt --tls-key my.key
|
||||
```
|
||||
|
||||
### Api
|
||||
## API
|
||||
|
||||
Download a file
|
||||
```
|
||||
|
||||
@@ -97,38 +97,46 @@ body {
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.main th {
|
||||
.uploaders-table th,
|
||||
.paths-table th {
|
||||
text-align: left;
|
||||
font-weight: unset;
|
||||
color: #5c5c5c;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.main td {
|
||||
.uploaders-table td,
|
||||
.paths-table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.main .cell-name {
|
||||
width: 400px;
|
||||
.uploaders-table .cell-name,
|
||||
.paths-table .cell-name {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.main .cell-mtime {
|
||||
.uploaders-table .cell-status {
|
||||
width: 80px;
|
||||
padding-left: 0.6em;
|
||||
}
|
||||
|
||||
.paths-table .cell-actions {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
padding-left: 0.6em;
|
||||
}
|
||||
|
||||
.paths-table .cell-mtime {
|
||||
width: 120px;
|
||||
padding-left: 0.6em;
|
||||
}
|
||||
|
||||
.main .cell-size {
|
||||
.paths-table .cell-size {
|
||||
text-align: right;
|
||||
width: 70px;
|
||||
padding-left: 0.6em;
|
||||
}
|
||||
|
||||
.main .cell-actions {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
padding-left: 0.6em;
|
||||
}
|
||||
|
||||
|
||||
.path svg {
|
||||
height: 100%;
|
||||
@@ -158,7 +166,7 @@ body {
|
||||
padding-left: 0.4em;
|
||||
}
|
||||
|
||||
.uploaders {
|
||||
.uploaders-table {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,15 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="uploaders">
|
||||
</div>
|
||||
<table>
|
||||
<table class="uploaders-table hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cell-name">Name</th>
|
||||
<th class="cell-status">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<table class="paths-table hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cell-name">Name</th>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
let $tbody, $uploaders;
|
||||
let $pathsTable, $pathsTableBody, $uploadersTable;
|
||||
let baseDir;
|
||||
|
||||
class Uploader {
|
||||
idx;
|
||||
file;
|
||||
name;
|
||||
$elem;
|
||||
$uploadStatus;
|
||||
static globalIdx = 0;
|
||||
constructor(file, dirs) {
|
||||
this.name = [...dirs, file.name].join("/");
|
||||
@@ -19,12 +19,16 @@ class Uploader {
|
||||
if (file.name == baseDir + ".zip") {
|
||||
url += "?unzip";
|
||||
}
|
||||
$uploaders.insertAdjacentHTML("beforeend", `
|
||||
<div class="uploader path">
|
||||
<div><svg height="16" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M6 5H2V4h4v1zM2 8h7V7H2v1zm0 2h7V9H2v1zm0 2h7v-1H2v1zm10-7.5V14c0 .55-.45 1-1 1H1c-.55 0-1-.45-1-1V2c0-.55.45-1 1-1h7.5L12 4.5zM11 5L8 2H1v12h10V5z"></path></svg></div>
|
||||
<a href="${url}" id="file${idx}">${name} (0%)</a>
|
||||
</div>`);
|
||||
this.$elem = document.getElementById(`file${idx}`);
|
||||
$uploadersTable.insertAdjacentHTML("beforeend", `
|
||||
<tr id="upload${idx}" class="uploader">
|
||||
<td class="path cell-name">
|
||||
<div>${getSvg("File")}</div>
|
||||
<a href="${url}">${name}</a>
|
||||
</td>
|
||||
<td class="cell-status" id="uploadStatus${idx}"></td>
|
||||
</tr>`);
|
||||
$uploadersTable.classList.remove("hidden");
|
||||
this.$uploadStatus = document.getElementById(`uploadStatus${idx}`);
|
||||
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.upload.addEventListener("progress", e => this.progress(e), false);
|
||||
@@ -45,15 +49,15 @@ class Uploader {
|
||||
|
||||
progress(event) {
|
||||
const percent = (event.loaded / event.total) * 100;
|
||||
this.$elem.innerHTML = `${this.name} (${percent.toFixed(2)}%)`;
|
||||
this.$uploadStatus.innerHTML = `${percent.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
complete() {
|
||||
this.$elem.innerHTML = `${this.name}`;
|
||||
this.$uploadStatus.innerHTML = `✓`;
|
||||
}
|
||||
|
||||
fail() {
|
||||
this.$elem.innerHTML = `<strike>${this.name}</strike>`;
|
||||
this.$uploadStatus.innerHTML = `✗`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,15 +114,15 @@ function addPath(file, index) {
|
||||
${actionDelete}
|
||||
</td>`
|
||||
|
||||
$tbody.insertAdjacentHTML("beforeend", `
|
||||
$pathsTableBody.insertAdjacentHTML("beforeend", `
|
||||
<tr id="addPath${index}">
|
||||
<td class="path cell-name">
|
||||
<td class="path cell-name">
|
||||
<div>${getSvg(file.path_type)}</div>
|
||||
<a href="${url}" title="${file.name}">${file.name}</a>
|
||||
</td>
|
||||
<td class="cell-mtime">${formatMtime(file.mtime)}</td>
|
||||
<td class="cell-size">${formatSize(file.size)}</td>
|
||||
${actionCell}
|
||||
</td>
|
||||
<td class="cell-mtime">${formatMtime(file.mtime)}</td>
|
||||
<td class="cell-size">${formatSize(file.size)}</td>
|
||||
${actionCell}
|
||||
</tr>`)
|
||||
}
|
||||
|
||||
@@ -134,6 +138,10 @@ async function deletePath(index) {
|
||||
});
|
||||
if (res.status === 200) {
|
||||
document.getElementById(`addPath${index}`).remove();
|
||||
DATA.paths[index] = null;
|
||||
if (!DATA.paths.find(v => !!v)) {
|
||||
$pathsTable.classList.add("hidden");
|
||||
}
|
||||
} else {
|
||||
throw new Error(await res.text())
|
||||
}
|
||||
@@ -224,19 +232,23 @@ function formatSize(size) {
|
||||
}
|
||||
|
||||
function ready() {
|
||||
$tbody = document.querySelector(".main tbody");
|
||||
$uploaders = document.querySelector(".uploaders");
|
||||
$pathsTable = document.querySelector(".paths-table")
|
||||
$pathsTableBody = document.querySelector(".paths-table tbody");
|
||||
$uploadersTable = document.querySelector(".uploaders-table");
|
||||
|
||||
addBreadcrumb(DATA.breadcrumb);
|
||||
if (Array.isArray(DATA.paths)) {
|
||||
const len = DATA.paths.length;
|
||||
if (len > 0) {
|
||||
$pathsTable.classList.remove("hidden");
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
addPath(DATA.paths[i], i);
|
||||
}
|
||||
}
|
||||
if (DATA.allow_upload) {
|
||||
dropzone();
|
||||
document.querySelector(".upload-control").classList.remove(["hidden"]);
|
||||
document.querySelector(".upload-control").classList.remove("hidden");
|
||||
document.getElementById("file").addEventListener("change", e => {
|
||||
const files = e.target.files;
|
||||
for (let file of files) {
|
||||
|
||||
26
src/args.rs
26
src/args.rs
@@ -49,39 +49,41 @@ fn app() -> clap::Command<'static> {
|
||||
.arg(
|
||||
Arg::new("allow-upload")
|
||||
.long("allow-upload")
|
||||
.help("Allow upload operation"),
|
||||
.help("Allow upload files/folders"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("allow-delete")
|
||||
.long("allow-delete")
|
||||
.help("Allow delete operation"),
|
||||
.help("Allow delete files/folders"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("allow-symlink")
|
||||
.long("allow-symlink")
|
||||
.help("Allow symlink to directories/files outside root directory"),
|
||||
.help("Allow symlink to files/folders outside root directory"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("render-index")
|
||||
.long("render-index")
|
||||
.help("Render existing index.html when requesting a directory"),
|
||||
.help("Render index.html when requesting a directory"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("render-spa")
|
||||
.long("render-spa")
|
||||
.help("Render spa, rewrite all not-found requests to `index.html"),
|
||||
.help("Render for single-page application"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("auth")
|
||||
.short('a')
|
||||
.display_order(1)
|
||||
.long("auth")
|
||||
.help("Use HTTP authentication for all operations")
|
||||
.help("Use HTTP authentication")
|
||||
.value_name("user:pass"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-auth-read")
|
||||
.long("no-auth-read")
|
||||
.help("Do not authenticate read operations like static serving"),
|
||||
Arg::new("no-auth-access")
|
||||
.display_order(1)
|
||||
.long("no-auth-access")
|
||||
.help("Not required auth when access static files"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("cors")
|
||||
@@ -113,7 +115,7 @@ pub struct Args {
|
||||
pub path: PathBuf,
|
||||
pub path_prefix: Option<String>,
|
||||
pub auth: Option<String>,
|
||||
pub no_auth_read: bool,
|
||||
pub no_auth_access: bool,
|
||||
pub allow_upload: bool,
|
||||
pub allow_delete: bool,
|
||||
pub allow_symlink: bool,
|
||||
@@ -135,7 +137,7 @@ impl Args {
|
||||
let path_prefix = matches.value_of("path-prefix").map(|v| v.to_owned());
|
||||
let cors = matches.is_present("cors");
|
||||
let auth = matches.value_of("auth").map(|v| v.to_owned());
|
||||
let no_auth_read = matches.is_present("no-auth-read");
|
||||
let no_auth_access = matches.is_present("no-auth-access");
|
||||
let allow_upload = matches.is_present("allow-all") || matches.is_present("allow-upload");
|
||||
let allow_delete = matches.is_present("allow-all") || matches.is_present("allow-delete");
|
||||
let allow_symlink = matches.is_present("allow-all") || matches.is_present("allow-symlink");
|
||||
@@ -156,7 +158,7 @@ impl Args {
|
||||
path,
|
||||
path_prefix,
|
||||
auth,
|
||||
no_auth_read,
|
||||
no_auth_access,
|
||||
cors,
|
||||
allow_delete,
|
||||
allow_upload,
|
||||
|
||||
@@ -260,27 +260,9 @@ impl InnerService {
|
||||
|
||||
let query = req.uri().query().unwrap_or_default();
|
||||
if query == "unzip" {
|
||||
let root = path.parent().unwrap();
|
||||
let mut zip = ZipFileReader::new(File::open(&path).await?).await?;
|
||||
for i in 0..zip.entries().len() {
|
||||
let entry = &zip.entries()[i];
|
||||
let entry_name = entry.name();
|
||||
let entry_path = root.join(entry_name);
|
||||
if entry_name.ends_with('/') {
|
||||
fs::create_dir_all(entry_path).await?;
|
||||
} else {
|
||||
if !self.args.allow_delete && fs::metadata(&entry_path).await.is_ok() {
|
||||
continue;
|
||||
}
|
||||
if let Some(parent) = entry_path.parent() {
|
||||
if fs::symlink_metadata(parent).await.is_err() {
|
||||
fs::create_dir_all(&parent).await?;
|
||||
}
|
||||
}
|
||||
let mut outfile = fs::File::create(&entry_path).await?;
|
||||
let mut reader = zip.entry_reader(i).await?;
|
||||
io::copy(&mut reader, &mut outfile).await?;
|
||||
}
|
||||
if let Err(e) = self.unzip_file(path).await {
|
||||
eprintln!("Failed to unzip {}, {}", path.display(), e);
|
||||
status!(res, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
fs::remove_file(&path).await?;
|
||||
}
|
||||
@@ -519,7 +501,7 @@ impl InnerService {
|
||||
.unwrap_or_default(),
|
||||
_ => false,
|
||||
},
|
||||
None => self.args.no_auth_read && req.method() == Method::GET,
|
||||
None => self.args.no_auth_access && req.method() == Method::GET,
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -539,6 +521,32 @@ impl InnerService {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
async fn unzip_file(&self, path: &Path) -> BoxResult<()> {
|
||||
let root = path.parent().unwrap();
|
||||
let mut zip = ZipFileReader::new(File::open(&path).await?).await?;
|
||||
for i in 0..zip.entries().len() {
|
||||
let entry = &zip.entries()[i];
|
||||
let entry_name = entry.name();
|
||||
let entry_path = root.join(entry_name);
|
||||
if entry_name.ends_with('/') {
|
||||
fs::create_dir_all(entry_path).await?;
|
||||
} else {
|
||||
if !self.args.allow_delete && fs::metadata(&entry_path).await.is_ok() {
|
||||
continue;
|
||||
}
|
||||
if let Some(parent) = entry_path.parent() {
|
||||
if fs::symlink_metadata(parent).await.is_err() {
|
||||
fs::create_dir_all(&parent).await?;
|
||||
}
|
||||
}
|
||||
let mut outfile = fs::File::create(&entry_path).await?;
|
||||
let mut reader = zip.entry_reader(i).await?;
|
||||
io::copy(&mut reader, &mut outfile).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_path(&self, path: &str) -> Option<PathBuf> {
|
||||
let decoded_path = percent_decode(path[1..].as_bytes()).decode_utf8().ok()?;
|
||||
let slashes_switched = if cfg!(windows) {
|
||||
@@ -725,6 +733,7 @@ fn print_listening(address: &str, port: u16, tls: bool) {
|
||||
for addr in addrs {
|
||||
eprintln!(" {}://{}:{}", protocol, addr, port);
|
||||
}
|
||||
eprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user