From 31c832a7426f487dd047771a5bfe534b33f72f4d Mon Sep 17 00:00:00 2001 From: sigoden Date: Tue, 23 Aug 2022 14:24:42 +0800 Subject: [PATCH] feat: support sort by name, mtime, size (#128) --- Cargo.lock | 9 ++++ Cargo.toml | 3 ++ assets/index.css | 13 +++++- assets/index.html | 6 --- assets/index.js | 117 +++++++++++++++++++++++++++++++++++++--------- src/server.rs | 71 ++++++++++++++++++++-------- tests/sort.rs | 29 ++++++++++++ tests/utils.rs | 6 +-- 8 files changed, 203 insertions(+), 51 deletions(-) create mode 100644 tests/sort.rs diff --git a/Cargo.lock b/Cargo.lock index 799350f..3952d43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "alphanumeric-sort" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0" + [[package]] name = "assert_cmd" version = "2.0.4" @@ -341,6 +347,7 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" name = "dufs" version = "0.29.0" dependencies = [ + "alphanumeric-sort", "assert_cmd", "assert_fs", "async-stream", @@ -350,10 +357,12 @@ dependencies = [ "clap", "clap_complete", "diqwest", + "form_urlencoded", "futures", "headers", "hyper", "if-addrs", + "indexmap", "lazy_static", "log", "md5", diff --git a/Cargo.toml b/Cargo.toml index a0ab903..2776d6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ log = "0.4" socket2 = "0.4" async-stream = "0.3" walkdir = "2.3" +form_urlencoded = "1.0" +alphanumeric-sort = "1.4" [features] default = ["tls"] @@ -53,6 +55,7 @@ regex = "1" url = "2" diqwest = { version = "1", features = ["blocking"] } predicates = "2" +indexmap = "1.9" [profile.release] lto = true diff --git a/assets/index.css b/assets/index.css index ff17861..c42ae00 100644 --- a/assets/index.css +++ b/assets/index.css @@ -131,7 +131,16 @@ body { padding-left: 0.6em; } -.paths-table tr:hover { +.paths-table thead a { + color: unset; + text-decoration: none; +} + +.paths-table thead a > span { + padding-left: 2px; +} + +.paths-table tbody tr:hover { background-color: #fafafa; } @@ -232,7 +241,7 @@ body { color: #3191ff; } - .paths-table tr:hover { + .paths-table tbody tr:hover { background-color: #1a1a1a; } } diff --git a/assets/index.html b/assets/index.html index e5cc799..927d7fc 100644 --- a/assets/index.html +++ b/assets/index.html @@ -48,12 +48,6 @@ - - - - - - diff --git a/assets/index.js b/assets/index.js index df1e2b7..d5d5c1e 100644 --- a/assets/index.js +++ b/assets/index.js @@ -6,17 +6,41 @@ * @property {number} size */ -// https://stackoverflow.com/a/901144/3642588 -const params = new Proxy(new URLSearchParams(window.location.search), { - get: (searchParams, prop) => searchParams.get(prop), -}); +/** + * @typedef {object} DATA + * @property {string} href + * @property {string} uri_prefix + * @property {PathItem[]} paths + * @property {boolean} allow_upload + * @property {boolean} allow_delete + * @property {boolean} allow_search + * @property {boolean} dir_exists + */ -const dirEmptyNote = params.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded'; +/** + * @type {DATA} DATA + */ +var DATA; + +/** + * @type {PARAMS} + * @typedef {object} PARAMS + * @property {string} q + * @property {string} sort + * @property {string} order + */ +const PARAMS = Object.fromEntries(new URLSearchParams(window.location.search).entries()); + +const dirEmptyNote = PARAMS.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded'; /** * @type Element */ let $pathsTable; +/** + * @type Element + */ +let $pathsTableHead; /** * @type Element */ @@ -175,6 +199,67 @@ function addBreadcrumb(href, uri_prefix) { } } +/** + * Render path table thead + */ +function renderPathsTableHead() { + const headerItems = [ + { + name: "name", + props: `colspan="2"`, + text: "Name", + }, + { + name: "mtime", + props: ``, + text: "Last Modified", + }, + { + name: "size", + props: ``, + text: "Size", + } + ]; + $pathsTableHead.insertAdjacentHTML("beforeend", ` + + ${headerItems.map(item => { + let svg = ``; + let order = "asc"; + if (PARAMS.sort === item.name) { + if (PARAMS.order === "asc") { + order = "desc"; + svg = `` + } else { + svg = `` + } + } + const qs = new URLSearchParams({...PARAMS, order, sort: item.name }).toString(); + const icon = `${svg}` + return `` + }).join("\n")} + + + `); +} + +/** + * Render path table tbody + */ +function renderPathsTableBody() { + if (DATA.paths && DATA.paths.length > 0) { + const len = DATA.paths.length; + if (len > 0) { + $pathsTable.classList.remove("hidden"); + } + for (let i = 0; i < len; i++) { + addPath(DATA.paths[i], i); + } + } else { + $emptyFolder.textContent = dirEmptyNote; + $emptyFolder.classList.remove("hidden"); + } +} + /** * Add pathitem * @param {PathItem} file @@ -430,6 +515,7 @@ function encodedStr(rawStr) { function ready() { document.title = `Index of ${DATA.href} - Dufs`; $pathsTable = document.querySelector(".paths-table") + $pathsTableHead = document.querySelector(".paths-table thead"); $pathsTableBody = document.querySelector(".paths-table tbody"); $uploadersTable = document.querySelector(".uploaders-table"); $emptyFolder = document.querySelector(".empty-folder"); @@ -437,26 +523,15 @@ function ready() { if (DATA.allow_search) { document.querySelector(".searchbar").classList.remove("hidden"); - if (params.q) { - document.getElementById('search').value = params.q; + if (PARAMS.q) { + document.getElementById('search').value = PARAMS.q; } } - addBreadcrumb(DATA.href, DATA.uri_prefix); - if (Array.isArray(DATA.paths)) { - const len = DATA.paths.length; - if (len > 0) { - $pathsTable.classList.remove("hidden"); - } - for (let i = 0; i < len; i++) { - addPath(DATA.paths[i], i); - } - if (len == 0) { - $emptyFolder.textContent = dirEmptyNote; - $emptyFolder.classList.remove("hidden"); - } - } + renderPathsTableHead(); + renderPathsTableBody(); + if (DATA.allow_upload) { dropzone(); if (DATA.allow_delete) { diff --git a/src/server.rs b/src/server.rs index cac6620..47afc58 100644 --- a/src/server.rs +++ b/src/server.rs @@ -19,6 +19,7 @@ use hyper::header::{ }; use hyper::{Body, Method, StatusCode, Uri}; use serde::Serialize; +use std::collections::HashMap; use std::fs::Metadata; use std::io::SeekFrom; use std::net::SocketAddr; @@ -160,6 +161,9 @@ impl Server { let path = path.as_path(); let query = req.uri().query().unwrap_or_default(); + let query_params: HashMap = form_urlencoded::parse(query.as_bytes()) + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); let (is_miss, is_dir, is_file, size) = match fs::metadata(path).await.ok() { Some(meta) => (false, meta.is_dir(), meta.is_file(), meta.len()), @@ -182,27 +186,32 @@ impl Server { Method::GET | Method::HEAD => { if is_dir { if render_try_index { - if query == "zip" { + if query_params.contains_key("zip") { self.handle_zip_dir(path, head_only, &mut res).await?; - } else if allow_search && query.starts_with("q=") { - let q = decode_uri(&query[2..]).unwrap_or_default(); - self.handle_search_dir(path, &q, head_only, &mut res) + } else if allow_search && query_params.contains_key("q") { + self.handle_search_dir(path, &query_params, head_only, &mut res) .await?; } else { - self.handle_render_index(path, headers, head_only, &mut res) - .await?; + self.handle_render_index( + path, + &query_params, + headers, + head_only, + &mut res, + ) + .await?; } } else if render_index || render_spa { - self.handle_render_index(path, headers, head_only, &mut res) + self.handle_render_index(path, &query_params, headers, head_only, &mut res) .await?; - } else if query == "zip" { + } else if query_params.contains_key("zip") { self.handle_zip_dir(path, head_only, &mut res).await?; - } else if allow_search && query.starts_with("q=") { - let q = decode_uri(&query[2..]).unwrap_or_default(); - self.handle_search_dir(path, &q, head_only, &mut res) + } else if allow_search && query_params.contains_key("q") { + self.handle_search_dir(path, &query_params, head_only, &mut res) .await?; } else { - self.handle_ls_dir(path, true, head_only, &mut res).await?; + self.handle_ls_dir(path, true, &query_params, head_only, &mut res) + .await?; } } else if is_file { self.handle_send_file(path, headers, head_only, &mut res) @@ -211,7 +220,8 @@ impl Server { self.handle_render_spa(path, headers, head_only, &mut res) .await?; } else if allow_upload && req_path.ends_with('/') { - self.handle_ls_dir(path, false, head_only, &mut res).await?; + self.handle_ls_dir(path, false, &query_params, head_only, &mut res) + .await?; } else { status_not_found(&mut res); } @@ -344,6 +354,7 @@ impl Server { &self, path: &Path, exist: bool, + query_params: &HashMap, head_only: bool, res: &mut Response, ) -> BoxResult<()> { @@ -357,13 +368,13 @@ impl Server { } } }; - self.send_index(path, paths, exist, head_only, res) + self.send_index(path, paths, exist, query_params, head_only, res) } async fn handle_search_dir( &self, path: &Path, - search: &str, + query_params: &HashMap, head_only: bool, res: &mut Response, ) -> BoxResult<()> { @@ -372,7 +383,7 @@ impl Server { let hidden = Arc::new(self.args.hidden.to_vec()); let hidden = hidden.clone(); let running = self.running.clone(); - let search = search.to_lowercase(); + let search = query_params.get("q").unwrap().to_lowercase(); let search_paths = tokio::task::spawn_blocking(move || { let mut it = WalkDir::new(&path_buf).into_iter(); let mut paths: Vec = vec![]; @@ -405,7 +416,7 @@ impl Server { paths.push(item); } } - self.send_index(path, paths, true, head_only, res) + self.send_index(path, paths, true, query_params, head_only, res) } async fn handle_zip_dir( @@ -445,6 +456,7 @@ impl Server { async fn handle_render_index( &self, path: &Path, + query_params: &HashMap, headers: &HeaderMap, head_only: bool, res: &mut Response, @@ -459,7 +471,8 @@ impl Server { self.handle_send_file(&index_path, headers, head_only, res) .await?; } else if self.args.render_try_index { - self.handle_ls_dir(path, true, head_only, res).await?; + self.handle_ls_dir(path, true, query_params, head_only, res) + .await?; } else { status_not_found(res) } @@ -754,10 +767,30 @@ impl Server { path: &Path, mut paths: Vec, exist: bool, + query_params: &HashMap, head_only: bool, res: &mut Response, ) -> BoxResult<()> { - paths.sort_unstable(); + if let Some(sort) = query_params.get("sort") { + if sort == "name" { + paths.sort_by(|v1, v2| { + alphanumeric_sort::compare_str(v1.name.to_lowercase(), v2.name.to_lowercase()) + }) + } else if sort == "mtime" { + paths.sort_by(|v1, v2| v1.mtime.cmp(&v2.mtime)) + } else if sort == "size" { + paths.sort_by(|v1, v2| v1.size.unwrap_or(0).cmp(&v2.size.unwrap_or(0))) + } + if query_params + .get("order") + .map(|v| v == "desc") + .unwrap_or_default() + { + paths.reverse() + } + } else { + paths.sort_unstable(); + } let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?)); let data = IndexData { href, diff --git a/tests/sort.rs b/tests/sort.rs new file mode 100644 index 0000000..4563a51 --- /dev/null +++ b/tests/sort.rs @@ -0,0 +1,29 @@ +mod fixtures; +mod utils; + +use fixtures::{server, Error, TestServer}; +use rstest::rstest; + +#[rstest] +fn ls_dir_sort_by_name(server: TestServer) -> Result<(), Error> { + let url = server.url(); + let resp = reqwest::blocking::get(format!("{}?sort=name&order=asc", url))?; + let paths1 = self::utils::retrieve_index_paths(&resp.text()?); + let resp = reqwest::blocking::get(format!("{}?sort=name&order=desc", url))?; + let mut paths2 = self::utils::retrieve_index_paths(&resp.text()?); + paths2.reverse(); + assert_eq!(paths1, paths2); + Ok(()) +} + +#[rstest] +fn search_dir_sort_by_name(server: TestServer) -> Result<(), Error> { + let url = server.url(); + let resp = reqwest::blocking::get(format!("{}?q={}&sort=name&order=asc", url, "test.html"))?; + let paths1 = self::utils::retrieve_index_paths(&resp.text()?); + let resp = reqwest::blocking::get(format!("{}?q={}&sort=name&order=desc", url, "test.html"))?; + let mut paths2 = self::utils::retrieve_index_paths(&resp.text()?); + paths2.reverse(); + assert_eq!(paths1, paths2); + Ok(()) +} diff --git a/tests/utils.rs b/tests/utils.rs index 5b01cbe..449fbbf 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -1,5 +1,5 @@ +use indexmap::IndexSet; use serde_json::Value; -use std::collections::HashSet; #[macro_export] macro_rules! assert_resp_paths { @@ -25,7 +25,7 @@ macro_rules! fetch { } #[allow(dead_code)] -pub fn retrieve_index_paths(index: &str) -> HashSet { +pub fn retrieve_index_paths(index: &str) -> IndexSet { retrieve_index_paths_impl(index).unwrap_or_default() } @@ -35,7 +35,7 @@ pub fn encode_uri(v: &str) -> String { parts.join("/") } -fn retrieve_index_paths_impl(index: &str) -> Option> { +fn retrieve_index_paths_impl(index: &str) -> Option> { let lines: Vec<&str> = index.lines().collect(); let line = lines.iter().find(|v| v.contains("DATA ="))?; let value: Value = line[7..].parse().ok()?;