feat: support searching

This commit is contained in:
sigoden 2022-05-29 12:43:40 +08:00
parent 2eba975066
commit 586b209c89
6 changed files with 161 additions and 80 deletions

2
Cargo.lock generated
View file

@ -258,7 +258,7 @@ dependencies = [
[[package]]
name = "duf"
version = "0.2.1"
version = "0.3.0"
dependencies = [
"async-walkdir",
"async_zip",

View file

@ -1,6 +1,6 @@
[package]
name = "duf"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
authors = ["sigoden <sigoden@gmail.com>"]
description = "Duf is a simple file server."

View file

@ -5,12 +5,13 @@
Duf is a simple file server.
![demo](https://user-images.githubusercontent.com/4012553/170822562-c6594de5-0bb2-4d5e-ba66-5731ab6481fd.png)
![demo](https://user-images.githubusercontent.com/4012553/170853143-05dc713e-f0d5-478d-baea-0914599b846c.png)
## Features
- Serve static files
- Download folder as zip file
- Search files
- Upload files
- Delete files
- Basic authentication

View file

@ -4,14 +4,15 @@ html {
color: #24292e;
}
.head {
display: flex;
align-items: baseline;
padding: 1em 1em 0;
body {
width: 700px;
}
.head input {
display: none;
.head {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 1em 1em 0;
}
.breadcrumb {
@ -44,11 +45,50 @@ html {
padding-left: 0.5em;
}
.toolbox {
display: flex;
}
.searchbar {
display: flex;
flex-wrap: nowrap;
width: 246px;
height: 22px;
background-color: #fafafa;
transition: all .15s;
border: 1px #ddd solid;
border-radius: 15px;
margin: 0 0 2px 10px;
}
.searchbar #search {
box-sizing: border-box;
width: 100%;
height: 100%;
font-size: 16px;
line-height: 16px;
padding: 1px;
font-family: helvetica neue,luxi sans,Tahoma,hiragino sans gb,STHeiti,sans-serif;
background-color: transparent;
border: none;
outline: none;
}
.searchbar .icon {
color: #9a9a9a;
padding: 3px 3px;
cursor: pointer;
}
.upload-control {
cursor: pointer;
padding-left: 0.25em;
}
.upload-control input {
display: none;
}
.main {
padding: 0 1em;
}

View file

@ -10,11 +10,20 @@
<body>
<div class="head">
<div class="breadcrumb"></div>
<div>
<a href="?zip" title="Download folder as a .zip file">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>
</a>
<div class="toolbox">
<div>
<a href="?zip" title="Download folder as a .zip file">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>
</a>
</div>
</div>
<form class="searchbar">
<div class="icon">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/></svg>
</div>
<input id="search" name="q" type="text" maxlength="128" autocomplete="off" tabindex="1">
<input type="submit" hidden />
</form>
</div>
<div class="main">
<div class="uploaders">
@ -34,7 +43,7 @@
</div>
<script>
const $head = document.querySelector(".head");
const $toolbox = document.querySelector(".toolbox");
const $tbody = document.querySelector(".main tbody");
const $breadcrumb = document.querySelector(".breadcrumb");
const $uploaders = document.querySelector(".uploaders");
@ -53,9 +62,7 @@
upload() {
const { file, idx } = this;
let url = location.href.split('?')[0];
if (!url.endsWith("/")) url += "/";
url += encodeURI(file.name);
const url = getUrl(file.name);
$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>
@ -107,7 +114,7 @@
}
function addPath(file, index) {
const url = encodeURI(file.path);
const url = getUrl(file.name)
let actionDelete = "";
let actionDownload = "";
if (file.path_type.endsWith("Dir")) {
@ -154,7 +161,7 @@
if (!file) return;
const ajax = new XMLHttpRequest();
ajax.open("DELETE", encodeURI(file.path));
ajax.open("DELETE", getUrl(file.name));
ajax.addEventListener("readystatechange", function() {
if(ajax.readyState === 4 && ajax.status === 200) {
document.getElementById(`addPath${index}`).remove();
@ -164,7 +171,7 @@
}
function addUploadControl() {
$head.insertAdjacentHTML("beforeend", `
$toolbox.insertAdjacentHTML("beforeend", `
<div class="upload-control" title="Upload file">
<label for="file">
<svg width="16" height="16" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/></svg>
@ -174,6 +181,13 @@
`);
}
function getUrl(name) {
let url = location.href.split('?')[0];
if (!url.endsWith("/")) url += "/";
url += encodeURI(name);
return url;
}
function getSvg(path_type) {
switch (path_type) {
case "Dir":

View file

@ -108,18 +108,21 @@ impl InnerService {
match fs::metadata(&path).await {
Ok(meta) => {
if meta.is_dir() {
if req.uri().query().map(|v| v == "zip").unwrap_or_default() {
self.handle_send_dir_zip(path.as_path()).await
} else {
self.handle_send_dir(path.as_path(), true).await
let req_query = req.uri().query().unwrap_or_default();
if req_query == "zip" {
return self.handle_send_dir_zip(path.as_path()).await;
}
if let Some(q) = req_query.strip_prefix("q=") {
return self.handle_query_dir(path.as_path(), q).await;
}
self.handle_ls_dir(path.as_path(), true).await
} else {
self.handle_send_file(path.as_path()).await
}
}
Err(_) => {
if req_path.ends_with('/') {
self.handle_send_dir(path.as_path(), false).await
self.handle_ls_dir(path.as_path(), false).await
} else {
Ok(status_code!(StatusCode::NOT_FOUND))
}
@ -176,31 +179,42 @@ impl InnerService {
Ok(status_code!(StatusCode::OK))
}
async fn handle_send_dir(&self, path: &Path, exist: bool) -> BoxResult<Response> {
async fn handle_ls_dir(&self, path: &Path, exist: bool) -> BoxResult<Response> {
let mut paths: Vec<PathItem> = vec![];
if exist {
let mut rd = fs::read_dir(path).await?;
while let Some(entry) = rd.next_entry().await? {
let entry_path = entry.path();
if let Ok(item) = self.get_path_item(entry_path).await {
if let Ok(item) = get_path_item(entry_path, path.to_path_buf()).await {
paths.push(item);
}
}
paths.sort_unstable();
}
let breadcrumb = self.get_breadcrumb(path);
let data = SendDirData {
breadcrumb,
paths,
readonly: self.args.readonly,
};
let data = serde_json::to_string(&data).unwrap();
self.send_index(path, paths)
}
let mut output =
INDEX_HTML.replace("__STYLE__", &format!("<style>\n{}</style>", INDEX_CSS));
output = output.replace("__DATA__", &data);
Ok(hyper::Response::builder().body(output.into()).unwrap())
async fn handle_query_dir(&self, path: &Path, q: &str) -> BoxResult<Response> {
let mut paths: Vec<PathItem> = vec![];
let mut walkdir = WalkDir::new(path);
while let Some(entry) = walkdir.next().await {
if let Ok(entry) = entry {
if !entry
.file_name()
.to_string_lossy()
.to_lowercase()
.contains(&q.to_lowercase())
{
continue;
}
if fs::symlink_metadata(entry.path()).await.is_err() {
continue;
}
if let Ok(item) = get_path_item(entry.path(), path.to_path_buf()).await {
paths.push(item);
}
}
}
self.send_index(path, paths)
}
async fn handle_send_dir_zip(&self, path: &Path) -> BoxResult<Response> {
@ -223,6 +237,22 @@ impl InnerService {
Ok(Response::new(body))
}
fn send_index(&self, path: &Path, mut paths: Vec<PathItem>) -> BoxResult<Response> {
paths.sort_unstable();
let breadcrumb = self.get_breadcrumb(path);
let data = IndexData {
breadcrumb,
paths,
readonly: self.args.readonly,
};
let data = serde_json::to_string(&data).unwrap();
let mut output =
INDEX_HTML.replace("__STYLE__", &format!("<style>\n{}</style>", INDEX_CSS));
output = output.replace("__DATA__", &data);
Ok(hyper::Response::builder().body(output.into()).unwrap())
}
fn auth_guard(&self, req: &Request) -> BoxResult<bool> {
if let Some(auth) = &self.args.auth {
if let Some(value) = req.headers().get("Authorization") {
@ -242,43 +272,6 @@ impl InnerService {
Ok(true)
}
async fn get_path_item<P: AsRef<Path>>(&self, path: P) -> BoxResult<PathItem> {
let path = path.as_ref();
let base_path = &self.args.path;
let rel_path = path.strip_prefix(base_path).unwrap();
let meta = fs::metadata(&path).await?;
let s_meta = fs::symlink_metadata(&path).await?;
let is_dir = meta.is_dir();
let is_symlink = s_meta.file_type().is_symlink();
let path_type = match (is_symlink, is_dir) {
(true, true) => PathType::SymlinkDir,
(false, true) => PathType::Dir,
(true, false) => PathType::SymlinkFile,
(false, false) => PathType::File,
};
let mtime = meta
.modified()?
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.map(|v| v.as_millis() as u64);
let size = match path_type {
PathType::Dir | PathType::SymlinkDir => None,
PathType::File | PathType::SymlinkFile => Some(meta.len()),
};
let name = rel_path
.file_name()
.and_then(|v| v.to_str())
.unwrap_or_default()
.to_owned();
Ok(PathItem {
path_type,
name,
path: format!("/{}", normalize_path(rel_path)),
mtime,
size,
})
}
fn get_breadcrumb(&self, path: &Path) -> String {
let path = match self.args.path.parent() {
Some(p) => path.strip_prefix(p).unwrap(),
@ -304,7 +297,7 @@ impl InnerService {
}
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
struct SendDirData {
struct IndexData {
breadcrumb: String,
paths: Vec<PathItem>,
readonly: bool,
@ -314,7 +307,6 @@ struct SendDirData {
struct PathItem {
path_type: PathType,
name: String,
path: String,
mtime: Option<u64>,
size: Option<u64>,
}
@ -327,6 +319,37 @@ enum PathType {
SymlinkFile,
}
async fn get_path_item<P: AsRef<Path>>(path: P, base_path: P) -> BoxResult<PathItem> {
let path = path.as_ref();
let rel_path = path.strip_prefix(base_path).unwrap();
let meta = fs::metadata(&path).await?;
let s_meta = fs::symlink_metadata(&path).await?;
let is_dir = meta.is_dir();
let is_symlink = s_meta.file_type().is_symlink();
let path_type = match (is_symlink, is_dir) {
(true, true) => PathType::SymlinkDir,
(false, true) => PathType::Dir,
(true, false) => PathType::SymlinkFile,
(false, false) => PathType::File,
};
let mtime = meta
.modified()?
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.map(|v| v.as_millis() as u64);
let size = match path_type {
PathType::Dir | PathType::SymlinkDir => None,
PathType::File | PathType::SymlinkFile => Some(meta.len()),
};
let name = normalize_path(rel_path);
Ok(PathItem {
path_type,
name,
mtime,
size,
})
}
fn normalize_path<P: AsRef<Path>>(path: P) -> String {
let path = path.as_ref().to_str().unwrap_or_default();
if cfg!(windows) {
@ -341,7 +364,10 @@ async fn dir_zip<W: AsyncWrite + Unpin>(writer: &mut W, dir: &Path) -> BoxResult
let mut walkdir = WalkDir::new(dir);
while let Some(entry) = walkdir.next().await {
if let Ok(entry) = entry {
let meta = fs::symlink_metadata(entry.path()).await?;
let meta = match fs::symlink_metadata(entry.path()).await {
Ok(meta) => meta,
Err(_) => continue,
};
if meta.is_file() {
let filepath = entry.path();
let filename = match filepath.strip_prefix(dir).ok().and_then(|v| v.to_str()) {