feat: support sort by name, mtime, size (#128)
This commit is contained in:
parent
9f8171a22f
commit
31c832a742
8 changed files with 203 additions and 51 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -17,6 +17,12 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alphanumeric-sort"
|
||||||
|
version = "1.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77e9c9abb82613923ec78d7a461595d52491ba7240f3c64c0bbe0e6d98e0fce0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert_cmd"
|
name = "assert_cmd"
|
||||||
version = "2.0.4"
|
version = "2.0.4"
|
||||||
|
@ -341,6 +347,7 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||||
name = "dufs"
|
name = "dufs"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"alphanumeric-sort",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
"assert_fs",
|
"assert_fs",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
@ -350,10 +357,12 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"diqwest",
|
"diqwest",
|
||||||
|
"form_urlencoded",
|
||||||
"futures",
|
"futures",
|
||||||
"headers",
|
"headers",
|
||||||
"hyper",
|
"hyper",
|
||||||
"if-addrs",
|
"if-addrs",
|
||||||
|
"indexmap",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"md5",
|
"md5",
|
||||||
|
|
|
@ -38,6 +38,8 @@ log = "0.4"
|
||||||
socket2 = "0.4"
|
socket2 = "0.4"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
walkdir = "2.3"
|
walkdir = "2.3"
|
||||||
|
form_urlencoded = "1.0"
|
||||||
|
alphanumeric-sort = "1.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tls"]
|
default = ["tls"]
|
||||||
|
@ -53,6 +55,7 @@ regex = "1"
|
||||||
url = "2"
|
url = "2"
|
||||||
diqwest = { version = "1", features = ["blocking"] }
|
diqwest = { version = "1", features = ["blocking"] }
|
||||||
predicates = "2"
|
predicates = "2"
|
||||||
|
indexmap = "1.9"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|
|
@ -131,7 +131,16 @@ body {
|
||||||
padding-left: 0.6em;
|
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;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +241,7 @@ body {
|
||||||
color: #3191ff;
|
color: #3191ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paths-table tr:hover {
|
.paths-table tbody tr:hover {
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,12 +48,6 @@
|
||||||
</table>
|
</table>
|
||||||
<table class="paths-table hidden">
|
<table class="paths-table hidden">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
|
||||||
<th class="cell-name" colspan="2">Name</th>
|
|
||||||
<th class="cell-mtime">Last modified</th>
|
|
||||||
<th class="cell-size">Size</th>
|
|
||||||
<th class="cell-actions">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
117
assets/index.js
117
assets/index.js
|
@ -6,17 +6,41 @@
|
||||||
* @property {number} size
|
* @property {number} size
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// https://stackoverflow.com/a/901144/3642588
|
/**
|
||||||
const params = new Proxy(new URLSearchParams(window.location.search), {
|
* @typedef {object} DATA
|
||||||
get: (searchParams, prop) => searchParams.get(prop),
|
* @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
|
* @type Element
|
||||||
*/
|
*/
|
||||||
let $pathsTable;
|
let $pathsTable;
|
||||||
|
/**
|
||||||
|
* @type Element
|
||||||
|
*/
|
||||||
|
let $pathsTableHead;
|
||||||
/**
|
/**
|
||||||
* @type Element
|
* @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", `
|
||||||
|
<tr>
|
||||||
|
${headerItems.map(item => {
|
||||||
|
let svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5zm-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5z"/></svg>`;
|
||||||
|
let order = "asc";
|
||||||
|
if (PARAMS.sort === item.name) {
|
||||||
|
if (PARAMS.order === "asc") {
|
||||||
|
order = "desc";
|
||||||
|
svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"/></svg>`
|
||||||
|
} else {
|
||||||
|
svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/></svg>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const qs = new URLSearchParams({...PARAMS, order, sort: item.name }).toString();
|
||||||
|
const icon = `<span>${svg}</span>`
|
||||||
|
return `<th class="cell-${item.name}" ${item.props}><a href="?${qs}">${item.text}${icon}</a></th>`
|
||||||
|
}).join("\n")}
|
||||||
|
<th class="cell-actions">Actions</th>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Add pathitem
|
||||||
* @param {PathItem} file
|
* @param {PathItem} file
|
||||||
|
@ -430,6 +515,7 @@ function encodedStr(rawStr) {
|
||||||
function ready() {
|
function ready() {
|
||||||
document.title = `Index of ${DATA.href} - Dufs`;
|
document.title = `Index of ${DATA.href} - Dufs`;
|
||||||
$pathsTable = document.querySelector(".paths-table")
|
$pathsTable = document.querySelector(".paths-table")
|
||||||
|
$pathsTableHead = document.querySelector(".paths-table thead");
|
||||||
$pathsTableBody = document.querySelector(".paths-table tbody");
|
$pathsTableBody = document.querySelector(".paths-table tbody");
|
||||||
$uploadersTable = document.querySelector(".uploaders-table");
|
$uploadersTable = document.querySelector(".uploaders-table");
|
||||||
$emptyFolder = document.querySelector(".empty-folder");
|
$emptyFolder = document.querySelector(".empty-folder");
|
||||||
|
@ -437,26 +523,15 @@ function ready() {
|
||||||
|
|
||||||
if (DATA.allow_search) {
|
if (DATA.allow_search) {
|
||||||
document.querySelector(".searchbar").classList.remove("hidden");
|
document.querySelector(".searchbar").classList.remove("hidden");
|
||||||
if (params.q) {
|
if (PARAMS.q) {
|
||||||
document.getElementById('search').value = params.q;
|
document.getElementById('search').value = PARAMS.q;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
addBreadcrumb(DATA.href, DATA.uri_prefix);
|
addBreadcrumb(DATA.href, DATA.uri_prefix);
|
||||||
if (Array.isArray(DATA.paths)) {
|
renderPathsTableHead();
|
||||||
const len = DATA.paths.length;
|
renderPathsTableBody();
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (DATA.allow_upload) {
|
if (DATA.allow_upload) {
|
||||||
dropzone();
|
dropzone();
|
||||||
if (DATA.allow_delete) {
|
if (DATA.allow_delete) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ use hyper::header::{
|
||||||
};
|
};
|
||||||
use hyper::{Body, Method, StatusCode, Uri};
|
use hyper::{Body, Method, StatusCode, Uri};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs::Metadata;
|
use std::fs::Metadata;
|
||||||
use std::io::SeekFrom;
|
use std::io::SeekFrom;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
@ -160,6 +161,9 @@ impl Server {
|
||||||
let path = path.as_path();
|
let path = path.as_path();
|
||||||
|
|
||||||
let query = req.uri().query().unwrap_or_default();
|
let query = req.uri().query().unwrap_or_default();
|
||||||
|
let query_params: HashMap<String, String> = 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() {
|
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()),
|
Some(meta) => (false, meta.is_dir(), meta.is_file(), meta.len()),
|
||||||
|
@ -182,27 +186,32 @@ impl Server {
|
||||||
Method::GET | Method::HEAD => {
|
Method::GET | Method::HEAD => {
|
||||||
if is_dir {
|
if is_dir {
|
||||||
if render_try_index {
|
if render_try_index {
|
||||||
if query == "zip" {
|
if query_params.contains_key("zip") {
|
||||||
self.handle_zip_dir(path, head_only, &mut res).await?;
|
self.handle_zip_dir(path, head_only, &mut res).await?;
|
||||||
} else if allow_search && query.starts_with("q=") {
|
} else if allow_search && query_params.contains_key("q") {
|
||||||
let q = decode_uri(&query[2..]).unwrap_or_default();
|
self.handle_search_dir(path, &query_params, head_only, &mut res)
|
||||||
self.handle_search_dir(path, &q, head_only, &mut res)
|
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
self.handle_render_index(path, headers, head_only, &mut res)
|
self.handle_render_index(
|
||||||
.await?;
|
path,
|
||||||
|
&query_params,
|
||||||
|
headers,
|
||||||
|
head_only,
|
||||||
|
&mut res,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
} else if render_index || render_spa {
|
} 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?;
|
.await?;
|
||||||
} else if query == "zip" {
|
} else if query_params.contains_key("zip") {
|
||||||
self.handle_zip_dir(path, head_only, &mut res).await?;
|
self.handle_zip_dir(path, head_only, &mut res).await?;
|
||||||
} else if allow_search && query.starts_with("q=") {
|
} else if allow_search && query_params.contains_key("q") {
|
||||||
let q = decode_uri(&query[2..]).unwrap_or_default();
|
self.handle_search_dir(path, &query_params, head_only, &mut res)
|
||||||
self.handle_search_dir(path, &q, head_only, &mut res)
|
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} 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 {
|
} else if is_file {
|
||||||
self.handle_send_file(path, headers, head_only, &mut res)
|
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)
|
self.handle_render_spa(path, headers, head_only, &mut res)
|
||||||
.await?;
|
.await?;
|
||||||
} else if allow_upload && req_path.ends_with('/') {
|
} 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 {
|
} else {
|
||||||
status_not_found(&mut res);
|
status_not_found(&mut res);
|
||||||
}
|
}
|
||||||
|
@ -344,6 +354,7 @@ impl Server {
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
exist: bool,
|
exist: bool,
|
||||||
|
query_params: &HashMap<String, String>,
|
||||||
head_only: bool,
|
head_only: bool,
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> BoxResult<()> {
|
) -> 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(
|
async fn handle_search_dir(
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
search: &str,
|
query_params: &HashMap<String, String>,
|
||||||
head_only: bool,
|
head_only: bool,
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> BoxResult<()> {
|
) -> BoxResult<()> {
|
||||||
|
@ -372,7 +383,7 @@ impl Server {
|
||||||
let hidden = Arc::new(self.args.hidden.to_vec());
|
let hidden = Arc::new(self.args.hidden.to_vec());
|
||||||
let hidden = hidden.clone();
|
let hidden = hidden.clone();
|
||||||
let running = self.running.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 search_paths = tokio::task::spawn_blocking(move || {
|
||||||
let mut it = WalkDir::new(&path_buf).into_iter();
|
let mut it = WalkDir::new(&path_buf).into_iter();
|
||||||
let mut paths: Vec<PathBuf> = vec![];
|
let mut paths: Vec<PathBuf> = vec![];
|
||||||
|
@ -405,7 +416,7 @@ impl Server {
|
||||||
paths.push(item);
|
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(
|
async fn handle_zip_dir(
|
||||||
|
@ -445,6 +456,7 @@ impl Server {
|
||||||
async fn handle_render_index(
|
async fn handle_render_index(
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
query_params: &HashMap<String, String>,
|
||||||
headers: &HeaderMap<HeaderValue>,
|
headers: &HeaderMap<HeaderValue>,
|
||||||
head_only: bool,
|
head_only: bool,
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
|
@ -459,7 +471,8 @@ impl Server {
|
||||||
self.handle_send_file(&index_path, headers, head_only, res)
|
self.handle_send_file(&index_path, headers, head_only, res)
|
||||||
.await?;
|
.await?;
|
||||||
} else if self.args.render_try_index {
|
} 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 {
|
} else {
|
||||||
status_not_found(res)
|
status_not_found(res)
|
||||||
}
|
}
|
||||||
|
@ -754,10 +767,30 @@ impl Server {
|
||||||
path: &Path,
|
path: &Path,
|
||||||
mut paths: Vec<PathItem>,
|
mut paths: Vec<PathItem>,
|
||||||
exist: bool,
|
exist: bool,
|
||||||
|
query_params: &HashMap<String, String>,
|
||||||
head_only: bool,
|
head_only: bool,
|
||||||
res: &mut Response,
|
res: &mut Response,
|
||||||
) -> BoxResult<()> {
|
) -> 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 href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
|
||||||
let data = IndexData {
|
let data = IndexData {
|
||||||
href,
|
href,
|
||||||
|
|
29
tests/sort.rs
Normal file
29
tests/sort.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
|
use indexmap::IndexSet;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! assert_resp_paths {
|
macro_rules! assert_resp_paths {
|
||||||
|
@ -25,7 +25,7 @@ macro_rules! fetch {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn retrieve_index_paths(index: &str) -> HashSet<String> {
|
pub fn retrieve_index_paths(index: &str) -> IndexSet<String> {
|
||||||
retrieve_index_paths_impl(index).unwrap_or_default()
|
retrieve_index_paths_impl(index).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ pub fn encode_uri(v: &str) -> String {
|
||||||
parts.join("/")
|
parts.join("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retrieve_index_paths_impl(index: &str) -> Option<HashSet<String>> {
|
fn retrieve_index_paths_impl(index: &str) -> Option<IndexSet<String>> {
|
||||||
let lines: Vec<&str> = index.lines().collect();
|
let lines: Vec<&str> = index.lines().collect();
|
||||||
let line = lines.iter().find(|v| v.contains("DATA ="))?;
|
let line = lines.iter().find(|v| v.contains("DATA ="))?;
|
||||||
let value: Value = line[7..].parse().ok()?;
|
let value: Value = line[7..].parse().ok()?;
|
||||||
|
|
Loading…
Reference in a new issue