mirror of
https://github.com/sigoden/dufs.git
synced 2026-04-09 17:13:02 +03:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5cc60965 | ||
|
|
972cf2377f | ||
|
|
5b338c40da | ||
|
|
964bf61c37 | ||
|
|
4bf92cc47a | ||
|
|
7d17d9c415 | ||
|
|
1db263efae | ||
|
|
5d26103ea2 | ||
|
|
3727dec115 | ||
|
|
0311c9fb90 | ||
|
|
e9ce4b2dc3 | ||
|
|
7aba3fe0b6 | ||
|
|
ca5c3d7c54 | ||
|
|
ec2b064a9a | ||
|
|
cadea9a3bf | ||
|
|
3e0e6b2e8a | ||
|
|
632f7a41bf | ||
|
|
f1e90686dc | ||
|
|
dc7a7cbb3f |
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -5,20 +5,24 @@ about: Create a report to help us improve
|
||||
|
||||
**Problem**
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
<!-- Provide a clear and concise description of the bug you're experiencing. What did you expect to happen, and what actually happened? -->
|
||||
|
||||
**Configuration**
|
||||
|
||||
<!-- The dufs command-line arguments or configuration -->
|
||||
<!-- Please specify the Dufs command-line arguments or configuration used. -->
|
||||
|
||||
<!-- If the problems are related to auth/perm, please conceal only the user:pass, but do not hide the entire `auth` configuration. -->
|
||||
<!-- If the issue is related to authentication/permissions, include auth configurations while concealing sensitive information (e.g., passwords). -->
|
||||
|
||||
**Log**
|
||||
|
||||
The dufs log is crucial for locating the problem, so please do not omit it.
|
||||
<!-- Attach relevant log outputs that can help diagnose the issue. -->
|
||||
|
||||
**Environment:**
|
||||
**Screenshots/Media**
|
||||
|
||||
<!-- If applicable, add screenshots or videos that help illustrate the issue, especially for WebUI problems. -->
|
||||
|
||||
**Environment Information**
|
||||
- Dufs version:
|
||||
- Browser/Webdav info:
|
||||
- OS info:
|
||||
- Proxy server: e.g. nginx, cloudflare
|
||||
- Proxy server (if any): <!-- e.g. nginx, cloudflare -->
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -2,6 +2,30 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.42.0] - 2024-09-01
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Garbled characters caused by atob ([#422](https://github.com/sigoden/dufs/issues/422))
|
||||
- Webui unexpected save-btn when file is non-editable ([#429](https://github.com/sigoden/dufs/issues/429))
|
||||
- Login succeeded but popup `Forbidden` ([#437](https://github.com/sigoden/dufs/issues/437))
|
||||
|
||||
### Features
|
||||
|
||||
- Implements remaining http cache conditionalss ([#407](https://github.com/sigoden/dufs/issues/407))
|
||||
- Base64 index-data to avoid misencoding ([#421](https://github.com/sigoden/dufs/issues/421))
|
||||
- Webui support logout ([#439](https://github.com/sigoden/dufs/issues/439))
|
||||
|
||||
### Refactor
|
||||
|
||||
- No inline scripts in HTML ([#391](https://github.com/sigoden/dufs/issues/391))
|
||||
- Return 400 for propfind request when depth is neither 0 nor 1 ([#403](https://github.com/sigoden/dufs/issues/403))
|
||||
- Remove sabredav-partialupdate from DAV res header ([#415](https://github.com/sigoden/dufs/issues/415))
|
||||
- Date formatting in cache tests ([#428](https://github.com/sigoden/dufs/issues/428))
|
||||
- Some query params work as flag and must not accept a value ([#431](https://github.com/sigoden/dufs/issues/431))
|
||||
- Improve logout at asserts/index.js ([#440](https://github.com/sigoden/dufs/issues/440))
|
||||
- Make logout works on safari ([#442](https://github.com/sigoden/dufs/issues/442))
|
||||
|
||||
## [0.41.0] - 2024-05-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
689
Cargo.lock
generated
689
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dufs"
|
||||
version = "0.41.0"
|
||||
version = "0.42.0"
|
||||
edition = "2021"
|
||||
authors = ["sigoden <sigoden@gmail.com>"]
|
||||
description = "Dufs is a distinctive utility file server"
|
||||
@@ -24,7 +24,7 @@ futures-util = { version = "0.3", default-features = false, features = ["alloc"]
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "bzip2", "xz", "chrono", "tokio"] }
|
||||
headers = "0.4"
|
||||
mime_guess = "2.0"
|
||||
if-addrs = "0.12"
|
||||
if-addrs = "0.13"
|
||||
rustls-pemfile = { version = "2.0", optional = true }
|
||||
tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["ring", "tls12"]}
|
||||
md5 = "0.7"
|
||||
@@ -63,7 +63,7 @@ assert_cmd = "2"
|
||||
reqwest = { version = "0.12", features = ["blocking", "multipart", "rustls-tls"], default-features = false }
|
||||
assert_fs = "1"
|
||||
port_check = "0.2"
|
||||
rstest = "0.19"
|
||||
rstest = "0.22"
|
||||
regex = "1"
|
||||
url = "2"
|
||||
predicates = "3"
|
||||
|
||||
@@ -222,15 +222,20 @@ body {
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.logout-btn {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/x-icon" href="__ASSETS_PREFIX__favicon.ico">
|
||||
<link rel="stylesheet" href="__ASSETS_PREFIX__index.css">
|
||||
<script>
|
||||
DATA = __INDEX_DATA__
|
||||
</script>
|
||||
<script src="__ASSETS_PREFIX__index.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -74,11 +70,12 @@
|
||||
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" title="Searching for folders or files" name="q" type="text" maxlength="128" autocomplete="off" tabindex="1">
|
||||
<input id="search" title="Searching for folders or files" name="q" type="text" maxlength="128" autocomplete="off"
|
||||
tabindex="1">
|
||||
<input type="submit" hidden />
|
||||
</form>
|
||||
<div class="toolbox-right">
|
||||
<div class="login-btn hidden" title="Login for upload/move/delete/edit permissions">
|
||||
<div class="login-btn hidden" title="Login">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z" />
|
||||
@@ -86,10 +83,10 @@
|
||||
d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="user-btn hidden">
|
||||
<div class="logout-btn hidden" title="Logout">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z" />
|
||||
<path fill-rule="evenodd" d="M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0z"/>
|
||||
<path fill-rule="evenodd" d="M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708z"/>
|
||||
</svg>
|
||||
<span class="user-name"></span>
|
||||
</div>
|
||||
@@ -125,9 +122,8 @@
|
||||
<textarea id="editor" class="editor hidden" aria-label="Editor" cols="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener("DOMContentLoaded", ready);
|
||||
</script>
|
||||
<template id="index-data">__INDEX_DATA__</template>
|
||||
<script src="__ASSETS_PREFIX__index.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
169
assets/index.js
169
assets/index.js
@@ -29,6 +29,11 @@ var DUFS_MAX_UPLOADINGS = 1;
|
||||
*/
|
||||
var DATA;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
var DIR_EMPTY_NOTE;
|
||||
|
||||
/**
|
||||
* @type {PARAMS}
|
||||
* @typedef {object} PARAMS
|
||||
@@ -45,8 +50,6 @@ const IFRAME_FORMATS = [
|
||||
".mp3", ".ogg", ".wav", ".m4a",
|
||||
];
|
||||
|
||||
const dirEmptyNote = PARAMS.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded';
|
||||
|
||||
const ICONS = {
|
||||
dir: `<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>`,
|
||||
symlinkFile: `<svg height="16" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM6 4.5l4 3-4 3v-2c-.98-.02-1.84.22-2.55.7-.71.48-1.19 1.25-1.45 2.3.02-1.64.39-2.88 1.13-3.73.73-.84 1.69-1.27 2.88-1.27v-2H6z"></path></svg>`,
|
||||
@@ -91,43 +94,61 @@ let $editor;
|
||||
/**
|
||||
* @type Element
|
||||
*/
|
||||
let $userBtn;
|
||||
let $loginBtn;
|
||||
/**
|
||||
* @type Element
|
||||
*/
|
||||
let $logoutBtn;
|
||||
/**
|
||||
* @type Element
|
||||
*/
|
||||
let $userName;
|
||||
|
||||
function ready() {
|
||||
$pathsTable = document.querySelector(".paths-table")
|
||||
// Produce table when window loads
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
const $indexData = document.getElementById('index-data');
|
||||
if (!$indexData) {
|
||||
alert("No data");
|
||||
return;
|
||||
}
|
||||
|
||||
DATA = JSON.parse(decodeBase64($indexData.innerHTML));
|
||||
DIR_EMPTY_NOTE = PARAMS.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded';
|
||||
|
||||
await ready();
|
||||
});
|
||||
|
||||
async function ready() {
|
||||
$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");
|
||||
$userBtn = document.querySelector(".user-btn");
|
||||
$loginBtn = document.querySelector(".login-btn");
|
||||
$logoutBtn = document.querySelector(".logout-btn");
|
||||
$userName = document.querySelector(".user-name");
|
||||
|
||||
addBreadcrumb(DATA.href, DATA.uri_prefix);
|
||||
|
||||
if (DATA.kind == "Index") {
|
||||
if (DATA.kind === "Index") {
|
||||
document.title = `Index of ${DATA.href} - Dufs`;
|
||||
document.querySelector(".index-page").classList.remove("hidden");
|
||||
|
||||
setupIndexPage();
|
||||
} else if (DATA.kind == "Edit") {
|
||||
await setupIndexPage();
|
||||
} else if (DATA.kind === "Edit") {
|
||||
document.title = `Edit ${DATA.href} - Dufs`;
|
||||
document.querySelector(".editor-page").classList.remove("hidden");;
|
||||
document.querySelector(".editor-page").classList.remove("hidden");
|
||||
|
||||
setupEditorPage();
|
||||
} else if (DATA.kind == "View") {
|
||||
await setupEditorPage();
|
||||
} else if (DATA.kind === "View") {
|
||||
document.title = `View ${DATA.href} - Dufs`;
|
||||
document.querySelector(".editor-page").classList.remove("hidden");;
|
||||
document.querySelector(".editor-page").classList.remove("hidden");
|
||||
|
||||
setupEditorPage();
|
||||
await setupEditorPage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Uploader {
|
||||
/**
|
||||
*
|
||||
@@ -221,7 +242,7 @@ class Uploader {
|
||||
uploadOffset = parseInt(value) || 0;
|
||||
}
|
||||
this.uploadOffset = uploadOffset;
|
||||
this.ajax()
|
||||
this.ajax();
|
||||
}
|
||||
|
||||
progress(event) {
|
||||
@@ -230,7 +251,7 @@ class Uploader {
|
||||
const [speedValue, speedUnit] = formatSize(speed);
|
||||
const speedText = `${speedValue} ${speedUnit}/s`;
|
||||
const progress = formatPercent(((event.loaded + this.uploadOffset) / this.file.size) * 100);
|
||||
const duration = formatDuration((event.total - event.loaded) / speed)
|
||||
const duration = formatDuration((event.total - event.loaded) / speed);
|
||||
this.$uploadStatus.innerHTML = `<span style="width: 80px;">${speedText}</span><span>${progress} ${duration}</span>`;
|
||||
this.uploaded = event.loaded;
|
||||
this.lastUptime = now;
|
||||
@@ -274,7 +295,7 @@ Uploader.runQueue = async () => {
|
||||
if (!Uploader.auth) {
|
||||
Uploader.auth = true;
|
||||
try {
|
||||
await checkAuth()
|
||||
await checkAuth();
|
||||
} catch {
|
||||
Uploader.auth = false;
|
||||
}
|
||||
@@ -319,7 +340,7 @@ function addBreadcrumb(href, uri_prefix) {
|
||||
}
|
||||
}
|
||||
|
||||
function setupIndexPage() {
|
||||
async function setupIndexPage() {
|
||||
if (DATA.allow_archive) {
|
||||
const $download = document.querySelector(".download");
|
||||
$download.href = baseUrl() + "?zip";
|
||||
@@ -335,11 +356,11 @@ function setupIndexPage() {
|
||||
}
|
||||
|
||||
if (DATA.auth) {
|
||||
setupAuth();
|
||||
await setupAuth();
|
||||
}
|
||||
|
||||
if (DATA.allow_search) {
|
||||
setupSearch()
|
||||
setupSearch();
|
||||
}
|
||||
|
||||
renderPathsTableHead();
|
||||
@@ -402,7 +423,7 @@ function renderPathsTableBody() {
|
||||
addPath(DATA.paths[i], i);
|
||||
}
|
||||
} else {
|
||||
$emptyFolder.textContent = dirEmptyNote;
|
||||
$emptyFolder.textContent = DIR_EMPTY_NOTE;
|
||||
$emptyFolder.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
@@ -414,7 +435,7 @@ function renderPathsTableBody() {
|
||||
*/
|
||||
function addPath(file, index) {
|
||||
const encodedName = encodedStr(file.name);
|
||||
let url = newUrl(file.name)
|
||||
let url = newUrl(file.name);
|
||||
let actionDelete = "";
|
||||
let actionDownload = "";
|
||||
let actionMove = "";
|
||||
@@ -455,7 +476,7 @@ function addPath(file, index) {
|
||||
${actionMove}
|
||||
${actionDelete}
|
||||
${actionEdit}
|
||||
</td>`
|
||||
</td>`;
|
||||
|
||||
$pathsTableBody.insertAdjacentHTML("beforeend", `
|
||||
<tr id="addPath${index}">
|
||||
@@ -468,7 +489,7 @@ function addPath(file, index) {
|
||||
<td class="cell-mtime">${formatMtime(file.mtime)}</td>
|
||||
<td class="cell-size">${formatSize(file.size).join(" ")}</td>
|
||||
${actionCell}
|
||||
</tr>`)
|
||||
</tr>`);
|
||||
}
|
||||
|
||||
function setupDropzone() {
|
||||
@@ -480,7 +501,7 @@ function setupDropzone() {
|
||||
});
|
||||
document.addEventListener("drop", async e => {
|
||||
if (!e.dataTransfer.items[0].webkitGetAsEntry) {
|
||||
const files = e.dataTransfer.files.filter(v => v.size > 0);
|
||||
const files = Array.from(e.dataTransfer.files).filter(v => v.size > 0);
|
||||
for (const file of files) {
|
||||
new Uploader(file, []).upload();
|
||||
}
|
||||
@@ -490,25 +511,23 @@ function setupDropzone() {
|
||||
for (let i = 0; i < len; i++) {
|
||||
entries.push(e.dataTransfer.items[i].webkitGetAsEntry());
|
||||
}
|
||||
addFileEntries(entries, [])
|
||||
addFileEntries(entries, []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupAuth() {
|
||||
async function setupAuth() {
|
||||
if (DATA.user) {
|
||||
$userBtn.classList.remove("hidden");
|
||||
$logoutBtn.classList.remove("hidden");
|
||||
$logoutBtn.addEventListener("click", logout);
|
||||
$userName.textContent = DATA.user;
|
||||
} else {
|
||||
const $loginBtn = document.querySelector(".login-btn");
|
||||
$loginBtn.classList.remove("hidden");
|
||||
$loginBtn.addEventListener("click", async () => {
|
||||
try {
|
||||
await checkAuth()
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
await checkAuth();
|
||||
} catch {}
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -585,11 +604,13 @@ async function setupEditorPage() {
|
||||
await doDeletePath(name, url, () => {
|
||||
location.href = location.href.split("/").slice(0, -1).join("/");
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
const $saveBtn = document.querySelector(".save-btn");
|
||||
$saveBtn.classList.remove("hidden");
|
||||
$saveBtn.addEventListener("click", saveChange);
|
||||
if (DATA.editable) {
|
||||
const $saveBtn = document.querySelector(".save-btn");
|
||||
$saveBtn.classList.remove("hidden");
|
||||
$saveBtn.addEventListener("click", saveChange);
|
||||
}
|
||||
} else if (DATA.kind == "View") {
|
||||
$editor.readonly = true;
|
||||
}
|
||||
@@ -599,7 +620,7 @@ async function setupEditorPage() {
|
||||
const url = baseUrl();
|
||||
const ext = extName(baseName(url));
|
||||
if (IFRAME_FORMATS.find(v => v === ext)) {
|
||||
$notEditable.insertAdjacentHTML("afterend", `<iframe src="${url}" sandbox width="100%" height="${window.innerHeight - 100}px"></iframe>`)
|
||||
$notEditable.insertAdjacentHTML("afterend", `<iframe src="${url}" sandbox width="100%" height="${window.innerHeight - 100}px"></iframe>`);
|
||||
} else {
|
||||
$notEditable.classList.remove("hidden");
|
||||
$notEditable.textContent = "Cannot edit because file is too large or binary.";
|
||||
@@ -616,8 +637,8 @@ async function setupEditorPage() {
|
||||
$editor.value = await res.text();
|
||||
} else {
|
||||
const bytes = await res.arrayBuffer();
|
||||
const dataView = new DataView(bytes)
|
||||
const decoder = new TextDecoder(encoding)
|
||||
const dataView = new DataView(bytes);
|
||||
const decoder = new TextDecoder(encoding);
|
||||
$editor.value = decoder.decode(dataView);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -638,10 +659,10 @@ async function deletePath(index) {
|
||||
DATA.paths[index] = null;
|
||||
if (!DATA.paths.find(v => !!v)) {
|
||||
$pathsTable.classList.add("hidden");
|
||||
$emptyFolder.textContent = dirEmptyNote;
|
||||
$emptyFolder.textContent = DIR_EMPTY_NOTE;
|
||||
$emptyFolder.classList.remove("hidden");
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function doDeletePath(name, url, cb) {
|
||||
@@ -674,13 +695,13 @@ async function movePath(index) {
|
||||
}
|
||||
|
||||
async function doMovePath(fileUrl) {
|
||||
const fileUrlObj = new URL(fileUrl)
|
||||
const fileUrlObj = new URL(fileUrl);
|
||||
|
||||
const prefix = DATA.uri_prefix.slice(0, -1);
|
||||
|
||||
const filePath = decodeURIComponent(fileUrlObj.pathname.slice(prefix.length));
|
||||
|
||||
let newPath = prompt("Enter new path", filePath)
|
||||
let newPath = prompt("Enter new path", filePath);
|
||||
if (!newPath) return;
|
||||
if (!newPath.startsWith("/")) newPath = "/" + newPath;
|
||||
if (filePath === newPath) return;
|
||||
@@ -728,12 +749,23 @@ async function saveChange() {
|
||||
async function checkAuth() {
|
||||
if (!DATA.auth) return;
|
||||
const res = await fetch(baseUrl(), {
|
||||
method: "WRITEABLE",
|
||||
method: "CHECKAUTH",
|
||||
});
|
||||
await assertResOK(res);
|
||||
document.querySelector(".login-btn").classList.add("hidden");
|
||||
$userBtn.classList.remove("hidden");
|
||||
$userName.textContent = "";
|
||||
$loginBtn.classList.add("hidden");
|
||||
$logoutBtn.classList.remove("hidden");
|
||||
$userName.textContent = await res.text();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
if (!DATA.auth) return;
|
||||
const url = baseUrl();
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("LOGOUT", url, true, DATA.user);
|
||||
xhr.onload = () => {
|
||||
location.href = url;
|
||||
}
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -803,7 +835,7 @@ function baseUrl() {
|
||||
}
|
||||
|
||||
function baseName(url) {
|
||||
return decodeURIComponent(url.split("/").filter(v => v.length > 0).slice(-1)[0])
|
||||
return decodeURIComponent(url.split("/").filter(v => v.length > 0).slice(-1)[0]);
|
||||
}
|
||||
|
||||
function extName(filename) {
|
||||
@@ -830,7 +862,7 @@ function getPathSvg(path_type) {
|
||||
}
|
||||
|
||||
function formatMtime(mtime) {
|
||||
if (!mtime) return ""
|
||||
if (!mtime) return "";
|
||||
const date = new Date(mtime);
|
||||
const year = date.getFullYear();
|
||||
const month = padZero(date.getMonth() + 1, 2);
|
||||
@@ -841,17 +873,17 @@ function formatMtime(mtime) {
|
||||
}
|
||||
|
||||
function padZero(value, size) {
|
||||
return ("0".repeat(size) + value).slice(-1 * size)
|
||||
return ("0".repeat(size) + value).slice(-1 * size);
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
if (size == null) return [0, "B"]
|
||||
if (size == null) return [0, "B"];
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (size == 0) return [0, "B"];
|
||||
const i = parseInt(Math.floor(Math.log(size) / Math.log(1024)));
|
||||
ratio = 1
|
||||
let ratio = 1;
|
||||
if (i >= 3) {
|
||||
ratio = 100
|
||||
ratio = 100;
|
||||
}
|
||||
return [Math.round(size * ratio / Math.pow(1024, i), 2) / ratio, sizes[i]];
|
||||
}
|
||||
@@ -860,7 +892,7 @@ function formatDuration(seconds) {
|
||||
seconds = Math.ceil(seconds);
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds - h * 3600) / 60);
|
||||
const s = seconds - h * 3600 - m * 60
|
||||
const s = seconds - h * 3600 - m * 60;
|
||||
return `${padZero(h, 2)}:${padZero(m, 2)}:${padZero(s, 2)}`;
|
||||
}
|
||||
|
||||
@@ -889,8 +921,27 @@ function getEncoding(contentType) {
|
||||
if (/charset/i.test(charset)) {
|
||||
let encoding = charset.split("=")[1];
|
||||
if (encoding) {
|
||||
return encoding.toLowerCase()
|
||||
return encoding.toLowerCase();
|
||||
}
|
||||
}
|
||||
return 'utf-8'
|
||||
return 'utf-8';
|
||||
}
|
||||
|
||||
// Parsing base64 strings with Unicode characters
|
||||
function decodeBase64(base64String) {
|
||||
const binString = atob(base64String);
|
||||
const len = binString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
const arr = new Uint32Array(bytes.buffer, 0, Math.floor(len / 4));
|
||||
let i = 0;
|
||||
for (; i < arr.length; i++) {
|
||||
arr[i] = binString.charCodeAt(i * 4) |
|
||||
(binString.charCodeAt(i * 4 + 1) << 8) |
|
||||
(binString.charCodeAt(i * 4 + 2) << 16) |
|
||||
(binString.charCodeAt(i * 4 + 3) << 24);
|
||||
}
|
||||
for (i = i * 4; i < len; i++) {
|
||||
bytes[i] = binString.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{args::Args, server::Response, utils::unix_now};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use headers::HeaderValue;
|
||||
use hyper::{header::WWW_AUTHENTICATE, Method};
|
||||
use indexmap::IndexMap;
|
||||
@@ -287,7 +287,7 @@ pub fn www_authenticate(res: &mut Response, args: &Args) -> Result<()> {
|
||||
|
||||
pub fn get_auth_user(authorization: &HeaderValue) -> Option<String> {
|
||||
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
|
||||
let value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?;
|
||||
let value: Vec<u8> = STANDARD.decode(value).ok()?;
|
||||
let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
|
||||
Some(parts[0].to_string())
|
||||
} else if let Some(value) = strip_prefix(authorization.as_bytes(), b"Digest ") {
|
||||
@@ -306,7 +306,7 @@ pub fn check_auth(
|
||||
auth_pass: &str,
|
||||
) -> Option<()> {
|
||||
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
|
||||
let value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?;
|
||||
let value: Vec<u8> = STANDARD.decode(value).ok()?;
|
||||
let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
|
||||
|
||||
if parts[0] != auth_user {
|
||||
@@ -429,6 +429,8 @@ fn is_readonly_method(method: &Method) -> bool {
|
||||
|| method == Method::OPTIONS
|
||||
|| method == Method::HEAD
|
||||
|| method.as_str() == "PROPFIND"
|
||||
|| method.as_str() == "CHECKAUTH"
|
||||
|| method.as_str() == "LOGOUT"
|
||||
}
|
||||
|
||||
fn strip_prefix<'a>(search: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
|
||||
|
||||
@@ -10,13 +10,14 @@ use crate::Args;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_zip::{tokio::write::ZipFileWriter, Compression, ZipDateTime, ZipEntryBuilder};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use bytes::Bytes;
|
||||
use chrono::{LocalResult, TimeZone, Utc};
|
||||
use futures_util::{pin_mut, TryStreamExt};
|
||||
use headers::{
|
||||
AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, CacheControl,
|
||||
ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfModifiedSince, IfNoneMatch,
|
||||
IfRange, LastModified, Range,
|
||||
ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfMatch, IfModifiedSince,
|
||||
IfNoneMatch, IfRange, IfUnmodifiedSince, LastModified, Range,
|
||||
};
|
||||
use http_body_util::{combinators::BoxBody, BodyExt, StreamBody};
|
||||
use hyper::body::Frame;
|
||||
@@ -199,7 +200,16 @@ impl Server {
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
if method.as_str() == "WRITEABLE" {
|
||||
if method.as_str() == "CHECKAUTH" {
|
||||
match user.clone() {
|
||||
Some(user) => {
|
||||
*res.body_mut() = body_full(user);
|
||||
}
|
||||
None => self.auth_reject(&mut res)?,
|
||||
}
|
||||
return Ok(res);
|
||||
} else if method.as_str() == "LOGOUT" {
|
||||
self.auth_reject(&mut res)?;
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
@@ -250,7 +260,7 @@ impl Server {
|
||||
Method::GET | Method::HEAD => {
|
||||
if is_dir {
|
||||
if render_try_index {
|
||||
if allow_archive && query_params.contains_key("zip") {
|
||||
if allow_archive && has_query_flag(&query_params, "zip") {
|
||||
if !allow_archive {
|
||||
status_not_found(&mut res);
|
||||
return Ok(res);
|
||||
@@ -290,7 +300,7 @@ impl Server {
|
||||
&mut res,
|
||||
)
|
||||
.await?;
|
||||
} else if query_params.contains_key("zip") {
|
||||
} else if has_query_flag(&query_params, "zip") {
|
||||
if !allow_archive {
|
||||
status_not_found(&mut res);
|
||||
return Ok(res);
|
||||
@@ -320,13 +330,13 @@ impl Server {
|
||||
.await?;
|
||||
}
|
||||
} else if is_file {
|
||||
if query_params.contains_key("edit") {
|
||||
if has_query_flag(&query_params, "edit") {
|
||||
self.handle_edit_file(path, DataKind::Edit, head_only, user, &mut res)
|
||||
.await?;
|
||||
} else if query_params.contains_key("view") {
|
||||
} else if has_query_flag(&query_params, "view") {
|
||||
self.handle_edit_file(path, DataKind::View, head_only, user, &mut res)
|
||||
.await?;
|
||||
} else if query_params.contains_key("hash") {
|
||||
} else if has_query_flag(&query_params, "hash") {
|
||||
self.handle_hash_file(path, head_only, &mut res).await?;
|
||||
} else {
|
||||
self.handle_send_file(path, headers, head_only, &mut res)
|
||||
@@ -796,18 +806,29 @@ impl Server {
|
||||
let size = meta.len();
|
||||
let mut use_range = true;
|
||||
if let Some((etag, last_modified)) = extract_cache_headers(&meta) {
|
||||
let cached = {
|
||||
if let Some(if_none_match) = headers.typed_get::<IfNoneMatch>() {
|
||||
!if_none_match.precondition_passes(&etag)
|
||||
} else if let Some(if_modified_since) = headers.typed_get::<IfModifiedSince>() {
|
||||
!if_modified_since.is_modified(last_modified.into())
|
||||
} else {
|
||||
false
|
||||
if let Some(if_unmodified_since) = headers.typed_get::<IfUnmodifiedSince>() {
|
||||
if !if_unmodified_since.precondition_passes(last_modified.into()) {
|
||||
*res.status_mut() = StatusCode::PRECONDITION_FAILED;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if let Some(if_match) = headers.typed_get::<IfMatch>() {
|
||||
if !if_match.precondition_passes(&etag) {
|
||||
*res.status_mut() = StatusCode::PRECONDITION_FAILED;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if let Some(if_modified_since) = headers.typed_get::<IfModifiedSince>() {
|
||||
if !if_modified_since.is_modified(last_modified.into()) {
|
||||
*res.status_mut() = StatusCode::NOT_MODIFIED;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if let Some(if_none_match) = headers.typed_get::<IfNoneMatch>() {
|
||||
if !if_none_match.precondition_passes(&etag) {
|
||||
*res.status_mut() = StatusCode::NOT_MODIFIED;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
if cached {
|
||||
*res.status_mut() = StatusCode::NOT_MODIFIED;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
res.headers_mut().typed_insert(last_modified);
|
||||
@@ -920,13 +941,14 @@ impl Server {
|
||||
};
|
||||
res.headers_mut()
|
||||
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
|
||||
let index_data = STANDARD.encode(serde_json::to_string(&data)?);
|
||||
let output = self
|
||||
.html
|
||||
.replace(
|
||||
"__ASSETS_PREFIX__",
|
||||
&format!("{}{}", self.args.uri_prefix, self.assets_prefix),
|
||||
)
|
||||
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?);
|
||||
.replace("__INDEX_DATA__", &index_data);
|
||||
res.headers_mut()
|
||||
.typed_insert(ContentLength(output.as_bytes().len() as u64));
|
||||
if head_only {
|
||||
@@ -963,9 +985,10 @@ impl Server {
|
||||
) -> Result<()> {
|
||||
let depth: u32 = match headers.get("depth") {
|
||||
Some(v) => match v.to_str().ok().and_then(|v| v.parse().ok()) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
status_bad_request(res, "");
|
||||
Some(0) => 0,
|
||||
Some(1) => 1,
|
||||
_ => {
|
||||
status_bad_request(res, "Invalid depth: only 0 and 1 are allowed.");
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
@@ -975,7 +998,7 @@ impl Server {
|
||||
Some(v) => vec![v],
|
||||
None => vec![],
|
||||
};
|
||||
if depth != 0 {
|
||||
if depth == 1 {
|
||||
match self
|
||||
.list_dir(path, &self.args.serve_path, access_paths)
|
||||
.await
|
||||
@@ -1120,7 +1143,7 @@ impl Server {
|
||||
} else {
|
||||
paths.sort_by(|v1, v2| v1.sort_by_name(v2))
|
||||
}
|
||||
if query_params.contains_key("simple") {
|
||||
if has_query_flag(query_params, "simple") {
|
||||
let output = paths
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
@@ -1160,19 +1183,21 @@ impl Server {
|
||||
user,
|
||||
paths,
|
||||
};
|
||||
let output = if query_params.contains_key("json") {
|
||||
let output = if has_query_flag(query_params, "json") {
|
||||
res.headers_mut()
|
||||
.typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON));
|
||||
serde_json::to_string_pretty(&data)?
|
||||
} else {
|
||||
res.headers_mut()
|
||||
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
|
||||
|
||||
let index_data = STANDARD.encode(serde_json::to_string(&data)?);
|
||||
self.html
|
||||
.replace(
|
||||
"__ASSETS_PREFIX__",
|
||||
&format!("{}{}", self.args.uri_prefix, self.assets_prefix),
|
||||
)
|
||||
.replace("__INDEX_DATA__", &serde_json::to_string(&data)?)
|
||||
.replace("__INDEX_DATA__", &index_data)
|
||||
};
|
||||
res.headers_mut()
|
||||
.typed_insert(ContentLength(output.as_bytes().len() as u64));
|
||||
@@ -1703,12 +1728,12 @@ fn is_hidden(hidden: &[String], file_name: &str, is_dir_type: bool) -> bool {
|
||||
fn set_webdav_headers(res: &mut Response) {
|
||||
res.headers_mut().insert(
|
||||
"Allow",
|
||||
HeaderValue::from_static("GET,HEAD,PUT,OPTIONS,DELETE,PATCH,PROPFIND,COPY,MOVE"),
|
||||
);
|
||||
res.headers_mut().insert(
|
||||
"DAV",
|
||||
HeaderValue::from_static("1, 2, 3, sabredav-partialupdate"),
|
||||
HeaderValue::from_static(
|
||||
"GET,HEAD,PUT,OPTIONS,DELETE,PATCH,PROPFIND,COPY,MOVE,CHECKAUTH,LOGOUT",
|
||||
),
|
||||
);
|
||||
res.headers_mut()
|
||||
.insert("DAV", HeaderValue::from_static("1, 2, 3"));
|
||||
}
|
||||
|
||||
async fn get_content_type(path: &Path) -> Result<String> {
|
||||
@@ -1772,3 +1797,10 @@ async fn sha256_file(path: &Path) -> Result<String> {
|
||||
let result = hasher.finalize();
|
||||
Ok(format!("{:x}", result))
|
||||
}
|
||||
|
||||
fn has_query_flag(query_params: &HashMap<String, String>, name: &str) -> bool {
|
||||
query_params
|
||||
.get(name)
|
||||
.map(|v| v.is_empty())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ fn assets_override(tmpdir: TempDir, port: u16) -> Result<(), Error> {
|
||||
let url = format!("http://localhost:{port}");
|
||||
let resp = reqwest::blocking::get(&url)?;
|
||||
assert!(resp.text()?.starts_with(&format!(
|
||||
"/__dufs_v{}__/index.js;DATA",
|
||||
"/__dufs_v{}__/index.js;<template id=\"index-data\">",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)));
|
||||
let resp = reqwest::blocking::get(&url)?;
|
||||
|
||||
@@ -119,26 +119,38 @@ fn auth_check(
|
||||
#[with(&["--auth", "user:pass@/:rw", "--auth", "user2:pass2@/", "-A"])] server: TestServer,
|
||||
) -> Result<(), Error> {
|
||||
let url = format!("{}index.html", server.url());
|
||||
let resp = fetch!(b"WRITEABLE", &url).send()?;
|
||||
let resp = fetch!(b"CHECKAUTH", &url).send()?;
|
||||
assert_eq!(resp.status(), 401);
|
||||
let resp = send_with_digest_auth(fetch!(b"WRITEABLE", &url), "user2", "pass2")?;
|
||||
assert_eq!(resp.status(), 403);
|
||||
let resp = send_with_digest_auth(fetch!(b"WRITEABLE", &url), "user", "pass")?;
|
||||
let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user2", "pass2")?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn auth_compact_rules(
|
||||
fn auth_check2(
|
||||
#[with(&["--auth", "user:pass@/:rw|user2:pass2@/", "-A"])] server: TestServer,
|
||||
) -> Result<(), Error> {
|
||||
let url = format!("{}index.html", server.url());
|
||||
let resp = fetch!(b"WRITEABLE", &url).send()?;
|
||||
let resp = fetch!(b"CHECKAUTH", &url).send()?;
|
||||
assert_eq!(resp.status(), 401);
|
||||
let resp = send_with_digest_auth(fetch!(b"WRITEABLE", &url), "user2", "pass2")?;
|
||||
assert_eq!(resp.status(), 403);
|
||||
let resp = send_with_digest_auth(fetch!(b"WRITEABLE", &url), "user", "pass")?;
|
||||
let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user", "pass")?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
let resp = send_with_digest_auth(fetch!(b"CHECKAUTH", &url), "user2", "pass2")?;
|
||||
assert_eq!(resp.status(), 200);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn auth_logout(
|
||||
#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer,
|
||||
) -> Result<(), Error> {
|
||||
let url = format!("{}index.html", server.url());
|
||||
let resp = fetch!(b"LOGOUT", &url).send()?;
|
||||
assert_eq!(resp.status(), 401);
|
||||
let resp = send_with_digest_auth(fetch!(b"LOGOUT", &url), "user", "pass")?;
|
||||
assert_eq!(resp.status(), 401);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
80
tests/cache.rs
Normal file
80
tests/cache.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
mod fixtures;
|
||||
mod utils;
|
||||
|
||||
use chrono::{DateTime, Duration};
|
||||
use fixtures::{server, Error, TestServer};
|
||||
use reqwest::header::{
|
||||
HeaderName, ETAG, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_UNMODIFIED_SINCE,
|
||||
LAST_MODIFIED,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case(IF_UNMODIFIED_SINCE, Duration::days(1), StatusCode::OK)]
|
||||
#[case(IF_UNMODIFIED_SINCE, Duration::days(0), StatusCode::OK)]
|
||||
#[case(IF_UNMODIFIED_SINCE, Duration::days(-1), StatusCode::PRECONDITION_FAILED)]
|
||||
#[case(IF_MODIFIED_SINCE, Duration::days(1), StatusCode::NOT_MODIFIED)]
|
||||
#[case(IF_MODIFIED_SINCE, Duration::days(0), StatusCode::NOT_MODIFIED)]
|
||||
#[case(IF_MODIFIED_SINCE, Duration::days(-1), StatusCode::OK)]
|
||||
fn get_file_with_if_modified_since_condition(
|
||||
#[case] header_condition: HeaderName,
|
||||
#[case] duration_after_file_modified: Duration,
|
||||
#[case] expected_code: StatusCode,
|
||||
server: TestServer,
|
||||
) -> Result<(), Error> {
|
||||
let resp = fetch!(b"HEAD", format!("{}index.html", server.url())).send()?;
|
||||
|
||||
let last_modified = resp
|
||||
.headers()
|
||||
.get(LAST_MODIFIED)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|s| DateTime::parse_from_rfc2822(s).ok())
|
||||
.expect("Received no valid last modified header");
|
||||
|
||||
let req_modified_time = (last_modified + duration_after_file_modified)
|
||||
.format("%a, %d %b %Y %T GMT")
|
||||
.to_string();
|
||||
|
||||
let resp = fetch!(b"GET", format!("{}index.html", server.url()))
|
||||
.header(header_condition, req_modified_time)
|
||||
.send()?;
|
||||
|
||||
assert_eq!(resp.status(), expected_code);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn same_etag(etag: &str) -> String {
|
||||
etag.to_owned()
|
||||
}
|
||||
|
||||
fn different_etag(etag: &str) -> String {
|
||||
format!("{}1234", etag)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(IF_MATCH, same_etag, StatusCode::OK)]
|
||||
#[case(IF_MATCH, different_etag, StatusCode::PRECONDITION_FAILED)]
|
||||
#[case(IF_NONE_MATCH, same_etag, StatusCode::NOT_MODIFIED)]
|
||||
#[case(IF_NONE_MATCH, different_etag, StatusCode::OK)]
|
||||
fn get_file_with_etag_match(
|
||||
#[case] header_condition: HeaderName,
|
||||
#[case] etag_modifier: fn(&str) -> String,
|
||||
#[case] expected_code: StatusCode,
|
||||
server: TestServer,
|
||||
) -> Result<(), Error> {
|
||||
let resp = fetch!(b"HEAD", format!("{}index.html", server.url())).send()?;
|
||||
|
||||
let etag = resp
|
||||
.headers()
|
||||
.get(ETAG)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.expect("Received no valid etag header");
|
||||
|
||||
let resp = fetch!(b"GET", format!("{}index.html", server.url()))
|
||||
.header(header_condition, etag_modifier(etag))
|
||||
.send()?;
|
||||
|
||||
assert_eq!(resp.status(), expected_code);
|
||||
Ok(())
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub fn tmpdir() -> TempDir {
|
||||
if *directory == DIR_ASSETS {
|
||||
tmpdir
|
||||
.child(format!("{}{}", directory, "index.html"))
|
||||
.write_str("__ASSETS_PREFIX__index.js;DATA = __INDEX_DATA__")
|
||||
.write_str("__ASSETS_PREFIX__index.js;<template id=\"index-data\">__INDEX_DATA__</template>")
|
||||
.unwrap();
|
||||
} else {
|
||||
for file in FILES {
|
||||
|
||||
@@ -265,12 +265,9 @@ fn options_dir(server: TestServer) -> Result<(), Error> {
|
||||
assert_eq!(resp.status(), 200);
|
||||
assert_eq!(
|
||||
resp.headers().get("allow").unwrap(),
|
||||
"GET,HEAD,PUT,OPTIONS,DELETE,PATCH,PROPFIND,COPY,MOVE"
|
||||
);
|
||||
assert_eq!(
|
||||
resp.headers().get("dav").unwrap(),
|
||||
"1, 2, 3, sabredav-partialupdate"
|
||||
"GET,HEAD,PUT,OPTIONS,DELETE,PATCH,PROPFIND,COPY,MOVE,CHECKAUTH,LOGOUT"
|
||||
);
|
||||
assert_eq!(resp.headers().get("dav").unwrap(), "1, 2, 3");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use indexmap::IndexSet;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -48,7 +49,7 @@ pub fn retrieve_index_paths(content: &str) -> IndexSet<String> {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn retrieve_edit_file(content: &str) -> Option<bool> {
|
||||
let value = retrieve_json(content)?;
|
||||
let value = retrieve_json(content).unwrap();
|
||||
let value = value.get("editable").unwrap();
|
||||
Some(value.as_bool().unwrap())
|
||||
}
|
||||
@@ -62,8 +63,20 @@ pub fn encode_uri(v: &str) -> String {
|
||||
#[allow(dead_code)]
|
||||
pub fn retrieve_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();
|
||||
let start_tag = "<template id=\"index-data\">";
|
||||
let end_tag = "</template>";
|
||||
|
||||
let line = lines.iter().find(|v| v.contains(start_tag))?;
|
||||
|
||||
let start_index = line.find(start_tag)?;
|
||||
let start_content_index = start_index + start_tag.len();
|
||||
|
||||
let end_index = line[start_content_index..].find(end_tag)?;
|
||||
let end_content_index = start_content_index + end_index;
|
||||
|
||||
let value = &line[start_content_index..end_content_index];
|
||||
let value = STANDARD.decode(value).ok()?;
|
||||
let value = serde_json::from_slice(&value).ok()?;
|
||||
|
||||
Some(value)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,17 @@ fn propfind_dir_depth0(server: TestServer) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn propfind_dir_depth2(server: TestServer) -> Result<(), Error> {
|
||||
let resp = fetch!(b"PROPFIND", format!("{}dir1", server.url()))
|
||||
.header("depth", "2")
|
||||
.send()?;
|
||||
assert_eq!(resp.status(), 400);
|
||||
let body = resp.text()?;
|
||||
assert_eq!(body, "Invalid depth: only 0 and 1 are allowed.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn propfind_404(server: TestServer) -> Result<(), Error> {
|
||||
let resp = fetch!(b"PROPFIND", format!("{}404", server.url())).send()?;
|
||||
|
||||
Reference in New Issue
Block a user