/**
- * @typedef {object} PathItem
- * @property {"Dir"|"SymlinkDir"|"File"|"SymlinkFile"} path_type
- * @property {string} name
- * @property {number} mtime
- * @property {number} size
- */
+* @typedef {object} PathItem
+* @property {"Dir"|"SymlinkDir"|"File"|"SymlinkFile"} path_type
+* @property {string} name
+* @property {number} mtime
+* @property {number} size
+*/
/**
- * @typedef {object} DATA
- * @property {string} href
- * @property {string} uri_prefix
- * @property {"Index" | "Edit" | "View"} kind
- * @property {PathItem[]} paths
- * @property {boolean} allow_upload
- * @property {boolean} allow_delete
- * @property {boolean} allow_search
- * @property {boolean} allow_archive
- * @property {boolean} auth
- * @property {string} user
- * @property {boolean} dir_exists
- * @property {string} editable
- */
-
-var DUFS_MAX_UPLOADINGS = 1;
+* @typedef {object} DATA
+* @property {string} href
+* @property {string} uri_prefix
+* @property {"Index" | "Edit" | "View"} kind
+* @property {PathItem[]} paths
+* @property {boolean} allow_upload
+* @property {boolean} allow_delete
+* @property {boolean} allow_search
+* @property {boolean} allow_archive
+* @property {boolean} auth
+* @property {string} user
+* @property {boolean} dir_exists
+* @property {string} editable
+*/
+
+var OZVA_MAX_UPLOADINGS = 1;
/**
- * @type {DATA} DATA
- */
+* @type {DATA} DATA
+*/
var DATA;
/**
- * @type {string}
- */
+* @type {string}
+*/
var DIR_EMPTY_NOTE;
/**
- * @type {PARAMS}
- * @typedef {object} PARAMS
- * @property {string} q
- * @property {string} sort
- * @property {string} order
- */
+* @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 IFRAME_FORMATS = [
- ".pdf",
- ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg",
- ".mp4", ".mov", ".avi", ".wmv", ".flv", ".webm",
- ".mp3", ".ogg", ".wav", ".m4a",
+ ".pdf",
+ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg",
+ ".mp4", ".mov", ".avi", ".wmv", ".flv", ".webm",
+ ".mp3", ".ogg", ".wav", ".m4a",
];
const MAX_SUBPATHS_COUNT = 1000;
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>`,
- symlinkDir: `<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-1zM1 3h5v1H1V3zm6 9v-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.73C4.86 8.43 5.82 8 7.01 8V6l4 3-4 3H7z"></path></svg>`,
- file: `<svg height="16" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M6 5H2V4h4v1zM2 8h7V7H2v1zm0 2h7V9H2v1zm0 2h7v-1H2v1zm10-7.5V14c0 .55-.45 1-1 1H1c-.55 0-1-.45-1-1V2c0-.55.45-1 1-1h7.5L12 4.5zM11 5L8 2H1v12h10V5z"></path></svg>`,
- 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>`,
- move: `<svg width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/></svg>`,
- edit: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>`,
- delete: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M6.854 7.146a.5.5 0 1 0-.708.708L7.293 9l-1.147 1.146a.5.5 0 0 0 .708.708L8 9.707l1.146 1.147a.5.5 0 0 0 .708-.708L8.707 9l1.147-1.146a.5.5 0 0 0-.708-.708L8 8.293 6.854 7.146z"/><path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/></svg>`,
- view: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1"/></svg>`,
+ 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>`,
+ symlinkDir: `<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-1zM1 3h5v1H1V3zm6 9v-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.73C4.86 8.43 5.82 8 7.01 8V6l4 3-4 3H7z"></path></svg>`,
+ file: `<svg height="16" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M6 5H2V4h4v1zM2 8h7V7H2v1zm0 2h7V9H2v1zm0 2h7v-1H2v1zm10-7.5V14c0 .55-.45 1-1 1H1c-.55 0-1-.45-1-1V2c0-.55.45-1 1-1h7.5L12 4.5zM11 5L8 2H1v12h10V5z"></path></svg>`,
+ 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>`,
+ move: `<svg width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/></svg>`,
+ edit: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>`,
+ delete: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M6.854 7.146a.5.5 0 1 0-.708.708L7.293 9l-1.147 1.146a.5.5 0 0 0 .708.708L8 9.707l1.146 1.147a.5.5 0 0 0 .708-.708L8.707 9l1.147-1.146a.5.5 0 0 0-.708-.708L8 8.293 6.854 7.146z"/><path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/></svg>`,
+ view: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1"/></svg>`,
}
/**
- * @type Map<string, Uploader>
- */
+* @type Map<string, Uploader>
+*/
const failUploaders = new Map();
/**
- * @type Element
- */
+* @type Element
+*/
let $pathsTable;
/**
- * @type Element
- */
+* @type Element
+*/
let $pathsTableHead;
/**
- * @type Element
- */
+* @type Element
+*/
let $pathsTableBody;
/**
- * @type Element
- */
+* @type Element
+*/
let $uploadersTable;
/**
- * @type Element
- */
+* @type Element
+*/
let $emptyFolder;
/**
- * @type Element
- */
+* @type Element
+*/
let $editor;
/**
- * @type Element
- */
+* @type Element
+*/
let $loginBtn;
/**
- * @type Element
- */
+* @type Element
+*/
let $logoutBtn;
/**
- * @type Element
- */
+* @type Element
+*/
let $userName;
// Produce table when window loads
window.addEventListener("DOMContentLoaded", async () => {
- const $indexData = document.getElementById('index-data');
- if (!$indexData) {
- alert("No data");
- return;
- }
+ 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';
+ 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();
+ 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");
- $loginBtn = document.querySelector(".login-btn");
- $logoutBtn = document.querySelector(".logout-btn");
- $userName = document.querySelector(".user-name");
+function makeDrag ( e ) {
+ ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(name => {
+ e.addEventListener(name, ev => {
+ ev.stopPropagation();
+ });
+ });
+
+ e.addEventListener("dragstart", ev => {
+ ev.dataTransfer.effectAllowed = 'move';
+
+ ev.dataTransfer.setData('text/plain', e.dataset.i);
+
+ for (zone of document.getElementsByClassName("internaldrop")) {
+ zone.classList.add("dragging");
+ }
- addBreadcrumb(DATA.href, DATA.uri_prefix);
+ console.log("drag start");
+ });
- if (DATA.kind === "Index") {
- document.title = `Index of ${DATA.href} - Dufs`;
- document.querySelector(".index-page").classList.remove("hidden");
+ e.addEventListener("dragend", (ev) => {
+ if (ev.target.classList.contains("dropzone")) {
+ ev.target.classList.remove("dragover");
+ }
- await setupIndexPage();
- } else if (DATA.kind === "Edit") {
- document.title = `Edit ${DATA.href} - Dufs`;
- document.querySelector(".editor-page").classList.remove("hidden");
+ for (zone of document.getElementsByClassName("internaldrop")) {
+ zone.classList.remove("dragging");
+ zone.classList.remove("dragover");
+ }
+ },);
+}
- await setupEditorPage();
- } else if (DATA.kind === "View") {
- document.title = `View ${DATA.href} - Dufs`;
- document.querySelector(".editor-page").classList.remove("hidden");
+function makeDrop ( e ) {
+ e.addEventListener("dragenter", (ev) => {
+ ev.stopPropagation();
+ if (ev.target.classList.contains("internaldrop")) {
+ ev.target.classList.add("dragover");
+ }
+ },);
+
+ e.addEventListener("dragleave", (ev) => {
+ ev.stopPropagation();
+ if (ev.target.classList.contains("dragover")) {
+ ev.target.classList.remove("dragover");
+ }
+ },);
+
+ e.addEventListener("dragover", (ev) => {
+ ev.preventDefault()
+ },);
+
+ let method;
+ if (e.classList.contains("dropcopy")) {
+ method = "COPY";
+ } else {
+ method = "MOVE";
+ }
+ e.addEventListener("drop", getDrop(method, e),);
+}
- await setupEditorPage();
- }
+var getDrop = function ( method, e ) {
+ return async function doDrop (ev) {
+ ev.stopPropagation();
+
+ const index = ev.dataTransfer.getData("text/plain");
+ const file = DATA.paths[index];
+ if (!file) return;
+ const fileUrl = newUrl(file.name);
+ const fileName = fileUrl.split("/").at(-1)
+
+ const dir = DATA.paths[e.dataset.i];
+ if (!dir) return;
+ const regex = /\/[^\/]+\/\.\./i;
+ const dirUrl = newUrl(dir.name).replace(regex, "");
+
+ let newFileUrl = `${dirUrl}/${fileName}`;
+
+ try {
+ await checkAuth();
+ const res1 = await fetch(newFileUrl, {
+ method: "HEAD",
+ });
+ if (res1.status === 200) {
+ if (!confirm("Override existing file?")) {
+ return;
+ }
+ }
+ const res2 = await fetch(fileUrl, {
+ method: method,
+ headers: {
+ "Destination": newFileUrl,
+ }
+ });
+ await assertResOK(res2);
+
+ if (method == "MOVE") {
+ document.getElementById(`addPath${index}`)?.remove();
+ DATA.paths[index] = null;
+ if (!DATA.paths.find(v => !!v)) {
+ $pathsTable.classList.add("hidden");
+ $emptyFolder.textContent = DIR_EMPTY_NOTE;
+ $emptyFolder.classList.remove("hidden");
+ }
+ }
+
+ } catch (err) {
+ alert(`Cannot ${method.toLowerCase()} \`${fileUrl}\` to \`${newFileUrl}\`, ${err.message}`);
+ }
+ }
+}
+
+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");
+ $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") {
+ document.title = `Index of ${DATA.href} - OzVa Cloud`;
+ document.querySelector(".index-page").classList.remove("hidden");
+
+ await setupIndexPage();
+
+ // make files draggable
+ for (e of document.querySelectorAll('[draggable="true"]')) {
+ makeDrag(e);
+ }
+
+ for (e of document.getElementsByClassName('internaldrop')) {
+ makeDrop(e);
+ }
+
+ } else if (DATA.kind === "Edit") {
+ document.title = `Edit ${DATA.href} - OzVa Cloud`;
+ document.querySelector(".editor-page").classList.remove("hidden");
+
+ await setupEditorPage();
+ } else if (DATA.kind === "View") {
+ document.title = `View ${DATA.href} - OzVa Cloud`;
+ document.querySelector(".editor-page").classList.remove("hidden");
+
+ await setupEditorPage();
+ }
}
class Uploader {
- /**
- *
- * @param {File} file
- * @param {string[]} pathParts
- */
- constructor(file, pathParts) {
- /**
- * @type Element
- */
- this.$uploadStatus = null
- this.uploaded = 0;
- this.uploadOffset = 0;
- this.lastUptime = 0;
- this.name = [...pathParts, file.name].join("/");
- this.idx = Uploader.globalIdx++;
- this.file = file;
- this.url = newUrl(this.name);
- }
-
- upload() {
- const { idx, name, url } = this;
- const encodedName = encodedStr(name);
- $uploadersTable.insertAdjacentHTML("beforeend", `
- <tr id="upload${idx}" class="uploader">
- <td class="path cell-icon">
- ${getPathSvg()}
- </td>
- <td class="path cell-name">
- <a href="${url}">${encodedName}</a>
- </td>
- <td class="cell-status upload-status" id="uploadStatus${idx}"></td>
- </tr>`);
- $uploadersTable.classList.remove("hidden");
- $emptyFolder.classList.add("hidden");
- this.$uploadStatus = document.getElementById(`uploadStatus${idx}`);
- this.$uploadStatus.innerHTML = '-';
- this.$uploadStatus.addEventListener("click", e => {
- const nodeId = e.target.id;
- const matches = /^retry(\d+)$/.exec(nodeId);
- if (matches) {
- const id = parseInt(matches[1]);
- let uploader = failUploaders.get(id);
- if (uploader) uploader.retry();
- }
- });
- Uploader.queues.push(this);
- Uploader.runQueue();
- }
-
- ajax() {
- const { url } = this;
-
- this.uploaded = 0;
- this.lastUptime = Date.now();
-
- const ajax = new XMLHttpRequest();
- ajax.upload.addEventListener("progress", e => this.progress(e), false);
- ajax.addEventListener("readystatechange", () => {
- if (ajax.readyState === 4) {
- if (ajax.status >= 200 && ajax.status < 300) {
- this.complete();
- } else {
- if (ajax.status != 0) {
- this.fail(`${ajax.status} ${ajax.statusText}`);
- }
- }
- }
- })
- ajax.addEventListener("error", () => this.fail(), false);
- ajax.addEventListener("abort", () => this.fail(), false);
- if (this.uploadOffset > 0) {
- ajax.open("PATCH", url);
- ajax.setRequestHeader("X-Update-Range", "append");
- ajax.send(this.file.slice(this.uploadOffset));
- } else {
- ajax.open("PUT", url);
- ajax.send(this.file);
- // setTimeout(() => ajax.abort(), 3000);
- }
- }
-
- async retry() {
- const { url } = this;
- let res = await fetch(url, {
- method: "HEAD",
- });
- let uploadOffset = 0;
- if (res.status == 200) {
- let value = res.headers.get("content-length");
- uploadOffset = parseInt(value) || 0;
- }
- this.uploadOffset = uploadOffset;
- this.ajax();
- }
-
- progress(event) {
- const now = Date.now();
- const speed = (event.loaded - this.uploaded) / (now - this.lastUptime) * 1000;
- const [speedValue, speedUnit] = formatFileSize(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);
- this.$uploadStatus.innerHTML = `<span style="width: 80px;">${speedText}</span><span>${progress} ${duration}</span>`;
- this.uploaded = event.loaded;
- this.lastUptime = now;
- }
-
- complete() {
- const $uploadStatusNew = this.$uploadStatus.cloneNode(true);
- $uploadStatusNew.innerHTML = `✓`;
- this.$uploadStatus.parentNode.replaceChild($uploadStatusNew, this.$uploadStatus);
- this.$uploadStatus = null;
- failUploaders.delete(this.idx);
- Uploader.runnings--;
- Uploader.runQueue();
- }
-
- fail(reason = "") {
- this.$uploadStatus.innerHTML = `<span style="width: 20px;" title="${reason}">✗</span><span class="retry-btn" id="retry${this.idx}" title="Retry">↻</span>`;
- failUploaders.set(this.idx, this);
- Uploader.runnings--;
- Uploader.runQueue();
- }
+ /**
+ *
+ * @param {File} file
+ * @param {string[]} pathParts
+ */
+ constructor(file, pathParts) {
+ /**
+ * @type Element
+ */
+ this.$uploadStatus = null
+ this.uploaded = 0;
+ this.uploadOffset = 0;
+ this.lastUptime = 0;
+ this.name = [...pathParts, file.name].join("/");
+ this.idx = Uploader.globalIdx++;
+ this.file = file;
+ this.url = newUrl(this.name);
+ }
+
+ upload() {
+ const { idx, name, url } = this;
+ const encodedName = encodedStr(name);
+ $uploadersTable.insertAdjacentHTML("beforeend", `
+ <tr id="upload${idx}" class="uploader">
+ <td class="path cell-icon">
+ ${getPathSvg()}
+ </td>
+ <td class="path cell-name">
+ <a href="${url}">${encodedName}</a>
+ </td>
+ <td class="cell-status upload-status" id="uploadStatus${idx}"></td>
+ </tr>`);
+ $uploadersTable.classList.remove("hidden");
+ $emptyFolder.classList.add("hidden");
+ this.$uploadStatus = document.getElementById(`uploadStatus${idx}`);
+ this.$uploadStatus.innerHTML = '-';
+ this.$uploadStatus.addEventListener("click", e => {
+ const nodeId = e.target.id;
+ const matches = /^retry(\d+)$/.exec(nodeId);
+ if (matches) {
+ const id = parseInt(matches[1]);
+ let uploader = failUploaders.get(id);
+ if (uploader) uploader.retry();
+ }
+ });
+ Uploader.queues.push(this);
+ Uploader.runQueue();
+ }
+
+ ajax() {
+ const { url } = this;
+
+ this.uploaded = 0;
+ this.lastUptime = Date.now();
+
+ const ajax = new XMLHttpRequest();
+ ajax.upload.addEventListener("progress", e => this.progress(e), false);
+ ajax.addEventListener("readystatechange", () => {
+ if (ajax.readyState === 4) {
+ if (ajax.status >= 200 && ajax.status < 300) {
+ this.complete();
+ } else {
+ if (ajax.status != 0) {
+ this.fail(`${ajax.status} ${ajax.statusText}`);
+ }
+ }
+ }
+ })
+ ajax.addEventListener("error", () => this.fail(), false);
+ ajax.addEventListener("abort", () => this.fail(), false);
+ if (this.uploadOffset > 0) {
+ ajax.open("PATCH", url);
+ ajax.setRequestHeader("X-Update-Range", "append");
+ ajax.send(this.file.slice(this.uploadOffset));
+ } else {
+ ajax.open("PUT", url);
+ ajax.send(this.file);
+ // setTimeout(() => ajax.abort(), 3000);
+ }
+ }
+
+ async retry() {
+ const { url } = this;
+ let res = await fetch(url, {
+ method: "HEAD",
+ });
+ let uploadOffset = 0;
+ if (res.status == 200) {
+ let value = res.headers.get("content-length");
+ uploadOffset = parseInt(value) || 0;
+ }
+ this.uploadOffset = uploadOffset;
+ this.ajax();
+ }
+
+ progress(event) {
+ const now = Date.now();
+ const speed = (event.loaded - this.uploaded) / (now - this.lastUptime) * 1000;
+ const [speedValue, speedUnit] = formatFileSize(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);
+ this.$uploadStatus.innerHTML = `<span style="width: 80px;">${speedText}</span><span>${progress} ${duration}</span>`;
+ this.uploaded = event.loaded;
+ this.lastUptime = now;
+ }
+
+ complete() {
+ const $uploadStatusNew = this.$uploadStatus.cloneNode(true);
+ $uploadStatusNew.innerHTML = `✓`;
+ this.$uploadStatus.parentNode.replaceChild($uploadStatusNew, this.$uploadStatus);
+ this.$uploadStatus = null;
+ failUploaders.delete(this.idx);
+ Uploader.runnings--;
+ Uploader.runQueue();
+ }
+
+ fail(reason = "") {
+ this.$uploadStatus.innerHTML = `<span style="width: 20px;" title="${reason}">✗</span><span class="retry-btn" id="retry${this.idx}" title="Retry">↻</span>`;
+ failUploaders.set(this.idx, this);
+ Uploader.runnings--;
+ Uploader.runQueue();
+ }
}
Uploader.globalIdx = 0;
Uploader.auth = false;
/**
- * @type Uploader[]
- */
+* @type Uploader[]
+*/
Uploader.queues = [];
Uploader.runQueue = async () => {
- if (Uploader.runnings >= DUFS_MAX_UPLOADINGS) return;
- if (Uploader.queues.length == 0) return;
- Uploader.runnings++;
- let uploader = Uploader.queues.shift();
- if (!Uploader.auth) {
- Uploader.auth = true;
- try {
- await checkAuth();
- } catch {
- Uploader.auth = false;
- }
- }
- uploader.ajax();
+ if (Uploader.runnings >= OZVA_MAX_UPLOADINGS) return;
+ if (Uploader.queues.length == 0) return;
+ Uploader.runnings++;
+ let uploader = Uploader.queues.shift();
+ if (!Uploader.auth) {
+ Uploader.auth = true;
+ try {
+ await checkAuth();
+ } catch {
+ Uploader.auth = false;
+ }
+ }
+ uploader.ajax();
}
/**
- * Add breadcrumb
- * @param {string} href
- * @param {string} uri_prefix
- */
+* Add breadcrumb
+* @param {string} href
+* @param {string} uri_prefix
+*/
function addBreadcrumb(href, uri_prefix) {
- const $breadcrumb = document.querySelector(".breadcrumb");
- let parts = [];
- if (href === "/") {
- parts = [""];
- } else {
- parts = href.split("/");
- }
- const len = parts.length;
- let path = uri_prefix;
- for (let i = 0; i < len; i++) {
- const name = parts[i];
- if (i > 0) {
- if (!path.endsWith("/")) {
- path += "/";
- }
- path += encodeURIComponent(name);
- }
- const encodedName = encodedStr(name);
- if (i === 0) {
- $breadcrumb.insertAdjacentHTML("beforeend", `<a href="${path}" title="Root"><svg width="16" height="16" viewBox="0 0 16 16"><path d="M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5z"/></svg></a>`);
- } else if (i === len - 1) {
- $breadcrumb.insertAdjacentHTML("beforeend", `<b>${encodedName}</b>`);
- } else {
- $breadcrumb.insertAdjacentHTML("beforeend", `<a href="${path}">${encodedName}</a>`);
- }
- if (i !== len - 1) {
- $breadcrumb.insertAdjacentHTML("beforeend", `<span class="separator">/</span>`);
- }
- }
+ const $breadcrumb = document.querySelector(".breadcrumb");
+ let parts = [];
+ if (href === "/") {
+ parts = [""];
+ } else {
+ parts = href.split("/");
+ }
+ const len = parts.length;
+ let path = uri_prefix;
+ for (let i = 0; i < len; i++) {
+ const name = parts[i];
+ if (i > 0) {
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
+ path += encodeURIComponent(name);
+ }
+ const encodedName = encodedStr(name);
+ if (i === 0) {
+ $breadcrumb.insertAdjacentHTML("beforeend", `<a href="${path}" title="Root"><svg width="16" height="16" viewBox="0 0 16 16"><path d="M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5z"/></svg></a>`);
+ } else if (i === len - 1) {
+ $breadcrumb.insertAdjacentHTML("beforeend", `<b>${encodedName}</b>`);
+ } else {
+ $breadcrumb.insertAdjacentHTML("beforeend", `<a href="${path}">${encodedName}</a>`);
+ }
+ if (i !== len - 1) {
+ $breadcrumb.insertAdjacentHTML("beforeend", `<span class="separator">/</span>`);
+ }
+ }
}
async function setupIndexPage() {
- if (DATA.allow_archive) {
- const $download = document.querySelector(".download");
- $download.href = baseUrl() + "?zip";
- $download.title = "Download folder as a .zip file";
- $download.classList.remove("hidden");
- }
-
- if (DATA.allow_upload) {
- setupDropzone();
- setupUploadFile();
- setupNewFolder();
- setupNewFile();
- }
-
- if (DATA.auth) {
- await setupAuth();
- }
-
- if (DATA.allow_search) {
- setupSearch();
- }
-
- renderPathsTableHead();
- renderPathsTableBody();
+ if (DATA.allow_archive) {
+ const $download = document.querySelector(".download");
+ $download.href = baseUrl() + "?zip";
+ $download.title = "Download folder as a .zip file";
+ $download.classList.remove("hidden");
+ }
+
+ if (DATA.allow_upload) {
+ setupDropzone();
+ setupUploadFile();
+ setupNewFolder();
+ setupNewFile();
+ }
+
+ if (DATA.auth) {
+ await setupAuth();
+ }
+
+ if (DATA.allow_search) {
+ setupSearch();
+ }
+
+ console.log(DATA.paths);
+
+ renderPathsTableHead();
+ renderPathsTableBody();
}
/**
- * Render path table thead
- */
+* 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 = "desc";
- if (PARAMS.sort === item.name) {
- if (PARAMS.order === "desc") {
- order = "asc";
- 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>`
- } else {
- 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>`
- }
- }
- 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>
- `);
+ 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 = "desc";
+ if (PARAMS.sort === item.name) {
+ if (PARAMS.order === "desc") {
+ order = "asc";
+ 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>`
+ } else {
+ 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>`
+ }
+ }
+ 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
- */
+* 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 = DIR_EMPTY_NOTE;
- $emptyFolder.classList.remove("hidden");
- }
+ 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 = DIR_EMPTY_NOTE;
+ $emptyFolder.classList.remove("hidden");
+ }
}
/**
- * Add pathitem
- * @param {PathItem} file
- * @param {number} index
- */
+* Add pathitem
+* @param {PathItem} file
+* @param {number} index
+*/
function addPath(file, index) {
- const encodedName = encodedStr(file.name);
- let url = newUrl(file.name);
- let actionDelete = "";
- let actionDownload = "";
- let actionMove = "";
- let actionEdit = "";
- let actionView = "";
- let isDir = file.path_type.endsWith("Dir");
- if (isDir) {
- url += "/";
- if (DATA.allow_archive) {
- actionDownload = `
- <div class="action-btn">
- <a href="${url}?zip" title="Download folder as a .zip file">${ICONS.download}</a>
- </div>`;
- }
- } else {
- actionDownload = `
- <div class="action-btn" >
- <a href="${url}" title="Download file" download>${ICONS.download}</a>
- </div>`;
- }
- if (DATA.allow_delete) {
- if (DATA.allow_upload) {
- actionMove = `<div onclick="movePath(${index})" class="action-btn" id="moveBtn${index}" title="Move to new path">${ICONS.move}</div>`;
- if (!isDir) {
- actionEdit = `<a class="action-btn" title="Edit file" target="_blank" href="${url}?edit">${ICONS.edit}</a>`;
- }
- }
- actionDelete = `
- <div onclick="deletePath(${index})" class="action-btn" id="deleteBtn${index}" title="Delete">${ICONS.delete}</div>`;
- }
- if (!actionEdit && !isDir) {
- actionView = `<a class="action-btn" title="View file" target="_blank" href="${url}?view">${ICONS.view}</a>`;
- }
- let actionCell = `
- <td class="cell-actions">
- ${actionDownload}
- ${actionView}
- ${actionMove}
- ${actionDelete}
- ${actionEdit}
- </td>`;
-
- let sizeDisplay = isDir ? formatDirSize(file.size) : formatFileSize(file.size).join(" ");
-
- $pathsTableBody.insertAdjacentHTML("beforeend", `
+ const encodedName = encodedStr(file.name);
+ let url = newUrl(file.name);
+ let actionDelete = "";
+ let actionDownload = "";
+ let actionMove = "";
+ let actionEdit = "";
+ let actionView = "";
+ let isDir = file.path_type.endsWith("Dir");
+ let isParent = (file.name == "..");
+ if (isDir) {
+ url += "/";
+ if (DATA.allow_archive) {
+ actionDownload = `
+ <div class="action-btn">
+ <a href="${url}?zip" title="Download folder as a .zip file">${ICONS.download}</a>
+ </div>`;
+ }
+ } else {
+ actionDownload = `
+ <div class="action-btn" >
+ <a href="${url}" title="Download file" download>${ICONS.download}</a>
+ </div>`;
+ }
+ if (DATA.allow_delete) {
+ if (DATA.allow_upload) {
+ actionMove = `<div onclick="movePath(${index})" class="action-btn" id="moveBtn${index}" title="Move to new path">${ICONS.move}</div>`;
+ if (!isDir) {
+ actionEdit = `<a class="action-btn" title="Edit file" target="_blank" href="${url}?edit">${ICONS.edit}</a>`;
+ }
+ }
+ actionDelete = `
+ <div onclick="deletePath(${index})" class="action-btn" id="deleteBtn${index}" title="Delete">${ICONS.delete}</div>`;
+ }
+ if (!actionEdit && !isDir) {
+ actionView = `<a class="action-btn" title="View file" target="_blank" href="${url}?view">${ICONS.view}</a>`;
+ }
+ let actionCell = `
+ <td class="cell-actions">
+ ${actionDownload}
+ ${actionView}
+ ${actionMove}
+ ${actionDelete}
+ ${actionEdit}
+ </td>`;
+
+ let sizeDisplay = isDir ? formatDirSize(file.size) : formatFileSize(file.size).join(" ");
+
+ $pathsTableBody.insertAdjacentHTML("beforeend", `
<tr id="addPath${index}">
- <td class="path cell-icon">
- ${getPathSvg(file.path_type)}
- </td>
- <td class="path cell-name">
- <a href="${url}" ${isDir ? "" : `target="_blank"`}>${encodedName}</a>
- </td>
- <td class="cell-mtime">${formatMtime(file.mtime)}</td>
- <td class="cell-size">${sizeDisplay}</td>
- ${actionCell}
+ <td class="path cell-icon">
+ ${getPathSvg(file.path_type)}
+ </td>
+ <td class="path cell-name">
+ <div class="drag-div" ${isParent ? "" : `draggable="true"`} data-i="${index}">
+ <a href="${url}" ${isDir ? "" : `target="_blank"`}>${encodedName}</a>
+ </div>
+ ${isDir ? `
+<div class="internaldrop dropcopy" data-i="${index}">Copy</div>
+<div class="internaldrop dropmove" data-i="${index}">Move</div>
+ ` : ''}
+ </td>
+ <td class="cell-mtime">${formatMtime(file.mtime)}</td>
+ <td class="cell-size">${sizeDisplay}</td>
+ ${actionCell}
</tr>`);
}
function setupDropzone() {
- ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(name => {
- document.addEventListener(name, e => {
- e.preventDefault();
- e.stopPropagation();
- });
- });
- document.addEventListener("drop", async e => {
- if (!e.dataTransfer.items[0].webkitGetAsEntry) {
- const files = Array.from(e.dataTransfer.files).filter(v => v.size > 0);
- for (const file of files) {
- new Uploader(file, []).upload();
- }
- } else {
- const entries = [];
- const len = e.dataTransfer.items.length;
- for (let i = 0; i < len; i++) {
- entries.push(e.dataTransfer.items[i].webkitGetAsEntry());
- }
- addFileEntries(entries, []);
- }
- });
+ const dropZone = document.getElementById("filedrop");
+ dropZone.addEventListener("dragenter", e => {
+ e.stopPropagation();
+
+ if (!e.dataTransfer.getData("text/plain")) {
+ dropZone.classList.add("dragoverfile");
+ }
+ });
+ dropZone.addEventListener("dragleave", e => {
+ e.stopPropagation();
+
+ dropZone.classList.remove("dragoverfile");
+ });
+
+ ["dragend", "dragleave"].forEach(name => {
+ document.body.addEventListener(name, e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ("dragging" in dropZone.classList) {
+ dropZone.classList.remove("dragging");
+ }
+ });
+ });
+
+ ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(name => {
+ dropZone.addEventListener(name, e => {
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ });
+ dropZone.addEventListener("drop", async e => {
+ dropZone.classList.remove("dragging");
+
+ if (!e.dataTransfer.items[0].webkitGetAsEntry) {
+ const files = Array.from(e.dataTransfer.files).filter(v => v.size > 0);
+ for (const file of files) {
+ new Uploader(file, []).upload();
+ }
+ } else {
+ const entries = [];
+ const len = e.dataTransfer.items.length;
+ for (let i = 0; i < len; i++) {
+ entries.push(e.dataTransfer.items[i].webkitGetAsEntry());
+ }
+ addFileEntries(entries, []);
+ }
+ });
}
async function setupAuth() {
- if (DATA.user) {
- $logoutBtn.classList.remove("hidden");
- $logoutBtn.addEventListener("click", logout);
- $userName.textContent = DATA.user;
- } else {
- $loginBtn.classList.remove("hidden");
- $loginBtn.addEventListener("click", async () => {
- try {
- await checkAuth();
- } catch {}
- location.reload();
- });
- }
+ if (DATA.user) {
+ $logoutBtn.classList.remove("hidden");
+ $logoutBtn.addEventListener("click", logout);
+ $userName.textContent = DATA.user;
+ } else {
+ $loginBtn.classList.remove("hidden");
+ $loginBtn.addEventListener("click", async () => {
+ try {
+ await checkAuth();
+ } catch {}
+ location.reload();
+ });
+ }
}
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 = baseUrl();
- if (q) {
- href += "?q=" + q;
- }
- location.href = href;
- });
- if (PARAMS.q) {
- document.getElementById('search').value = PARAMS.q;
- }
+ 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 = baseUrl();
+ if (q) {
+ href += "?q=" + q;
+ }
+ location.href = href;
+ });
+ if (PARAMS.q) {
+ document.getElementById('search').value = PARAMS.q;
+ }
}
function setupUploadFile() {
- document.querySelector(".upload-file").classList.remove("hidden");
- document.getElementById("file").addEventListener("change", async e => {
- const files = e.target.files;
- for (let file of files) {
- new Uploader(file, []).upload();
- }
- });
+ document.querySelector(".upload-file").classList.remove("hidden");
+ document.getElementById("file").addEventListener("change", async e => {
+ const files = e.target.files;
+ for (let file of files) {
+ new Uploader(file, []).upload();
+ }
+ });
}
function setupNewFolder() {
- const $newFolder = document.querySelector(".new-folder");
- $newFolder.classList.remove("hidden");
- $newFolder.addEventListener("click", () => {
- const name = prompt("Enter folder name");
- if (name) createFolder(name);
- });
+ const $newFolder = document.querySelector(".new-folder");
+ $newFolder.classList.remove("hidden");
+ $newFolder.addEventListener("click", () => {
+ const name = prompt("Enter folder name");
+ if (name) createFolder(name);
+ });
}
function setupNewFile() {
- const $newFile = document.querySelector(".new-file");
- $newFile.classList.remove("hidden");
- $newFile.addEventListener("click", () => {
- const name = prompt("Enter file name");
- if (name) createFile(name);
- });
+ const $newFile = document.querySelector(".new-file");
+ $newFile.classList.remove("hidden");
+ $newFile.addEventListener("click", () => {
+ const name = prompt("Enter file name");
+ if (name) createFile(name);
+ });
}
async function setupEditorPage() {
- const url = baseUrl();
-
- const $download = document.querySelector(".download");
- $download.classList.remove("hidden");
- $download.href = url;
-
- if (DATA.kind == "Edit") {
- const $moveFile = document.querySelector(".move-file");
- $moveFile.classList.remove("hidden");
- $moveFile.addEventListener("click", async () => {
- const query = location.href.slice(url.length);
- const newFileUrl = await doMovePath(url);
- if (newFileUrl) {
- location.href = newFileUrl + query;
- }
- });
-
- const $deleteFile = document.querySelector(".delete-file");
- $deleteFile.classList.remove("hidden");
- $deleteFile.addEventListener("click", async () => {
- const url = baseUrl();
- const name = baseName(url);
- await doDeletePath(name, url, () => {
- location.href = location.href.split("/").slice(0, -1).join("/");
- });
- });
-
- 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;
- }
-
- if (!DATA.editable) {
- const $notEditable = document.querySelector(".not-editable");
- 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>`);
- } else {
- $notEditable.classList.remove("hidden");
- $notEditable.textContent = "Cannot edit because file is too large or binary.";
- }
- return;
- }
-
- $editor.classList.remove("hidden");
- try {
- const res = await fetch(baseUrl());
- await assertResOK(res);
- const encoding = getEncoding(res.headers.get("content-type"));
- if (encoding === "utf-8") {
- $editor.value = await res.text();
- } else {
- const bytes = await res.arrayBuffer();
- const dataView = new DataView(bytes);
- const decoder = new TextDecoder(encoding);
- $editor.value = decoder.decode(dataView);
- }
- } catch (err) {
- alert(`Failed get file, ${err.message}`);
- }
+ const url = baseUrl();
+
+ const $download = document.querySelector(".download");
+ $download.classList.remove("hidden");
+ $download.href = url;
+
+ if (DATA.kind == "Edit") {
+ const $moveFile = document.querySelector(".move-file");
+ $moveFile.classList.remove("hidden");
+ $moveFile.addEventListener("click", async () => {
+ const query = location.href.slice(url.length);
+ const newFileUrl = await doMovePath(url);
+ if (newFileUrl) {
+ location.href = newFileUrl + query;
+ }
+ });
+
+ const $deleteFile = document.querySelector(".delete-file");
+ $deleteFile.classList.remove("hidden");
+ $deleteFile.addEventListener("click", async () => {
+ const url = baseUrl();
+ const name = baseName(url);
+ await doDeletePath(name, url, () => {
+ location.href = location.href.split("/").slice(0, -1).join("/");
+ });
+ });
+
+ 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;
+ }
+
+ if (!DATA.editable) {
+ const $notEditable = document.querySelector(".not-editable");
+ 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>`);
+ } else {
+ $notEditable.classList.remove("hidden");
+ $notEditable.textContent = "Cannot edit because file is too large or binary.";
+ }
+ return;
+ }
+
+ $editor.classList.remove("hidden");
+ try {
+ const res = await fetch(baseUrl());
+ await assertResOK(res);
+ const encoding = getEncoding(res.headers.get("content-type"));
+ if (encoding === "utf-8") {
+ $editor.value = await res.text();
+ } else {
+ const bytes = await res.arrayBuffer();
+ const dataView = new DataView(bytes);
+ const decoder = new TextDecoder(encoding);
+ $editor.value = decoder.decode(dataView);
+ }
+ } catch (err) {
+ alert(`Failed get file, ${err.message}`);
+ }
}
/**
- * Delete path
- * @param {number} index
- * @returns
- */
+* Delete path
+* @param {number} index
+* @returns
+*/
async function deletePath(index) {
- const file = DATA.paths[index];
- if (!file) return;
- await doDeletePath(file.name, newUrl(file.name), () => {
- document.getElementById(`addPath${index}`)?.remove();
- DATA.paths[index] = null;
- if (!DATA.paths.find(v => !!v)) {
- $pathsTable.classList.add("hidden");
- $emptyFolder.textContent = DIR_EMPTY_NOTE;
- $emptyFolder.classList.remove("hidden");
- }
- });
+ const file = DATA.paths[index];
+ if (!file) return;
+ await doDeletePath(file.name, newUrl(file.name), () => {
+ document.getElementById(`addPath${index}`)?.remove();
+ DATA.paths[index] = null;
+ if (!DATA.paths.find(v => !!v)) {
+ $pathsTable.classList.add("hidden");
+ $emptyFolder.textContent = DIR_EMPTY_NOTE;
+ $emptyFolder.classList.remove("hidden");
+ }
+ });
}
async function doDeletePath(name, url, cb) {
- if (!confirm(`Delete \`${name}\`?`)) return;
- try {
- await checkAuth();
- const res = await fetch(url, {
- method: "DELETE",
- });
- await assertResOK(res);
- cb();
- } catch (err) {
- alert(`Cannot delete \`${file.name}\`, ${err.message}`);
- }
+ if (!confirm(`Delete \`${name}\`?`)) return;
+ try {
+ await checkAuth();
+ const res = await fetch(url, {
+ method: "DELETE",
+ });
+ await assertResOK(res);
+ cb();
+ } catch (err) {
+ alert(`Cannot delete \`${file.name}\`, ${err.message}`);
+ }
}
/**
- * Move path
- * @param {number} index
- * @returns
- */
+* Move path
+* @param {number} index
+* @returns
+*/
async function movePath(index) {
- const file = DATA.paths[index];
- if (!file) return;
- const fileUrl = newUrl(file.name);
- const newFileUrl = await doMovePath(fileUrl);
- if (newFileUrl) {
- location.href = newFileUrl.split("/").slice(0, -1).join("/");
- }
+ const file = DATA.paths[index];
+ if (!file) return;
+ const fileUrl = newUrl(file.name);
+ const newFileUrl = await doMovePath(fileUrl);
+ if (newFileUrl) {
+ location.href = newFileUrl.split("/").slice(0, -1).join("/");
+ }
}
async function doMovePath(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);
- if (!newPath) return;
- if (!newPath.startsWith("/")) newPath = "/" + newPath;
- if (filePath === newPath) return;
- const newFileUrl = fileUrlObj.origin + prefix + newPath.split("/").map(encodeURIComponent).join("/");
-
- try {
- await checkAuth();
- const res1 = await fetch(newFileUrl, {
- method: "HEAD",
- });
- if (res1.status === 200) {
- if (!confirm("Override existing file?")) {
- return;
- }
- }
- const res2 = await fetch(fileUrl, {
- method: "MOVE",
- headers: {
- "Destination": newFileUrl,
- }
- });
- await assertResOK(res2);
- return newFileUrl;
- } catch (err) {
- alert(`Cannot move \`${filePath}\` to \`${newPath}\`, ${err.message}`);
- }
+ 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);
+ if (!newPath) return;
+ if (!newPath.startsWith("/")) newPath = "/" + newPath;
+ if (filePath === newPath) return;
+ const newFileUrl = fileUrlObj.origin + prefix + newPath.split("/").map(encodeURIComponent).join("/");
+
+ try {
+ await checkAuth();
+ const res1 = await fetch(newFileUrl, {
+ method: "HEAD",
+ });
+ if (res1.status === 200) {
+ if (!confirm("Override existing file?")) {
+ return;
+ }
+ }
+ const res2 = await fetch(fileUrl, {
+ method: "MOVE",
+ headers: {
+ "Destination": newFileUrl,
+ }
+ });
+ await assertResOK(res2);
+ return newFileUrl;
+ } catch (err) {
+ alert(`Cannot move \`${filePath}\` to \`${newPath}\`, ${err.message}`);
+ }
}
/**
- * Save editor change
- */
+* Save editor change
+*/
async function saveChange() {
- try {
- await fetch(baseUrl(), {
- method: "PUT",
- body: $editor.value,
- });
- location.reload();
- } catch (err) {
- alert(`Failed to save file, ${err.message}`);
- }
+ try {
+ await fetch(baseUrl(), {
+ method: "PUT",
+ body: $editor.value,
+ });
+ location.reload();
+ } catch (err) {
+ alert(`Failed to save file, ${err.message}`);
+ }
}
async function checkAuth() {
- if (!DATA.auth) return;
- const res = await fetch(baseUrl(), {
- method: "CHECKAUTH",
- });
- await assertResOK(res);
- $loginBtn.classList.add("hidden");
- $logoutBtn.classList.remove("hidden");
- $userName.textContent = await res.text();
+ if (!DATA.auth) return;
+ const res = await fetch(baseUrl(), {
+ method: "CHECKAUTH",
+ });
+ await assertResOK(res);
+ $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();
+ 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();
}
/**
- * Create a folder
- * @param {string} name
- */
+* Create a folder
+* @param {string} name
+*/
async function createFolder(name) {
- const url = newUrl(name);
- try {
- await checkAuth();
- const res = await fetch(url, {
- method: "MKCOL",
- });
- await assertResOK(res);
- location.href = url;
- } catch (err) {
- alert(`Cannot create folder \`${name}\`, ${err.message}`);
- }
+ const url = newUrl(name);
+ try {
+ await checkAuth();
+ const res = await fetch(url, {
+ method: "MKCOL",
+ });
+ await assertResOK(res);
+ location.href = url;
+ } catch (err) {
+ alert(`Cannot create folder \`${name}\`, ${err.message}`);
+ }
}
async function createFile(name) {
- const url = newUrl(name);
- try {
- await checkAuth();
- const res = await fetch(url, {
- method: "PUT",
- body: "",
- });
- await assertResOK(res);
- location.href = url + "?edit";
- } catch (err) {
- alert(`Cannot create file \`${name}\`, ${err.message}`);
- }
+ const url = newUrl(name);
+ try {
+ await checkAuth();
+ const res = await fetch(url, {
+ method: "PUT",
+ body: "",
+ });
+ await assertResOK(res);
+ location.href = url + "?edit";
+ } catch (err) {
+ alert(`Cannot create file \`${name}\`, ${err.message}`);
+ }
}
async function addFileEntries(entries, dirs) {
- for (const entry of entries) {
- if (entry.isFile) {
- entry.file(file => {
- new Uploader(file, dirs).upload();
- });
- } else if (entry.isDirectory) {
- const dirReader = entry.createReader();
-
- const successCallback = entries => {
- if (entries.length > 0) {
- addFileEntries(entries, [...dirs, entry.name]);
- dirReader.readEntries(successCallback);
- }
- };
-
- dirReader.readEntries(successCallback);
- }
- }
+ for (const entry of entries) {
+ if (entry == null) {
+ // this is the case if the user has dragged a file into the file upload area
+ // this can be made unlikely with fun css
+ return
+ } else if (entry.isFile) {
+ entry.file(file => {
+ new Uploader(file, dirs).upload();
+ });
+ } else if (entry.isDirectory) {
+ const dirReader = entry.createReader();
+
+ const successCallback = entries => {
+ if (entries.length > 0) {
+ addFileEntries(entries, [...dirs, entry.name]);
+ dirReader.readEntries(successCallback);
+ }
+ };
+
+ dirReader.readEntries(successCallback);
+ }
+ }
}
function newUrl(name) {
- let url = baseUrl();
- if (!url.endsWith("/")) url += "/";
- url += name.split("/").map(encodeURIComponent).join("/");
- return url;
+ let url = baseUrl();
+ if (!url.endsWith("/")) url += "/";
+ url += name.split("/").map(encodeURIComponent).join("/");
+ return url;
}
function baseUrl() {
- return location.href.split(/[?#]/)[0];
+ return location.href.split(/[?#]/)[0];
}
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) {
- const dotIndex = filename.lastIndexOf('.');
+ const dotIndex = filename.lastIndexOf('.');
- if (dotIndex === -1 || dotIndex === 0 || dotIndex === filename.length - 1) {
- return '';
- }
+ if (dotIndex === -1 || dotIndex === 0 || dotIndex === filename.length - 1) {
+ return '';
+ }
- return filename.substring(dotIndex);
+ return filename.substring(dotIndex);
}
function getPathSvg(path_type) {
- switch (path_type) {
- case "Dir":
- return ICONS.dir;
- case "SymlinkFile":
- return ICONS.symlinkFile;
- case "SymlinkDir":
- return ICONS.symlinkDir;
- default:
- return ICONS.file;
- }
+ switch (path_type) {
+ case "Dir":
+ return ICONS.dir;
+ case "SymlinkFile":
+ return ICONS.symlinkFile;
+ case "SymlinkDir":
+ return ICONS.symlinkDir;
+ default:
+ return ICONS.file;
+ }
}
function formatMtime(mtime) {
- if (!mtime) return "";
- const date = new Date(mtime);
- const year = date.getFullYear();
- const month = padZero(date.getMonth() + 1, 2);
- const day = padZero(date.getDate(), 2);
- const hours = padZero(date.getHours(), 2);
- const minutes = padZero(date.getMinutes(), 2);
- return `${year}-${month}-${day} ${hours}:${minutes}`;
+ if (!mtime) return "";
+ const date = new Date(mtime);
+ const year = date.getFullYear();
+ const month = padZero(date.getMonth() + 1, 2);
+ const day = padZero(date.getDate(), 2);
+ const hours = padZero(date.getHours(), 2);
+ const minutes = padZero(date.getMinutes(), 2);
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
}
function padZero(value, size) {
- return ("0".repeat(size) + value).slice(-1 * size);
+ return ("0".repeat(size) + value).slice(-1 * size);
}
function formatDirSize(size) {
- const unit = size === 1 ? "item" : "items";
- const num = size >= MAX_SUBPATHS_COUNT ? `>${MAX_SUBPATHS_COUNT - 1}` : `${size}`;
- return ` ${num} ${unit}`;
+ const unit = size === 1 ? "item" : "items";
+ const num = size >= MAX_SUBPATHS_COUNT ? `>${MAX_SUBPATHS_COUNT - 1}` : `${size}`;
+ return ` ${num} ${unit}`;
}
function formatFileSize(size) {
- 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)));
- let ratio = 1;
- if (i >= 3) {
- ratio = 100;
- }
- return [Math.round(size * ratio / Math.pow(1024, i), 2) / ratio, sizes[i]];
+ 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)));
+ let ratio = 1;
+ if (i >= 3) {
+ ratio = 100;
+ }
+ return [Math.round(size * ratio / Math.pow(1024, i), 2) / ratio, sizes[i]];
}
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;
- return `${padZero(h, 2)}:${padZero(m, 2)}:${padZero(s, 2)}`;
+ 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;
+ return `${padZero(h, 2)}:${padZero(m, 2)}:${padZero(s, 2)}`;
}
function formatPercent(percent) {
- if (percent > 10) {
- return percent.toFixed(1) + "%";
- } else {
- return percent.toFixed(2) + "%";
- }
+ if (percent > 10) {
+ return percent.toFixed(1) + "%";
+ } else {
+ return percent.toFixed(2) + "%";
+ }
}
function encodedStr(rawStr) {
- return rawStr.replace(/[\u00A0-\u9999<>\&]/g, function (i) {
- return '&#' + i.charCodeAt(0) + ';';
- });
+ return rawStr.replace(/[\u00A0-\u9999<>\&]/g, function (i) {
+ return '&#' + i.charCodeAt(0) + ';';
+ });
}
async function assertResOK(res) {
- if (!(res.status >= 200 && res.status < 300)) {
- throw new Error(await res.text() || `Invalid status ${res.status}`);
- }
+ if (!(res.status >= 200 && res.status < 300)) {
+ throw new Error(await res.text() || `Invalid status ${res.status}`);
+ }
}
function getEncoding(contentType) {
- const charset = contentType?.split(";")[1];
- if (/charset/i.test(charset)) {
- let encoding = charset.split("=")[1];
- if (encoding) {
- return encoding.toLowerCase();
- }
- }
- return 'utf-8';
+ const charset = contentType?.split(";")[1];
+ if (/charset/i.test(charset)) {
+ let encoding = charset.split("=")[1];
+ if (encoding) {
+ return encoding.toLowerCase();
+ }
+ }
+ 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);
+ 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);
}
use crate::auth::{www_authenticate, AccessPaths, AccessPerm};
use crate::http_utils::{body_full, IncomingStream, LengthLimitedStream};
use crate::utils::{
- decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, parse_range,
- try_get_file_name,
+ decode_uri, encode_uri, get_file_mtime_and_mode, get_file_name, glob, parse_range,
+ try_get_file_name,
};
use crate::Args;
use chrono::{LocalResult, TimeZone, Utc};
use futures_util::{pin_mut, TryStreamExt};
use headers::{
- AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, CacheControl,
- ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfMatch, IfModifiedSince,
- IfNoneMatch, IfRange, IfUnmodifiedSince, LastModified, Range,
+ AcceptRanges, AccessControlAllowCredentials, AccessControlAllowOrigin, CacheControl,
+ ContentLength, ContentType, ETag, HeaderMap, HeaderMapExt, IfMatch, IfModifiedSince,
+ IfNoneMatch, IfRange, IfUnmodifiedSince, LastModified, Range,
};
use http_body_util::{combinators::BoxBody, BodyExt, StreamBody};
use hyper::body::Frame;
use hyper::{
- body::Incoming,
- header::{
- HeaderValue, AUTHORIZATION, CONNECTION, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE,
- CONTENT_TYPE, RANGE,
- },
- Method, StatusCode, Uri,
+ body::Incoming,
+ header::{
+ HeaderValue, AUTHORIZATION, CONNECTION, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE,
+ CONTENT_TYPE, RANGE,
+ },
+ Method, StatusCode, Uri,
};
use serde::Serialize;
use sha2::{Digest, Sha256};
const BUF_SIZE: usize = 65536;
const EDITABLE_TEXT_MAX_SIZE: u64 = 4194304; // 4M
const RESUMABLE_UPLOAD_MIN_SIZE: u64 = 20971520; // 20M
-const HEALTH_CHECK_PATH: &str = "__dufs__/health";
+const HEALTH_CHECK_PATH: &str = "__ozva__/health";
const MAX_SUBPATHS_COUNT: u64 = 1000;
pub struct Server {
- args: Args,
- assets_prefix: String,
- html: Cow<'static, str>,
- single_file_req_paths: Vec<String>,
- running: Arc<AtomicBool>,
+ args: Args,
+ assets_prefix: String,
+ html: Cow<'static, str>,
+ single_file_req_paths: Vec<String>,
+ running: Arc<AtomicBool>,
}
impl Server {
- pub fn init(args: Args, running: Arc<AtomicBool>) -> Result<Self> {
- let assets_prefix = format!("__dufs_v{}__/", env!("CARGO_PKG_VERSION"));
- let single_file_req_paths = if args.path_is_file {
- vec![
- args.uri_prefix.to_string(),
- args.uri_prefix[0..args.uri_prefix.len() - 1].to_string(),
- encode_uri(&format!(
- "{}{}",
- &args.uri_prefix,
- get_file_name(&args.serve_path)
- )),
- ]
- } else {
- vec![]
- };
- let html = match args.assets.as_ref() {
- Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html"))?),
- None => Cow::Borrowed(INDEX_HTML),
- };
- Ok(Self {
- args,
- running,
- single_file_req_paths,
- assets_prefix,
- html,
- })
- }
-
- pub async fn call(
- self: Arc<Self>,
- req: Request,
- addr: Option<SocketAddr>,
- ) -> Result<Response, hyper::Error> {
- let uri = req.uri().clone();
- let assets_prefix = &self.assets_prefix;
- let enable_cors = self.args.enable_cors;
- let is_microsoft_webdav = req
- .headers()
- .get("user-agent")
- .and_then(|v| v.to_str().ok())
- .map(|v| v.starts_with("Microsoft-WebDAV-MiniRedir/"))
- .unwrap_or_default();
- let mut http_log_data = self.args.http_logger.data(&req);
- if let Some(addr) = addr {
- http_log_data.insert("remote_addr".to_string(), addr.ip().to_string());
- }
-
- let mut res = match self.clone().handle(req, is_microsoft_webdav).await {
- Ok(res) => {
- http_log_data.insert("status".to_string(), res.status().as_u16().to_string());
- if !uri.path().starts_with(assets_prefix) {
- self.args.http_logger.log(&http_log_data, None);
- }
- res
- }
- Err(err) => {
- let mut res = Response::default();
- let status = StatusCode::INTERNAL_SERVER_ERROR;
- *res.status_mut() = status;
- http_log_data.insert("status".to_string(), status.as_u16().to_string());
- self.args
- .http_logger
- .log(&http_log_data, Some(err.to_string()));
- res
- }
- };
-
- if is_microsoft_webdav {
- // microsoft webdav requires this.
- res.headers_mut()
- .insert(CONNECTION, HeaderValue::from_static("close"));
- }
- if enable_cors {
- add_cors(&mut res);
- }
- Ok(res)
- }
-
- pub async fn handle(
- self: Arc<Self>,
- req: Request,
- is_microsoft_webdav: bool,
- ) -> Result<Response> {
- let mut res = Response::default();
-
- let req_path = req.uri().path();
- let headers = req.headers();
- let method = req.method().clone();
-
- let relative_path = match self.resolve_path(req_path) {
- Some(v) => v,
- None => {
- status_bad_request(&mut res, "Invalid Path");
- return Ok(res);
- }
- };
-
- if method == Method::GET
- && self
- .handle_internal(&relative_path, headers, &mut res)
- .await?
- {
- return Ok(res);
- }
-
- let authorization = headers.get(AUTHORIZATION);
- let guard =
- self.args
- .auth
- .guard(&relative_path, &method, authorization, is_microsoft_webdav);
-
- let (user, access_paths) = match guard {
- (None, None) => {
- self.auth_reject(&mut res)?;
- return Ok(res);
- }
- (Some(_), None) => {
- status_forbid(&mut res);
- return Ok(res);
- }
- (x, Some(y)) => (x, y),
- };
-
- 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();
-
- if method.as_str() == "CHECKAUTH" {
- *res.body_mut() = body_full(user.clone().unwrap_or_default());
- return Ok(res);
- } else if method.as_str() == "LOGOUT" {
- self.auth_reject(&mut res)?;
- return Ok(res);
- }
-
- let head_only = method == Method::HEAD;
-
- if self.args.path_is_file {
- if self
- .single_file_req_paths
- .iter()
- .any(|v| v.as_str() == req_path)
- {
- self.handle_send_file(&self.args.serve_path, headers, head_only, &mut res)
- .await?;
- } else {
- status_not_found(&mut res);
- }
- return Ok(res);
- }
- let path = match self.join_path(&relative_path) {
- Some(v) => v,
- None => {
- status_forbid(&mut res);
- return Ok(res);
- }
- };
-
- let path = path.as_path();
-
- 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()),
- None => (true, false, false, 0),
- };
-
- let allow_upload = self.args.allow_upload;
- let allow_delete = self.args.allow_delete;
- let allow_search = self.args.allow_search;
- let allow_archive = self.args.allow_archive;
- let render_index = self.args.render_index;
- let render_spa = self.args.render_spa;
- let render_try_index = self.args.render_try_index;
-
- if !self.args.allow_symlink && !is_miss && !self.is_root_contained(path).await {
- status_not_found(&mut res);
- return Ok(res);
- }
-
- match method {
- Method::GET | Method::HEAD => {
- if is_dir {
- if render_try_index {
- if allow_archive && has_query_flag(&query_params, "zip") {
- if !allow_archive {
- status_not_found(&mut res);
- return Ok(res);
- }
- self.handle_zip_dir(path, head_only, access_paths, &mut res)
- .await?;
- } else if allow_search && query_params.contains_key("q") {
- self.handle_search_dir(
- path,
- &query_params,
- head_only,
- user,
- access_paths,
- &mut res,
- )
- .await?;
- } else {
- self.handle_render_index(
- path,
- &query_params,
- headers,
- head_only,
- user,
- access_paths,
- &mut res,
- )
- .await?;
- }
- } else if render_index || render_spa {
- self.handle_render_index(
- path,
- &query_params,
- headers,
- head_only,
- user,
- access_paths,
- &mut res,
- )
- .await?;
- } else if has_query_flag(&query_params, "zip") {
- if !allow_archive {
- status_not_found(&mut res);
- return Ok(res);
- }
- self.handle_zip_dir(path, head_only, access_paths, &mut res)
- .await?;
- } else if allow_search && query_params.contains_key("q") {
- self.handle_search_dir(
- path,
- &query_params,
- head_only,
- user,
- access_paths,
- &mut res,
- )
- .await?;
- } else {
- self.handle_ls_dir(
- path,
- true,
- &query_params,
- head_only,
- user,
- access_paths,
- &mut res,
- )
- .await?;
- }
- } else if is_file {
- if has_query_flag(&query_params, "edit") {
- self.handle_edit_file(path, DataKind::Edit, head_only, user, &mut res)
- .await?;
- } else if has_query_flag(&query_params, "view") {
- self.handle_edit_file(path, DataKind::View, head_only, user, &mut res)
- .await?;
- } 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)
- .await?;
- }
- } else if render_spa {
- 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,
- &query_params,
- head_only,
- user,
- access_paths,
- &mut res,
- )
- .await?;
- } else {
- status_not_found(&mut res);
- }
- }
- Method::OPTIONS => {
- set_webdav_headers(&mut res);
- }
- Method::PUT => {
- if is_dir || !allow_upload || (!allow_delete && size > 0) {
- status_forbid(&mut res);
- } else {
- self.handle_upload(path, None, size, req, &mut res).await?;
- }
- }
- Method::PATCH => {
- if is_miss {
- status_not_found(&mut res);
- } else if !allow_upload {
- status_forbid(&mut res);
- } else {
- let offset = match parse_upload_offset(headers, size) {
- Ok(v) => v,
- Err(err) => {
- status_bad_request(&mut res, &err.to_string());
- return Ok(res);
- }
- };
- match offset {
- Some(offset) => {
- if offset < size && !allow_delete {
- status_forbid(&mut res);
- }
- self.handle_upload(path, Some(offset), size, req, &mut res)
- .await?;
- }
- None => {
- *res.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
- }
- }
- }
- }
- Method::DELETE => {
- if !allow_delete {
- status_forbid(&mut res);
- } else if !is_miss {
- self.handle_delete(path, is_dir, &mut res).await?
- } else {
- status_not_found(&mut res);
- }
- }
- method => match method.as_str() {
- "PROPFIND" => {
- if is_dir {
- let access_paths =
- if access_paths.perm().indexonly() && authorization.is_none() {
- // see https://github.com/sigoden/dufs/issues/229
- AccessPaths::new(AccessPerm::ReadOnly)
- } else {
- access_paths
- };
- self.handle_propfind_dir(path, headers, access_paths, &mut res)
- .await?;
- } else if is_file {
- self.handle_propfind_file(path, &mut res).await?;
- } else {
- status_not_found(&mut res);
- }
- }
- "PROPPATCH" => {
- if is_file {
- self.handle_proppatch(req_path, &mut res).await?;
- } else {
- status_not_found(&mut res);
- }
- }
- "MKCOL" => {
- if !allow_upload {
- status_forbid(&mut res);
- } else if !is_miss {
- *res.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
- *res.body_mut() = body_full("Already exists");
- } else {
- self.handle_mkcol(path, &mut res).await?;
- }
- }
- "COPY" => {
- if !allow_upload {
- status_forbid(&mut res);
- } else if is_miss {
- status_not_found(&mut res);
- } else {
- self.handle_copy(path, &req, &mut res).await?
- }
- }
- "MOVE" => {
- if !allow_upload || !allow_delete {
- status_forbid(&mut res);
- } else if is_miss {
- status_not_found(&mut res);
- } else {
- self.handle_move(path, &req, &mut res).await?
- }
- }
- "LOCK" => {
- // Fake lock
- if is_file {
- let has_auth = authorization.is_some();
- self.handle_lock(req_path, has_auth, &mut res).await?;
- } else {
- status_not_found(&mut res);
- }
- }
- "UNLOCK" => {
- // Fake unlock
- if is_miss {
- status_not_found(&mut res);
- }
- }
- _ => {
- *res.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
- }
- },
- }
- Ok(res)
- }
-
- async fn handle_upload(
- &self,
- path: &Path,
- upload_offset: Option<u64>,
- size: u64,
- req: Request,
- res: &mut Response,
- ) -> Result<()> {
- ensure_path_parent(path).await?;
- let (mut file, status) = match upload_offset {
- None => (fs::File::create(path).await?, StatusCode::CREATED),
- Some(offset) if offset == size => (
- fs::OpenOptions::new().append(true).open(path).await?,
- StatusCode::NO_CONTENT,
- ),
- Some(offset) => {
- let mut file = fs::OpenOptions::new().write(true).open(path).await?;
- file.seek(SeekFrom::Start(offset)).await?;
- (file, StatusCode::NO_CONTENT)
- }
- };
- let stream = IncomingStream::new(req.into_body());
-
- let body_with_io_error = stream.map_err(|err| io::Error::new(io::ErrorKind::Other, err));
- let body_reader = StreamReader::new(body_with_io_error);
-
- pin_mut!(body_reader);
-
- let ret = io::copy(&mut body_reader, &mut file).await;
- let size = fs::metadata(path)
- .await
- .map(|v| v.len())
- .unwrap_or_default();
- if ret.is_err() {
- if upload_offset.is_none() && size < RESUMABLE_UPLOAD_MIN_SIZE {
- let _ = tokio::fs::remove_file(&path).await;
- }
- ret?;
- }
-
- *res.status_mut() = status;
-
- Ok(())
- }
-
- async fn handle_delete(&self, path: &Path, is_dir: bool, res: &mut Response) -> Result<()> {
- match is_dir {
- true => fs::remove_dir_all(path).await?,
- false => fs::remove_file(path).await?,
- }
-
- status_no_content(res);
- Ok(())
- }
-
- async fn handle_ls_dir(
- &self,
- path: &Path,
- exist: bool,
- query_params: &HashMap<String, String>,
- head_only: bool,
- user: Option<String>,
- access_paths: AccessPaths,
- res: &mut Response,
- ) -> Result<()> {
- let mut paths = vec![];
- if exist {
- paths = match self.list_dir(path, path, access_paths.clone()).await {
- Ok(paths) => paths,
- Err(_) => {
- status_forbid(res);
- return Ok(());
- }
- }
- };
- self.send_index(
- path,
- paths,
- exist,
- query_params,
- head_only,
- user,
- access_paths,
- res,
- )
- }
-
- async fn handle_search_dir(
- &self,
- path: &Path,
- query_params: &HashMap<String, String>,
- head_only: bool,
- user: Option<String>,
- access_paths: AccessPaths,
- res: &mut Response,
- ) -> Result<()> {
- let mut paths: Vec<PathItem> = vec![];
- let search = query_params
- .get("q")
- .ok_or_else(|| anyhow!("invalid q"))?
- .to_lowercase();
- if search.is_empty() {
- return self
- .handle_ls_dir(path, true, query_params, head_only, user, access_paths, res)
- .await;
- } else {
- let path_buf = path.to_path_buf();
- let hidden = Arc::new(self.args.hidden.to_vec());
- let search = search.clone();
-
- let access_paths = access_paths.clone();
- let search_paths = tokio::spawn(collect_dir_entries(
- access_paths,
- self.running.clone(),
- path_buf,
- hidden,
- self.args.allow_symlink,
- self.args.serve_path.clone(),
- move |x| get_file_name(x.path()).to_lowercase().contains(&search),
- ))
- .await?;
-
- for search_path in search_paths.into_iter() {
- if let Ok(Some(item)) = self.to_pathitem(search_path, path.to_path_buf()).await {
- paths.push(item);
- }
- }
- }
- self.send_index(
- path,
- paths,
- true,
- query_params,
- head_only,
- user,
- access_paths,
- res,
- )
- }
-
- async fn handle_zip_dir(
- &self,
- path: &Path,
- head_only: bool,
- access_paths: AccessPaths,
- res: &mut Response,
- ) -> Result<()> {
- let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
- let filename = try_get_file_name(path)?;
- set_content_disposition(res, false, &format!("{}.zip", filename))?;
- res.headers_mut()
- .insert("content-type", HeaderValue::from_static("application/zip"));
- if head_only {
- return Ok(());
- }
- let path = path.to_owned();
- let hidden = self.args.hidden.clone();
- let running = self.running.clone();
- let compression = self.args.compress.to_compression();
- let follow_symlinks = self.args.allow_symlink;
- let serve_path = self.args.serve_path.clone();
- tokio::spawn(async move {
- if let Err(e) = zip_dir(
- &mut writer,
- &path,
- access_paths,
- &hidden,
- compression,
- follow_symlinks,
- serve_path,
- running,
- )
- .await
- {
- error!("Failed to zip {}, {e}", path.display());
- }
- });
- let reader_stream = ReaderStream::with_capacity(reader, BUF_SIZE);
- let stream_body = StreamBody::new(
- reader_stream
- .map_ok(Frame::data)
- .map_err(|err| anyhow!("{err}")),
- );
- let boxed_body = stream_body.boxed();
- *res.body_mut() = boxed_body;
- Ok(())
- }
-
- async fn handle_render_index(
- &self,
- path: &Path,
- query_params: &HashMap<String, String>,
- headers: &HeaderMap<HeaderValue>,
- head_only: bool,
- user: Option<String>,
- access_paths: AccessPaths,
- res: &mut Response,
- ) -> Result<()> {
- let index_path = path.join(INDEX_NAME);
- if fs::metadata(&index_path)
- .await
- .ok()
- .map(|v| v.is_file())
- .unwrap_or_default()
- {
- self.handle_send_file(&index_path, headers, head_only, res)
- .await?;
- } else if self.args.render_try_index {
- self.handle_ls_dir(path, true, query_params, head_only, user, access_paths, res)
- .await?;
- } else {
- status_not_found(res)
- }
- Ok(())
- }
-
- async fn handle_render_spa(
- &self,
- path: &Path,
- headers: &HeaderMap<HeaderValue>,
- head_only: bool,
- res: &mut Response,
- ) -> Result<()> {
- if path.extension().is_none() {
- let path = self.args.serve_path.join(INDEX_NAME);
- self.handle_send_file(&path, headers, head_only, res)
- .await?;
- } else {
- status_not_found(res)
- }
- Ok(())
- }
-
- async fn handle_internal(
- &self,
- req_path: &str,
- headers: &HeaderMap<HeaderValue>,
- res: &mut Response,
- ) -> Result<bool> {
- if let Some(name) = req_path.strip_prefix(&self.assets_prefix) {
- match self.args.assets.as_ref() {
- Some(assets_path) => {
- let path = assets_path.join(name);
- if path.exists() {
- self.handle_send_file(&path, headers, false, res).await?;
- } else {
- status_not_found(res);
- return Ok(true);
- }
- }
- None => match name {
- "index.js" => {
- *res.body_mut() = body_full(INDEX_JS);
- res.headers_mut().insert(
- "content-type",
- HeaderValue::from_static("application/javascript; charset=UTF-8"),
- );
- }
- "index.css" => {
- *res.body_mut() = body_full(INDEX_CSS);
- res.headers_mut().insert(
- "content-type",
- HeaderValue::from_static("text/css; charset=UTF-8"),
- );
- }
- "favicon.ico" => {
- *res.body_mut() = body_full(FAVICON_ICO);
- res.headers_mut()
- .insert("content-type", HeaderValue::from_static("image/x-icon"));
- }
- _ => {
- status_not_found(res);
- }
- },
- }
- res.headers_mut().insert(
- "cache-control",
- HeaderValue::from_static("public, max-age=31536000, immutable"),
- );
- res.headers_mut().insert(
- "x-content-type-options",
- HeaderValue::from_static("nosniff"),
- );
- Ok(true)
- } else if req_path == HEALTH_CHECK_PATH {
- res.headers_mut()
- .typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON));
-
- *res.body_mut() = body_full(r#"{"status":"OK"}"#);
- Ok(true)
- } else {
- Ok(false)
- }
- }
-
- async fn handle_send_file(
- &self,
- path: &Path,
- headers: &HeaderMap<HeaderValue>,
- head_only: bool,
- res: &mut Response,
- ) -> Result<()> {
- let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
- let (mut file, meta) = (file?, meta?);
- let size = meta.len();
- let mut use_range = true;
- if let Some((etag, last_modified)) = extract_cache_headers(&meta) {
- 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(());
- }
- }
-
- res.headers_mut()
- .typed_insert(CacheControl::new().with_no_cache());
- res.headers_mut().typed_insert(last_modified);
- res.headers_mut().typed_insert(etag.clone());
-
- if headers.typed_get::<Range>().is_some() {
- use_range = headers
- .typed_get::<IfRange>()
- .map(|if_range| !if_range.is_modified(Some(&etag), Some(&last_modified)))
- // Always be fresh if there is no validators
- .unwrap_or(true);
- } else {
- use_range = false;
- }
- }
-
- let ranges = if use_range {
- headers.get(RANGE).map(|range| {
- range
- .to_str()
- .ok()
- .and_then(|range| parse_range(range, size))
- })
- } else {
- None
- };
-
- res.headers_mut().insert(
- CONTENT_TYPE,
- HeaderValue::from_str(&get_content_type(path).await?)?,
- );
-
- let filename = try_get_file_name(path)?;
- set_content_disposition(res, true, filename)?;
-
- res.headers_mut().typed_insert(AcceptRanges::bytes());
-
- if let Some(ranges) = ranges {
- if let Some(ranges) = ranges {
- if ranges.len() == 1 {
- let (start, end) = ranges[0];
- file.seek(SeekFrom::Start(start)).await?;
- let range_size = end - start + 1;
- *res.status_mut() = StatusCode::PARTIAL_CONTENT;
- let content_range = format!("bytes {}-{}/{}", start, end, size);
- res.headers_mut()
- .insert(CONTENT_RANGE, content_range.parse()?);
- res.headers_mut()
- .insert(CONTENT_LENGTH, format!("{range_size}").parse()?);
- if head_only {
- return Ok(());
- }
-
- let stream_body = StreamBody::new(
- LengthLimitedStream::new(file, range_size as usize)
- .map_ok(Frame::data)
- .map_err(|err| anyhow!("{err}")),
- );
- let boxed_body = stream_body.boxed();
- *res.body_mut() = boxed_body;
- } else {
- *res.status_mut() = StatusCode::PARTIAL_CONTENT;
- let boundary = Uuid::new_v4();
- let mut body = Vec::new();
- let content_type = get_content_type(path).await?;
- for (start, end) in ranges {
- file.seek(SeekFrom::Start(start)).await?;
- let range_size = end - start + 1;
- let content_range = format!("bytes {}-{}/{}", start, end, size);
- let part_header = format!(
- "--{boundary}\r\nContent-Type: {content_type}\r\nContent-Range: {content_range}\r\n\r\n",
- );
- body.extend_from_slice(part_header.as_bytes());
- let mut buffer = vec![0; range_size as usize];
- file.read_exact(&mut buffer).await?;
- body.extend_from_slice(&buffer);
- body.extend_from_slice(b"\r\n");
- }
- body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
- res.headers_mut().insert(
- CONTENT_TYPE,
- format!("multipart/byteranges; boundary={boundary}").parse()?,
- );
- res.headers_mut()
- .insert(CONTENT_LENGTH, format!("{}", body.len()).parse()?);
- if head_only {
- return Ok(());
- }
- *res.body_mut() = body_full(body);
- }
- } else {
- *res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
- res.headers_mut()
- .insert(CONTENT_RANGE, format!("bytes */{size}").parse()?);
- }
- } else {
- res.headers_mut()
- .insert(CONTENT_LENGTH, format!("{size}").parse()?);
- if head_only {
- return Ok(());
- }
-
- let reader_stream = ReaderStream::with_capacity(file, BUF_SIZE);
- let stream_body = StreamBody::new(
- reader_stream
- .map_ok(Frame::data)
- .map_err(|err| anyhow!("{err}")),
- );
- let boxed_body = stream_body.boxed();
- *res.body_mut() = boxed_body;
- }
- Ok(())
- }
-
- async fn handle_edit_file(
- &self,
- path: &Path,
- kind: DataKind,
- head_only: bool,
- user: Option<String>,
- res: &mut Response,
- ) -> Result<()> {
- 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.serve_path)?)
- );
- let mut buffer: Vec<u8> = vec![];
- file.take(1024).read_to_end(&mut buffer).await?;
- let editable =
- meta.len() <= EDITABLE_TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text();
- let data = EditData {
- href,
- kind,
- uri_prefix: self.args.uri_prefix.clone(),
- allow_upload: self.args.allow_upload,
- allow_delete: self.args.allow_delete,
- auth: self.args.auth.exist(),
- user,
- editable,
- };
- 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__", &index_data);
- res.headers_mut()
- .typed_insert(ContentLength(output.len() as u64));
- res.headers_mut()
- .typed_insert(CacheControl::new().with_no_cache());
- if head_only {
- return Ok(());
- }
- *res.body_mut() = body_full(output);
- Ok(())
- }
-
- async fn handle_hash_file(
- &self,
- path: &Path,
- head_only: bool,
- res: &mut Response,
- ) -> Result<()> {
- let output = sha256_file(path).await?;
- res.headers_mut()
- .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
- res.headers_mut()
- .typed_insert(ContentLength(output.len() as u64));
- if head_only {
- return Ok(());
- }
- *res.body_mut() = body_full(output);
- Ok(())
- }
-
- async fn handle_propfind_dir(
- &self,
- path: &Path,
- headers: &HeaderMap<HeaderValue>,
- access_paths: AccessPaths,
- res: &mut Response,
- ) -> Result<()> {
- let depth: u32 = match headers.get("depth") {
- Some(v) => match v.to_str().ok().and_then(|v| v.parse().ok()) {
- Some(0) => 0,
- Some(1) => 1,
- _ => {
- status_bad_request(res, "Invalid depth: only 0 and 1 are allowed.");
- return Ok(());
- }
- },
- None => 1,
- };
- let mut paths = match self.to_pathitem(path, &self.args.serve_path).await? {
- Some(v) => vec![v],
- None => vec![],
- };
- if depth == 1 {
- match self
- .list_dir(path, &self.args.serve_path, access_paths)
- .await
- {
- Ok(child) => paths.extend(child),
- Err(_) => {
- status_forbid(res);
- return Ok(());
- }
- }
- }
- let output = paths
- .iter()
- .map(|v| v.to_dav_xml(self.args.uri_prefix.as_str()))
- .fold(String::new(), |mut acc, v| {
- acc.push_str(&v);
- acc
- });
- res_multistatus(res, &output);
- Ok(())
- }
-
- async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> Result<()> {
- if let Some(pathitem) = self.to_pathitem(path, &self.args.serve_path).await? {
- res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
- } else {
- status_not_found(res);
- }
- Ok(())
- }
-
- async fn handle_mkcol(&self, path: &Path, res: &mut Response) -> Result<()> {
- fs::create_dir_all(path).await?;
- *res.status_mut() = StatusCode::CREATED;
- Ok(())
- }
-
- async fn handle_copy(&self, path: &Path, req: &Request, res: &mut Response) -> Result<()> {
- let dest = match self.extract_dest(req, res) {
- Some(dest) => dest,
- None => {
- return Ok(());
- }
- };
-
- let meta = fs::symlink_metadata(path).await?;
- if meta.is_dir() {
- status_forbid(res);
- return Ok(());
- }
-
- ensure_path_parent(&dest).await?;
-
- fs::copy(path, &dest).await?;
-
- status_no_content(res);
- Ok(())
- }
-
- async fn handle_move(&self, path: &Path, req: &Request, res: &mut Response) -> Result<()> {
- let dest = match self.extract_dest(req, res) {
- Some(dest) => dest,
- None => {
- return Ok(());
- }
- };
-
- ensure_path_parent(&dest).await?;
-
- fs::rename(path, &dest).await?;
-
- status_no_content(res);
- Ok(())
- }
-
- async fn handle_lock(&self, req_path: &str, auth: bool, res: &mut Response) -> Result<()> {
- let token = if auth {
- format!("opaquelocktoken:{}", Uuid::new_v4())
- } else {
- Utc::now().timestamp().to_string()
- };
-
- res.headers_mut().insert(
- "content-type",
- HeaderValue::from_static("application/xml; charset=utf-8"),
- );
- res.headers_mut()
- .insert("lock-token", format!("<{token}>").parse()?);
-
- *res.body_mut() = body_full(format!(
- r#"<?xml version="1.0" encoding="utf-8"?>
+ pub fn init(args: Args, running: Arc<AtomicBool>) -> Result<Self> {
+ let assets_prefix = format!("__ozva_v{}__/", env!("CARGO_PKG_VERSION"));
+ let single_file_req_paths = if args.path_is_file {
+ vec![
+ args.uri_prefix.to_string(),
+ args.uri_prefix[0..args.uri_prefix.len() - 1].to_string(),
+ encode_uri(&format!(
+ "{}{}",
+ &args.uri_prefix,
+ get_file_name(&args.serve_path)
+ )),
+ ]
+ } else {
+ vec![]
+ };
+ let html = match args.assets.as_ref() {
+ Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html"))?),
+ None => Cow::Borrowed(INDEX_HTML),
+ };
+ Ok(Self {
+ args,
+ running,
+ single_file_req_paths,
+ assets_prefix,
+ html,
+ })
+ }
+
+ pub async fn call(
+ self: Arc<Self>,
+ req: Request,
+ addr: Option<SocketAddr>,
+ ) -> Result<Response, hyper::Error> {
+ let uri = req.uri().clone();
+ let assets_prefix = &self.assets_prefix;
+ let enable_cors = self.args.enable_cors;
+ let is_microsoft_webdav = req
+ .headers()
+ .get("user-agent")
+ .and_then(|v| v.to_str().ok())
+ .map(|v| v.starts_with("Microsoft-WebDAV-MiniRedir/"))
+ .unwrap_or_default();
+ let mut http_log_data = self.args.http_logger.data(&req);
+ if let Some(addr) = addr {
+ http_log_data.insert("remote_addr".to_string(), addr.ip().to_string());
+ }
+
+ let mut res = match self.clone().handle(req, is_microsoft_webdav).await {
+ Ok(res) => {
+ http_log_data.insert("status".to_string(), res.status().as_u16().to_string());
+ if !uri.path().starts_with(assets_prefix) {
+ self.args.http_logger.log(&http_log_data, None);
+ }
+ res
+ }
+ Err(err) => {
+ let mut res = Response::default();
+ let status = StatusCode::INTERNAL_SERVER_ERROR;
+ *res.status_mut() = status;
+ http_log_data.insert("status".to_string(), status.as_u16().to_string());
+ self.args
+ .http_logger
+ .log(&http_log_data, Some(err.to_string()));
+ res
+ }
+ };
+
+ if is_microsoft_webdav {
+ // microsoft webdav requires this.
+ res.headers_mut()
+ .insert(CONNECTION, HeaderValue::from_static("close"));
+ }
+ if enable_cors {
+ add_cors(&mut res);
+ }
+ Ok(res)
+ }
+
+ pub async fn handle(
+ self: Arc<Self>,
+ req: Request,
+ is_microsoft_webdav: bool,
+ ) -> Result<Response> {
+ let mut res = Response::default();
+
+ let req_path = req.uri().path();
+ let headers = req.headers();
+ let method = req.method().clone();
+
+ let relative_path = match self.resolve_path(req_path) {
+ Some(v) => v,
+ None => {
+ status_bad_request(&mut res, "Invalid Path");
+ return Ok(res);
+ }
+ };
+
+ if method == Method::GET
+ && self
+ .handle_internal(&relative_path, headers, &mut res)
+ .await?
+ {
+ return Ok(res);
+ }
+
+ let authorization = headers.get(AUTHORIZATION);
+ let guard =
+ self.args
+ .auth
+ .guard(&relative_path, &method, authorization, is_microsoft_webdav);
+
+ let (user, access_paths) = match guard {
+ (None, None) => {
+ self.auth_reject(&mut res)?;
+ return Ok(res);
+ }
+ (Some(_), None) => {
+ status_forbid(&mut res);
+ return Ok(res);
+ }
+ (x, Some(y)) => (x, y),
+ };
+
+ 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();
+
+ if method.as_str() == "CHECKAUTH" {
+ *res.body_mut() = body_full(user.clone().unwrap_or_default());
+ return Ok(res);
+ } else if method.as_str() == "LOGOUT" {
+ self.auth_reject(&mut res)?;
+ return Ok(res);
+ }
+
+ let head_only = method == Method::HEAD;
+
+ if self.args.path_is_file {
+ if self
+ .single_file_req_paths
+ .iter()
+ .any(|v| v.as_str() == req_path)
+ {
+ self.handle_send_file(&self.args.serve_path, headers, head_only, &mut res)
+ .await?;
+ } else {
+ status_not_found(&mut res);
+ }
+ return Ok(res);
+ }
+ let path = match self.join_path(&relative_path) {
+ Some(v) => v,
+ None => {
+ status_forbid(&mut res);
+ return Ok(res);
+ }
+ };
+
+ let path = path.as_path();
+
+ 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()),
+ None => (true, false, false, 0),
+ };
+
+ let allow_upload = self.args.allow_upload;
+ let allow_delete = self.args.allow_delete;
+ let allow_search = self.args.allow_search;
+ let allow_archive = self.args.allow_archive;
+ let render_index = self.args.render_index;
+ let render_spa = self.args.render_spa;
+ let render_try_index = self.args.render_try_index;
+
+ if !self.args.allow_symlink && !is_miss && !self.is_root_contained(path).await {
+ status_not_found(&mut res);
+ return Ok(res);
+ }
+
+ match method {
+ Method::GET | Method::HEAD => {
+ if is_dir {
+ if render_try_index {
+ if allow_archive && has_query_flag(&query_params, "zip") {
+ if !allow_archive {
+ status_not_found(&mut res);
+ return Ok(res);
+ }
+ self.handle_zip_dir(path, head_only, access_paths, &mut res)
+ .await?;
+ } else if allow_search && query_params.contains_key("q") {
+ self.handle_search_dir(
+ path,
+ &query_params,
+ head_only,
+ user,
+ access_paths,
+ &mut res,
+ )
+ .await?;
+ } else {
+ self.handle_render_index(
+ path,
+ &query_params,
+ headers,
+ head_only,
+ user,
+ access_paths,
+ &mut res,
+ )
+ .await?;
+ }
+ } else if render_index || render_spa {
+ self.handle_render_index(
+ path,
+ &query_params,
+ headers,
+ head_only,
+ user,
+ access_paths,
+ &mut res,
+ )
+ .await?;
+ } else if has_query_flag(&query_params, "zip") {
+ if !allow_archive {
+ status_not_found(&mut res);
+ return Ok(res);
+ }
+ self.handle_zip_dir(path, head_only, access_paths, &mut res)
+ .await?;
+ } else if allow_search && query_params.contains_key("q") {
+ self.handle_search_dir(
+ path,
+ &query_params,
+ head_only,
+ user,
+ access_paths,
+ &mut res,
+ )
+ .await?;
+ } else {
+ self.handle_ls_dir(
+ path,
+ true,
+ &query_params,
+ head_only,
+ user,
+ access_paths,
+ &mut res,
+ )
+ .await?;
+ }
+ } else if is_file {
+ if has_query_flag(&query_params, "edit") {
+ self.handle_edit_file(path, DataKind::Edit, head_only, user, &mut res)
+ .await?;
+ } else if has_query_flag(&query_params, "view") {
+ self.handle_edit_file(path, DataKind::View, head_only, user, &mut res)
+ .await?;
+ } 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)
+ .await?;
+ }
+ } else if render_spa {
+ 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,
+ &query_params,
+ head_only,
+ user,
+ access_paths,
+ &mut res,
+ )
+ .await?;
+ } else {
+ status_not_found(&mut res);
+ }
+ }
+ Method::OPTIONS => {
+ set_webdav_headers(&mut res);
+ }
+ Method::PUT => {
+ if is_dir || !allow_upload || (!allow_delete && size > 0) {
+ status_forbid(&mut res);
+ } else {
+ self.handle_upload(path, None, size, req, &mut res).await?;
+ }
+ }
+ Method::PATCH => {
+ if is_miss {
+ status_not_found(&mut res);
+ } else if !allow_upload {
+ status_forbid(&mut res);
+ } else {
+ let offset = match parse_upload_offset(headers, size) {
+ Ok(v) => v,
+ Err(err) => {
+ status_bad_request(&mut res, &err.to_string());
+ return Ok(res);
+ }
+ };
+ match offset {
+ Some(offset) => {
+ if offset < size && !allow_delete {
+ status_forbid(&mut res);
+ }
+ self.handle_upload(path, Some(offset), size, req, &mut res)
+ .await?;
+ }
+ None => {
+ *res.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
+ }
+ }
+ }
+ }
+ Method::DELETE => {
+ if !allow_delete {
+ status_forbid(&mut res);
+ } else if !is_miss {
+ self.handle_delete(path, is_dir, &mut res).await?
+ } else {
+ status_not_found(&mut res);
+ }
+ }
+ method => match method.as_str() {
+ "PROPFIND" => {
+ if is_dir {
+ let access_paths =
+ if access_paths.perm().indexonly() && authorization.is_none() {
+ // see https://github.com/sigoden/dufs/issues/229
+ AccessPaths::new(AccessPerm::ReadOnly)
+ } else {
+ access_paths
+ };
+ self.handle_propfind_dir(path, headers, access_paths, &mut res)
+ .await?;
+ } else if is_file {
+ self.handle_propfind_file(path, &mut res).await?;
+ } else {
+ status_not_found(&mut res);
+ }
+ }
+ "PROPPATCH" => {
+ if is_file {
+ self.handle_proppatch(req_path, &mut res).await?;
+ } else {
+ status_not_found(&mut res);
+ }
+ }
+ "MKCOL" => {
+ if !allow_upload {
+ status_forbid(&mut res);
+ } else if !is_miss {
+ *res.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
+ *res.body_mut() = body_full("Already exists");
+ } else {
+ self.handle_mkcol(path, &mut res).await?;
+ }
+ }
+ "COPY" => {
+ if !allow_upload {
+ status_forbid(&mut res);
+ } else if is_miss {
+ status_not_found(&mut res);
+ } else {
+ self.handle_copy(path, &req, &mut res).await?
+ }
+ }
+ "MOVE" => {
+ if !allow_upload || !allow_delete {
+ status_forbid(&mut res);
+ } else if is_miss {
+ status_not_found(&mut res);
+ } else {
+ self.handle_move(path, &req, &mut res).await?
+ }
+ }
+ "LOCK" => {
+ // Fake lock
+ if is_file {
+ let has_auth = authorization.is_some();
+ self.handle_lock(req_path, has_auth, &mut res).await?;
+ } else {
+ status_not_found(&mut res);
+ }
+ }
+ "UNLOCK" => {
+ // Fake unlock
+ if is_miss {
+ status_not_found(&mut res);
+ }
+ }
+ _ => {
+ *res.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
+ }
+ },
+ }
+ Ok(res)
+ }
+
+ async fn handle_upload(
+ &self,
+ path: &Path,
+ upload_offset: Option<u64>,
+ size: u64,
+ req: Request,
+ res: &mut Response,
+ ) -> Result<()> {
+ ensure_path_parent(path).await?;
+ let (mut file, status) = match upload_offset {
+ None => (fs::File::create(path).await?, StatusCode::CREATED),
+ Some(offset) if offset == size => (
+ fs::OpenOptions::new().append(true).open(path).await?,
+ StatusCode::NO_CONTENT,
+ ),
+ Some(offset) => {
+ let mut file = fs::OpenOptions::new().write(true).open(path).await?;
+ file.seek(SeekFrom::Start(offset)).await?;
+ (file, StatusCode::NO_CONTENT)
+ }
+ };
+ let stream = IncomingStream::new(req.into_body());
+
+ let body_with_io_error = stream.map_err(|err| io::Error::new(io::ErrorKind::Other, err));
+ let body_reader = StreamReader::new(body_with_io_error);
+
+ pin_mut!(body_reader);
+
+ let ret = io::copy(&mut body_reader, &mut file).await;
+ let size = fs::metadata(path)
+ .await
+ .map(|v| v.len())
+ .unwrap_or_default();
+ if ret.is_err() {
+ if upload_offset.is_none() && size < RESUMABLE_UPLOAD_MIN_SIZE {
+ let _ = tokio::fs::remove_file(&path).await;
+ }
+ ret?;
+ }
+
+ *res.status_mut() = status;
+
+ Ok(())
+ }
+
+ async fn handle_delete(&self, path: &Path, is_dir: bool, res: &mut Response) -> Result<()> {
+ match is_dir {
+ true => fs::remove_dir_all(path).await?,
+ false => fs::remove_file(path).await?,
+ }
+
+ status_no_content(res);
+ Ok(())
+ }
+
+ async fn handle_ls_dir(
+ &self,
+ path: &Path,
+ exist: bool,
+ query_params: &HashMap<String, String>,
+ head_only: bool,
+ user: Option<String>,
+ access_paths: AccessPaths,
+ res: &mut Response,
+ ) -> Result<()> {
+ let mut paths = vec![];
+ if exist {
+ paths = match self.list_dir(path, path, access_paths.clone()).await {
+ Ok(paths) => paths,
+ Err(_) => {
+ status_forbid(res);
+ return Ok(());
+ }
+ }
+ };
+ self.send_index(
+ path,
+ paths,
+ exist,
+ query_params,
+ head_only,
+ user,
+ access_paths,
+ res,
+ )
+ }
+
+ async fn handle_search_dir(
+ &self,
+ path: &Path,
+ query_params: &HashMap<String, String>,
+ head_only: bool,
+ user: Option<String>,
+ access_paths: AccessPaths,
+ res: &mut Response,
+ ) -> Result<()> {
+ let mut paths: Vec<PathItem> = vec![];
+ let search = query_params
+ .get("q")
+ .ok_or_else(|| anyhow!("invalid q"))?
+ .to_lowercase();
+ if search.is_empty() {
+ return self
+ .handle_ls_dir(path, true, query_params, head_only, user, access_paths, res)
+ .await;
+ } else {
+ let path_buf = path.to_path_buf();
+ let hidden = Arc::new(self.args.hidden.to_vec());
+ let search = search.clone();
+
+ let access_paths = access_paths.clone();
+ let search_paths = tokio::spawn(collect_dir_entries(
+ access_paths,
+ self.running.clone(),
+ path_buf,
+ hidden,
+ self.args.allow_symlink,
+ self.args.serve_path.clone(),
+ move |x| get_file_name(x.path()).to_lowercase().contains(&search),
+ ))
+ .await?;
+
+ for search_path in search_paths.into_iter() {
+ if let Ok(Some(item)) = self.to_pathitem(search_path, path.to_path_buf()).await {
+ paths.push(item);
+ }
+ }
+ }
+ self.send_index(
+ path,
+ paths,
+ true,
+ query_params,
+ head_only,
+ user,
+ access_paths,
+ res,
+ )
+ }
+
+ async fn handle_zip_dir(
+ &self,
+ path: &Path,
+ head_only: bool,
+ access_paths: AccessPaths,
+ res: &mut Response,
+ ) -> Result<()> {
+ let (mut writer, reader) = tokio::io::duplex(BUF_SIZE);
+ let filename = try_get_file_name(path)?;
+ set_content_disposition(res, false, &format!("{}.zip", filename))?;
+ res.headers_mut()
+ .insert("content-type", HeaderValue::from_static("application/zip"));
+ if head_only {
+ return Ok(());
+ }
+ let path = path.to_owned();
+ let hidden = self.args.hidden.clone();
+ let running = self.running.clone();
+ let compression = self.args.compress.to_compression();
+ let follow_symlinks = self.args.allow_symlink;
+ let serve_path = self.args.serve_path.clone();
+ tokio::spawn(async move {
+ if let Err(e) = zip_dir(
+ &mut writer,
+ &path,
+ access_paths,
+ &hidden,
+ compression,
+ follow_symlinks,
+ serve_path,
+ running,
+ )
+ .await
+ {
+ error!("Failed to zip {}, {e}", path.display());
+ }
+ });
+ let reader_stream = ReaderStream::with_capacity(reader, BUF_SIZE);
+ let stream_body = StreamBody::new(
+ reader_stream
+ .map_ok(Frame::data)
+ .map_err(|err| anyhow!("{err}")),
+ );
+ let boxed_body = stream_body.boxed();
+ *res.body_mut() = boxed_body;
+ Ok(())
+ }
+
+ async fn handle_render_index(
+ &self,
+ path: &Path,
+ query_params: &HashMap<String, String>,
+ headers: &HeaderMap<HeaderValue>,
+ head_only: bool,
+ user: Option<String>,
+ access_paths: AccessPaths,
+ res: &mut Response,
+ ) -> Result<()> {
+ let index_path = path.join(INDEX_NAME);
+ if fs::metadata(&index_path)
+ .await
+ .ok()
+ .map(|v| v.is_file())
+ .unwrap_or_default()
+ {
+ self.handle_send_file(&index_path, headers, head_only, res)
+ .await?;
+ } else if self.args.render_try_index {
+ self.handle_ls_dir(path, true, query_params, head_only, user, access_paths, res)
+ .await?;
+ } else {
+ status_not_found(res)
+ }
+ Ok(())
+ }
+
+ async fn handle_render_spa(
+ &self,
+ path: &Path,
+ headers: &HeaderMap<HeaderValue>,
+ head_only: bool,
+ res: &mut Response,
+ ) -> Result<()> {
+ if path.extension().is_none() {
+ let path = self.args.serve_path.join(INDEX_NAME);
+ self.handle_send_file(&path, headers, head_only, res)
+ .await?;
+ } else {
+ status_not_found(res)
+ }
+ Ok(())
+ }
+
+ async fn handle_internal(
+ &self,
+ req_path: &str,
+ headers: &HeaderMap<HeaderValue>,
+ res: &mut Response,
+ ) -> Result<bool> {
+ if let Some(name) = req_path.strip_prefix(&self.assets_prefix) {
+ match self.args.assets.as_ref() {
+ Some(assets_path) => {
+ let path = assets_path.join(name);
+ if path.exists() {
+ self.handle_send_file(&path, headers, false, res).await?;
+ } else {
+ status_not_found(res);
+ return Ok(true);
+ }
+ }
+ None => match name {
+ "index.js" => {
+ *res.body_mut() = body_full(INDEX_JS);
+ res.headers_mut().insert(
+ "content-type",
+ HeaderValue::from_static("application/javascript; charset=UTF-8"),
+ );
+ }
+ "index.css" => {
+ *res.body_mut() = body_full(INDEX_CSS);
+ res.headers_mut().insert(
+ "content-type",
+ HeaderValue::from_static("text/css; charset=UTF-8"),
+ );
+ }
+ "favicon.ico" => {
+ *res.body_mut() = body_full(FAVICON_ICO);
+ res.headers_mut()
+ .insert("content-type", HeaderValue::from_static("image/x-icon"));
+ }
+ _ => {
+ status_not_found(res);
+ }
+ },
+ }
+ res.headers_mut().insert(
+ "cache-control",
+ HeaderValue::from_static("public, max-age=31536000, immutable"),
+ );
+ res.headers_mut().insert(
+ "x-content-type-options",
+ HeaderValue::from_static("nosniff"),
+ );
+ Ok(true)
+ } else if req_path == HEALTH_CHECK_PATH {
+ res.headers_mut()
+ .typed_insert(ContentType::from(mime_guess::mime::APPLICATION_JSON));
+
+ *res.body_mut() = body_full(r#"{"status":"OK"}"#);
+ Ok(true)
+ } else {
+ Ok(false)
+ }
+ }
+
+ async fn handle_send_file(
+ &self,
+ path: &Path,
+ headers: &HeaderMap<HeaderValue>,
+ head_only: bool,
+ res: &mut Response,
+ ) -> Result<()> {
+ let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
+ let (mut file, meta) = (file?, meta?);
+ let size = meta.len();
+ let mut use_range = true;
+ if let Some((etag, last_modified)) = extract_cache_headers(&meta) {
+ 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(());
+ }
+ }
+
+ res.headers_mut()
+ .typed_insert(CacheControl::new().with_no_cache());
+ res.headers_mut().typed_insert(last_modified);
+ res.headers_mut().typed_insert(etag.clone());
+
+ if headers.typed_get::<Range>().is_some() {
+ use_range = headers
+ .typed_get::<IfRange>()
+ .map(|if_range| !if_range.is_modified(Some(&etag), Some(&last_modified)))
+ // Always be fresh if there is no validators
+ .unwrap_or(true);
+ } else {
+ use_range = false;
+ }
+ }
+
+ let ranges = if use_range {
+ headers.get(RANGE).map(|range| {
+ range
+ .to_str()
+ .ok()
+ .and_then(|range| parse_range(range, size))
+ })
+ } else {
+ None
+ };
+
+ res.headers_mut().insert(
+ CONTENT_TYPE,
+ HeaderValue::from_str(&get_content_type(path).await?)?,
+ );
+
+ let filename = try_get_file_name(path)?;
+ set_content_disposition(res, true, filename)?;
+
+ res.headers_mut().typed_insert(AcceptRanges::bytes());
+
+ if let Some(ranges) = ranges {
+ if let Some(ranges) = ranges {
+ if ranges.len() == 1 {
+ let (start, end) = ranges[0];
+ file.seek(SeekFrom::Start(start)).await?;
+ let range_size = end - start + 1;
+ *res.status_mut() = StatusCode::PARTIAL_CONTENT;
+ let content_range = format!("bytes {}-{}/{}", start, end, size);
+ res.headers_mut()
+ .insert(CONTENT_RANGE, content_range.parse()?);
+ res.headers_mut()
+ .insert(CONTENT_LENGTH, format!("{range_size}").parse()?);
+ if head_only {
+ return Ok(());
+ }
+
+ let stream_body = StreamBody::new(
+ LengthLimitedStream::new(file, range_size as usize)
+ .map_ok(Frame::data)
+ .map_err(|err| anyhow!("{err}")),
+ );
+ let boxed_body = stream_body.boxed();
+ *res.body_mut() = boxed_body;
+ } else {
+ *res.status_mut() = StatusCode::PARTIAL_CONTENT;
+ let boundary = Uuid::new_v4();
+ let mut body = Vec::new();
+ let content_type = get_content_type(path).await?;
+ for (start, end) in ranges {
+ file.seek(SeekFrom::Start(start)).await?;
+ let range_size = end - start + 1;
+ let content_range = format!("bytes {}-{}/{}", start, end, size);
+ let part_header = format!(
+ "--{boundary}\r\nContent-Type: {content_type}\r\nContent-Range: {content_range}\r\n\r\n",
+ );
+ body.extend_from_slice(part_header.as_bytes());
+ let mut buffer = vec![0; range_size as usize];
+ file.read_exact(&mut buffer).await?;
+ body.extend_from_slice(&buffer);
+ body.extend_from_slice(b"\r\n");
+ }
+ body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
+ res.headers_mut().insert(
+ CONTENT_TYPE,
+ format!("multipart/byteranges; boundary={boundary}").parse()?,
+ );
+ res.headers_mut()
+ .insert(CONTENT_LENGTH, format!("{}", body.len()).parse()?);
+ if head_only {
+ return Ok(());
+ }
+ *res.body_mut() = body_full(body);
+ }
+ } else {
+ *res.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
+ res.headers_mut()
+ .insert(CONTENT_RANGE, format!("bytes */{size}").parse()?);
+ }
+ } else {
+ res.headers_mut()
+ .insert(CONTENT_LENGTH, format!("{size}").parse()?);
+ if head_only {
+ return Ok(());
+ }
+
+ let reader_stream = ReaderStream::with_capacity(file, BUF_SIZE);
+ let stream_body = StreamBody::new(
+ reader_stream
+ .map_ok(Frame::data)
+ .map_err(|err| anyhow!("{err}")),
+ );
+ let boxed_body = stream_body.boxed();
+ *res.body_mut() = boxed_body;
+ }
+ Ok(())
+ }
+
+ async fn handle_edit_file(
+ &self,
+ path: &Path,
+ kind: DataKind,
+ head_only: bool,
+ user: Option<String>,
+ res: &mut Response,
+ ) -> Result<()> {
+ 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.serve_path)?)
+ );
+ let mut buffer: Vec<u8> = vec![];
+ file.take(1024).read_to_end(&mut buffer).await?;
+ let editable =
+ meta.len() <= EDITABLE_TEXT_MAX_SIZE && content_inspector::inspect(&buffer).is_text();
+ let data = EditData {
+ href,
+ kind,
+ uri_prefix: self.args.uri_prefix.clone(),
+ allow_upload: self.args.allow_upload,
+ allow_delete: self.args.allow_delete,
+ auth: self.args.auth.exist(),
+ user,
+ editable,
+ };
+ 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__", &index_data);
+ res.headers_mut()
+ .typed_insert(ContentLength(output.len() as u64));
+ res.headers_mut()
+ .typed_insert(CacheControl::new().with_no_cache());
+ if head_only {
+ return Ok(());
+ }
+ *res.body_mut() = body_full(output);
+ Ok(())
+ }
+
+ async fn handle_hash_file(
+ &self,
+ path: &Path,
+ head_only: bool,
+ res: &mut Response,
+ ) -> Result<()> {
+ let output = sha256_file(path).await?;
+ res.headers_mut()
+ .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
+ res.headers_mut()
+ .typed_insert(ContentLength(output.len() as u64));
+ if head_only {
+ return Ok(());
+ }
+ *res.body_mut() = body_full(output);
+ Ok(())
+ }
+
+ async fn handle_propfind_dir(
+ &self,
+ path: &Path,
+ headers: &HeaderMap<HeaderValue>,
+ access_paths: AccessPaths,
+ res: &mut Response,
+ ) -> Result<()> {
+ let depth: u32 = match headers.get("depth") {
+ Some(v) => match v.to_str().ok().and_then(|v| v.parse().ok()) {
+ Some(0) => 0,
+ Some(1) => 1,
+ _ => {
+ status_bad_request(res, "Invalid depth: only 0 and 1 are allowed.");
+ return Ok(());
+ }
+ },
+ None => 1,
+ };
+ let mut paths = match self.to_pathitem(path, &self.args.serve_path).await? {
+ Some(v) => vec![v],
+ None => vec![],
+ };
+ if depth == 1 {
+ match self
+ .list_dir(path, &self.args.serve_path, access_paths)
+ .await
+ {
+ Ok(child) => paths.extend(child),
+ Err(_) => {
+ status_forbid(res);
+ return Ok(());
+ }
+ }
+ }
+ let output = paths
+ .iter()
+ .map(|v| v.to_dav_xml(self.args.uri_prefix.as_str()))
+ .fold(String::new(), |mut acc, v| {
+ acc.push_str(&v);
+ acc
+ });
+ res_multistatus(res, &output);
+ Ok(())
+ }
+
+ async fn handle_propfind_file(&self, path: &Path, res: &mut Response) -> Result<()> {
+ if let Some(pathitem) = self.to_pathitem(path, &self.args.serve_path).await? {
+ res_multistatus(res, &pathitem.to_dav_xml(self.args.uri_prefix.as_str()));
+ } else {
+ status_not_found(res);
+ }
+ Ok(())
+ }
+
+ async fn handle_mkcol(&self, path: &Path, res: &mut Response) -> Result<()> {
+ fs::create_dir_all(path).await?;
+ *res.status_mut() = StatusCode::CREATED;
+ Ok(())
+ }
+
+ async fn handle_copy(&self, path: &Path, req: &Request, res: &mut Response) -> Result<()> {
+ let dest = match self.extract_dest(req, res) {
+ Some(dest) => dest,
+ None => {
+ return Ok(());
+ }
+ };
+
+ let meta = fs::symlink_metadata(path).await?;
+ if meta.is_dir() {
+ status_forbid(res);
+ return Ok(());
+ }
+
+ ensure_path_parent(&dest).await?;
+
+ fs::copy(path, &dest).await?;
+
+ status_no_content(res);
+ Ok(())
+ }
+
+ async fn handle_move(&self, path: &Path, req: &Request, res: &mut Response) -> Result<()> {
+ let dest = match self.extract_dest(req, res) {
+ Some(dest) => dest,
+ None => {
+ return Ok(());
+ }
+ };
+
+ ensure_path_parent(&dest).await?;
+
+ fs::rename(path, &dest).await?;
+
+ status_no_content(res);
+ Ok(())
+ }
+
+ async fn handle_lock(&self, req_path: &str, auth: bool, res: &mut Response) -> Result<()> {
+ let token = if auth {
+ format!("opaquelocktoken:{}", Uuid::new_v4())
+ } else {
+ Utc::now().timestamp().to_string()
+ };
+
+ res.headers_mut().insert(
+ "content-type",
+ HeaderValue::from_static("application/xml; charset=utf-8"),
+ );
+ res.headers_mut()
+ .insert("lock-token", format!("<{token}>").parse()?);
+
+ *res.body_mut() = body_full(format!(
+ r#"<?xml version="1.0" encoding="utf-8"?>
<D:prop xmlns:D="DAV:"><D:lockdiscovery><D:activelock>
<D:locktoken><D:href>{token}</D:href></D:locktoken>
<D:lockroot><D:href>{req_path}</D:href></D:lockroot>
</D:activelock></D:lockdiscovery></D:prop>"#
- ));
- Ok(())
- }
+ ));
+ Ok(())
+ }
- async fn handle_proppatch(&self, req_path: &str, res: &mut Response) -> Result<()> {
- let output = format!(
- r#"<D:response>
+ async fn handle_proppatch(&self, req_path: &str, res: &mut Response) -> Result<()> {
+ let output = format!(
+ r#"<D:response>
<D:href>{req_path}</D:href>
<D:propstat>
<D:prop>
<D:status>HTTP/1.1 403 Forbidden</D:status>
</D:propstat>
</D:response>"#
- );
- res_multistatus(res, &output);
- Ok(())
- }
-
- #[allow(clippy::too_many_arguments)]
- fn send_index(
- &self,
- path: &Path,
- mut paths: Vec<PathItem>,
- exist: bool,
- query_params: &HashMap<String, String>,
- head_only: bool,
- user: Option<String>,
- access_paths: AccessPaths,
- res: &mut Response,
- ) -> Result<()> {
- if let Some(sort) = query_params.get("sort") {
- if sort == "name" {
- paths.sort_by(|v1, v2| v1.sort_by_name(v2))
- } else if sort == "mtime" {
- paths.sort_by(|v1, v2| v1.sort_by_mtime(v2))
- } else if sort == "size" {
- paths.sort_by(|v1, v2| v1.sort_by_size(v2))
- }
- if query_params
- .get("order")
- .map(|v| v == "desc")
- .unwrap_or_default()
- {
- paths.reverse()
- }
- } else {
- paths.sort_by(|v1, v2| v1.sort_by_name(v2))
- }
- if has_query_flag(query_params, "simple") {
- let output = paths
- .into_iter()
- .map(|v| {
- if v.is_dir() {
- format!("{}/\n", v.name)
- } else {
- format!("{}\n", v.name)
- }
- })
- .collect::<Vec<String>>()
- .join("");
- res.headers_mut()
- .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
- res.headers_mut()
- .typed_insert(ContentLength(output.len() as u64));
- *res.body_mut() = body_full(output);
- if head_only {
- return Ok(());
- }
- return Ok(());
- }
- let href = format!(
- "/{}",
- normalize_path(path.strip_prefix(&self.args.serve_path)?)
- );
- let readwrite = access_paths.perm().readwrite();
- let data = IndexData {
- kind: DataKind::Index,
- href,
- uri_prefix: self.args.uri_prefix.clone(),
- allow_upload: self.args.allow_upload && readwrite,
- allow_delete: self.args.allow_delete && readwrite,
- allow_search: self.args.allow_search,
- allow_archive: self.args.allow_archive,
- dir_exists: exist,
- auth: self.args.auth.exist(),
- user,
- paths,
- };
- 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__", &index_data)
- };
- res.headers_mut()
- .typed_insert(ContentLength(output.len() as u64));
- res.headers_mut()
- .typed_insert(CacheControl::new().with_no_cache());
- res.headers_mut().insert(
- "x-content-type-options",
- HeaderValue::from_static("nosniff"),
- );
- if head_only {
- return Ok(());
- }
- *res.body_mut() = body_full(output);
- Ok(())
- }
-
- fn auth_reject(&self, res: &mut Response) -> Result<()> {
- set_webdav_headers(res);
-
- www_authenticate(res, &self.args)?;
- *res.status_mut() = StatusCode::UNAUTHORIZED;
- Ok(())
- }
-
- async fn is_root_contained(&self, path: &Path) -> bool {
- fs::canonicalize(path)
- .await
- .ok()
- .map(|v| v.starts_with(&self.args.serve_path))
- .unwrap_or_default()
- }
-
- fn extract_dest(&self, req: &Request, res: &mut Response) -> Option<PathBuf> {
- let headers = req.headers();
- let dest_path = match self
- .extract_destination_header(headers)
- .and_then(|dest| self.resolve_path(&dest))
- {
- Some(dest) => dest,
- None => {
- status_bad_request(res, "Invalid Destination");
- return None;
- }
- };
-
- let authorization = headers.get(AUTHORIZATION);
- let guard = self
- .args
- .auth
- .guard(&dest_path, req.method(), authorization, false);
-
- match guard {
- (_, Some(_)) => {}
- _ => {
- status_forbid(res);
- return None;
- }
- };
-
- let dest = match self.join_path(&dest_path) {
- Some(dest) => dest,
- None => {
- *res.status_mut() = StatusCode::BAD_REQUEST;
- return None;
- }
- };
-
- Some(dest)
- }
-
- fn extract_destination_header(&self, headers: &HeaderMap<HeaderValue>) -> Option<String> {
- let dest = headers.get("Destination")?.to_str().ok()?;
- let uri: Uri = dest.parse().ok()?;
- Some(uri.path().to_string())
- }
-
- fn resolve_path(&self, path: &str) -> Option<String> {
- let path = decode_uri(path)?;
- let path = path.trim_matches('/');
- let mut parts = vec![];
- for comp in Path::new(path).components() {
- if let Component::Normal(v) = comp {
- let v = v.to_string_lossy();
- if cfg!(windows) {
- let chars: Vec<char> = v.chars().collect();
- if chars.len() == 2 && chars[1] == ':' && chars[0].is_ascii_alphabetic() {
- return None;
- }
- }
- parts.push(v);
- } else {
- return None;
- }
- }
- let new_path = parts.join("/");
- let path_prefix = self.args.path_prefix.as_str();
- if path_prefix.is_empty() {
- return Some(new_path);
- }
- new_path
- .strip_prefix(path_prefix.trim_start_matches('/'))
- .map(|v| v.trim_matches('/').to_string())
- }
-
- fn join_path(&self, path: &str) -> Option<PathBuf> {
- if path.is_empty() {
- return Some(self.args.serve_path.clone());
- }
- let path = if cfg!(windows) {
- path.replace('/', "\\")
- } else {
- path.to_string()
- };
- Some(self.args.serve_path.join(path))
- }
-
- async fn list_dir(
- &self,
- entry_path: &Path,
- base_path: &Path,
- access_paths: AccessPaths,
- ) -> Result<Vec<PathItem>> {
- let mut paths: Vec<PathItem> = vec![];
- if access_paths.perm().indexonly() {
- for name in access_paths.child_names() {
- let entry_path = entry_path.join(name);
- self.add_pathitem(&mut paths, base_path, &entry_path).await;
- }
- } else {
- let mut rd = fs::read_dir(entry_path).await?;
- while let Ok(Some(entry)) = rd.next_entry().await {
- let entry_path = entry.path();
- self.add_pathitem(&mut paths, base_path, &entry_path).await;
- }
- }
- Ok(paths)
- }
-
- async fn add_pathitem(&self, paths: &mut Vec<PathItem>, base_path: &Path, entry_path: &Path) {
- let base_name = get_file_name(entry_path);
- if let Ok(Some(item)) = self.to_pathitem(entry_path, base_path).await {
- if is_hidden(&self.args.hidden, base_name, item.is_dir()) {
- return;
- }
- paths.push(item);
- }
- }
-
- async fn to_pathitem<P: AsRef<Path>>(&self, path: P, base_path: P) -> Result<Option<PathItem>> {
- let path = path.as_ref();
- let (meta, meta2) = tokio::join!(fs::metadata(&path), fs::symlink_metadata(&path));
- let (meta, meta2) = (meta?, meta2?);
- let is_symlink = meta2.is_symlink();
- if !self.args.allow_symlink && is_symlink && !self.is_root_contained(path).await {
- return Ok(None);
- }
- let is_dir = meta.is_dir();
- let path_type = match (is_symlink, is_dir) {
- (true, true) => PathType::SymlinkDir,
- (false, true) => PathType::Dir,
- (true, false) => PathType::SymlinkFile,
- (false, false) => PathType::File,
- };
- let mtime = match meta.modified().ok().or_else(|| meta.created().ok()) {
- Some(v) => to_timestamp(&v),
- None => 0,
- };
- let size = match path_type {
- PathType::Dir | PathType::SymlinkDir => {
- let mut count = 0;
- let mut entries = tokio::fs::read_dir(&path).await?;
- while let Some(entry) = entries.next_entry().await? {
- let entry_path = entry.path();
- let base_name = get_file_name(&entry_path);
- let is_dir = entry
- .file_type()
- .await
- .map(|v| v.is_dir())
- .unwrap_or_default();
- if is_hidden(&self.args.hidden, base_name, is_dir) {
- continue;
- }
- count += 1;
- if count >= MAX_SUBPATHS_COUNT {
- break;
- }
- }
- count
- }
- PathType::File | PathType::SymlinkFile => meta.len(),
- };
- let rel_path = path.strip_prefix(base_path)?;
- let name = normalize_path(rel_path);
- Ok(Some(PathItem {
- path_type,
- name,
- mtime,
- size,
- }))
- }
+ );
+ res_multistatus(res, &output);
+ Ok(())
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn send_index(
+ &self,
+ path: &Path,
+ mut paths: Vec<PathItem>,
+ exist: bool,
+ query_params: &HashMap<String, String>,
+ head_only: bool,
+ user: Option<String>,
+ access_paths: AccessPaths,
+ res: &mut Response,
+ ) -> Result<()> {
+ if let Some(sort) = query_params.get("sort") {
+ if sort == "name" {
+ paths.sort_by(|v1, v2| v1.sort_by_name(v2))
+ } else if sort == "mtime" {
+ paths.sort_by(|v1, v2| v1.sort_by_mtime(v2))
+ } else if sort == "size" {
+ paths.sort_by(|v1, v2| v1.sort_by_size(v2))
+ }
+ if query_params
+ .get("order")
+ .map(|v| v == "desc")
+ .unwrap_or_default()
+ {
+ paths.reverse()
+ }
+ } else {
+ paths.sort_by(|v1, v2| v1.sort_by_name(v2))
+ }
+ if has_query_flag(query_params, "simple") {
+ let output = paths
+ .into_iter()
+ .map(|v| {
+ if v.is_dir() {
+ format!("{}/\n", v.name)
+ } else {
+ format!("{}\n", v.name)
+ }
+ })
+ .collect::<Vec<String>>()
+ .join("");
+ res.headers_mut()
+ .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
+ res.headers_mut()
+ .typed_insert(ContentLength(output.len() as u64));
+ *res.body_mut() = body_full(output);
+ if head_only {
+ return Ok(());
+ }
+ return Ok(());
+ }
+ let href = format!(
+ "/{}",
+ normalize_path(path.strip_prefix(&self.args.serve_path)?)
+ );
+ let readwrite = access_paths.perm().readwrite();
+ let data = IndexData {
+ kind: DataKind::Index,
+ href,
+ uri_prefix: self.args.uri_prefix.clone(),
+ allow_upload: self.args.allow_upload && readwrite,
+ allow_delete: self.args.allow_delete && readwrite,
+ allow_search: self.args.allow_search,
+ allow_archive: self.args.allow_archive,
+ dir_exists: exist,
+ auth: self.args.auth.exist(),
+ user,
+ paths,
+ };
+ 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__", &index_data)
+ };
+ res.headers_mut()
+ .typed_insert(ContentLength(output.len() as u64));
+ res.headers_mut()
+ .typed_insert(CacheControl::new().with_no_cache());
+ res.headers_mut().insert(
+ "x-content-type-options",
+ HeaderValue::from_static("nosniff"),
+ );
+ if head_only {
+ return Ok(());
+ }
+ *res.body_mut() = body_full(output);
+ Ok(())
+ }
+
+ fn auth_reject(&self, res: &mut Response) -> Result<()> {
+ set_webdav_headers(res);
+
+ www_authenticate(res, &self.args)?;
+ *res.status_mut() = StatusCode::UNAUTHORIZED;
+ Ok(())
+ }
+
+ async fn is_root_contained(&self, path: &Path) -> bool {
+ fs::canonicalize(path)
+ .await
+ .ok()
+ .map(|v| v.starts_with(&self.args.serve_path))
+ .unwrap_or_default()
+ }
+
+ fn extract_dest(&self, req: &Request, res: &mut Response) -> Option<PathBuf> {
+ let headers = req.headers();
+ let dest_path = match self
+ .extract_destination_header(headers)
+ .and_then(|dest| self.resolve_path(&dest))
+ {
+ Some(dest) => dest,
+ None => {
+ status_bad_request(res, "Invalid Destination");
+ return None;
+ }
+ };
+
+ let authorization = headers.get(AUTHORIZATION);
+ let guard = self
+ .args
+ .auth
+ .guard(&dest_path, req.method(), authorization, false);
+
+ match guard {
+ (_, Some(_)) => {}
+ _ => {
+ status_forbid(res);
+ return None;
+ }
+ };
+
+ let dest = match self.join_path(&dest_path) {
+ Some(dest) => dest,
+ None => {
+ *res.status_mut() = StatusCode::BAD_REQUEST;
+ return None;
+ }
+ };
+
+ Some(dest)
+ }
+
+ fn extract_destination_header(&self, headers: &HeaderMap<HeaderValue>) -> Option<String> {
+ let dest = headers.get("Destination")?.to_str().ok()?;
+ let uri: Uri = dest.parse().ok()?;
+ Some(uri.path().to_string())
+ }
+
+ fn resolve_path(&self, path: &str) -> Option<String> {
+ let path = decode_uri(path)?;
+ let path = path.trim_matches('/');
+ let mut parts = vec![];
+ for comp in Path::new(path).components() {
+ if let Component::Normal(v) = comp {
+ let v = v.to_string_lossy();
+ if cfg!(windows) {
+ let chars: Vec<char> = v.chars().collect();
+ if chars.len() == 2 && chars[1] == ':' && chars[0].is_ascii_alphabetic() {
+ return None;
+ }
+ }
+ parts.push(v);
+ } else {
+ return None;
+ }
+ }
+ let new_path = parts.join("/");
+ let path_prefix = self.args.path_prefix.as_str();
+ if path_prefix.is_empty() {
+ return Some(new_path);
+ }
+ new_path
+ .strip_prefix(path_prefix.trim_start_matches('/'))
+ .map(|v| v.trim_matches('/').to_string())
+ }
+
+ fn join_path(&self, path: &str) -> Option<PathBuf> {
+ if path.is_empty() {
+ return Some(self.args.serve_path.clone());
+ }
+ let path = if cfg!(windows) {
+ path.replace('/', "\\")
+ } else {
+ path.to_string()
+ };
+ Some(self.args.serve_path.join(path))
+ }
+
+ async fn list_dir(
+ &self,
+ entry_path: &Path,
+ base_path: &Path,
+ access_paths: AccessPaths,
+ ) -> Result<Vec<PathItem>> {
+ let mut paths: Vec<PathItem> = vec![];
+ if access_paths.perm().indexonly() {
+ for name in access_paths.child_names() {
+ let entry_path = entry_path.join(name);
+ self.add_pathitem(&mut paths, base_path, &entry_path).await;
+ }
+ } else {
+ let mut rd = fs::read_dir(entry_path).await?;
+ while let Ok(Some(entry)) = rd.next_entry().await {
+ let entry_path = entry.path();
+ self.add_pathitem(&mut paths, base_path, &entry_path).await;
+ }
+
+ // add the .. directory
+ if base_path != self.args.serve_path {
+ let mut path = PathBuf::from(base_path);
+ path.push("..");
+ self.add_pathitem(&mut paths, base_path, &path).await;
+ }
+ }
+ Ok(paths)
+ }
+
+ async fn add_pathitem(&self, paths: &mut Vec<PathItem>, base_path: &Path, entry_path: &Path) {
+ let base_name = get_file_name(entry_path);
+ if let Ok(Some(item)) = self.to_pathitem(entry_path, base_path).await {
+ if is_hidden(&self.args.hidden, base_name, item.is_dir()) {
+ return;
+ }
+ paths.push(item);
+ }
+ }
+
+ async fn to_pathitem<P: AsRef<Path>>(&self, path: P, base_path: P) -> Result<Option<PathItem>> {
+ let path = path.as_ref();
+ let (meta, meta2) = tokio::join!(fs::metadata(&path), fs::symlink_metadata(&path));
+ let (meta, meta2) = (meta?, meta2?);
+ let is_symlink = meta2.is_symlink();
+ if !self.args.allow_symlink && is_symlink && !self.is_root_contained(path).await {
+ return Ok(None);
+ }
+ let is_dir = meta.is_dir();
+ let path_type = match (is_symlink, is_dir) {
+ (true, true) => PathType::SymlinkDir,
+ (false, true) => PathType::Dir,
+ (true, false) => PathType::SymlinkFile,
+ (false, false) => PathType::File,
+ };
+ let mtime = match meta.modified().ok().or_else(|| meta.created().ok()) {
+ Some(v) => to_timestamp(&v),
+ None => 0,
+ };
+ let size = match path_type {
+ PathType::Dir | PathType::SymlinkDir => {
+ let mut count = 0;
+ let mut entries = tokio::fs::read_dir(&path).await?;
+ while let Some(entry) = entries.next_entry().await? {
+ let entry_path = entry.path();
+ let base_name = get_file_name(&entry_path);
+ let is_dir = entry
+ .file_type()
+ .await
+ .map(|v| v.is_dir())
+ .unwrap_or_default();
+ if is_hidden(&self.args.hidden, base_name, is_dir) {
+ continue;
+ }
+ count += 1;
+ if count >= MAX_SUBPATHS_COUNT {
+ break;
+ }
+ }
+ count
+ }
+ PathType::File | PathType::SymlinkFile => meta.len(),
+ };
+ let rel_path = path.strip_prefix(base_path)?;
+ let name = normalize_path(rel_path);
+ Ok(Some(PathItem {
+ path_type,
+ name,
+ mtime,
+ size,
+ }))
+ }
}
#[derive(Debug, Serialize, PartialEq)]
enum DataKind {
- Index,
- Edit,
- View,
+ Index,
+ Edit,
+ View,
}
#[derive(Debug, Serialize)]
struct IndexData {
- href: String,
- kind: DataKind,
- uri_prefix: String,
- allow_upload: bool,
- allow_delete: bool,
- allow_search: bool,
- allow_archive: bool,
- dir_exists: bool,
- auth: bool,
- user: Option<String>,
- paths: Vec<PathItem>,
+ href: String,
+ kind: DataKind,
+ uri_prefix: String,
+ allow_upload: bool,
+ allow_delete: bool,
+ allow_search: bool,
+ allow_archive: bool,
+ dir_exists: bool,
+ auth: bool,
+ user: Option<String>,
+ paths: Vec<PathItem>,
}
#[derive(Debug, Serialize)]
struct EditData {
- href: String,
- kind: DataKind,
- uri_prefix: String,
- allow_upload: bool,
- allow_delete: bool,
- auth: bool,
- user: Option<String>,
- editable: bool,
+ href: String,
+ kind: DataKind,
+ uri_prefix: String,
+ allow_upload: bool,
+ allow_delete: bool,
+ auth: bool,
+ user: Option<String>,
+ editable: bool,
}
#[derive(Debug, Serialize, Eq, PartialEq, Ord, PartialOrd)]
struct PathItem {
- path_type: PathType,
- name: String,
- mtime: u64,
- size: u64,
+ path_type: PathType,
+ name: String,
+ mtime: u64,
+ size: u64,
}
impl PathItem {
- pub fn is_dir(&self) -> bool {
- self.path_type == PathType::Dir || self.path_type == PathType::SymlinkDir
- }
-
- pub fn to_dav_xml(&self, prefix: &str) -> String {
- let mtime = match Utc.timestamp_millis_opt(self.mtime as i64) {
- LocalResult::Single(v) => format!("{}", v.format("%a, %d %b %Y %H:%M:%S GMT")),
- _ => String::new(),
- };
- let mut href = encode_uri(&format!("{}{}", prefix, &self.name));
- if self.is_dir() && !href.ends_with('/') {
- href.push('/');
- }
- let displayname = escape_str_pcdata(self.base_name());
- match self.path_type {
- PathType::Dir | PathType::SymlinkDir => format!(
- r#"<D:response>
+ pub fn is_dir(&self) -> bool {
+ self.path_type == PathType::Dir || self.path_type == PathType::SymlinkDir
+ }
+
+ pub fn to_dav_xml(&self, prefix: &str) -> String {
+ let mtime = match Utc.timestamp_millis_opt(self.mtime as i64) {
+ LocalResult::Single(v) => format!("{}", v.format("%a, %d %b %Y %H:%M:%S GMT")),
+ _ => String::new(),
+ };
+ let mut href = encode_uri(&format!("{}{}", prefix, &self.name));
+ if self.is_dir() && !href.ends_with('/') {
+ href.push('/');
+ }
+ let displayname = escape_str_pcdata(self.base_name());
+ match self.path_type {
+ PathType::Dir | PathType::SymlinkDir => format!(
+ r#"<D:response>
<D:href>{href}</D:href>
<D:propstat>
<D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>"#
- ),
- PathType::File | PathType::SymlinkFile => format!(
- r#"<D:response>
+ ),
+ PathType::File | PathType::SymlinkFile => format!(
+ r#"<D:response>
<D:href>{href}</D:href>
<D:propstat>
<D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>"#,
- self.size
- ),
- }
- }
-
- pub fn base_name(&self) -> &str {
- self.name.split('/').next_back().unwrap_or_default()
- }
-
- pub fn sort_by_name(&self, other: &Self) -> Ordering {
- match self.path_type.cmp(&other.path_type) {
- Ordering::Equal => {
- alphanumeric_sort::compare_str(self.name.to_lowercase(), other.name.to_lowercase())
- }
- v => v,
- }
- }
-
- pub fn sort_by_mtime(&self, other: &Self) -> Ordering {
- match self.path_type.cmp(&other.path_type) {
- Ordering::Equal => self.mtime.cmp(&other.mtime),
- v => v,
- }
- }
-
- pub fn sort_by_size(&self, other: &Self) -> Ordering {
- match self.path_type.cmp(&other.path_type) {
- Ordering::Equal => self.size.cmp(&other.size),
- v => v,
- }
- }
+ self.size
+ ),
+ }
+ }
+
+ pub fn base_name(&self) -> &str {
+ self.name.split('/').next_back().unwrap_or_default()
+ }
+
+ pub fn sort_by_name(&self, other: &Self) -> Ordering {
+ match self.path_type.cmp(&other.path_type) {
+ Ordering::Equal => {
+ alphanumeric_sort::compare_str(self.name.to_lowercase(), other.name.to_lowercase())
+ }
+ v => v,
+ }
+ }
+
+ pub fn sort_by_mtime(&self, other: &Self) -> Ordering {
+ match self.path_type.cmp(&other.path_type) {
+ Ordering::Equal => self.mtime.cmp(&other.mtime),
+ v => v,
+ }
+ }
+
+ pub fn sort_by_size(&self, other: &Self) -> Ordering {
+ match self.path_type.cmp(&other.path_type) {
+ Ordering::Equal => self.size.cmp(&other.size),
+ v => v,
+ }
+ }
}
#[derive(Debug, Serialize, Eq, PartialEq)]
enum PathType {
- Dir,
- SymlinkDir,
- File,
- SymlinkFile,
+ Dir,
+ SymlinkDir,
+ File,
+ SymlinkFile,
}
impl Ord for PathType {
- fn cmp(&self, other: &Self) -> Ordering {
- let to_value = |t: &Self| -> u8 {
- if matches!(t, Self::Dir | Self::SymlinkDir) {
- 0
- } else {
- 1
- }
- };
- to_value(self).cmp(&to_value(other))
- }
+ fn cmp(&self, other: &Self) -> Ordering {
+ let to_value = |t: &Self| -> u8 {
+ if matches!(t, Self::Dir | Self::SymlinkDir) {
+ 0
+ } else {
+ 1
+ }
+ };
+ to_value(self).cmp(&to_value(other))
+ }
}
impl PartialOrd for PathType {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- Some(self.cmp(other))
- }
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
}
fn to_timestamp(time: &SystemTime) -> u64 {
- time.duration_since(SystemTime::UNIX_EPOCH)
- .unwrap_or_default()
- .as_millis() as u64
+ time.duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_millis() as u64
}
fn normalize_path<P: AsRef<Path>>(path: P) -> String {
- let path = path.as_ref().to_str().unwrap_or_default();
- if cfg!(windows) {
- path.replace('\\', "/")
- } else {
- path.to_string()
- }
+ let path = path.as_ref().to_str().unwrap_or_default();
+ if cfg!(windows) {
+ path.replace('\\', "/")
+ } else {
+ path.to_string()
+ }
}
async fn ensure_path_parent(path: &Path) -> Result<()> {
- if let Some(parent) = path.parent() {
- if fs::symlink_metadata(parent).await.is_err() {
- fs::create_dir_all(&parent).await?;
- }
- }
- Ok(())
+ if let Some(parent) = path.parent() {
+ if fs::symlink_metadata(parent).await.is_err() {
+ fs::create_dir_all(&parent).await?;
+ }
+ }
+ Ok(())
}
fn add_cors(res: &mut Response) {
- res.headers_mut()
- .typed_insert(AccessControlAllowOrigin::ANY);
- res.headers_mut()
- .typed_insert(AccessControlAllowCredentials);
- res.headers_mut().insert(
- "Access-Control-Allow-Methods",
- HeaderValue::from_static("*"),
- );
- res.headers_mut().insert(
- "Access-Control-Allow-Headers",
- HeaderValue::from_static("Authorization,*"),
- );
- res.headers_mut().insert(
- "Access-Control-Expose-Headers",
- HeaderValue::from_static("Authorization,*"),
- );
+ res.headers_mut()
+ .typed_insert(AccessControlAllowOrigin::ANY);
+ res.headers_mut()
+ .typed_insert(AccessControlAllowCredentials);
+ res.headers_mut().insert(
+ "Access-Control-Allow-Methods",
+ HeaderValue::from_static("*"),
+ );
+ res.headers_mut().insert(
+ "Access-Control-Allow-Headers",
+ HeaderValue::from_static("Authorization,*"),
+ );
+ res.headers_mut().insert(
+ "Access-Control-Expose-Headers",
+ HeaderValue::from_static("Authorization,*"),
+ );
}
fn res_multistatus(res: &mut Response, content: &str) {
- *res.status_mut() = StatusCode::MULTI_STATUS;
- res.headers_mut().insert(
- "content-type",
- HeaderValue::from_static("application/xml; charset=utf-8"),
- );
- *res.body_mut() = body_full(format!(
- r#"<?xml version="1.0" encoding="utf-8" ?>
+ *res.status_mut() = StatusCode::MULTI_STATUS;
+ res.headers_mut().insert(
+ "content-type",
+ HeaderValue::from_static("application/xml; charset=utf-8"),
+ );
+ *res.body_mut() = body_full(format!(
+ r#"<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
{content}
</D:multistatus>"#,
- ));
+ ));
}
async fn zip_dir<W: AsyncWrite + Unpin>(
- writer: &mut W,
- dir: &Path,
- access_paths: AccessPaths,
- hidden: &[String],
- compression: Compression,
- follow_symlinks: bool,
- serve_path: PathBuf,
- running: Arc<AtomicBool>,
+ writer: &mut W,
+ dir: &Path,
+ access_paths: AccessPaths,
+ hidden: &[String],
+ compression: Compression,
+ follow_symlinks: bool,
+ serve_path: PathBuf,
+ running: Arc<AtomicBool>,
) -> Result<()> {
- let mut writer = ZipFileWriter::with_tokio(writer);
- let hidden = Arc::new(hidden.to_vec());
- let zip_paths = tokio::task::spawn(collect_dir_entries(
- access_paths,
- running,
- dir.to_path_buf(),
- hidden,
- follow_symlinks,
- serve_path,
- move |x| x.path().symlink_metadata().is_ok() && x.file_type().is_file(),
- ))
- .await?;
- for zip_path in zip_paths.into_iter() {
- let filename = match zip_path
- .strip_prefix(dir)
- .ok()
- .and_then(|v| v.to_str())
- .map(|v| v.replace(MAIN_SEPARATOR, "/"))
- {
- Some(v) => v,
- None => continue,
- };
- let (datetime, mode) = get_file_mtime_and_mode(&zip_path).await?;
- let builder = ZipEntryBuilder::new(filename.into(), compression)
- .unix_permissions(mode)
- .last_modification_date(ZipDateTime::from_chrono(&datetime));
- let mut file = File::open(&zip_path).await?;
- let mut file_writer = writer.write_entry_stream(builder).await?.compat_write();
- io::copy(&mut file, &mut file_writer).await?;
- file_writer.into_inner().close().await?;
- }
- writer.close().await?;
- Ok(())
+ let mut writer = ZipFileWriter::with_tokio(writer);
+ let hidden = Arc::new(hidden.to_vec());
+ let zip_paths = tokio::task::spawn(collect_dir_entries(
+ access_paths,
+ running,
+ dir.to_path_buf(),
+ hidden,
+ follow_symlinks,
+ serve_path,
+ move |x| x.path().symlink_metadata().is_ok() && x.file_type().is_file(),
+ ))
+ .await?;
+ for zip_path in zip_paths.into_iter() {
+ let filename = match zip_path
+ .strip_prefix(dir)
+ .ok()
+ .and_then(|v| v.to_str())
+ .map(|v| v.replace(MAIN_SEPARATOR, "/"))
+ {
+ Some(v) => v,
+ None => continue,
+ };
+ let (datetime, mode) = get_file_mtime_and_mode(&zip_path).await?;
+ let builder = ZipEntryBuilder::new(filename.into(), compression)
+ .unix_permissions(mode)
+ .last_modification_date(ZipDateTime::from_chrono(&datetime));
+ let mut file = File::open(&zip_path).await?;
+ let mut file_writer = writer.write_entry_stream(builder).await?.compat_write();
+ io::copy(&mut file, &mut file_writer).await?;
+ file_writer.into_inner().close().await?;
+ }
+ writer.close().await?;
+ Ok(())
}
fn extract_cache_headers(meta: &Metadata) -> Option<(ETag, LastModified)> {
- let mtime = meta.modified().ok().or_else(|| meta.created().ok())?;
- let timestamp = to_timestamp(&mtime);
- let size = meta.len();
- let etag = format!(r#""{timestamp}-{size}""#).parse::<ETag>().ok()?;
- let last_modified = LastModified::from(mtime);
- Some((etag, last_modified))
+ let mtime = meta.modified().ok().or_else(|| meta.created().ok())?;
+ let timestamp = to_timestamp(&mtime);
+ let size = meta.len();
+ let etag = format!(r#""{timestamp}-{size}""#).parse::<ETag>().ok()?;
+ let last_modified = LastModified::from(mtime);
+ Some((etag, last_modified))
}
fn status_forbid(res: &mut Response) {
- *res.status_mut() = StatusCode::FORBIDDEN;
- *res.body_mut() = body_full("Forbidden");
+ *res.status_mut() = StatusCode::FORBIDDEN;
+ *res.body_mut() = body_full("Forbidden");
}
fn status_not_found(res: &mut Response) {
- *res.status_mut() = StatusCode::NOT_FOUND;
- *res.body_mut() = body_full("Not Found");
+ *res.status_mut() = StatusCode::NOT_FOUND;
+ *res.body_mut() = body_full("Not Found");
}
fn status_no_content(res: &mut Response) {
- *res.status_mut() = StatusCode::NO_CONTENT;
+ *res.status_mut() = StatusCode::NO_CONTENT;
}
fn status_bad_request(res: &mut Response, body: &str) {
- *res.status_mut() = StatusCode::BAD_REQUEST;
- if !body.is_empty() {
- *res.body_mut() = body_full(body.to_string());
- }
+ *res.status_mut() = StatusCode::BAD_REQUEST;
+ if !body.is_empty() {
+ *res.body_mut() = body_full(body.to_string());
+ }
}
fn set_content_disposition(res: &mut Response, inline: bool, filename: &str) -> Result<()> {
- let kind = if inline { "inline" } else { "attachment" };
- let filename: String = filename
- .chars()
- .map(|ch| {
- if ch.is_ascii_control() && ch != '\t' {
- ' '
- } else {
- ch
- }
- })
- .collect();
- let value = if filename.is_ascii() {
- HeaderValue::from_str(&format!("{kind}; filename=\"{}\"", filename,))?
- } else {
- HeaderValue::from_str(&format!(
- "{kind}; filename=\"{}\"; filename*=UTF-8''{}",
- filename,
- encode_uri(&filename),
- ))?
- };
- res.headers_mut().insert(CONTENT_DISPOSITION, value);
- Ok(())
+ let kind = if inline { "inline" } else { "attachment" };
+ let filename: String = filename
+ .chars()
+ .map(|ch| {
+ if ch.is_ascii_control() && ch != '\t' {
+ ' '
+ } else {
+ ch
+ }
+ })
+ .collect();
+ let value = if filename.is_ascii() {
+ HeaderValue::from_str(&format!("{kind}; filename=\"{}\"", filename,))?
+ } else {
+ HeaderValue::from_str(&format!(
+ "{kind}; filename=\"{}\"; filename*=UTF-8''{}",
+ filename,
+ encode_uri(&filename),
+ ))?
+ };
+ res.headers_mut().insert(CONTENT_DISPOSITION, value);
+ Ok(())
}
fn is_hidden(hidden: &[String], file_name: &str, is_dir: bool) -> bool {
- hidden.iter().any(|v| {
- if is_dir {
- if let Some(x) = v.strip_suffix('/') {
- return glob(x, file_name);
- }
- }
- glob(v, file_name)
- })
+ hidden.iter().any(|v| {
+ if is_dir {
+ if let Some(x) = v.strip_suffix('/') {
+ return glob(x, file_name);
+ }
+ }
+ glob(v, file_name)
+ })
}
fn set_webdav_headers(res: &mut Response) {
- res.headers_mut().insert(
- "Allow",
- 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"));
+ res.headers_mut().insert(
+ "Allow",
+ 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> {
- let mut buffer: Vec<u8> = vec![];
- fs::File::open(path)
- .await?
- .take(1024)
- .read_to_end(&mut buffer)
- .await?;
- let mime = mime_guess::from_path(path).first();
- let is_text = content_inspector::inspect(&buffer).is_text();
- let content_type = if is_text {
- let mut detector = chardetng::EncodingDetector::new();
- detector.feed(&buffer, buffer.len() < 1024);
- let (enc, confident) = detector.guess_assess(None, true);
- let charset = if confident {
- format!("; charset={}", enc.name())
- } else {
- "".into()
- };
- match mime {
- Some(m) => format!("{m}{charset}"),
- None => format!("text/plain{charset}"),
- }
- } else {
- match mime {
- Some(m) => m.to_string(),
- None => "application/octet-stream".into(),
- }
- };
- Ok(content_type)
+ let mut buffer: Vec<u8> = vec![];
+ fs::File::open(path)
+ .await?
+ .take(1024)
+ .read_to_end(&mut buffer)
+ .await?;
+ let mime = mime_guess::from_path(path).first();
+ let is_text = content_inspector::inspect(&buffer).is_text();
+ let content_type = if is_text {
+ let mut detector = chardetng::EncodingDetector::new();
+ detector.feed(&buffer, buffer.len() < 1024);
+ let (enc, confident) = detector.guess_assess(None, true);
+ let charset = if confident {
+ format!("; charset={}", enc.name())
+ } else {
+ "".into()
+ };
+ match mime {
+ Some(m) => format!("{m}{charset}"),
+ None => format!("text/plain{charset}"),
+ }
+ } else {
+ match mime {
+ Some(m) => m.to_string(),
+ None => "application/octet-stream".into(),
+ }
+ };
+ Ok(content_type)
}
fn parse_upload_offset(headers: &HeaderMap<HeaderValue>, size: u64) -> Result<Option<u64>> {
- let value = match headers.get("x-update-range") {
- Some(v) => v,
- None => return Ok(None),
- };
- let err = || anyhow!("Invalid X-Update-Range Header");
- let value = value.to_str().map_err(|_| err())?;
- if value == "append" {
- return Ok(Some(size));
- }
- // use the first range
- let ranges = parse_range(value, size).ok_or_else(err)?;
- let (start, _) = ranges.first().ok_or_else(err)?;
- Ok(Some(*start))
+ let value = match headers.get("x-update-range") {
+ Some(v) => v,
+ None => return Ok(None),
+ };
+ let err = || anyhow!("Invalid X-Update-Range Header");
+ let value = value.to_str().map_err(|_| err())?;
+ if value == "append" {
+ return Ok(Some(size));
+ }
+ // use the first range
+ let ranges = parse_range(value, size).ok_or_else(err)?;
+ let (start, _) = ranges.first().ok_or_else(err)?;
+ Ok(Some(*start))
}
async fn sha256_file(path: &Path) -> Result<String> {
- let mut file = fs::File::open(path).await?;
- let mut hasher = Sha256::new();
- let mut buffer = [0u8; 8192];
-
- loop {
- let bytes_read = file.read(&mut buffer).await?;
- if bytes_read == 0 {
- break;
- }
- hasher.update(&buffer[..bytes_read]);
- }
-
- let result = hasher.finalize();
- Ok(format!("{:x}", result))
+ let mut file = fs::File::open(path).await?;
+ let mut hasher = Sha256::new();
+ let mut buffer = [0u8; 8192];
+
+ loop {
+ let bytes_read = file.read(&mut buffer).await?;
+ if bytes_read == 0 {
+ break;
+ }
+ hasher.update(&buffer[..bytes_read]);
+ }
+
+ 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()
+ query_params
+ .get(name)
+ .map(|v| v.is_empty())
+ .unwrap_or_default()
}
async fn collect_dir_entries<F>(
- access_paths: AccessPaths,
- running: Arc<AtomicBool>,
- path: PathBuf,
- hidden: Arc<Vec<String>>,
- follow_symlinks: bool,
- serve_path: PathBuf,
- include_entry: F,
+ access_paths: AccessPaths,
+ running: Arc<AtomicBool>,
+ path: PathBuf,
+ hidden: Arc<Vec<String>>,
+ follow_symlinks: bool,
+ serve_path: PathBuf,
+ include_entry: F,
) -> Vec<PathBuf>
where
- F: Fn(&DirEntry) -> bool,
+ F: Fn(&DirEntry) -> bool,
{
- let mut paths: Vec<PathBuf> = vec![];
- for dir in access_paths.entry_paths(&path) {
- let mut it = WalkDir::new(&dir).follow_links(true).into_iter();
- it.next();
- while let Some(Ok(entry)) = it.next() {
- if !running.load(atomic::Ordering::SeqCst) {
- break;
- }
- let entry_path = entry.path();
- let base_name = get_file_name(entry_path);
- let is_dir = entry.file_type().is_dir();
- if is_hidden(&hidden, base_name, is_dir) {
- if is_dir {
- it.skip_current_dir();
- }
- continue;
- }
-
- if !follow_symlinks
- && !fs::canonicalize(entry_path)
- .await
- .ok()
- .map(|v| v.starts_with(&serve_path))
- .unwrap_or_default()
- {
- // We walked outside the server's root. This could only have
- // happened if we followed a symlink, and hence we only allow it
- // if allow_symlink is enabled, otherwise we skip this entry.
- if is_dir {
- it.skip_current_dir();
- }
- continue;
- }
- if !include_entry(&entry) {
- continue;
- }
- paths.push(entry_path.to_path_buf());
- }
- }
- paths
+ let mut paths: Vec<PathBuf> = vec![];
+ for dir in access_paths.entry_paths(&path) {
+ let mut it = WalkDir::new(&dir).follow_links(true).into_iter();
+ it.next();
+ while let Some(Ok(entry)) = it.next() {
+ if !running.load(atomic::Ordering::SeqCst) {
+ break;
+ }
+ let entry_path = entry.path();
+ let base_name = get_file_name(entry_path);
+ let is_dir = entry.file_type().is_dir();
+ if is_hidden(&hidden, base_name, is_dir) {
+ if is_dir {
+ it.skip_current_dir();
+ }
+ continue;
+ }
+
+ if !follow_symlinks
+ && !fs::canonicalize(entry_path)
+ .await
+ .ok()
+ .map(|v| v.starts_with(&serve_path))
+ .unwrap_or_default()
+ {
+ // We walked outside the server's root. This could only have
+ // happened if we followed a symlink, and hence we only allow it
+ // if allow_symlink is enabled, otherwise we skip this entry.
+ if is_dir {
+ it.skip_current_dir();
+ }
+ continue;
+ }
+ if !include_entry(&entry) {
+ continue;
+ }
+ paths.push(entry_path.to_path_buf());
+ }
+ }
+ paths
}