feat: support edit files (#179)

close #172
This commit is contained in:
sigoden 2023-02-20 22:50:24 +08:00 committed by GitHub
parent c6c78a16c5
commit dd6973468c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 357 additions and 126 deletions

10
Cargo.lock generated
View file

@ -277,6 +277,15 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "content_inspector"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
dependencies = [
"memchr",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
@ -442,6 +451,7 @@ dependencies = [
"chrono",
"clap",
"clap_complete",
"content_inspector",
"diqwest",
"form_urlencoded",
"futures",

View file

@ -40,6 +40,7 @@ async-stream = "0.3"
walkdir = "2.3"
form_urlencoded = "1.0"
alphanumeric-sort = "1.4"
content_inspector = "0.2.4"
[features]
default = ["tls"]

View file

@ -108,11 +108,10 @@ body {
}
.main {
padding: 3em 1em 0;
padding: 3.3em 1em 0;
}
.empty-folder {
padding-top: 1rem;
font-style: italic;
}
@ -202,6 +201,25 @@ body {
padding-right: 1em;
}
.editor {
width: 100%;
height: calc(100vh - 5rem);
border: 1px solid #ced4da;
outline: none;
}
.save-btn {
margin-left: auto;
margin-right: 2em;
cursor: pointer;
user-select: none;
}
.not-editable {
font-style: italic;
}
@media (min-width: 768px) {
.path a {
min-width: 400px;

View file

@ -11,37 +11,69 @@
</script>
<script src="__ASSERTS_PREFIX__index.js"></script>
</head>
<body>
<div class="head">
<div class="breadcrumb"></div>
<div class="toolbox">
<div>
<a href="?zip" class="zip-root hidden" 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>
<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>
<a href="" class="download hidden" title="Download file" download="">
<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 class="control upload-file hidden" title="Upload files">
<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>
<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>
</label>
<input type="file" id="file" name="file" multiple>
</div>
<div class="control new-folder hidden" title="New folder">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="m.5 3 .04.87a1.99 1.99 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2zm5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19c-.24 0-.47.042-.683.12L1.5 2.98a1 1 0 0 1 1-.98h3.672z"/>
<path d="M13.5 10a.5.5 0 0 1 .5.5V12h1.5a.5.5 0 1 1 0 1H14v1.5a.5.5 0 1 1-1 0V13h-1.5a.5.5 0 0 1 0-1H13v-1.5a.5.5 0 0 1 .5-.5z"/>
<path
d="m.5 3 .04.87a1.99 1.99 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2zm5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19c-.24 0-.47.042-.683.12L1.5 2.98a1 1 0 0 1 1-.98h3.672z" />
<path
d="M13.5 10a.5.5 0 0 1 .5.5V12h1.5a.5.5 0 1 1 0 1H14v1.5a.5.5 0 1 1-1 0V13h-1.5a.5.5 0 0 1 0-1H13v-1.5a.5.5 0 0 1 .5-.5z" />
</svg>
</div>
</div>
<form class="searchbar hidden">
<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>
<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 class="save-btn hidden" title="Save file">
<svg viewBox="0 0 1024 1024" width="24" height="24">
<path
d="M426.666667 682.666667v42.666666h170.666666v-42.666666h-170.666666z m-42.666667-85.333334h298.666667v128h42.666666V418.133333L605.866667 298.666667H298.666667v426.666666h42.666666v-128h42.666667z m260.266667-384L810.666667 379.733333V810.666667H213.333333V213.333333h430.933334zM341.333333 341.333333h85.333334v170.666667H341.333333V341.333333z"
fill="#444444" p-id="8311"></path>
</svg>
</div>
</div>
<div class="main">
<div class="index-page hidden">
<div class="empty-folder hidden"></div>
<table class="uploaders-table hidden">
<thead>
@ -58,8 +90,14 @@
</tbody>
</table>
</div>
<div class="editor-page hidden">
<div class="not-editable hidden"></div>
<textarea class="editor hidden" cols="10"></textarea>
</div>
</div>
<script>
window.addEventListener("DOMContentLoaded", ready);
</script>
</body>
</html>

View file

@ -7,9 +7,14 @@
*/
/**
* @typedef {object} DATA
* @typedef {IndexDATA|EditDATA} DATA
*/
/**
* @typedef {object} IndexDATA
* @property {string} href
* @property {string} uri_prefix
* @property {"Index"} kind
* @property {PathItem[]} paths
* @property {boolean} allow_upload
* @property {boolean} allow_delete
@ -18,6 +23,14 @@
* @property {boolean} dir_exists
*/
/**
* @typedef {object} EditDATA
* @property {string} href
* @property {string} uri_prefix
* @property {"Edit"} kind
* @property {string} editable
*/
/**
* @type {DATA} DATA
*/
@ -57,11 +70,43 @@ let $emptyFolder;
/**
* @type Element
*/
let $newFolder;
/**
* @type Element
*/
let $searchbar;
let $editor;
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");
$editor = document.querySelector(".editor");
addBreadcrumb(DATA.href, DATA.uri_prefix);
if (DATA.kind == "Index") {
document.querySelector(".index-page").classList.remove("hidden");
if (DATA.allow_search) {
setupSearch()
}
if (DATA.allow_archive) {
document.querySelector(".zip-root").classList.remove("hidden");
}
renderPathsTableHead();
renderPathsTableBody();
if (DATA.allow_upload) {
dropzone();
setupUpload();
}
} else if (DATA.kind == "Edit") {
setupEditor();
}
}
class Uploader {
/**
@ -83,12 +128,12 @@ class Uploader {
upload() {
const { idx, name } = this;
const url = getUrl(name);
const url = newUrl(name);
const encodedName = encodedStr(name);
$uploadersTable.insertAdjacentHTML("beforeend", `
<tr id="upload${idx}" class="uploader">
<td class="path cell-icon">
${getSvg()}
${getPathSvg()}
</td>
<td class="path cell-name">
<a href="${url}">${encodedName}</a>
@ -105,7 +150,7 @@ class Uploader {
ajax() {
Uploader.runnings += 1;
const url = getUrl(this.name);
const url = newUrl(this.name);
this.lastUptime = Date.now();
const ajax = new XMLHttpRequest();
ajax.upload.addEventListener("progress", e => this.progress(e), false);
@ -272,7 +317,7 @@ function renderPathsTableBody() {
*/
function addPath(file, index) {
const encodedName = encodedStr(file.name);
let url = getUrl(file.name)
let url = newUrl(file.name)
let actionDelete = "";
let actionDownload = "";
let actionMove = "";
@ -316,10 +361,10 @@ function addPath(file, index) {
$pathsTableBody.insertAdjacentHTML("beforeend", `
<tr id="addPath${index}">
<td class="path cell-icon">
${getSvg(file.path_type)}
${getPathSvg(file.path_type)}
</td>
<td class="path cell-name">
<a href="${url}">${encodedName}</a>
<a href="${url}?edit" target="_blank">${encodedName}</a>
</td>
<td class="cell-mtime">${formatMtime(file.mtime)}</td>
<td class="cell-size">${formatSize(file.size).join(" ")}</td>
@ -339,10 +384,10 @@ async function deletePath(index) {
if (!confirm(`Delete \`${file.name}\`?`)) return;
try {
const res = await fetch(getUrl(file.name), {
const res = await fetch(newUrl(file.name), {
method: "DELETE",
});
if (res.status >= 200 && res.status < 300) {
await assertFetch(res);
document.getElementById(`addPath${index}`).remove();
DATA.paths[index] = null;
if (!DATA.paths.find(v => !!v)) {
@ -350,9 +395,6 @@ async function deletePath(index) {
$emptyFolder.textContent = dirEmptyNote;
$emptyFolder.classList.remove("hidden");
}
} else {
throw new Error(await res.text())
}
} catch (err) {
alert(`Cannot delete \`${file.name}\`, ${err.message}`);
}
@ -368,7 +410,7 @@ async function movePath(index) {
const file = DATA.paths[index];
if (!file) return;
const fileUrl = getUrl(file.name);
const fileUrl = newUrl(file.name);
const fileUrlObj = new URL(fileUrl)
const prefix = DATA.uri_prefix.slice(0, -1);
@ -388,11 +430,8 @@ async function movePath(index) {
"Destination": newFileUrl,
}
});
if (res.status >= 200 && res.status < 300) {
await assertFetch(res);
location.href = newFileUrl.split("/").slice(0, -1).join("/")
} else {
throw new Error(await res.text())
}
} catch (err) {
alert(`Cannot move \`${filePath}\` to \`${newPath}\`, ${err.message}`);
}
@ -426,12 +465,13 @@ function dropzone() {
* Setup searchbar
*/
function setupSearch() {
const $searchbar = document.querySelector(".searchbar");
$searchbar.classList.remove("hidden");
$searchbar.addEventListener("submit", event => {
event.preventDefault();
const formData = new FormData($searchbar);
const q = formData.get("q");
let href = getUrl();
let href = baseUrl();
if (q) {
href += "?q=" + q;
}
@ -442,10 +482,8 @@ function setupSearch() {
}
}
/**
* Setup upload
*/
function setupUpload() {
const $newFolder = document.querySelector(".new-folder");
$newFolder.classList.remove("hidden");
$newFolder.addEventListener("click", () => {
const name = prompt("Enter folder name");
@ -460,19 +498,61 @@ function setupUpload() {
});
}
async function setupEditor() {
document.querySelector(".editor-page").classList.remove("hidden");;
const $download = document.querySelector(".download")
$download.classList.remove("hidden");
$download.href = baseUrl()
if (!DATA.editable) {
const $notEditable = document.querySelector(".not-editable");
$notEditable.classList.remove("hidden");
$notEditable.textContent = "File is binary or too large.";
return;
}
const $saveBtn = document.querySelector(".save-btn");
$saveBtn.classList.remove("hidden");
$saveBtn.addEventListener("click", saveChange);
$editor.classList.remove("hidden");
try {
const res = await fetch(baseUrl());
await assertFetch(res);
const text = await res.text();
$editor.value = text;
} catch (err) {
alert(`Failed get file, ${err.message}`);
}
}
/**
* Save editor change
*/
async function saveChange() {
try {
await fetch(baseUrl(), {
method: "PUT",
body: $editor.value,
});
} catch (err) {
alert(`Failed to save file, ${err.message}`);
}
}
/**
* Create a folder
* @param {string} name
*/
async function createFolder(name) {
const url = getUrl(name);
const url = newUrl(name);
try {
const res = await fetch(url, {
method: "MKCOL",
});
if (res.status >= 200 && res.status < 300) {
await assertFetch(res);
location.href = url;
}
} catch (err) {
alert(`Cannot create folder \`${name}\`, ${err.message}`);
}
@ -492,15 +572,18 @@ async function addFileEntries(entries, dirs) {
}
function getUrl(name) {
let url = location.href.split('?')[0];
function newUrl(name) {
let url = baseUrl();
if (!url.endsWith("/")) url += "/";
if (!name) return url;
url += name.split("/").map(encodeURIComponent).join("/");
return url;
}
function getSvg(path_type) {
function baseUrl() {
return location.href.split('?')[0];
}
function getPathSvg(path_type) {
switch (path_type) {
case "Dir":
return `<svg height="16" viewBox="0 0 14 16" width="14"><path fill-rule="evenodd" d="M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM6 4H1V3h5v1z"></path></svg>`;
@ -558,30 +641,8 @@ 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");
$newFolder = document.querySelector(".new-folder");
$searchbar = document.querySelector(".searchbar");
if (DATA.allow_search) {
setupSearch()
}
if (DATA.allow_archive) {
document.querySelector(".zip-root").classList.remove("hidden");
}
addBreadcrumb(DATA.href, DATA.uri_prefix);
renderPathsTableHead();
renderPathsTableBody();
if (DATA.allow_upload) {
dropzone();
setupUpload();
async function assertFetch(res) {
if (!(res.status >= 200 && res.status < 300)) {
throw new Error(await res.text())
}
}

View file

@ -29,7 +29,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::SystemTime;
use tokio::fs::File;
use tokio::io::{AsyncSeekExt, AsyncWrite};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWrite};
use tokio::{fs, io};
use tokio_util::io::StreamReader;
use uuid::Uuid;
@ -43,6 +43,7 @@ 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 BUF_SIZE: usize = 65536;
const TEXT_MAX_SIZE: u64 = 4194304; // 4M
pub struct Server {
args: Arc<Args>,
@ -232,8 +233,12 @@ impl Server {
.await?;
}
} else if is_file {
if query_params.contains_key("edit") {
self.handle_edit_file(path, head_only, &mut res).await?;
} else {
self.handle_send_file(path, headers, head_only, &mut res)
.await?;
}
} else if render_spa {
self.handle_render_spa(path, headers, head_only, &mut res)
.await?;
@ -673,6 +678,41 @@ impl Server {
Ok(())
}
async fn handle_edit_file(
&self,
path: &Path,
head_only: bool,
res: &mut Response,
) -> BoxResult<()> {
let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
let (file, meta) = (file?, meta?);
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
let mut buffer: Vec<u8> = vec![];
file.take(1024).read_to_end(&mut buffer).await?;
let editable = meta.len() <= TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text();
let data = EditData {
href,
kind: DataKind::Edit,
uri_prefix: self.args.uri_prefix.clone(),
allow_upload: self.args.allow_upload,
allow_delete: self.args.allow_delete,
editable,
};
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
let output = self
.html
.replace("__ASSERTS_PREFIX__", &self.assets_prefix)
.replace("__INDEX_DATA__", &serde_json::to_string(&data).unwrap());
res.headers_mut()
.typed_insert(ContentLength(output.as_bytes().len() as u64));
if head_only {
return Ok(());
}
*res.body_mut() = output.into();
Ok(())
}
async fn handle_propfind_dir(
&self,
path: &Path,
@ -855,6 +895,7 @@ impl Server {
}
let href = format!("/{}", normalize_path(path.strip_prefix(&self.args.path)?));
let data = IndexData {
kind: DataKind::Index,
href,
uri_prefix: self.args.uri_prefix.clone(),
allow_upload: self.args.allow_upload,
@ -1018,9 +1059,16 @@ impl Server {
}
}
#[derive(Debug, Serialize)]
enum DataKind {
Index,
Edit,
}
#[derive(Debug, Serialize)]
struct IndexData {
href: String,
kind: DataKind,
uri_prefix: String,
allow_upload: bool,
allow_delete: bool,
@ -1030,6 +1078,16 @@ struct IndexData {
paths: Vec<PathItem>,
}
#[derive(Debug, Serialize)]
struct EditData {
href: String,
kind: DataKind,
uri_prefix: String,
allow_upload: bool,
allow_delete: bool,
editable: bool,
}
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
struct PathItem {
path_type: PathType,

View file

@ -11,9 +11,12 @@ use std::time::{Duration, Instant};
#[allow(dead_code)]
pub type Error = Box<dyn std::error::Error>;
#[allow(dead_code)]
pub const BIN_FILE: &str = "😀.bin";
/// File names for testing purpose
#[allow(dead_code)]
pub static FILES: &[&str] = &["test.txt", "test.html", "index.html", "😀.bin"];
pub static FILES: &[&str] = &["test.txt", "test.html", "index.html", BIN_FILE];
/// Directory names for testing directory don't exist
#[allow(dead_code)]
@ -42,11 +45,18 @@ pub static DIRECTORIES: &[&str] = &["dir1/", "dir2/", "dir3/", DIR_NO_INDEX, DIR
pub fn tmpdir() -> TempDir {
let tmpdir = assert_fs::TempDir::new().expect("Couldn't create a temp dir for tests");
for file in FILES {
if *file == BIN_FILE {
tmpdir
.child(file)
.write_binary(b"bin\0\0123")
.expect("Couldn't write to file");
} else {
tmpdir
.child(file)
.write_str(&format!("This is {file}"))
.expect("Couldn't write to file");
}
}
for directory in DIRECTORIES {
if *directory == DIR_ASSETS {
tmpdir
@ -58,6 +68,12 @@ pub fn tmpdir() -> TempDir {
if *directory == DIR_NO_INDEX && *file == "index.html" {
continue;
}
if *file == BIN_FILE {
tmpdir
.child(format!("{directory}{file}"))
.write_binary(b"bin\0\0123")
.expect("Couldn't write to file");
} else {
tmpdir
.child(format!("{directory}{file}"))
.write_str(&format!("This is {directory}{file}"))
@ -65,6 +81,7 @@ pub fn tmpdir() -> TempDir {
}
}
}
}
tmpdir.child("dir4/hidden").touch().unwrap();
tmpdir

View file

@ -1,9 +1,10 @@
mod fixtures;
mod utils;
use fixtures::{server, Error, TestServer};
use fixtures::{server, Error, TestServer, BIN_FILE};
use rstest::rstest;
use serde_json::Value;
use utils::retrive_edit_file;
#[rstest]
fn get_dir(server: TestServer) -> Result<(), Error> {
@ -103,12 +104,12 @@ fn get_dir_search(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
#[rstest]
fn get_dir_search2(#[with(&["-A"])] server: TestServer) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}?q={}", server.url(), "😀.bin"))?;
let resp = reqwest::blocking::get(format!("{}?q={BIN_FILE}", server.url()))?;
assert_eq!(resp.status(), 200);
let paths = utils::retrieve_index_paths(&resp.text()?);
assert!(!paths.is_empty());
for p in paths {
assert!(p.contains("😀.bin"));
assert!(p.contains(BIN_FILE));
}
Ok(())
}
@ -177,6 +178,24 @@ fn get_file_404(server: TestServer) -> Result<(), Error> {
Ok(())
}
#[rstest]
fn get_file_edit(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"GET", format!("{}index.html?edit", server.url())).send()?;
assert_eq!(resp.status(), 200);
let editable = retrive_edit_file(&resp.text().unwrap()).unwrap();
assert!(editable);
Ok(())
}
#[rstest]
fn get_file_edit_bin(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"GET", format!("{}{BIN_FILE}?edit", server.url())).send()?;
assert_eq!(resp.status(), 200);
let editable = retrive_edit_file(&resp.text().unwrap()).unwrap();
assert!(!editable);
Ok(())
}
#[rstest]
fn head_file_404(server: TestServer) -> Result<(), Error> {
let resp = fetch!(b"HEAD", format!("{}404", server.url())).send()?;

View file

@ -1,7 +1,7 @@
mod fixtures;
mod utils;
use fixtures::{server, Error, TestServer, DIR_NO_FOUND, DIR_NO_INDEX, FILES};
use fixtures::{server, Error, TestServer, BIN_FILE, DIR_NO_FOUND, DIR_NO_INDEX, FILES};
use rstest::rstest;
#[rstest]
@ -56,11 +56,10 @@ fn render_try_index3(
#[case(server(&["--render-try-index"] as &[&str]), false)]
#[case(server(&["--render-try-index", "--allow-search"] as &[&str]), true)]
fn render_try_index4(#[case] server: TestServer, #[case] searched: bool) -> Result<(), Error> {
let resp = reqwest::blocking::get(format!("{}{}?q={}", server.url(), DIR_NO_INDEX, "😀.bin"))?;
let resp = reqwest::blocking::get(format!("{}{}?q={}", server.url(), DIR_NO_INDEX, BIN_FILE))?;
assert_eq!(resp.status(), 200);
let paths = utils::retrieve_index_paths(&resp.text()?);
assert!(!paths.is_empty());
assert_eq!(paths.iter().all(|v| v.contains("😀.bin")), searched);
assert_eq!(paths.iter().all(|v| v.contains(BIN_FILE)), searched);
Ok(())
}

View file

@ -25,24 +25,13 @@ macro_rules! fetch {
}
#[allow(dead_code)]
pub fn retrieve_index_paths(index: &str) -> IndexSet<String> {
retrieve_index_paths_impl(index).unwrap_or_default()
}
#[allow(dead_code)]
pub fn encode_uri(v: &str) -> String {
let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
parts.join("/")
}
fn retrieve_index_paths_impl(index: &str) -> Option<IndexSet<String>> {
let lines: Vec<&str> = index.lines().collect();
let line = lines.iter().find(|v| v.contains("DATA ="))?;
let line_col = line.find("DATA =").unwrap() + 6;
let value: Value = line[line_col..].parse().ok()?;
pub fn retrieve_index_paths(content: &str) -> IndexSet<String> {
let value = retrive_json(content).unwrap();
let paths = value
.get("paths")?
.as_array()?
.get("paths")
.unwrap()
.as_array()
.unwrap()
.iter()
.flat_map(|v| {
let name = v.get("name")?.as_str()?;
@ -54,5 +43,26 @@ fn retrieve_index_paths_impl(index: &str) -> Option<IndexSet<String>> {
}
})
.collect();
Some(paths)
paths
}
#[allow(dead_code)]
pub fn retrive_edit_file(content: &str) -> Option<bool> {
let value = retrive_json(content)?;
let value = value.get("editable").unwrap();
Some(value.as_bool().unwrap())
}
#[allow(dead_code)]
pub fn encode_uri(v: &str) -> String {
let parts: Vec<_> = v.split('/').map(urlencoding::encode).collect();
parts.join("/")
}
fn retrive_json(content: &str) -> Option<Value> {
let lines: Vec<&str> = content.lines().collect();
let line = lines.iter().find(|v| v.contains("DATA ="))?;
let line_col = line.find("DATA =").unwrap() + 6;
let value: Value = line[line_col..].parse().unwrap();
Some(value)
}