Compare commits

...

17 Commits

Author SHA1 Message Date
sigoden
e9383d71ed chore(release): version v0.15.0 2022-06-10 08:41:51 +08:00
sigoden
8258dabe4a fix: query dir param 2022-06-10 08:00:27 +08:00
Joe Koop
0e236b61f6 feat: add empty state placeholder to page(#30)
* added "Empty folder" text to the page

* added text for nonexistent directory and no search results
2022-06-10 07:41:09 +08:00
sigoden
09788ed031 chore: update favicon 2022-06-09 22:49:01 +08:00
Joe Koop
46ebe978ae feat: add basic dark theme (#29) 2022-06-09 22:16:43 +08:00
sigoden
e01f2030e1 chore: optimize code 2022-06-09 21:35:52 +08:00
Joe Koop
8d03ec151a fix: encode webdav href as uri (#28)
* Revert "fix: filename xml escaping"

This reverts commit ce154d9ebc.

* webdav filenames are fixed
2022-06-09 21:28:35 +08:00
sigoden
870e92e306 chore(release): version v0.14.0 2022-06-07 09:02:43 +08:00
sigoden
261c8b6ee5 feat: add favicon (#27)
Return favicon only if requested, avoid 404 errors

close #16
2022-06-07 08:59:44 +08:00
sigoden
5ce7bde05c fix: send index page with content-type (#26) 2022-06-06 11:20:42 +08:00
sigoden
63a7b530bb feat: support ipv6 (#25) 2022-06-06 10:52:12 +08:00
sigoden
7481db5071 chore(release): version v0.13.2 2022-06-06 08:03:00 +08:00
sigoden
b0cc901416 fix: escape path-prefix/url-prefix different 2022-06-06 08:00:26 +08:00
Joe Koop
ce154d9ebc fix: filename xml escaping 2022-06-06 07:54:12 +08:00
sigoden
7c4c264206 chore(release): version v0.13.1 2022-06-06 07:15:48 +08:00
sigoden
c1e0c6bb2f refactor: use logger (#22) 2022-06-06 07:13:22 +08:00
sigoden
f138915f20 fix: escape filename (#21)
close #19
2022-06-06 06:51:35 +08:00
10 changed files with 249 additions and 91 deletions

View File

@@ -145,7 +145,7 @@ jobs:
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
push: ${{ needs.release.outputs.rc == 'false' }} push: ${{ needs.release.outputs.rc == 'false' }}
tags: sigoden/duf:latest, sigoden/duf:${{ github.ref_name }} tags: ${{ github.repository }}:latest, ${{ github.repository }}:${{ github.ref_name }}
publish-crate: publish-crate:
name: Publish to crates.io name: Publish to crates.io

View File

@@ -2,6 +2,46 @@
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.15.0] - 2022-06-10
### Bug Fixes
- Encode webdav href as uri ([#28](https://github.com/sigoden/duf/issues/28))
- Query dir param
### Features
- Add basic dark theme ([#29](https://github.com/sigoden/duf/issues/29))
- Add empty state placeholder to page([#30](https://github.com/sigoden/duf/issues/30))
## [0.14.0] - 2022-06-07
### Bug Fixes
- Send index page with content-type ([#26](https://github.com/sigoden/duf/issues/26))
### Features
- Support ipv6 ([#25](https://github.com/sigoden/duf/issues/25))
- Add favicon ([#27](https://github.com/sigoden/duf/issues/27))
## [0.13.2] - 2022-06-06
### Bug Fixes
- Filename xml escaping
- Escape path-prefix/url-prefix different
## [0.13.1] - 2022-06-05
### Bug Fixes
- Escape filename ([#21](https://github.com/sigoden/duf/issues/21))
### Refactor
- Use logger ([#22](https://github.com/sigoden/duf/issues/22))
## [0.13.0] - 2022-06-05 ## [0.13.0] - 2022-06-05
### Bug Fixes ### Bug Fixes

90
Cargo.lock generated
View File

@@ -286,18 +286,20 @@ dependencies = [
[[package]] [[package]]
name = "duf" name = "duf"
version = "0.13.0" version = "0.15.0"
dependencies = [ dependencies = [
"async-walkdir", "async-walkdir",
"async_zip", "async_zip",
"base64", "base64",
"chrono", "chrono",
"clap", "clap",
"env_logger",
"futures", "futures",
"get_if_addrs", "get_if_addrs",
"headers", "headers",
"hyper", "hyper",
"lazy_static", "lazy_static",
"log",
"md5", "md5",
"mime_guess", "mime_guess",
"percent-encoding", "percent-encoding",
@@ -309,7 +311,19 @@ dependencies = [
"tokio-rustls", "tokio-rustls",
"tokio-stream", "tokio-stream",
"tokio-util", "tokio-util",
"urlencoding",
"uuid", "uuid",
"xml-rs",
]
[[package]]
name = "env_logger"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"humantime",
"log",
] ]
[[package]] [[package]]
@@ -329,13 +343,11 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.23" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [ dependencies = [
"cfg-if",
"crc32fast", "crc32fast",
"libc",
"miniz_oxide", "miniz_oxide",
] ]
@@ -540,9 +552,9 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -572,6 +584,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.19" version = "0.14.19"
@@ -661,9 +679,9 @@ dependencies = [
[[package]] [[package]]
name = "lzma-sys" name = "lzma-sys"
version = "0.1.17" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb4b7c3eddad11d3af9e86c487607d2d2442d185d848575365c4856ba96d619" checksum = "e06754c4acf47d49c727d5665ca9fb828851cda315ed3bd51edd148ef78a8772"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -700,9 +718,9 @@ dependencies = [
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.5.1" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
dependencies = [ dependencies = [
"adler", "adler",
] ]
@@ -971,9 +989,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.95" version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1019,9 +1037,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.18.2" version = "1.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -1038,9 +1056,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.7.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1060,9 +1078,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
@@ -1071,9 +1089,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.2" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
@@ -1097,21 +1115,9 @@ checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.26" version = "0.1.26"
@@ -1154,6 +1160,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "urlencoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.1.1" version = "1.1.1"
@@ -1344,10 +1356,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]] [[package]]
name = "xz2" name = "xml-rs"
version = "0.1.6" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c179869f34fc7c01830d3ce7ea2086bc3a07e0d35289b667d0a8bf910258926c" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [ dependencies = [
"lzma-sys", "lzma-sys",
] ]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "duf" name = "duf"
version = "0.13.0" version = "0.15.0"
edition = "2021" edition = "2021"
authors = ["sigoden <sigoden@gmail.com>"] authors = ["sigoden <sigoden@gmail.com>"]
description = "Duf is a simple file server." description = "Duf is a simple file server."
@@ -34,6 +34,10 @@ rustls-pemfile = "1"
md5 = "0.7.0" md5 = "0.7.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
uuid = { version = "1.1.1", features = ["v4", "fast-rng"] } uuid = { version = "1.1.1", features = ["v4", "fast-rng"] }
urlencoding = "2.1.0"
xml-rs = "0.8"
env_logger = { version = "0.9.0", default-features = false, features = ["humantime"] }
log = "0.4.17"
[profile.release] [profile.release]
lto = true lto = true

BIN
assets/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -97,6 +97,11 @@ body {
padding: 0 1em; padding: 0 1em;
} }
.empty-folder {
display: block;
padding-top: 1rem;
}
.uploaders-table th, .uploaders-table th,
.paths-table th { .paths-table th {
text-align: left; text-align: left;
@@ -173,3 +178,39 @@ body {
.uploader { .uploader {
padding-right: 1em; padding-right: 1em;
} }
/* dark theme */
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
html,
.breadcrumb>b,
.searchbar #search {
color: #fff;
}
.uploaders-table th,
.paths-table th {
color: #ddd;
}
svg,
.path svg {
fill: #d0e6ff;
}
.searchbar {
background-color: #111;
border-color: #fff6;
}
.searchbar svg {
fill: #fff6;
}
.path a {
color: #3191ff;
}
}

View File

@@ -7,6 +7,12 @@
* @property {number} size * @property {number} size
*/ */
// https://stackoverflow.com/a/901144/3642588
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
const dirEmptyNote = params.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded';
/** /**
* @type Element * @type Element
@@ -54,6 +60,7 @@ class Uploader {
</tr>`); </tr>`);
$uploadersTable.classList.remove("hidden"); $uploadersTable.classList.remove("hidden");
this.$uploadStatus = document.getElementById(`uploadStatus${idx}`); this.$uploadStatus = document.getElementById(`uploadStatus${idx}`);
document.querySelector('.main i.empty-folder').remove();
const ajax = new XMLHttpRequest(); const ajax = new XMLHttpRequest();
ajax.upload.addEventListener("progress", e => this.progress(e), false); ajax.upload.addEventListener("progress", e => this.progress(e), false);
@@ -180,6 +187,8 @@ async function deletePath(index) {
DATA.paths[index] = null; DATA.paths[index] = null;
if (!DATA.paths.find(v => !!v)) { if (!DATA.paths.find(v => !!v)) {
$pathsTable.classList.add("hidden"); $pathsTable.classList.add("hidden");
document.querySelector('.main').insertAdjacentHTML("afterbegin", '<i class="empty-folder"></i>');
document.querySelector('.main .empty-folder').textContent = dirEmptyNote;
} }
} else { } else {
throw new Error(await res.text()) throw new Error(await res.text())
@@ -275,6 +284,10 @@ function ready() {
$pathsTableBody = document.querySelector(".paths-table tbody"); $pathsTableBody = document.querySelector(".paths-table tbody");
$uploadersTable = document.querySelector(".uploaders-table"); $uploadersTable = document.querySelector(".uploaders-table");
if (params.q) {
document.getElementById('search').value = params.q;
}
addBreadcrumb(DATA.breadcrumb); addBreadcrumb(DATA.breadcrumb);
if (Array.isArray(DATA.paths)) { if (Array.isArray(DATA.paths)) {
const len = DATA.paths.length; const len = DATA.paths.length;
@@ -284,6 +297,10 @@ function ready() {
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
addPath(DATA.paths[i], i); addPath(DATA.paths[i], i);
} }
if (len == 0) {
document.querySelector('.main').insertAdjacentHTML("afterbegin", '<i class="empty-folder"></i>');
document.querySelector('.main .empty-folder').textContent = dirEmptyNote;
}
} }
if (DATA.allow_upload) { if (DATA.allow_upload) {
dropzone(); dropzone();

View File

@@ -1,7 +1,7 @@
use clap::crate_description; use clap::crate_description;
use clap::{Arg, ArgMatches}; use clap::{Arg, ArgMatches};
use rustls::{Certificate, PrivateKey}; use rustls::{Certificate, PrivateKey};
use std::net::SocketAddr; use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{env, fs, io}; use std::{env, fs, io};
@@ -111,8 +111,7 @@ pub fn matches() -> ArgMatches {
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub struct Args { pub struct Args {
pub address: String, pub addr: SocketAddr,
pub port: u16,
pub path: PathBuf, pub path: PathBuf,
pub path_prefix: String, pub path_prefix: String,
pub uri_prefix: String, pub uri_prefix: String,
@@ -133,8 +132,9 @@ impl Args {
/// If a parsing error ocurred, exit the process and print out informative /// If a parsing error ocurred, exit the process and print out informative
/// error message to user. /// error message to user.
pub fn parse(matches: ArgMatches) -> BoxResult<Args> { pub fn parse(matches: ArgMatches) -> BoxResult<Args> {
let address = matches.value_of("address").unwrap_or_default().to_owned(); let ip = matches.value_of("address").unwrap_or_default();
let port = matches.value_of_t::<u16>("port")?; let port = matches.value_of_t::<u16>("port")?;
let addr = to_addr(ip, 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 let path_prefix = matches
.value_of("path-prefix") .value_of("path-prefix")
@@ -166,8 +166,7 @@ impl Args {
}; };
Ok(Args { Ok(Args {
address, addr,
port,
path, path,
path_prefix, path_prefix,
uri_prefix, uri_prefix,
@@ -197,17 +196,15 @@ impl Args {
}) })
.map_err(|err| format!("Failed to access path `{}`: {}", path.display(), err,).into()) .map_err(|err| format!("Failed to access path `{}`: {}", path.display(), err,).into())
} }
}
/// Construct socket address from arguments. fn to_addr(ip: &str, port: u16) -> BoxResult<SocketAddr> {
pub fn address(&self) -> BoxResult<SocketAddr> { let ip: IpAddr = ip.parse()?;
format!("{}:{}", self.address, self.port) Ok(SocketAddr::new(ip, port))
.parse()
.map_err(|_| format!("Invalid bind address `{}:{}`", self.address, self.port).into())
}
} }
// Load public certificate from file. // Load public certificate from file.
pub fn load_certs(filename: &str) -> BoxResult<Vec<Certificate>> { fn load_certs(filename: &str) -> BoxResult<Vec<Certificate>> {
// Open certificate file. // Open certificate file.
let certfile = let certfile =
fs::File::open(&filename).map_err(|e| format!("Failed to open {}: {}", &filename, e))?; fs::File::open(&filename).map_err(|e| format!("Failed to open {}: {}", &filename, e))?;
@@ -222,7 +219,7 @@ pub fn load_certs(filename: &str) -> BoxResult<Vec<Certificate>> {
} }
// Load private key from file. // Load private key from file.
pub fn load_private_key(filename: &str) -> BoxResult<PrivateKey> { fn load_private_key(filename: &str) -> BoxResult<PrivateKey> {
// Open keyfile. // Open keyfile.
let keyfile = let keyfile =
fs::File::open(&filename).map_err(|e| format!("Failed to open {}: {}", &filename, e))?; fs::File::open(&filename).map_err(|e| format!("Failed to open {}: {}", &filename, e))?;

View File

@@ -2,8 +2,14 @@ mod args;
mod auth; mod auth;
mod server; mod server;
#[macro_use]
extern crate log;
pub type BoxResult<T> = Result<T, Box<dyn std::error::Error>>; pub type BoxResult<T> = Result<T, Box<dyn std::error::Error>>;
use std::env;
use std::io::Write;
use crate::args::{matches, Args}; use crate::args::{matches, Args};
use crate::server::serve; use crate::server::serve;
@@ -13,6 +19,16 @@ async fn main() {
} }
async fn run() -> BoxResult<()> { async fn run() -> BoxResult<()> {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info")
}
env_logger::builder()
.format(|buf, record| {
let timestamp = buf.timestamp();
writeln!(buf, "[{} {}] {}", timestamp, record.level(), record.args())
})
.init();
let args = Args::parse(matches())?; let args = Args::parse(matches())?;
tokio::select! { tokio::select! {
ret = serve(args) => { ret = serve(args) => {

View File

@@ -1,10 +1,11 @@
use crate::auth::{generate_www_auth, valid_digest}; use crate::auth::{generate_www_auth, valid_digest};
use crate::{Args, BoxResult}; use crate::{Args, BoxResult};
use xml::escape::escape_str_pcdata;
use async_walkdir::WalkDir; use async_walkdir::WalkDir;
use async_zip::write::{EntryOptions, ZipFileWriter}; use async_zip::write::{EntryOptions, ZipFileWriter};
use async_zip::Compression; use async_zip::Compression;
use chrono::{Local, TimeZone, Utc}; use chrono::{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;
@@ -24,7 +25,7 @@ use rustls::ServerConfig;
use serde::Serialize; use serde::Serialize;
use std::convert::Infallible; use std::convert::Infallible;
use std::fs::Metadata; use std::fs::Metadata;
use std::net::IpAddr; use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::time::SystemTime; use std::time::SystemTime;
@@ -43,6 +44,7 @@ type Response = hyper::Response<Body>;
const INDEX_HTML: &str = include_str!("../assets/index.html"); const INDEX_HTML: &str = include_str!("../assets/index.html");
const INDEX_CSS: &str = include_str!("../assets/index.css"); const INDEX_CSS: &str = include_str!("../assets/index.css");
const INDEX_JS: &str = include_str!("../assets/index.js"); const INDEX_JS: &str = include_str!("../assets/index.js");
const FAVICON_ICO: &[u8] = include_bytes!("../assets/favicon.ico");
const INDEX_NAME: &str = "index.html"; const INDEX_NAME: &str = "index.html";
const BUF_SIZE: usize = 1024 * 16; const BUF_SIZE: usize = 1024 * 16;
@@ -55,7 +57,6 @@ macro_rules! status {
pub async fn serve(args: Args) -> BoxResult<()> { pub async fn serve(args: Args) -> BoxResult<()> {
let args = Arc::new(args); let args = Arc::new(args);
let socket_addr = args.address()?;
let inner = Arc::new(InnerService::new(args.clone())); let inner = Arc::new(InnerService::new(args.clone()));
match args.tls.clone() { match args.tls.clone() {
Some((certs, key)) => { Some((certs, key)) => {
@@ -65,7 +66,7 @@ pub async fn serve(args: Args) -> BoxResult<()> {
.with_single_cert(certs, key)?; .with_single_cert(certs, key)?;
let tls_acceptor = TlsAcceptor::from(Arc::new(config)); let tls_acceptor = TlsAcceptor::from(Arc::new(config));
let arc_acceptor = Arc::new(tls_acceptor); let arc_acceptor = Arc::new(tls_acceptor);
let listener = TcpListener::bind(&socket_addr).await?; let listener = TcpListener::bind(&args.addr).await?;
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener); let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
let incoming = let incoming =
hyper::server::accept::from_stream(incoming.filter_map(|socket| async { hyper::server::accept::from_stream(incoming.filter_map(|socket| async {
@@ -86,11 +87,11 @@ pub async fn serve(args: Args) -> BoxResult<()> {
})) }))
} }
})); }));
print_listening(args.address.as_str(), args.port, &args.uri_prefix, true); print_listening(&args.addr, &args.uri_prefix, true);
server.await?; server.await?;
} }
None => { None => {
let server = hyper::Server::try_bind(&socket_addr)?.serve(make_service_fn(move |_| { let server = hyper::Server::try_bind(&args.addr)?.serve(make_service_fn(move |_| {
let inner = inner.clone(); let inner = inner.clone();
async move { async move {
Ok::<_, Infallible>(service_fn(move |req| { Ok::<_, Infallible>(service_fn(move |req| {
@@ -99,7 +100,7 @@ pub async fn serve(args: Args) -> BoxResult<()> {
})) }))
} }
})); }));
print_listening(args.address.as_str(), args.port, &args.uri_prefix, false); print_listening(&args.addr, &args.uri_prefix, false);
server.await?; server.await?;
} }
} }
@@ -120,20 +121,16 @@ impl InnerService {
let uri = req.uri().clone(); let uri = req.uri().clone();
let cors = self.args.cors; let cors = self.args.cors;
let timestamp = Local::now().format("%d/%b/%Y %H:%M:%S");
let mut res = match self.handle(req).await { let mut res = match self.handle(req).await {
Ok(res) => { Ok(res) => {
println!(r#"[{}] "{} {}" - {}"#, timestamp, method, uri, res.status()); info!(r#""{} {}" - {}"#, method, uri, res.status());
res res
} }
Err(err) => { Err(err) => {
let mut res = Response::default(); let mut res = Response::default();
let status = StatusCode::INTERNAL_SERVER_ERROR; let status = StatusCode::INTERNAL_SERVER_ERROR;
status!(res, status); status!(res, status);
eprintln!( error!(r#""{} {}" - {} {}"#, method, uri, status, err);
r#"[{}] "{} {}" - {} {}"#,
timestamp, method, uri, status, err
);
res res
} }
}; };
@@ -178,6 +175,12 @@ impl InnerService {
status!(res, StatusCode::NOT_FOUND); status!(res, StatusCode::NOT_FOUND);
return Ok(res); return Ok(res);
} }
if is_miss && path.ends_with("favicon.ico") {
*res.body_mut() = Body::from(FAVICON_ICO);
res.headers_mut()
.insert("content-type", "image/x-icon".parse().unwrap());
return Ok(res);
}
let headers = req.headers(); let headers = req.headers();
@@ -188,8 +191,8 @@ impl InnerService {
self.handle_render_index(path, headers, &mut res).await?; self.handle_render_index(path, headers, &mut res).await?;
} else if query == "zip" { } else if query == "zip" {
self.handle_zip_dir(path, &mut res).await?; self.handle_zip_dir(path, &mut res).await?;
} else if query.starts_with("q=") { } else if let Some(q) = query.strip_prefix("q=") {
self.handle_query_dir(path, &query[3..], &mut res).await?; self.handle_query_dir(path, q, &mut res).await?;
} else { } else {
self.handle_ls_dir(path, true, &mut res).await?; self.handle_ls_dir(path, true, &mut res).await?;
} }
@@ -322,7 +325,7 @@ impl InnerService {
} }
} }
}; };
self.send_index(path, paths, res) self.send_index(path, paths, res, exist)
} }
async fn handle_query_dir( async fn handle_query_dir(
@@ -351,7 +354,7 @@ impl InnerService {
} }
} }
} }
self.send_index(path, paths, res) self.send_index(path, paths, res, true)
} }
async fn handle_zip_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> { async fn handle_zip_dir(&self, path: &Path, res: &mut Response) -> BoxResult<()> {
@@ -363,14 +366,18 @@ impl InnerService {
let path = path.to_owned(); let path = path.to_owned();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = zip_dir(&mut writer, &path).await { if let Err(e) = zip_dir(&mut writer, &path).await {
eprintln!("Failed to zip {}, {}", path.display(), e); error!("Failed to zip {}, {}", path.display(), e);
} }
}); });
let stream = ReaderStream::new(reader); let stream = ReaderStream::new(reader);
*res.body_mut() = Body::wrap_stream(stream); *res.body_mut() = Body::wrap_stream(stream);
res.headers_mut().insert( res.headers_mut().insert(
CONTENT_DISPOSITION, CONTENT_DISPOSITION,
HeaderValue::from_str(&format!("attachment; filename=\"{}.zip\"", filename,)).unwrap(), HeaderValue::from_str(&format!(
"attachment; filename=\"{}.zip\"",
encode_uri(filename),
))
.unwrap(),
); );
Ok(()) Ok(())
} }
@@ -638,6 +645,7 @@ impl InnerService {
path: &Path, path: &Path,
mut paths: Vec<PathItem>, mut paths: Vec<PathItem>,
res: &mut Response, res: &mut Response,
exist: bool,
) -> BoxResult<()> { ) -> BoxResult<()> {
paths.sort_unstable(); paths.sort_unstable();
let rel_path = match self.args.path.parent() { let rel_path = match self.args.path.parent() {
@@ -649,6 +657,7 @@ impl InnerService {
paths, paths,
allow_upload: self.args.allow_upload, allow_upload: self.args.allow_upload,
allow_delete: self.args.allow_delete, allow_delete: self.args.allow_delete,
dir_exists: exist,
}; };
let data = serde_json::to_string(&data).unwrap(); let data = serde_json::to_string(&data).unwrap();
let output = INDEX_HTML.replace( let output = INDEX_HTML.replace(
@@ -666,7 +675,8 @@ impl InnerService {
), ),
); );
*res.body_mut() = output.into(); *res.body_mut() = output.into();
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
Ok(()) Ok(())
} }
@@ -794,6 +804,7 @@ struct IndexData {
paths: Vec<PathItem>, paths: Vec<PathItem>,
allow_upload: bool, allow_upload: bool,
allow_delete: bool, allow_delete: bool,
dir_exists: bool,
} }
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)] #[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
@@ -808,10 +819,12 @@ struct PathItem {
impl PathItem { impl PathItem {
pub fn to_dav_xml(&self, prefix: &str) -> String { pub fn to_dav_xml(&self, prefix: &str) -> String {
let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822(); let mtime = Utc.timestamp_millis(self.mtime as i64).to_rfc2822();
let href = encode_uri(&format!("{}{}", prefix, &self.name));
let displayname = escape_str_pcdata(&self.base_name);
match self.path_type { match self.path_type {
PathType::Dir | PathType::SymlinkDir => format!( PathType::Dir | PathType::SymlinkDir => format!(
r#"<D:response> r#"<D:response>
<D:href>{}{}</D:href> <D:href>{}</D:href>
<D:propstat> <D:propstat>
<D:prop> <D:prop>
<D:displayname>{}</D:displayname> <D:displayname>{}</D:displayname>
@@ -821,11 +834,11 @@ impl PathItem {
<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>"#,
prefix, self.name, self.base_name, mtime href, displayname, mtime
), ),
PathType::File | PathType::SymlinkFile => format!( PathType::File | PathType::SymlinkFile => format!(
r#"<D:response> r#"<D:response>
<D:href>{}{}</D:href> <D:href>{}</D:href>
<D:propstat> <D:propstat>
<D:prop> <D:prop>
<D:displayname>{}</D:displayname> <D:displayname>{}</D:displayname>
@@ -836,9 +849,8 @@ impl PathItem {
<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>"#,
prefix, href,
self.name, displayname,
self.base_name,
self.size.unwrap_or_default(), self.size.unwrap_or_default(),
mtime mtime
), ),
@@ -970,35 +982,48 @@ fn to_content_range(range: &Range, complete_length: u64) -> Option<ContentRange>
}) })
} }
fn print_listening(address: &str, port: u16, prefix: &str, tls: bool) { fn print_listening(addr: &SocketAddr, prefix: &str, tls: bool) {
let prefix = prefix.trim_end_matches('/'); let prefix = encode_uri(prefix.trim_end_matches('/'));
let addrs = retrieve_listening_addrs(address); let addrs = retrieve_listening_addrs(addr);
let protocol = if tls { "https" } else { "http" }; let protocol = if tls { "https" } else { "http" };
if addrs.len() == 1 { if addrs.len() == 1 {
eprintln!( eprintln!("Listening on {}://{}{}", protocol, addr, prefix);
"Listening on {}://{}:{}{}",
protocol, addrs[0], port, prefix
);
} else { } else {
eprintln!("Listening on:"); eprintln!("Listening on:");
for addr in addrs { for addr in addrs {
eprintln!(" {}://{}:{}{}", protocol, addr, port, prefix); eprintln!(" {}://{}{}", protocol, addr, prefix);
} }
eprintln!(); eprintln!();
} }
} }
fn retrieve_listening_addrs(address: &str) -> Vec<String> { fn retrieve_listening_addrs(addr: &SocketAddr) -> Vec<SocketAddr> {
if address == "0.0.0.0" { let ip = addr.ip();
let port = addr.port();
if ip.is_unspecified() {
if let Ok(interfaces) = get_if_addrs() { if let Ok(interfaces) = get_if_addrs() {
let mut ifaces: Vec<IpAddr> = interfaces let mut ifaces: Vec<IpAddr> = interfaces
.into_iter() .into_iter()
.map(|v| v.ip()) .map(|v| v.ip())
.filter(|v| v.is_ipv4()) .filter(|v| {
if ip.is_ipv4() {
v.is_ipv4()
} else {
v.is_ipv6()
}
})
.collect(); .collect();
ifaces.sort(); ifaces.sort();
return ifaces.into_iter().map(|v| v.to_string()).collect(); return ifaces
.into_iter()
.map(|v| SocketAddr::new(v, port))
.collect();
} }
} }
vec![address.to_owned()] vec![addr.to_owned()]
}
fn encode_uri(v: &str) -> String {
let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
parts.join("/")
} }