]> OzVa Git service - ozva-cloud/commitdiff
Added drag drop and styling changes main
authorMax Value <greenwoodw50@gmail.com>
Tue, 29 Jul 2025 16:58:34 +0000 (17:58 +0100)
committerMax Value <greenwoodw50@gmail.com>
Tue, 29 Jul 2025 16:58:34 +0000 (17:58 +0100)
.github/ISSUE_TEMPLATE/bug_report.md [deleted file]
.github/ISSUE_TEMPLATE/feature_requst.md [deleted file]
.github/workflows/ci.yaml [deleted file]
.github/workflows/release.yaml [deleted file]
assets/favicon.ico
assets/index.css
assets/index.html
assets/index.js
src/server.rs

diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644 (file)
index 0fe37df..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve 
----
-
-**Problem**
-
-<!-- Provide a clear and concise description of the bug you're experiencing. What did you expect to happen, and what actually happened? -->
-
-**Configuration**
-
-<!-- Please specify the Dufs command-line arguments or configuration used. -->
-
-<!-- If the issue is related to authentication/permissions, include auth configurations while concealing sensitive information (e.g., passwords). -->
-
-**Log**
-
-<!-- Attach relevant log outputs that can help diagnose the issue. -->
-
-**Screenshots/Media**
-
-<!-- If applicable, add screenshots or videos that help illustrate the issue, especially for WebUI problems. -->
-
-**Environment Information**
- - Dufs version:
- - Browser/Webdav info:
- - OS info:
- - Proxy server (if any):  <!-- e.g. nginx, cloudflare -->
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_requst.md b/.github/ISSUE_TEMPLATE/feature_requst.md
deleted file mode 100644 (file)
index f8735b9..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
----
-name: Feature Request
-about: If you have any interesting advice, you can tell us.
----
-
-## Specific Demand
-
-<!--
-What feature do you need, please describe it in detail.
--->
-
-## Implement Suggestion
-
-<!--
-If you have any suggestion for complete this feature, you can tell us.
--->
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
deleted file mode 100644 (file)
index 9cbf67a..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-name: CI
-
-on:
-  pull_request:
-    branches:
-    - '*'
-  push:
-    branches:
-    - main
-
-defaults:
-  run:
-    shell: bash
-
-jobs:
-  all:
-    name: All
-
-    strategy:
-      matrix:
-        os:
-        - ubuntu-latest
-        - macos-latest
-        - windows-latest
-
-    runs-on: ${{matrix.os}}
-
-    env:
-      RUSTFLAGS: --deny warnings
-
-    steps:
-    - uses: actions/checkout@v4
-
-    - name: Install Rust Toolchain Components
-      uses: dtolnay/rust-toolchain@stable
-
-    - uses: Swatinem/rust-cache@v2
-
-    - name: Test
-      run: cargo test --all
-
-    - name: Clippy
-      run: cargo clippy --all --all-targets
-
-    - name: Format
-      run: cargo fmt --all --check
\ No newline at end of file
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
deleted file mode 100644 (file)
index e91ec12..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-name: Release
-
-on:
-  push:
-    tags:
-    - v[0-9]+.[0-9]+.[0-9]+*
-
-jobs:
-  release:
-    name: Publish to Github Releases
-    permissions:
-      contents: write
-    outputs:
-      rc: ${{ steps.check-tag.outputs.rc }}
-
-    strategy:
-      matrix:
-        include:
-        - target: aarch64-unknown-linux-musl
-          os: ubuntu-latest
-          use-cross: true
-          cargo-flags: ""
-        - target: aarch64-apple-darwin
-          os: macos-latest
-          use-cross: true
-          cargo-flags: ""
-        - target: aarch64-pc-windows-msvc
-          os: windows-latest
-          use-cross: true
-          cargo-flags: ""
-        - target: x86_64-apple-darwin
-          os: macos-latest
-          cargo-flags: ""
-        - target: x86_64-pc-windows-msvc
-          os: windows-latest
-          cargo-flags: ""
-        - target: x86_64-unknown-linux-musl
-          os: ubuntu-latest
-          use-cross: true
-          cargo-flags: ""
-        - target: i686-unknown-linux-musl
-          os: ubuntu-latest
-          use-cross: true
-          cargo-flags: ""
-        - target: i686-pc-windows-msvc
-          os: windows-latest
-          use-cross: true
-          cargo-flags: ""
-        - target: armv7-unknown-linux-musleabihf
-          os: ubuntu-latest
-          use-cross: true
-          cargo-flags: ""
-        - target: arm-unknown-linux-musleabihf
-          os: ubuntu-latest
-          use-cross: true
-          cargo-flags: ""
-
-    runs-on: ${{matrix.os}}
-    env:
-      BUILD_CMD: cargo
-
-    steps:
-    - uses: actions/checkout@v4
-
-    - name: Check Tag
-      id: check-tag
-      shell: bash
-      run: |
-        ver=${GITHUB_REF##*/}
-        echo "version=$ver" >> $GITHUB_OUTPUT
-        if [[ "$ver" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
-          echo "rc=false" >> $GITHUB_OUTPUT
-        else
-          echo "rc=true" >> $GITHUB_OUTPUT
-        fi
-
-
-    - name: Install Rust Toolchain Components
-      uses: dtolnay/rust-toolchain@stable
-      with:
-        targets: ${{ matrix.target }}
-
-    - name: Install cross
-      if: matrix.use-cross
-      uses: taiki-e/install-action@v2
-      with:
-        tool: cross
-
-    - name: Overwrite build command env variable
-      if: matrix.use-cross
-      shell: bash
-      run: echo "BUILD_CMD=cross" >> $GITHUB_ENV
-  
-    - name: Show Version Information (Rust, cargo, GCC)
-      shell: bash
-      run: |
-        gcc --version || true
-        rustup -V
-        rustup toolchain list
-        rustup default
-        cargo -V
-        rustc -V
-      
-    - name: Build
-      shell: bash
-      run: $BUILD_CMD build --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
-
-    - name: Build Archive
-      shell: bash
-      id: package
-      env:
-        target: ${{ matrix.target }}
-        version:  ${{ steps.check-tag.outputs.version }}
-      run: |
-        set -euxo pipefail
-
-        bin=${GITHUB_REPOSITORY##*/}
-        dist_dir=`pwd`/dist
-        name=$bin-$version-$target
-        executable=target/$target/release/$bin
-
-        if [[ "$RUNNER_OS" == "Windows" ]]; then
-          executable=$executable.exe
-        fi
-
-        mkdir $dist_dir
-        cp $executable $dist_dir
-        cd $dist_dir
-
-        if [[ "$RUNNER_OS" == "Windows" ]]; then
-            archive=$dist_dir/$name.zip
-            7z a $archive *
-            echo "archive=dist/$name.zip" >> $GITHUB_OUTPUT
-        else
-            archive=$dist_dir/$name.tar.gz
-            tar -czf $archive *
-            echo "archive=dist/$name.tar.gz" >> $GITHUB_OUTPUT
-        fi
-
-    - name: Publish Archive
-      uses: softprops/action-gh-release@v2
-      if: ${{ startsWith(github.ref, 'refs/tags/') }}
-      with:
-        draft: false
-        files: ${{ steps.package.outputs.archive }}
-        prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
-
-  docker:
-    name: Publish to Docker Hub
-    if: startsWith(github.ref, 'refs/tags/')
-    runs-on: ubuntu-latest
-    needs: release
-    steps:
-      - name: Set up QEMU
-        uses: docker/setup-qemu-action@v3
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
-      - name: Login to DockerHub
-        uses: docker/login-action@v3
-        with:
-          username: ${{ github.repository_owner        }}
-          password: ${{ secrets.DOCKERHUB_TOKEN }}
-      - name: Build and push
-        uses: docker/build-push-action@v5
-        with:
-          file: Dockerfile-release
-          build-args: |
-            REPO=${{ github.repository }}
-            VER=${{ github.ref_name }}
-          platforms: |
-            linux/amd64
-            linux/arm64
-            linux/386
-            linux/arm/v7
-          push: ${{ needs.release.outputs.rc == 'false' }}
-          tags: ${{ github.repository }}:latest, ${{ github.repository }}:${{ github.ref_name }}
-
-  publish-crate:
-    name: Publish to crates.io
-    if: ${{ needs.release.outputs.rc == 'false' }}
-    runs-on: ubuntu-latest
-    needs: release
-    steps:
-      - uses: actions/checkout@v4
-
-      - uses: dtolnay/rust-toolchain@stable
-
-      - name: Publish
-        env:
-          CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }}
-        run: cargo publish
\ No newline at end of file
index c40c5570dbe8aea77737957ef5f7ffa956c00e2d..8578ca365a8f3240abf2782d8e6f278f53d9f6d8 100644 (file)
Binary files a/assets/favicon.ico and b/assets/favicon.ico differ
index 42d8e01ef7d1398f5bbd6dbc613425cc83f8ee56..d2bb8d75d97fad3ccf43af1440a968df8cbb28ab 100644 (file)
@@ -1,5 +1,10 @@
+:root {
+  --lm-color: #004088;
+  --dm-color: #004088;
+}
+
 html {
 html {
-  font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
+  font-family: Helvetica, Arial, sans-serif;
   line-height: 1.5;
   color: #24292e;
 }
   line-height: 1.5;
   color: #24292e;
 }
@@ -182,14 +187,14 @@ body {
 }
 
 .path a {
 }
 
 .path a {
-  color: #0366d6;
+  color: var(--lm-color);
   text-overflow: ellipsis;
   white-space: nowrap;
   overflow: hidden;
   display: block;
   text-decoration: none;
   text-overflow: ellipsis;
   white-space: nowrap;
   overflow: hidden;
   display: block;
   text-decoration: none;
-  max-width: calc(100vw - 375px);
-  min-width: 170px;
+  max-width: calc(100vw - 375px - 200px);
+  min-width: calc(170px - 200px);
 }
 
 .path a:hover {
 }
 
 .path a:hover {
@@ -250,6 +255,54 @@ body {
   cursor: pointer;
 }
 
   cursor: pointer;
 }
 
+.cell-name .drag-div {
+  display: inline-block;
+  vertical-align: top;
+}
+
+.internaldrop {
+  width: 100px;
+  display: inline-block;
+  vertical-align: top;
+  text-align: center;
+
+  color: rgba(3, 47, 98, 0.5);
+  border: 2px solid rgba(3, 47, 98, 0.5);
+  border-radius: 4px;
+
+  background-color: rgba(3, 47, 98, 0);
+  opacity: 0;
+  transition: opacity 0.5s, background-color 0.5s;
+}
+
+.internaldrop.dragging {
+  opacity: 1;
+}
+
+.internaldrop.dragging.dragover {
+  background-color: rgba(3, 47, 98, 0.2);
+}
+
+#filedrop {
+  width: 100%;
+  height: 50px;
+  line-height: 50px;
+  margin-bottom: 10px;
+
+  text-align: center;
+
+  color: rgba(3, 47, 98, 0.5);
+  border: 2px solid rgba(3, 47, 98, 0.5);
+  border-radius: 4px;
+
+  background-color: rgba(3, 47, 98, 0);
+  transition: background-color 0.5s;
+}
+
+#filedrop.dragoverfile {
+  background-color: rgba(3, 47, 98, 0.2);
+}
+
 @media (min-width: 768px) {
   .path a {
     min-width: 400px;
 @media (min-width: 768px) {
   .path a {
     min-width: 400px;
@@ -293,7 +346,7 @@ body {
   }
 
   .path a {
   }
 
   .path a {
-    color: #3191ff;
+    color: var(--dm-color);
   }
 
   .paths-table tbody tr:hover {
   }
 
   .paths-table tbody tr:hover {
@@ -304,4 +357,4 @@ body {
     background: black;
     color: white;
   }
     background: black;
     color: white;
   }
-}
\ No newline at end of file
+}
index d814aa09fbd1f0e52d1ac14072394bed693cf8c0..de4e464d1839e4ede786f038a55fc73bb18a300d 100644 (file)
   <div class="main">
     <div class="index-page hidden">
       <div class="empty-folder hidden"></div>
   <div class="main">
     <div class="index-page hidden">
       <div class="empty-folder hidden"></div>
+      <div class="dropzone" id="filedrop">
+        Drop files here to upload
+      </div>
       <table class="uploaders-table hidden">
         <thead>
           <tr>
       <table class="uploaders-table hidden">
         <thead>
           <tr>
   <script src="__ASSETS_PREFIX__index.js"></script>
 </body>
 
   <script src="__ASSETS_PREFIX__index.js"></script>
 </body>
 
-</html>
\ No newline at end of file
+</html>
index b87e354bfcd9763e5e19b6cb0837af79a6ddd374..d848c1df9d6f3622ec9022c847e332fc8f61587e 100644 (file)
 /**
 /**
- * @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;
 
 /**
 var DATA;
 
 /**
- * @type {string}
- */
+* @type {string}
+*/
 var DIR_EMPTY_NOTE;
 
 /**
 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 = [
 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 = {
 ];
 
 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();
 
 /**
 const failUploaders = new Map();
 
 /**
- * @type Element
- */
+* @type Element
+*/
 let $pathsTable;
 /**
 let $pathsTable;
 /**
- * @type Element
- */
+* @type Element
+*/
 let $pathsTableHead;
 /**
 let $pathsTableHead;
 /**
- * @type Element
- */
+* @type Element
+*/
 let $pathsTableBody;
 /**
 let $pathsTableBody;
 /**
- * @type Element
- */
+* @type Element
+*/
 let $uploadersTable;
 /**
 let $uploadersTable;
 /**
- * @type Element
- */
+* @type Element
+*/
 let $emptyFolder;
 /**
 let $emptyFolder;
 /**
- * @type Element
- */
+* @type Element
+*/
 let $editor;
 /**
 let $editor;
 /**
- * @type Element
- */
+* @type Element
+*/
 let $loginBtn;
 /**
 let $loginBtn;
 /**
- * @type Element
- */
+* @type Element
+*/
 let $logoutBtn;
 /**
 let $logoutBtn;
 /**
- * @type Element
- */
+* @type Element
+*/
 let $userName;
 
 // Produce table when window loads
 window.addEventListener("DOMContentLoaded", async () => {
 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 {
 }
 
 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.globalIdx = 0;
@@ -284,674 +404,714 @@ Uploader.runnings = 0;
 Uploader.auth = false;
 
 /**
 Uploader.auth = false;
 
 /**
- * @type Uploader[]
- */
+* @type Uploader[]
+*/
 Uploader.queues = [];
 
 
 Uploader.runQueue = async () => {
 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) {
 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() {
 }
 
 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() {
 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() {
 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) {
 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}">
 <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() {
 </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() {
 }
 
 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() {
 }
 
 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() {
 }
 
 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() {
 }
 
 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() {
 }
 
 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() {
 }
 
 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) {
 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) {
 }
 
 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) {
 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) {
 }
 
 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() {
 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() {
 }
 
 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() {
 }
 
 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) {
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 
 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() {
 }
 
 function baseUrl() {
-  return location.href.split(/[?#]/)[0];
+       return location.href.split(/[?#]/)[0];
 }
 
 function baseName(url) {
 }
 
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 function padZero(value, size) {
-  return ("0".repeat(size) + value).slice(-1 * size);
+       return ("0".repeat(size) + value).slice(-1 * size);
 }
 
 function formatDirSize(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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 // 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);
 }
 }
index e78f0d5abcfbf305042653e08c83d3eb80a965a0..f376dfac3691f28f7e990aa6f916faf4635a4b9b 100644 (file)
@@ -3,8 +3,8 @@
 use crate::auth::{www_authenticate, AccessPaths, AccessPerm};
 use crate::http_utils::{body_full, IncomingStream, LengthLimitedStream};
 use crate::utils::{
 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 crate::Args;
 
@@ -15,19 +15,19 @@ use bytes::Bytes;
 use chrono::{LocalResult, TimeZone, Utc};
 use futures_util::{pin_mut, TryStreamExt};
 use headers::{
 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::{
 };
 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};
 };
 use serde::Serialize;
 use sha2::{Digest, Sha256};
@@ -62,1061 +62,1061 @@ const INDEX_NAME: &str = "index.html";
 const BUF_SIZE: usize = 65536;
 const EDITABLE_TEXT_MAX_SIZE: u64 = 4194304; // 4M
 const RESUMABLE_UPLOAD_MIN_SIZE: u64 = 20971520; // 20M
 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 {
 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 {
 }
 
 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>"#
 <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:href>{req_path}</D:href>
 <D:propstat>
 <D:prop>
@@ -1124,358 +1124,365 @@ impl Server {
 <D:status>HTTP/1.1 403 Forbidden</D:status>
 </D:propstat>
 </D:response>"#
 <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 {
 }
 
 #[derive(Debug, Serialize, PartialEq)]
 enum DataKind {
-    Index,
-    Edit,
-    View,
+       Index,
+       Edit,
+       View,
 }
 
 #[derive(Debug, Serialize)]
 struct IndexData {
 }
 
 #[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 {
 }
 
 #[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 {
 }
 
 #[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 {
 }
 
 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:href>{href}</D:href>
 <D:propstat>
 <D:prop>
@@ -1486,9 +1493,9 @@ impl PathItem {
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>"#
 <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:href>{href}</D:href>
 <D:propstat>
 <D:prop>
@@ -1500,365 +1507,365 @@ impl PathItem {
 <D:status>HTTP/1.1 200 OK</D:status>
 </D:propstat>
 </D:response>"#,
 <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 {
 }
 
 #[derive(Debug, Serialize, Eq, PartialEq)]
 enum PathType {
-    Dir,
-    SymlinkDir,
-    File,
-    SymlinkFile,
+       Dir,
+       SymlinkDir,
+       File,
+       SymlinkFile,
 }
 
 impl Ord for PathType {
 }
 
 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 {
 }
 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 {
 }
 
 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 {
 }
 
 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<()> {
 }
 
 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) {
 }
 
 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) {
 }
 
 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>"#,
 <D:multistatus xmlns:D="DAV:">
 {content}
 </D:multistatus>"#,
-    ));
+       ));
 }
 
 async fn zip_dir<W: AsyncWrite + Unpin>(
 }
 
 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<()> {
 ) -> 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)> {
 }
 
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 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) {
 }
 
 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<()> {
 }
 
 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 {
 }
 
 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) {
 }
 
 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> {
 }
 
 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>> {
 }
 
 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> {
 }
 
 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 {
 }
 
 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>(
 }
 
 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
 ) -> 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
 }
 }