mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 17:13:02 +03:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4605701366 | ||
|
|
b7c550e09b | ||
|
|
fff8fc3ac5 | ||
|
|
0616602659 | ||
|
|
0a64762df4 | ||
|
|
f103e15e15 | ||
|
|
9dda55b7c8 | ||
|
|
c3dd0f0ec5 | ||
|
|
4167e5c07e | ||
|
|
f66e129985 | ||
|
|
7c3970480e | ||
|
|
34bc8d411a |
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Directories
|
||||||
|
/.git/
|
||||||
|
/.github/
|
||||||
|
/target/
|
||||||
|
/examples/
|
||||||
|
/docs/
|
||||||
|
/benches/
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# Files
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
LICENSE*
|
||||||
43
.github/workflows/release.yaml
vendored
43
.github/workflows/release.yaml
vendored
@@ -6,8 +6,10 @@ on:
|
|||||||
- v[0-9]+.[0-9]+.[0-9]+*
|
- v[0-9]+.[0-9]+.[0-9]+*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
all:
|
release:
|
||||||
name: All
|
name: Publish to Github Reelases
|
||||||
|
outputs:
|
||||||
|
rc: ${{ steps.check-tag.outputs.rc }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -124,3 +126,40 @@ jobs:
|
|||||||
prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
|
prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: Publish to Docker Hub
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
steps:
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and push
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
push: ${{ needs.release.outputs.rc == 'false' }}
|
||||||
|
tags: sigoden/duf:latest, sigoden/duf:${{ github.ref_name }}
|
||||||
|
|
||||||
|
publish-crate:
|
||||||
|
name: Publish to crates.io
|
||||||
|
if: ${{ needs.release.outputs.rc == 'false' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
- name: Publish
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }}
|
||||||
|
run: cargo publish
|
||||||
57
CHANGELOG.md
57
CHANGELOG.md
@@ -2,6 +2,26 @@
|
|||||||
|
|
||||||
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.12.1] - 2022-06-04
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Support webdav ([#10](https://github.com/sigoden/duf/issues/10))
|
||||||
|
- Remove unzip uploaded feature ([#11](https://github.com/sigoden/duf/issues/11))
|
||||||
|
|
||||||
|
## [0.11.0] - 2022-06-03
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Support gracefully shutdown server
|
||||||
|
- Listen 0.0.0.0 by default
|
||||||
|
|
||||||
|
## [0.10.1] - 2022-06-02
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Panic when bind already used port
|
||||||
|
|
||||||
## [0.10.0] - 2022-06-02
|
## [0.10.0] - 2022-06-02
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
@@ -19,10 +39,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Change auth logic/options
|
- Change auth logic/options
|
||||||
- Improve ui
|
- Improve ui
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Insert cli output
|
|
||||||
|
|
||||||
### Refactor
|
### Refactor
|
||||||
|
|
||||||
- Small improvement
|
- Small improvement
|
||||||
@@ -52,12 +68,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Add some headers to res
|
- Add some headers to res
|
||||||
- Support render-index/render-spa
|
- Support render-index/render-spa
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Move src/assets out of src
|
|
||||||
- Update description
|
|
||||||
- Upgrade version
|
|
||||||
|
|
||||||
## [0.7.0] - 2022-05-31
|
## [0.7.0] - 2022-05-31
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
@@ -71,10 +81,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- Drag and drop uploads, upload folder
|
- Drag and drop uploads, upload folder
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Upgrade version
|
|
||||||
|
|
||||||
## [0.6.0] - 2022-05-31
|
## [0.6.0] - 2022-05-31
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -83,10 +89,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Distinct upload and delete operation
|
- Distinct upload and delete operation
|
||||||
- Support range requests
|
- Support range requests
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Upgrade version
|
|
||||||
|
|
||||||
### Refactor
|
### Refactor
|
||||||
|
|
||||||
- Improve code quality
|
- Improve code quality
|
||||||
@@ -99,12 +101,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Add no-auth-read options
|
- Add no-auth-read options
|
||||||
- Unzip zip file when unload
|
- Unzip zip file when unload
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Reorganize web static files
|
|
||||||
- Rename src/static to src/assets
|
|
||||||
- Upgrade version
|
|
||||||
|
|
||||||
## [0.4.0] - 2022-05-29
|
## [0.4.0] - 2022-05-29
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
@@ -112,10 +108,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Replace --static option to --no-edit
|
- Replace --static option to --no-edit
|
||||||
- Add cors
|
- Add cors
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Upgrade version
|
|
||||||
|
|
||||||
## [0.3.0] - 2022-05-29
|
## [0.3.0] - 2022-05-29
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
@@ -162,10 +154,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Add logger
|
- Add logger
|
||||||
- Download folder as zip file
|
- Download folder as zip file
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Update cargo metadata
|
|
||||||
|
|
||||||
## [0.1.0] - 2022-05-26
|
## [0.1.0] - 2022-05-26
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
@@ -183,11 +171,6 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Support delete operation
|
- Support delete operation
|
||||||
- Remove parent path
|
- Remove parent path
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Add readme and license
|
|
||||||
- Update cargo metadata
|
|
||||||
|
|
||||||
### Styling
|
### Styling
|
||||||
|
|
||||||
- Cargo fmt
|
- Cargo fmt
|
||||||
|
|||||||
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -286,7 +286,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "duf"
|
name = "duf"
|
||||||
version = "0.10.0"
|
version = "0.12.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-walkdir",
|
"async-walkdir",
|
||||||
"async_zip",
|
"async_zip",
|
||||||
@@ -882,6 +882,15 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -965,6 +974,7 @@ dependencies = [
|
|||||||
"num_cpus",
|
"num_cpus",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "duf"
|
name = "duf"
|
||||||
version = "0.10.0"
|
version = "0.12.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["sigoden <sigoden@gmail.com>"]
|
authors = ["sigoden <sigoden@gmail.com>"]
|
||||||
description = "Duf is a fully functional file server."
|
description = "Duf is a simple file server."
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
homepage = "https://github.com/sigoden/duf"
|
homepage = "https://github.com/sigoden/duf"
|
||||||
repository = "https://github.com/sigoden/duf"
|
repository = "https://github.com/sigoden/duf"
|
||||||
autotests = false
|
autotests = false
|
||||||
categories = ["command-line-utilities", "web-programming::http-server"]
|
categories = ["command-line-utilities", "web-programming::http-server"]
|
||||||
keywords = ["static", "file", "server", "http", "cli"]
|
keywords = ["static", "file", "server", "webdav", "cli"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "3", default-features = false, features = ["std", "cargo"] }
|
clap = { version = "3", default-features = false, features = ["std", "cargo"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util"]}
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"]}
|
||||||
tokio-rustls = "0.23"
|
tokio-rustls = "0.23"
|
||||||
tokio-stream = { version = "0.1", features = ["net"] }
|
tokio-stream = { version = "0.1", features = ["net"] }
|
||||||
tokio-util = { version = "0.7", features = ["codec", "io-util"] }
|
tokio-util = { version = "0.7", features = ["codec", "io-util"] }
|
||||||
|
|||||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM rust:1.61 as builder
|
||||||
|
RUN rustup target add x86_64-unknown-linux-musl
|
||||||
|
RUN apt-get update && apt-get install --no-install-recommends -y musl-tools
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/duf /bin/
|
||||||
|
ENTRYPOINT ["/bin/duf"]
|
||||||
32
README.md
32
README.md
@@ -3,7 +3,7 @@
|
|||||||
[](https://github.com/sigoden/duf/actions/workflows/ci.yaml)
|
[](https://github.com/sigoden/duf/actions/workflows/ci.yaml)
|
||||||
[](https://crates.io/crates/duf)
|
[](https://crates.io/crates/duf)
|
||||||
|
|
||||||
Duf is a fully functional file server.
|
Duf is a simple file server. Support static serve, search, upload, delete, webdav...
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ Duf is a fully functional file server.
|
|||||||
- Upload files and folders (Drag & Drop)
|
- Upload files and folders (Drag & Drop)
|
||||||
- Delete files
|
- Delete files
|
||||||
- Basic authentication
|
- Basic authentication
|
||||||
- Upload zip file then unzip
|
|
||||||
- Partial responses (Parallel/Resume download)
|
- Partial responses (Parallel/Resume download)
|
||||||
- Support https/tls
|
- Support https
|
||||||
|
- Support webdav
|
||||||
- Easy to use with curl
|
- Easy to use with curl
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -28,6 +28,12 @@ Duf is a fully functional file server.
|
|||||||
cargo install duf
|
cargo install duf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### With docker
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -v /tmp:/tmp -p 5000:5000 --rm -it docker.io/sigoden/duf /tmp
|
||||||
|
```
|
||||||
|
|
||||||
### Binaries on macOS, Linux, Windows
|
### Binaries on macOS, Linux, Windows
|
||||||
|
|
||||||
Download from [Github Releases](https://github.com/sigoden/duf/releases), unzip and add duf to your $PATH.
|
Download from [Github Releases](https://github.com/sigoden/duf/releases), unzip and add duf to your $PATH.
|
||||||
@@ -35,7 +41,7 @@ Download from [Github Releases](https://github.com/sigoden/duf/releases), unzip
|
|||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
Duf is a fully functional file server.
|
Duf is a simple file server.
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
duf [OPTIONS] [path]
|
duf [OPTIONS] [path]
|
||||||
@@ -50,7 +56,7 @@ OPTIONS:
|
|||||||
--allow-delete Allow delete files/folders
|
--allow-delete Allow delete files/folders
|
||||||
--allow-symlink Allow symlink to files/folders outside root directory
|
--allow-symlink Allow symlink to files/folders outside root directory
|
||||||
--allow-upload Allow upload files/folders
|
--allow-upload Allow upload files/folders
|
||||||
-b, --bind <address> Specify bind address [default: 127.0.0.1]
|
-b, --bind <address> Specify bind address [default: 0.0.0.0]
|
||||||
--cors Enable CORS, sets `Access-Control-Allow-Origin: *`
|
--cors Enable CORS, sets `Access-Control-Allow-Origin: *`
|
||||||
-h, --help Print help information
|
-h, --help Print help information
|
||||||
-p, --port <port> Specify port to listen on [default: 5000]
|
-p, --port <port> Specify port to listen on [default: 5000]
|
||||||
@@ -76,18 +82,10 @@ duf
|
|||||||
duf folder_name
|
duf folder_name
|
||||||
```
|
```
|
||||||
|
|
||||||
Listen on all Interfaces and port 3000
|
|
||||||
|
|
||||||
```
|
|
||||||
duf -b 0.0.0.0 -p 3000
|
|
||||||
```
|
|
||||||
|
|
||||||
Allow all operations such as upload, delete
|
Allow all operations such as upload, delete
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
duf --allow-all
|
duf --allow-all
|
||||||
# or
|
|
||||||
duf -A
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Only allow upload operation
|
Only allow upload operation
|
||||||
@@ -102,7 +100,7 @@ Serve a single page application (SPA)
|
|||||||
duf --render-spa
|
duf --render-spa
|
||||||
```
|
```
|
||||||
|
|
||||||
Serve https
|
Use https
|
||||||
|
|
||||||
```
|
```
|
||||||
duf --tls-cert my.crt --tls-key my.key
|
duf --tls-cert my.crt --tls-key my.key
|
||||||
@@ -129,12 +127,6 @@ Upload a file
|
|||||||
curl --upload-file some-file http://127.0.0.1:5000/some-file
|
curl --upload-file some-file http://127.0.0.1:5000/some-file
|
||||||
```
|
```
|
||||||
|
|
||||||
Unzip zip file when unload
|
|
||||||
|
|
||||||
```
|
|
||||||
curl --upload-file some-folder.zip http://127.0.0.1:5000/some-folder.zip?unzip
|
|
||||||
```
|
|
||||||
|
|
||||||
Delete a file/folder
|
Delete a file/folder
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,10 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {object} PathItem
|
||||||
|
* @property {"Dir"|"SymlinkDir"|"File"|"SymlinkFile"} path_type
|
||||||
|
* @property {boolean} is_symlink
|
||||||
|
* @property {string} name
|
||||||
|
* @property {number} mtime
|
||||||
|
* @property {number} size
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type Element
|
||||||
|
*/
|
||||||
let $pathsTable, $pathsTableBody, $uploadersTable;
|
let $pathsTable, $pathsTableBody, $uploadersTable;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
let baseDir;
|
let baseDir;
|
||||||
|
|
||||||
class Uploader {
|
class Uploader {
|
||||||
|
/**
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
idx;
|
idx;
|
||||||
|
/**
|
||||||
|
* @type File
|
||||||
|
*/
|
||||||
file;
|
file;
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
name;
|
name;
|
||||||
|
/**
|
||||||
|
* @type Element
|
||||||
|
*/
|
||||||
$uploadStatus;
|
$uploadStatus;
|
||||||
static globalIdx = 0;
|
static globalIdx = 0;
|
||||||
constructor(file, dirs) {
|
constructor(file, dirs) {
|
||||||
@@ -16,9 +44,6 @@ class Uploader {
|
|||||||
upload() {
|
upload() {
|
||||||
const { file, idx, name } = this;
|
const { file, idx, name } = this;
|
||||||
let url = getUrl(name);
|
let url = getUrl(name);
|
||||||
if (file.name == baseDir + ".zip") {
|
|
||||||
url += "?unzip";
|
|
||||||
}
|
|
||||||
$uploadersTable.insertAdjacentHTML("beforeend", `
|
$uploadersTable.insertAdjacentHTML("beforeend", `
|
||||||
<tr id="upload${idx}" class="uploader">
|
<tr id="upload${idx}" class="uploader">
|
||||||
<td class="path cell-name">
|
<td class="path cell-name">
|
||||||
@@ -34,7 +59,7 @@ class Uploader {
|
|||||||
ajax.upload.addEventListener("progress", e => this.progress(e), false);
|
ajax.upload.addEventListener("progress", e => this.progress(e), false);
|
||||||
ajax.addEventListener("readystatechange", () => {
|
ajax.addEventListener("readystatechange", () => {
|
||||||
if(ajax.readyState === 4) {
|
if(ajax.readyState === 4) {
|
||||||
if (ajax.status == 200) {
|
if (ajax.status >= 200 && ajax.status < 300) {
|
||||||
this.complete();
|
this.complete();
|
||||||
} else {
|
} else {
|
||||||
this.fail();
|
this.fail();
|
||||||
@@ -61,6 +86,10 @@ class Uploader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add breadcumb
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
function addBreadcrumb(value) {
|
function addBreadcrumb(value) {
|
||||||
const $breadcrumb = document.querySelector(".breadcrumb");
|
const $breadcrumb = document.querySelector(".breadcrumb");
|
||||||
const parts = value.split("/").filter(v => !!v);
|
const parts = value.split("/").filter(v => !!v);
|
||||||
@@ -83,6 +112,11 @@ function addBreadcrumb(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add pathitem
|
||||||
|
* @param {PathItem} file
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
function addPath(file, index) {
|
function addPath(file, index) {
|
||||||
const url = getUrl(file.name)
|
const url = getUrl(file.name)
|
||||||
let actionDelete = "";
|
let actionDelete = "";
|
||||||
@@ -126,6 +160,11 @@ function addPath(file, index) {
|
|||||||
</tr>`)
|
</tr>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete pathitem
|
||||||
|
* @param {number} index
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
async function deletePath(index) {
|
async function deletePath(index) {
|
||||||
const file = DATA.paths[index];
|
const file = DATA.paths[index];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@@ -136,7 +175,7 @@ async function deletePath(index) {
|
|||||||
const res = await fetch(getUrl(file.name), {
|
const res = await fetch(getUrl(file.name), {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
if (res.status === 200) {
|
if (res.status >= 200 && res.status < 300) {
|
||||||
document.getElementById(`addPath${index}`).remove();
|
document.getElementById(`addPath${index}`).remove();
|
||||||
DATA.paths[index] = null;
|
DATA.paths[index] = null;
|
||||||
if (!DATA.paths.find(v => !!v)) {
|
if (!DATA.paths.find(v => !!v)) {
|
||||||
|
|||||||
25
src/args.rs
25
src/args.rs
@@ -16,7 +16,7 @@ fn app() -> clap::Command<'static> {
|
|||||||
Arg::new("address")
|
Arg::new("address")
|
||||||
.short('b')
|
.short('b')
|
||||||
.long("bind")
|
.long("bind")
|
||||||
.default_value("127.0.0.1")
|
.default_value("0.0.0.0")
|
||||||
.help("Specify bind address")
|
.help("Specify bind address")
|
||||||
.value_name("address"),
|
.value_name("address"),
|
||||||
)
|
)
|
||||||
@@ -134,7 +134,9 @@ impl Args {
|
|||||||
let address = matches.value_of("address").unwrap_or_default().to_owned();
|
let address = matches.value_of("address").unwrap_or_default().to_owned();
|
||||||
let port = matches.value_of_t::<u16>("port")?;
|
let port = matches.value_of_t::<u16>("port")?;
|
||||||
let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
|
let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
|
||||||
let path_prefix = matches.value_of("path-prefix").map(|v| v.to_owned());
|
let path_prefix = matches
|
||||||
|
.value_of("path-prefix")
|
||||||
|
.map(|v| v.trim_matches('/').to_owned());
|
||||||
let cors = matches.is_present("cors");
|
let cors = matches.is_present("cors");
|
||||||
let auth = matches.value_of("auth").map(|v| v.to_owned());
|
let auth = matches.value_of("auth").map(|v| v.to_owned());
|
||||||
let no_auth_access = matches.is_present("no-auth-access");
|
let no_auth_access = matches.is_present("no-auth-access");
|
||||||
@@ -173,7 +175,7 @@ impl Args {
|
|||||||
fn parse_path<P: AsRef<Path>>(path: P) -> BoxResult<PathBuf> {
|
fn parse_path<P: AsRef<Path>>(path: P) -> BoxResult<PathBuf> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
bail!("error: path \"{}\" doesn't exist", path.display());
|
return Err(format!("Path `{}` doesn't exist", path.display()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
env::current_dir()
|
env::current_dir()
|
||||||
@@ -181,27 +183,14 @@ impl Args {
|
|||||||
p.push(path); // If path is absolute, it replaces the current path.
|
p.push(path); // If path is absolute, it replaces the current path.
|
||||||
std::fs::canonicalize(p)
|
std::fs::canonicalize(p)
|
||||||
})
|
})
|
||||||
.or_else(|err| {
|
.map_err(|err| format!("Failed to access path `{}`: {}", path.display(), err,).into())
|
||||||
bail!(
|
|
||||||
"error: failed to access path \"{}\": {}",
|
|
||||||
path.display(),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct socket address from arguments.
|
/// Construct socket address from arguments.
|
||||||
pub fn address(&self) -> BoxResult<SocketAddr> {
|
pub fn address(&self) -> BoxResult<SocketAddr> {
|
||||||
format!("{}:{}", self.address, self.port)
|
format!("{}:{}", self.address, self.port)
|
||||||
.parse()
|
.parse()
|
||||||
.or_else(|err| {
|
.map_err(|_| format!("Invalid bind address `{}:{}`", self.address, self.port).into())
|
||||||
bail!(
|
|
||||||
"error: invalid address {}:{} : {}",
|
|
||||||
self.address,
|
|
||||||
self.port,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
macro_rules! bail {
|
|
||||||
($($tt:tt)*) => {
|
|
||||||
return Err(From::from(format!($($tt)*)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
@@ -23,6 +17,6 @@ async fn run() -> BoxResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_err<T>(err: Box<dyn std::error::Error>) -> T {
|
fn handle_err<T>(err: Box<dyn std::error::Error>) -> T {
|
||||||
eprintln!("Server error: {}", err);
|
eprintln!("error: {}", err);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
423
src/server.rs
423
src/server.rs
@@ -1,10 +1,9 @@
|
|||||||
use crate::{Args, BoxResult};
|
use crate::{Args, BoxResult};
|
||||||
|
|
||||||
use async_walkdir::WalkDir;
|
use async_walkdir::WalkDir;
|
||||||
use async_zip::read::seek::ZipFileReader;
|
|
||||||
use async_zip::write::{EntryOptions, ZipFileWriter};
|
use async_zip::write::{EntryOptions, ZipFileWriter};
|
||||||
use async_zip::Compression;
|
use async_zip::Compression;
|
||||||
use chrono::Local;
|
use chrono::{Local, TimeZone, Utc};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use get_if_addrs::get_if_addrs;
|
use get_if_addrs::get_if_addrs;
|
||||||
@@ -18,7 +17,7 @@ use hyper::header::{
|
|||||||
WWW_AUTHENTICATE,
|
WWW_AUTHENTICATE,
|
||||||
};
|
};
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::{Body, Method, StatusCode};
|
use hyper::{Body, Method, StatusCode, Uri};
|
||||||
use percent_encoding::percent_decode;
|
use percent_encoding::percent_decode;
|
||||||
use rustls::ServerConfig;
|
use rustls::ServerConfig;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -53,52 +52,65 @@ macro_rules! status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve(args: Args) -> BoxResult<()> {
|
pub async fn serve(args: Args) -> BoxResult<()> {
|
||||||
|
match args.tls.as_ref() {
|
||||||
|
Some(_) => serve_https(args).await,
|
||||||
|
None => serve_http(args).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve_https(args: Args) -> BoxResult<()> {
|
||||||
|
let args = Arc::new(args);
|
||||||
|
let socket_addr = args.address()?;
|
||||||
|
let (certs, key) = args.tls.clone().unwrap();
|
||||||
|
let inner = Arc::new(InnerService::new(args.clone()));
|
||||||
|
let config = ServerConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(certs, key)?;
|
||||||
|
let tls_acceptor = TlsAcceptor::from(Arc::new(config));
|
||||||
|
let arc_acceptor = Arc::new(tls_acceptor);
|
||||||
|
let listener = TcpListener::bind(&socket_addr).await?;
|
||||||
|
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
|
||||||
|
let incoming = hyper::server::accept::from_stream(incoming.filter_map(|socket| async {
|
||||||
|
match socket {
|
||||||
|
Ok(stream) => match arc_acceptor.clone().accept(stream).await {
|
||||||
|
Ok(val) => Some(Ok::<_, Infallible>(val)),
|
||||||
|
Err(_) => None,
|
||||||
|
},
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
let server = hyper::Server::builder(incoming).serve(make_service_fn(move |_| {
|
||||||
|
let inner = inner.clone();
|
||||||
|
async move {
|
||||||
|
Ok::<_, Infallible>(service_fn(move |req| {
|
||||||
|
let inner = inner.clone();
|
||||||
|
inner.call(req)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
print_listening(args.address.as_str(), args.port, true);
|
||||||
|
let graceful = server.with_graceful_shutdown(shutdown_signal());
|
||||||
|
graceful.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve_http(args: Args) -> BoxResult<()> {
|
||||||
let args = Arc::new(args);
|
let args = Arc::new(args);
|
||||||
let socket_addr = args.address()?;
|
let socket_addr = args.address()?;
|
||||||
let inner = Arc::new(InnerService::new(args.clone()));
|
let inner = Arc::new(InnerService::new(args.clone()));
|
||||||
if let Some((certs, key)) = args.tls.as_ref() {
|
let server = hyper::Server::try_bind(&socket_addr)?.serve(make_service_fn(move |_| {
|
||||||
let config = ServerConfig::builder()
|
let inner = inner.clone();
|
||||||
.with_safe_defaults()
|
async move {
|
||||||
.with_no_client_auth()
|
Ok::<_, Infallible>(service_fn(move |req| {
|
||||||
.with_single_cert(certs.clone(), key.clone())?;
|
let inner = inner.clone();
|
||||||
let tls_acceptor = TlsAcceptor::from(Arc::new(config));
|
inner.call(req)
|
||||||
let arc_acceptor = Arc::new(tls_acceptor);
|
}))
|
||||||
let listener = TcpListener::bind(&socket_addr).await.unwrap();
|
}
|
||||||
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
|
}));
|
||||||
let incoming = hyper::server::accept::from_stream(incoming.filter_map(|socket| async {
|
print_listening(args.address.as_str(), args.port, false);
|
||||||
match socket {
|
let graceful = server.with_graceful_shutdown(shutdown_signal());
|
||||||
Ok(stream) => match arc_acceptor.clone().accept(stream).await {
|
graceful.await?;
|
||||||
Ok(val) => Some(Ok::<_, Infallible>(val)),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
let server = hyper::Server::builder(incoming).serve(make_service_fn(move |_| {
|
|
||||||
let inner = inner.clone();
|
|
||||||
async move {
|
|
||||||
Ok::<_, Infallible>(service_fn(move |req| {
|
|
||||||
let inner = inner.clone();
|
|
||||||
inner.call(req)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
print_listening(args.address.as_str(), args.port, true);
|
|
||||||
server.await?;
|
|
||||||
} else {
|
|
||||||
let server = hyper::Server::bind(&socket_addr).serve(make_service_fn(move |_| {
|
|
||||||
let inner = inner.clone();
|
|
||||||
async move {
|
|
||||||
Ok::<_, Infallible>(service_fn(move |req| {
|
|
||||||
let inner = inner.clone();
|
|
||||||
inner.call(req)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
print_listening(args.address.as_str(), args.port, false);
|
|
||||||
server.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +188,8 @@ impl InnerService {
|
|||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
match *req.method() {
|
match req.method() {
|
||||||
Method::GET => {
|
&Method::GET => {
|
||||||
let headers = req.headers();
|
let headers = req.headers();
|
||||||
if is_dir {
|
if is_dir {
|
||||||
if render_index || render_spa {
|
if render_index || render_spa {
|
||||||
@@ -199,28 +211,46 @@ impl InnerService {
|
|||||||
status!(res, StatusCode::NOT_FOUND);
|
status!(res, StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Method::OPTIONS => {
|
&Method::OPTIONS => {
|
||||||
status!(res, StatusCode::NO_CONTENT);
|
self.handle_method_options(&mut res);
|
||||||
}
|
}
|
||||||
Method::PUT => {
|
&Method::PUT => {
|
||||||
if !allow_upload || (!allow_delete && is_file) {
|
if !allow_upload || (!allow_delete && is_file) {
|
||||||
status!(res, StatusCode::FORBIDDEN);
|
status!(res, StatusCode::FORBIDDEN);
|
||||||
} else {
|
} else {
|
||||||
self.handle_upload(path, req, &mut res).await?;
|
self.handle_upload(path, req, &mut res).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Method::DELETE => {
|
&Method::DELETE => {
|
||||||
if !allow_delete {
|
if !allow_delete {
|
||||||
status!(res, StatusCode::FORBIDDEN);
|
status!(res, StatusCode::FORBIDDEN);
|
||||||
} else if !is_miss {
|
} else if !is_miss {
|
||||||
self.handle_delete(path, is_dir).await?
|
self.handle_delete(path, is_dir, &mut res).await?
|
||||||
} else {
|
} else {
|
||||||
status!(res, StatusCode::NOT_FOUND);
|
status!(res, StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
method => match method.as_str() {
|
||||||
status!(res, StatusCode::METHOD_NOT_ALLOWED);
|
"PROPFIND" => {
|
||||||
}
|
if is_dir {
|
||||||
|
self.handle_propfind_dir(path, &mut res).await?;
|
||||||
|
} else if is_file {
|
||||||
|
self.handle_propfind_file(path, &mut res).await?;
|
||||||
|
} else {
|
||||||
|
status!(res, StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"MKCOL" if allow_upload && is_miss => self.handle_mkcol(path, &mut res).await?,
|
||||||
|
"COPY" if allow_upload && !is_miss => {
|
||||||
|
self.handle_copy(path, req.headers(), &mut res).await?
|
||||||
|
}
|
||||||
|
"MOVE" if allow_upload && allow_delete && !is_miss => {
|
||||||
|
self.handle_move(path, req.headers(), &mut res).await?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
status!(res, StatusCode::METHOD_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
@@ -231,20 +261,7 @@ impl InnerService {
|
|||||||
mut req: Request,
|
mut req: Request,
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> BoxResult<()> {
|
) -> BoxResult<()> {
|
||||||
let ensure_parent = match path.parent() {
|
ensure_path_parent(path).await?;
|
||||||
Some(parent) => match fs::metadata(parent).await {
|
|
||||||
Ok(meta) => meta.is_dir(),
|
|
||||||
Err(_) => {
|
|
||||||
fs::create_dir_all(parent).await?;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => false,
|
|
||||||
};
|
|
||||||
if !ensure_parent {
|
|
||||||
status!(res, StatusCode::FORBIDDEN);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file = fs::File::create(&path).await?;
|
let mut file = fs::File::create(&path).await?;
|
||||||
|
|
||||||
@@ -258,43 +275,31 @@ impl InnerService {
|
|||||||
|
|
||||||
io::copy(&mut body_reader, &mut file).await?;
|
io::copy(&mut body_reader, &mut file).await?;
|
||||||
|
|
||||||
let query = req.uri().query().unwrap_or_default();
|
status!(res, StatusCode::CREATED);
|
||||||
if query == "unzip" {
|
|
||||||
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?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_delete(&self, path: &Path, is_dir: bool) -> BoxResult<()> {
|
async fn handle_delete(&self, path: &Path, is_dir: bool, res: &mut Response) -> BoxResult<()> {
|
||||||
match is_dir {
|
match is_dir {
|
||||||
true => fs::remove_dir_all(path).await?,
|
true => fs::remove_dir_all(path).await?,
|
||||||
false => fs::remove_file(path).await?,
|
false => fs::remove_file(path).await?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status!(res, StatusCode::NO_CONTENT);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_ls_dir(&self, path: &Path, exist: bool, res: &mut Response) -> BoxResult<()> {
|
async fn handle_ls_dir(&self, path: &Path, exist: bool, res: &mut Response) -> BoxResult<()> {
|
||||||
let mut paths: Vec<PathItem> = vec![];
|
let mut paths = vec![];
|
||||||
if exist {
|
if exist {
|
||||||
let mut rd = match fs::read_dir(path).await {
|
paths = match self.list_dir(path, path, false).await {
|
||||||
Ok(rd) => rd,
|
Ok(paths) => paths,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
status!(res, StatusCode::FORBIDDEN);
|
status!(res, StatusCode::FORBIDDEN);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
|
||||||
while let Some(entry) = rd.next_entry().await? {
|
|
||||||
let entry_path = entry.path();
|
|
||||||
if let Ok(Some(item)) = self.to_pathitem(entry_path, path.to_path_buf()).await {
|
|
||||||
paths.push(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
self.send_index(path, paths, res)
|
self.send_index(path, paths, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +453,110 @@ impl InnerService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_method_options(&self, res: &mut Response) {
|
||||||
|
let allow_upload = self.args.allow_upload;
|
||||||
|
let allow_delete = self.args.allow_delete;
|
||||||
|
let mut methods = vec!["GET", "PROPFIND", "OPTIONS"];
|
||||||
|
if allow_upload {
|
||||||
|
methods.extend(["PUT", "COPY", "MKCOL"]);
|
||||||
|
}
|
||||||
|
if allow_delete {
|
||||||
|
methods.push("DELETE");
|
||||||
|
}
|
||||||
|
if allow_upload && allow_delete {
|
||||||
|
methods.push("COPY");
|
||||||
|
}
|
||||||
|
let value = methods.join(",").parse().unwrap();
|
||||||
|
res.headers_mut().insert("Allow", value);
|
||||||
|
res.headers_mut().insert("DAV", "1".parse().unwrap());
|
||||||
|
|
||||||
|
status!(res, StatusCode::NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_propfind_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
|
||||||
|
let paths = match self.list_dir(path, &self.args.path, true).await {
|
||||||
|
Ok(paths) => paths,
|
||||||
|
Err(_) => {
|
||||||
|
status!(res, StatusCode::FORBIDDEN);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let output = paths
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.xml(self.args.path_prefix.as_ref()))
|
||||||
|
.fold(String::new(), |mut acc, v| {
|
||||||
|
acc.push_str(&v);
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
res_propfind(res, &output);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
|
||||||
|
if let Some(pathitem) = self.to_pathitem(path, &self.args.path).await? {
|
||||||
|
res_propfind(res, &pathitem.xml(self.args.path_prefix.as_ref()));
|
||||||
|
} else {
|
||||||
|
status!(res, StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_mkcol(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
|
||||||
|
fs::create_dir_all(path).await?;
|
||||||
|
status!(res, StatusCode::CREATED);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_copy(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
headers: &HeaderMap<HeaderValue>,
|
||||||
|
res: &mut Response,
|
||||||
|
) -> BoxResult<()> {
|
||||||
|
let dest = match self.extract_dest(headers) {
|
||||||
|
Some(dest) => dest,
|
||||||
|
None => {
|
||||||
|
status!(res, StatusCode::BAD_REQUEST);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = fs::symlink_metadata(path).await?;
|
||||||
|
if meta.is_dir() {
|
||||||
|
status!(res, StatusCode::BAD_REQUEST);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_path_parent(&dest).await?;
|
||||||
|
|
||||||
|
fs::copy(path, &dest).await?;
|
||||||
|
|
||||||
|
status!(res, StatusCode::NO_CONTENT);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_move(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
headers: &HeaderMap<HeaderValue>,
|
||||||
|
res: &mut Response,
|
||||||
|
) -> BoxResult<()> {
|
||||||
|
let dest = match self.extract_dest(headers) {
|
||||||
|
Some(dest) => dest,
|
||||||
|
None => {
|
||||||
|
status!(res, StatusCode::BAD_REQUEST);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ensure_path_parent(&dest).await?;
|
||||||
|
|
||||||
|
fs::rename(path, &dest).await?;
|
||||||
|
|
||||||
|
status!(res, StatusCode::NO_CONTENT);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn send_index(
|
fn send_index(
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
@@ -521,30 +630,10 @@ impl InnerService {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unzip_file(&self, path: &Path) -> BoxResult<()> {
|
fn extract_dest(&self, headers: &HeaderMap<HeaderValue>) -> Option<PathBuf> {
|
||||||
let root = path.parent().unwrap();
|
let dest = headers.get("Destination")?.to_str().ok()?;
|
||||||
let mut zip = ZipFileReader::new(File::open(&path).await?).await?;
|
let uri: Uri = dest.parse().ok()?;
|
||||||
for i in 0..zip.entries().len() {
|
self.extract_path(uri.path())
|
||||||
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> {
|
fn extract_path(&self, path: &str) -> Option<PathBuf> {
|
||||||
@@ -572,6 +661,26 @@ impl InnerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_dir(
|
||||||
|
&self,
|
||||||
|
entry_path: &Path,
|
||||||
|
base_path: &Path,
|
||||||
|
include_entry: bool,
|
||||||
|
) -> BoxResult<Vec<PathItem>> {
|
||||||
|
let mut paths: Vec<PathItem> = vec![];
|
||||||
|
if include_entry {
|
||||||
|
paths.push(self.to_pathitem(entry_path, base_path).await?.unwrap())
|
||||||
|
}
|
||||||
|
let mut rd = fs::read_dir(entry_path).await?;
|
||||||
|
while let Ok(Some(entry)) = rd.next_entry().await {
|
||||||
|
let entry_path = entry.path();
|
||||||
|
if let Ok(Some(item)) = self.to_pathitem(entry_path.as_path(), base_path).await {
|
||||||
|
paths.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(paths)
|
||||||
|
}
|
||||||
|
|
||||||
async fn to_pathitem<P: AsRef<Path>>(
|
async fn to_pathitem<P: AsRef<Path>>(
|
||||||
&self,
|
&self,
|
||||||
path: P,
|
path: P,
|
||||||
@@ -597,9 +706,15 @@ impl InnerService {
|
|||||||
PathType::Dir | PathType::SymlinkDir => None,
|
PathType::Dir | PathType::SymlinkDir => None,
|
||||||
PathType::File | PathType::SymlinkFile => Some(meta.len()),
|
PathType::File | PathType::SymlinkFile => Some(meta.len()),
|
||||||
};
|
};
|
||||||
|
let base_name = rel_path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|v| v.to_str())
|
||||||
|
.unwrap_or("/")
|
||||||
|
.to_owned();
|
||||||
let name = normalize_path(rel_path);
|
let name = normalize_path(rel_path);
|
||||||
Ok(Some(PathItem {
|
Ok(Some(PathItem {
|
||||||
path_type,
|
path_type,
|
||||||
|
base_name,
|
||||||
name,
|
name,
|
||||||
mtime,
|
mtime,
|
||||||
size,
|
size,
|
||||||
@@ -607,7 +722,7 @@ impl InnerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Serialize)]
|
||||||
struct IndexData {
|
struct IndexData {
|
||||||
breadcrumb: String,
|
breadcrumb: String,
|
||||||
paths: Vec<PathItem>,
|
paths: Vec<PathItem>,
|
||||||
@@ -618,11 +733,63 @@ struct IndexData {
|
|||||||
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
struct PathItem {
|
struct PathItem {
|
||||||
path_type: PathType,
|
path_type: PathType,
|
||||||
|
base_name: String,
|
||||||
name: String,
|
name: String,
|
||||||
mtime: u64,
|
mtime: u64,
|
||||||
size: Option<u64>,
|
size: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PathItem {
|
||||||
|
pub fn xml(&self, prefix: Option<&String>) -> String {
|
||||||
|
let prefix = match prefix {
|
||||||
|
Some(value) => format!("/{}/", value),
|
||||||
|
None => "/".to_owned(),
|
||||||
|
};
|
||||||
|
let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822();
|
||||||
|
match self.path_type {
|
||||||
|
PathType::Dir | PathType::SymlinkDir => format!(
|
||||||
|
r#"<D:response>
|
||||||
|
<D:href>{}{}</D:href>
|
||||||
|
<D:propstat>
|
||||||
|
<D:prop>
|
||||||
|
<D:displayname>{}</D:displayname>
|
||||||
|
<D:getlastmodified>{}</D:getlastmodified>
|
||||||
|
<D:resourcetype><D:collection/></D:resourcetype>
|
||||||
|
<D:lockdiscovery/>
|
||||||
|
<D:supportedlock>
|
||||||
|
</D:supportedlock>
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
</D:response>"#,
|
||||||
|
prefix, self.name, self.base_name, mtime
|
||||||
|
),
|
||||||
|
PathType::File | PathType::SymlinkFile => format!(
|
||||||
|
r#"<D:response>
|
||||||
|
<D:href>{}{}</D:href>
|
||||||
|
<D:propstat>
|
||||||
|
<D:prop>
|
||||||
|
<D:displayname>{}</D:displayname>
|
||||||
|
<D:getcontentlength>{}</D:getcontentlength>
|
||||||
|
<D:getlastmodified>{}</D:getlastmodified>
|
||||||
|
<D:resourcetype></D:resourcetype>
|
||||||
|
<D:lockdiscovery/>
|
||||||
|
<D:supportedlock>
|
||||||
|
</D:supportedlock>
|
||||||
|
</D:prop>
|
||||||
|
<D:status>HTTP/1.1 200 OK</D:status>
|
||||||
|
</D:propstat>
|
||||||
|
</D:response>"#,
|
||||||
|
prefix,
|
||||||
|
self.name,
|
||||||
|
self.base_name,
|
||||||
|
self.size.unwrap_or_default(),
|
||||||
|
mtime
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
enum PathType {
|
enum PathType {
|
||||||
Dir,
|
Dir,
|
||||||
@@ -646,6 +813,15 @@ fn normalize_path<P: AsRef<Path>>(path: P) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ensure_path_parent(path: &Path) -> BoxResult<()> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
if fs::symlink_metadata(parent).await.is_err() {
|
||||||
|
fs::create_dir_all(&parent).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn add_cors(res: &mut Response) {
|
fn add_cors(res: &mut Response) {
|
||||||
res.headers_mut()
|
res.headers_mut()
|
||||||
.typed_insert(AccessControlAllowOrigin::ANY);
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
||||||
@@ -656,6 +832,17 @@ fn add_cors(res: &mut Response) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn res_propfind(res: &mut Response, content: &str) {
|
||||||
|
*res.status_mut() = StatusCode::MULTI_STATUS;
|
||||||
|
*res.body_mut() = Body::from(format!(
|
||||||
|
r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<D:multistatus xmlns:D="DAV:">
|
||||||
|
{}
|
||||||
|
</D:multistatus>"#,
|
||||||
|
content,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
async fn zip_dir<W: AsyncWrite + Unpin>(writer: &mut W, dir: &Path) -> BoxResult<()> {
|
async fn zip_dir<W: AsyncWrite + Unpin>(writer: &mut W, dir: &Path) -> BoxResult<()> {
|
||||||
let mut writer = ZipFileWriter::new(writer);
|
let mut writer = ZipFileWriter::new(writer);
|
||||||
let mut walkdir = WalkDir::new(dir);
|
let mut walkdir = WalkDir::new(dir);
|
||||||
@@ -751,3 +938,9 @@ fn retrive_listening_addrs(address: &str) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
vec![address.to_owned()]
|
vec![address.to_owned()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn shutdown_signal() {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("Failed to install CTRL+C signal handler")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user