]> OzVa Git service - ozva-cloud/commitdiff
feat: support searching
authorsigoden <sigoden@gmail.com>
Sun, 29 May 2022 04:43:40 +0000 (12:43 +0800)
committersigoden <sigoden@gmail.com>
Sun, 29 May 2022 05:10:41 +0000 (13:10 +0800)
Cargo.lock
Cargo.toml
README.md
src/index.css
src/index.html
src/server.rs

index e2d6949c201c8bd99abbc3ede3b5c56194aab0ed..c2bcbb487d2fa7075647177a751eb9329d804c7c 100644 (file)
@@ -258,7 +258,7 @@ dependencies = [
 
 [[package]]
 name = "duf"
-version = "0.2.1"
+version = "0.3.0"
 dependencies = [
  "async-walkdir",
  "async_zip",
index beb81b489874055e3ae878509d3bdbd30993d12d..2ef2d4b38cfbd329618fd6ffcc56ac044e720afa 100644 (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."
index 79ebe8f9155457b5ad56381557be9e817b623622..3228e8886a137c3c0a9ea6982e5eef892abb22f1 100644 (file)
--- a/README.md
+++ b/README.md
@@ -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
index 88f3f45af44c9f97fcef11e18fa1cf66d9700519..718f7b4f635383459b6ba4db206e9e801b1cafc8 100644 (file)
@@ -4,16 +4,17 @@ html {
   color: #24292e;
 }
 
+body {
+  width: 700px;
+}
+
 .head {
   display: flex;
-  align-items: baseline;
+  flex-wrap: wrap;
+  align-items: center;
   padding: 1em 1em 0;
 }
 
-.head input {
-  display: none;
-}
-
 .breadcrumb {
   font-size: 1.25em;
 }
@@ -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;
 }
index e330dbc7474d9cea60d70ca43c154506161572e9..1009c16faf835e2ccdbc4d1d463761d8748844e2 100644 (file)
 <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>
     }
 
     function addPath(file, index) {
-      const url = encodeURI(file.path);
+      const url = getUrl(file.name)
       let actionDelete = "";
       let actionDownload = "";
       if (file.path_type.endsWith("Dir")) {
       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();
     }
 
     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>
 `);
     }
 
+    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":
index 7523682d2acc66c508a0c4c7e31af541a082e6cc..59ed997872f789abf502ae1b03bded184b36cc7b 100644 (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();
-
-        let mut output =
-            INDEX_HTML.replace("__STYLE__", &format!("<style>\n{}</style>", INDEX_CSS));
-        output = output.replace("__DATA__", &data);
+        self.send_index(path, paths)
+    }
 
-        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()) {