+++ /dev/null
-# Directories
-/.git/
-/.github/
-/target/
-/examples/
-/docs/
-/benches/
-/tmp/
-
-# Files
-.gitignore
-*.md
-LICENSE*
\ No newline at end of file
/target
-/.vscode
\ No newline at end of file
+/.vscode
+Cargo.lock
+*.min.js
+++ /dev/null
-# Changelog
-
-All notable changes to this project will be documented in this file.
-
-## [0.43.0] - 2024-11-04
-
-### Bug Fixes
-
-- Auth failed if password contains `:` ([#449](https://github.com/sigoden/dufs/issues/449))
-- Resolve speed bottleneck in 10G network ([#451](https://github.com/sigoden/dufs/issues/451))
-
-### Features
-
-- Webui displays subdirectory items ([#457](https://github.com/sigoden/dufs/issues/457))
-- Support binding abstract unix socket ([#468](https://github.com/sigoden/dufs/issues/468))
-- Provide healthcheck API ([#474](https://github.com/sigoden/dufs/issues/474))
-
-### Refactor
-
-- Do not show size for Dir ([#447](https://github.com/sigoden/dufs/issues/447))
-
-## [0.42.0] - 2024-09-01
-
-### Bug Fixes
-
-- Garbled characters caused by atob ([#422](https://github.com/sigoden/dufs/issues/422))
-- Webui unexpected save-btn when file is non-editable ([#429](https://github.com/sigoden/dufs/issues/429))
-- Login succeeded but popup `Forbidden` ([#437](https://github.com/sigoden/dufs/issues/437))
-
-### Features
-
-- Implements remaining http cache conditionalss ([#407](https://github.com/sigoden/dufs/issues/407))
-- Base64 index-data to avoid misencoding ([#421](https://github.com/sigoden/dufs/issues/421))
-- Webui support logout ([#439](https://github.com/sigoden/dufs/issues/439))
-
-### Refactor
-
-- No inline scripts in HTML ([#391](https://github.com/sigoden/dufs/issues/391))
-- Return 400 for propfind request when depth is neither 0 nor 1 ([#403](https://github.com/sigoden/dufs/issues/403))
-- Remove sabredav-partialupdate from DAV res header ([#415](https://github.com/sigoden/dufs/issues/415))
-- Date formatting in cache tests ([#428](https://github.com/sigoden/dufs/issues/428))
-- Some query params work as flag and must not accept a value ([#431](https://github.com/sigoden/dufs/issues/431))
-- Improve logout at asserts/index.js ([#440](https://github.com/sigoden/dufs/issues/440))
-- Make logout works on safari ([#442](https://github.com/sigoden/dufs/issues/442))
-
-## [0.41.0] - 2024-05-22
-
-### Bug Fixes
-
-- Timestamp format of getlastmodified in dav xml ([#366](https://github.com/sigoden/dufs/issues/366))
-- Strange issue that occurs only on Microsoft WebDAV ([#382](https://github.com/sigoden/dufs/issues/382))
-- Head div overlap main contents when wrap ([#386](https://github.com/sigoden/dufs/issues/386))
-
-### Features
-
-- Tls handshake timeout ([#368](https://github.com/sigoden/dufs/issues/368))
-- Add api to get the hash of a file ([#375](https://github.com/sigoden/dufs/issues/375))
-- Add log-file option ([#383](https://github.com/sigoden/dufs/issues/383))
-
-### Refactor
-
-- Digest_auth related tests ([#372](https://github.com/sigoden/dufs/issues/372))
-- Add fixed-width numerals to date and size on file list page ([#378](https://github.com/sigoden/dufs/issues/378))
-
-## [0.40.0] - 2024-02-13
-
-### Bug Fixes
-
-- Guard req and destination path ([#359](https://github.com/sigoden/dufs/issues/359))
-
-### Features
-
-- Revert supporting for forbidden permission ([#352](https://github.com/sigoden/dufs/issues/352))
-
-### Refactor
-
-- Do not try to bind ipv6 if no ipv6 ([#348](https://github.com/sigoden/dufs/issues/348))
-- Improve invalid auth ([#356](https://github.com/sigoden/dufs/issues/356))
-- Improve resolve_path and handle_assets, abandon guard_path ([#360](https://github.com/sigoden/dufs/issues/360))
-
-## [0.39.0] - 2024-01-11
-
-### Bug Fixes
-
-- Upload more than 100 files in directory ([#317](https://github.com/sigoden/dufs/issues/317))
-- Auth precedence ([#325](https://github.com/sigoden/dufs/issues/325))
-- Serve files with names containing newline char ([#328](https://github.com/sigoden/dufs/issues/328))
-- Corrupted zip when downloading large folders ([#337](https://github.com/sigoden/dufs/issues/337))
-
-### Features
-
-- Empty search `?q=` list all paths ([#311](https://github.com/sigoden/dufs/issues/311))
-- Add `--compress` option ([#319](https://github.com/sigoden/dufs/issues/319))
-- Upgrade to hyper 1.0 ([#321](https://github.com/sigoden/dufs/issues/321))
-- Auth supports forbidden permissions ([#329](https://github.com/sigoden/dufs/issues/329))
-- Supports resumable uploads ([#343](https://github.com/sigoden/dufs/issues/343))
-
-### Refactor
-
-- Change the format of www-authenticate ([#312](https://github.com/sigoden/dufs/issues/312))
-- Change the value name of `--config` ([#313](https://github.com/sigoden/dufs/issues/313))
-- Optimize http range parsing and handling ([#323](https://github.com/sigoden/dufs/issues/323))
-- Propfind with auth no need to list all ([#344](https://github.com/sigoden/dufs/issues/344))
-
-## [0.38.0] - 2023-11-28
-
-### Bug Fixes
-
-- Unable to start if config file omit bind/port fields ([#294](https://github.com/sigoden/dufs/issues/294))
-
-### Features
-
-- Password can contain `:` `@` `|` ([#297](https://github.com/sigoden/dufs/issues/297))
-- Deprecate the use of `|` to separate auth rules ([#298](https://github.com/sigoden/dufs/issues/298))
-- More flexible config values ([#299](https://github.com/sigoden/dufs/issues/299))
-- Ui supports view file ([#301](https://github.com/sigoden/dufs/issues/301))
-
-### Refactor
-
-- Take improvements from the edge browser ([#289](https://github.com/sigoden/dufs/issues/289))
-- Ui change the cursor for upload-btn to a pointer ([#291](https://github.com/sigoden/dufs/issues/291))
-- Ui improve uploading progress ([#296](https://github.com/sigoden/dufs/issues/296))
-
-## [0.37.1] - 2023-11-08
-
-### Bug Fixes
-
-- Use DUFS_CONFIG to specify the config file path ([#286](https://github.com/sigoden/dufs/issues/286)
-
-## [0.37.0] - 2023-11-08
-
-### Bug Fixes
-
-- Sort path ignore case ([#264](https://github.com/sigoden/dufs/issues/264))
-- Ui show user-name next to the user-icon ([#278](https://github.com/sigoden/dufs/issues/278))
-- Auto delete half-uploaded files ([#280](https://github.com/sigoden/dufs/issues/280))
-
-### Features
-
-- Deprecate `--auth-method`, as both options are available ([#279](https://github.com/sigoden/dufs/issues/279))
-- Support config file with `--config` option ([#281](https://github.com/sigoden/dufs/issues/281))
-- Support hashed password ([#283](https://github.com/sigoden/dufs/issues/283))
-
-### Refactor
-
-- Remove one clone on `assets_prefix` ([#270](https://github.com/sigoden/dufs/issues/270))
-- Optimize tests
-- Improve code quality ([#282](https://github.com/sigoden/dufs/issues/282))
-
-## [0.36.0] - 2023-08-24
-
-### Bug Fixes
-
-- Ui readonly if no write perm ([#258](https://github.com/sigoden/dufs/issues/258))
-
-### Testing
-
-- Remove dependency on native tls ([#255](https://github.com/sigoden/dufs/issues/255))
-
-## [0.35.0] - 2023-08-14
-
-### Bug Fixes
-
-- Search should ignore entry path ([#235](https://github.com/sigoden/dufs/issues/235))
-- Typo __ASSERTS_PREFIX__ ([#252](https://github.com/sigoden/dufs/issues/252))
-
-### Features
-
-- Sort by type first, then sort by name/mtime/size ([#241](https://github.com/sigoden/dufs/issues/241))
-
-## [0.34.2] - 2023-06-05
-
-### Bug Fixes
-
-- Ui refresh page after login ([#230](https://github.com/sigoden/dufs/issues/230))
-- Webdav only see public folder even logging in ([#231](https://github.com/sigoden/dufs/issues/231))
-
-## [0.34.1] - 2023-06-02
-
-### Bug Fixes
-
-- Auth logic ([#224](https://github.com/sigoden/dufs/issues/224))
-- Allow all cors headers and methods ([#225](https://github.com/sigoden/dufs/issues/225))
-
-### Refactor
-
-- Ui checkAuth ([#226](https://github.com/sigoden/dufs/issues/226))
-
-## [0.34.0] - 2023-06-01
-
-### Bug Fixes
-
-- URL-encoded filename when downloading in safari ([#203](https://github.com/sigoden/dufs/issues/203))
-- Ui path table show move action ([#219](https://github.com/sigoden/dufs/issues/219))
-- Ui set default max uploading to 1 ([#220](https://github.com/sigoden/dufs/issues/220))
-
-### Features
-
-- Webui editing support multiple encodings ([#197](https://github.com/sigoden/dufs/issues/197))
-- Add timestamp metadata to generated zip file ([#204](https://github.com/sigoden/dufs/issues/204))
-- Show precise file size with decimal ([#210](https://github.com/sigoden/dufs/issues/210))
-- [**breaking**] New auth ([#218](https://github.com/sigoden/dufs/issues/218))
-
-### Refactor
-
-- Cli positional rename root => SERVE_PATH([#215](https://github.com/sigoden/dufs/issues/215))
-
-## [0.33.0] - 2023-03-17
-
-### Bug Fixes
-
-- Cors allow-request-header add content-type ([#184](https://github.com/sigoden/dufs/issues/184))
-- Hidden don't works on some files ([#188](https://github.com/sigoden/dufs/issues/188))
-- Basic auth sometimes does not work ([#194](https://github.com/sigoden/dufs/issues/194))
-
-### Features
-
-- Guess plain text encoding then set content-type charset ([#186](https://github.com/sigoden/dufs/issues/186))
-
-### Refactor
-
-- Improve error handle ([#195](https://github.com/sigoden/dufs/issues/195))
-
-## [0.32.0] - 2023-02-22
-
-### Bug Fixes
-
-- Set the STOPSIGNAL to SIGINT for Dockerfile
-- Remove Method::Options auth check ([#168](https://github.com/sigoden/dufs/issues/168))
-- Clear search input also clear query ([#178](https://github.com/sigoden/dufs/issues/178))
-
-### Features
-
-- [**breaking**] Add option --allow-archive ([#152](https://github.com/sigoden/dufs/issues/152))
-- Use env var for args ([#170](https://github.com/sigoden/dufs/issues/170))
-- Hiding only directories instead of files ([#175](https://github.com/sigoden/dufs/issues/175))
-- API to search and list directories ([#177](https://github.com/sigoden/dufs/issues/177))
-- Support edit files ([#179](https://github.com/sigoden/dufs/issues/179))
-- Support new file ([#180](https://github.com/sigoden/dufs/issues/180))
-- Ui improves the login experience ([#182](https://github.com/sigoden/dufs/issues/182))
-
-## [0.31.0] - 2022-11-11
-
-### Bug Fixes
-
-- Auth not works with --path-prefix ([#138](https://github.com/sigoden/dufs/issues/138))
-- Don't search on empty query string ([#140](https://github.com/sigoden/dufs/issues/140))
-- Status code for MKCOL on existing resource ([#142](https://github.com/sigoden/dufs/issues/142))
-- Panic on PROPFIND // ([#144](https://github.com/sigoden/dufs/issues/144))
-
-### Features
-
-- Support unix sockets ([#145](https://github.com/sigoden/dufs/issues/145))
-
-## [0.30.0] - 2022-09-09
-
-### Bug Fixes
-
-- Hide path by ext name ([#126](https://github.com/sigoden/dufs/issues/126))
-
-### Features
-
-- Support sort by name, mtime, size ([#128](https://github.com/sigoden/dufs/issues/128))
-- Add --assets options to override assets ([#134](https://github.com/sigoden/dufs/issues/134))
-
-## [0.29.0] - 2022-08-03
-
-### Bug Fixes
-
-- Table row hover highlighting in dark mode ([#122](https://github.com/sigoden/dufs/issues/122))
-
-### Features
-
-- Support ecdsa tls cert ([#119](https://github.com/sigoden/dufs/issues/119))
-
-## [0.28.0] - 2022-08-01
-
-### Bug Fixes
-
-- File path contains special characters ([#114](https://github.com/sigoden/dufs/issues/114))
-
-### Features
-
-- Add table row hover ([#115](https://github.com/sigoden/dufs/issues/115))
-- Support customize http log format ([#116](https://github.com/sigoden/dufs/issues/116))
-
-## [0.27.0] - 2022-07-25
-
-### Features
-
-- Improve hidden to support glob ([#108](https://github.com/sigoden/dufs/issues/108))
-- Adjust digest auth timeout to 1day ([#110](https://github.com/sigoden/dufs/issues/110))
-
-## [0.26.0] - 2022-07-11
-
-### Bug Fixes
-
-- Cors headers ([#100](https://github.com/sigoden/dufs/issues/100))
-
-### Features
-
-- Make --path-prefix works on serving single file ([#102](https://github.com/sigoden/dufs/issues/102))
-
-## [0.25.0] - 2022-07-06
-
-### Features
-
-- Ui supports creating folder ([#91](https://github.com/sigoden/dufs/issues/91))
-- Ui supports move folder/file to new path ([#92](https://github.com/sigoden/dufs/issues/92))
-- Check permission on move/copy destination ([#93](https://github.com/sigoden/dufs/issues/93))
-- Add completions ([#97](https://github.com/sigoden/dufs/issues/97))
-- Limit the number of concurrent uploads ([#98](https://github.com/sigoden/dufs/issues/98))
-
-## [0.24.0] - 2022-07-02
-
-### Bug Fixes
-
-- Unexpected stack overflow when searching a lot ([#87](https://github.com/sigoden/dufs/issues/87))
-
-### Features
-
-- Allow search with --render-try-index ([#88](https://github.com/sigoden/dufs/issues/88))
-
-## [0.23.1] - 2022-06-30
-
-### Bug Fixes
-
-- Safari layout and compatibility ([#83](https://github.com/sigoden/dufs/issues/83))
-- Permissions of unzipped files ([#84](https://github.com/sigoden/dufs/issues/84))
-
-## [0.23.0] - 2022-06-29
-
-### Features
-
-- Use feature to conditional support tls ([#77](https://github.com/sigoden/dufs/issues/77))
-
-### Ci
-
-- Support more platforms ([#76](https://github.com/sigoden/dufs/issues/76))
-
-## [0.22.0] - 2022-06-26
-
-### Features
-
-- Support hiding folders with --hidden ([#73](https://github.com/sigoden/dufs/issues/73))
-
-## [0.21.0] - 2022-06-23
-
-### Bug Fixes
-
-- Escape name contains html escape code ([#65](https://github.com/sigoden/dufs/issues/65))
-
-### Features
-
-- Use custom logger with timestamp in rfc3339 ([#67](https://github.com/sigoden/dufs/issues/67))
-
-### Refactor
-
-- Split css/js from index.html ([#68](https://github.com/sigoden/dufs/issues/68))
-
-## [0.20.0] - 2022-06-20
-
-### Bug Fixes
-
-- DecodeURI searching string ([#61](https://github.com/sigoden/dufs/issues/61))
-
-### Features
-
-- Added basic auth ([#60](https://github.com/sigoden/dufs/issues/60))
-- Add option --allow-search ([#62](https://github.com/sigoden/dufs/issues/62))
-
-## [0.19.0] - 2022-06-19
-
-### Features
-
-- [**breaking**] Path level access control ([#52](https://github.com/sigoden/dufs/issues/52))
-- Serve single file ([#54](https://github.com/sigoden/dufs/issues/54))
-- Ui hidden root dirname ([#58](https://github.com/sigoden/dufs/issues/58))
-- Reactive webpage ([#51](https://github.com/sigoden/dufs/issues/51))
-- [**breaking**] Rename to dufs ([#59](https://github.com/sigoden/dufs/issues/59))
-
-### Refactor
-
-- [**breaking**] Rename --cors to --enable-cors ([#57](https://github.com/sigoden/dufs/issues/57))
-
-## [0.18.0] - 2022-06-18
-
-### Features
-
-- Add option --render-try-index ([#47](https://github.com/sigoden/dufs/issues/47))
-- Add slash to end of dir href
-
-## [0.17.1] - 2022-06-16
-
-### Bug Fixes
-
-- Range request ([#44](https://github.com/sigoden/dufs/issues/44))
-
-## [0.17.0] - 2022-06-15
-
-### Bug Fixes
-
-- Webdav propfind dir with slash ([#42](https://github.com/sigoden/dufs/issues/42))
-
-### Features
-
-- Listen both ipv4 and ipv6 by default ([#40](https://github.com/sigoden/dufs/issues/40))
-
-### Refactor
-
-- Trivial changes ([#41](https://github.com/sigoden/dufs/issues/41))
-
-## [0.16.0] - 2022-06-12
-
-### Features
-
-- Implement head method ([#33](https://github.com/sigoden/dufs/issues/33))
-- Display upload speed and time left ([#34](https://github.com/sigoden/dufs/issues/34))
-- Support tls-key in pkcs#8 format ([#35](https://github.com/sigoden/dufs/issues/35))
-- Options method return status 200
-
-### Testing
-
-- Add integration tests ([#36](https://github.com/sigoden/dufs/issues/36))
-
-## [0.15.1] - 2022-06-11
-
-### Bug Fixes
-
-- Cannot upload ([#32](https://github.com/sigoden/dufs/issues/32))
-
-## [0.15.0] - 2022-06-10
-
-### Bug Fixes
-
-- Encode webdav href as uri ([#28](https://github.com/sigoden/dufs/issues/28))
-- Query dir param
-
-### Features
-
-- Add basic dark theme ([#29](https://github.com/sigoden/dufs/issues/29))
-- Add empty state placeholder to page([#30](https://github.com/sigoden/dufs/issues/30))
-
-## [0.14.0] - 2022-06-07
-
-### Bug Fixes
-
-- Send index page with content-type ([#26](https://github.com/sigoden/dufs/issues/26))
-
-### Features
-
-- Support ipv6 ([#25](https://github.com/sigoden/dufs/issues/25))
-- Add favicon ([#27](https://github.com/sigoden/dufs/issues/27))
-
-## [0.13.2] - 2022-06-06
-
-### Bug Fixes
-
-- Filename xml escaping
-- Escape path-prefix/url-prefix different
-
-## [0.13.1] - 2022-06-05
-
-### Bug Fixes
-
-- Escape filename ([#21](https://github.com/sigoden/dufs/issues/21))
-
-### Refactor
-
-- Use logger ([#22](https://github.com/sigoden/dufs/issues/22))
-
-## [0.13.0] - 2022-06-05
-
-### Bug Fixes
-
-- Ctrl+c not exit sometimes
-
-### Features
-
-- Implement more webdav methods ([#13](https://github.com/sigoden/dufs/issues/13))
-- Use digest auth ([#14](https://github.com/sigoden/dufs/issues/14))
-- Add webdav proppatch handler ([#18](https://github.com/sigoden/dufs/issues/18))
-
-## [0.12.1] - 2022-06-04
-
-### Features
-
-- Support webdav ([#10](https://github.com/sigoden/dufs/issues/10))
-- Remove unzip uploaded feature ([#11](https://github.com/sigoden/dufs/issues/11))
-
-## [0.11.0] - 2022-06-03
-
-### Features
-
-- Support gracefully shutdown server
-- Listen 0.0.0.0 by default
-
-## [0.10.1] - 2022-06-02
-
-### Bug Fixes
-
-- Panic when bind already used port
-
-## [0.10.0] - 2022-06-02
-
-### Bug Fixes
-
-- Remove unzip file even failed to unzip
-- Rename --no-auth-read to --no-auth-access
-- Broken ui
-
-### Documentation
-
-- Refactor readme
-
-### Features
-
-- Change auth logic/options
-- Improve ui
-
-### Refactor
-
-- Small improvement
-
-## [0.9.0] - 2022-06-02
-
-### Documentation
-
-- Improve readme
-
-### Features
-
-- Support path prefix
-- List all ifaces when listening 0.0.0.0
-- Support tls
-
-## [0.8.0] - 2022-06-01
-
-### Bug Fixes
-
-- Some typos
-- Caught 500 if no permission to access dir
-
-### Features
-
-- Cli add allow-symlink option
-- Add some headers to res
-- Support render-index/render-spa
-
-## [0.7.0] - 2022-05-31
-
-### Bug Fixes
-
-- Downloaded zip file has no.zip ext in firefox
-- Unzip override existed file in uploadonly mode
-- Miss file 500
-- Not found dir when allow_upload is false
-
-### Features
-
-- Drag and drop uploads, upload folder
-
-## [0.6.0] - 2022-05-31
-
-### Features
-
-- Delete confirm
-- Distinct upload and delete operation
-- Support range requests
-
-### Refactor
-
-- Improve code quality
-
-## [0.5.0] - 2022-05-30
-
-### Features
-
-- Add mime and cache headers to response
-- Add no-auth-read options
-- Unzip zip file when unload
-
-## [0.4.0] - 2022-05-29
-
-### Features
-
-- Replace --static option to --no-edit
-- Add cors
-
-## [0.3.0] - 2022-05-29
-
-### Documentation
-
-- Update readme demo png
-
-### Features
-
-- Automatically create dir while uploading
-- Support searching
-
-### Refactor
-
-- Handler zip
-
-### Styling
-
-- Optimize css
-
-## [0.2.1] - 2022-05-28
-
-### Bug Fixes
-
-- Cannot upload in root
-- Optimize download zip
-
-### Documentation
-
-- Improve readme
-
-### Features
-
-- Aware RUST_LOG
-
-## [0.2.0] - 2022-05-28
-
-### Documentation
-
-- Update demo png
-- Improve readme
-
-### Features
-
-- Add logger
-- Download folder as zip file
-
-## [0.1.0] - 2022-05-26
-
-### Bug Fixes
-
-- Caught server error when symlink broken
-
-### Documentation
-
-- Improve readme
-- Update readme
-
-### Features
-
-- Add basic auth and readonly mode
-- Support delete operation
-- Remove parent path
-
-### Styling
-
-- Cargo fmt
-- Update index page
-
-### Build
-
-- Remove dev deps
-
-### Ci
-
-- Init ci
-
-<!-- generated by git-cliff -->
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
-[[package]]
-name = "dufs"
-version = "0.43.0"
-dependencies = [
- "alphanumeric-sort",
- "anyhow",
- "assert_cmd",
- "assert_fs",
- "async-stream",
- "async_zip",
- "base64 0.22.1",
- "bytes",
- "chardetng",
- "chrono",
- "clap",
- "clap_complete",
- "content_inspector",
- "digest_auth",
- "form_urlencoded",
- "futures-util",
- "glob",
- "headers",
- "http-body-util",
- "hyper",
- "hyper-util",
- "if-addrs",
- "indexmap",
- "lazy_static",
- "log",
- "md5",
- "mime_guess",
- "percent-encoding",
- "pin-project-lite",
- "port_check",
- "predicates",
- "regex",
- "reqwest",
- "rstest",
- "rustls-pemfile",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "serde_yaml",
- "sha-crypt",
- "sha2",
- "smart-default",
- "socket2",
- "tokio",
- "tokio-rustls",
- "tokio-util",
- "url",
- "urlencoding",
- "uuid",
- "walkdir",
- "xml-rs",
-]
-
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+[[package]]
+name = "ozva-cloud"
+version = "0.1.0"
+dependencies = [
+ "alphanumeric-sort",
+ "anyhow",
+ "assert_cmd",
+ "assert_fs",
+ "async-stream",
+ "async_zip",
+ "base64 0.22.1",
+ "bytes",
+ "chardetng",
+ "chrono",
+ "clap",
+ "clap_complete",
+ "content_inspector",
+ "digest_auth",
+ "form_urlencoded",
+ "futures-util",
+ "glob",
+ "headers",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "if-addrs",
+ "indexmap",
+ "lazy_static",
+ "log",
+ "md5",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "port_check",
+ "predicates",
+ "regex",
+ "reqwest",
+ "rstest",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "sha-crypt",
+ "sha2",
+ "smart-default",
+ "socket2",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "url",
+ "urlencoding",
+ "uuid",
+ "walkdir",
+ "xml-rs",
+]
+
[[package]]
name = "parking"
version = "2.2.1"
[package]
-name = "dufs"
-version = "0.43.0"
+name = "ozva-cloud"
+version = "0.1.0"
edition = "2021"
-authors = ["sigoden <sigoden@gmail.com>"]
-description = "Dufs is a distinctive utility file server"
-license = "MIT OR Apache-2.0"
-homepage = "https://github.com/sigoden/dufs"
-repository = "https://github.com/sigoden/dufs"
+authors = ["William Greenwood <greenwoodbilly250@gmail.com>"]
+description = "Ozone-Value Holdings file sharing service"
+license = "MIT"
+homepage = "https://www.ozva.co.uk"
+repository = "https://git.ozva.co.uk/ozva-cloud"
categories = ["command-line-utilities", "web-programming::http-server"]
keywords = ["static", "file", "server", "webdav", "cli"]
+++ /dev/null
-FROM --platform=linux/amd64 messense/rust-musl-cross:x86_64-musl AS amd64
-COPY . .
-RUN cargo install --path . --root /
-
-FROM --platform=linux/amd64 messense/rust-musl-cross:aarch64-musl AS arm64
-COPY . .
-RUN cargo install --path . --root /
-
-FROM ${TARGETARCH} AS builder
-
-FROM scratch
-COPY --from=builder /bin/dufs /bin/dufs
-STOPSIGNAL SIGINT
-ENTRYPOINT ["/bin/dufs"]
+++ /dev/null
-FROM alpine as builder
-ARG REPO VER TARGETPLATFORM
-RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
- TARGET="x86_64-unknown-linux-musl"; \
- elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
- TARGET="aarch64-unknown-linux-musl"; \
- elif [ "$TARGETPLATFORM" = "linux/386" ]; then \
- TARGET="i686-unknown-linux-musl"; \
- elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
- TARGET="armv7-unknown-linux-musleabihf"; \
- fi && \
- wget https://github.com/${REPO}/releases/download/${VER}/dufs-${VER}-${TARGET}.tar.gz && \
- tar -xf dufs-${VER}-${TARGET}.tar.gz && \
- mv dufs /bin/
-
-FROM scratch
-COPY --from=builder /bin/dufs /bin/dufs
-STOPSIGNAL SIGINT
-ENTRYPOINT ["/bin/dufs"]
+++ /dev/null
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--- /dev/null
+<h1>OzVa Cloud</h1>
+<p>Forked from <a href="https://github.com/sigoden/dufs">Dufs</a> at <a href="https://github.com/sigoden/dufs/commit/f8b69f4df8ee4eb8ca7ac94beb8d46e3644374aa">this commit.</a></p>
+
+<pre><code>
+Usage: ozva-cloud [OPTIONS] [serve-path]
+
+Arguments:
+ [serve-path] Specific path to serve [default: .]
+
+Options:
+ -c, --config <file> Specify configuration file
+ -b, --bind <addrs> Specify bind address or unix socket
+ -p, --port <port> Specify port to listen on [default: 5000]
+ --path-prefix <path> Specify a path prefix
+ --hidden <value> Hide paths from directory listings, e.g. tmp,*.log,*.lock
+ -a, --auth <rules> Add auth roles, e.g. user:pass@/dir1:rw,/dir2
+ -A, --allow-all Allow all operations
+ --allow-upload Allow upload files/folders
+ --allow-delete Allow delete files/folders
+ --allow-search Allow search files/folders
+ --allow-symlink Allow symlink to files/folders outside root directory
+ --allow-archive Allow download folders as archive file
+ --enable-cors Enable CORS, sets `Access-Control-Allow-Origin: *`
+ --render-index Serve index.html when requesting a directory, returns 404 if not found index.html
+ --render-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html
+ --render-spa Serve SPA(Single Page Application)
+ --assets <path> Set the path to the assets directory for overriding the built-in assets
+ --log-format <format> Customize http log format
+ --log-file <file> Specify the file to save logs to, other than stdout/stderr
+ --compress <level> Set zip compress level [default: low] [possible values: none, low, medium, high]
+ --completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh]
+ --tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
+ --tls-key <path> Path to the SSL/TLS certificate's private key
+ -h, --help Print help
+ -V, --version Print version
+</code></pre>
+<h2>Examples</h2>
+<p>Serve current working directory in read-only mode</p>
+<pre><code>ozva-cloud
+</code></pre>
+<p>Allow all operations like upload/delete/search/create/edit...</p>
+<pre><code>ozva-cloud -A
+</code></pre>
+<p>Only allow upload operation</p>
+<pre><code>ozva-cloud --allow-upload
+</code></pre>
+<p>Serve a specific directory</p>
+<pre><code>ozva-cloud Downloads
+</code></pre>
+<p>Serve a single file</p>
+<pre><code>ozva-cloud linux-distro.iso
+</code></pre>
+<p>Serve a single-page application like react/vue</p>
+<pre><code>ozva-cloud --render-spa
+</code></pre>
+<p>Serve a static website with index.html</p>
+<pre><code>ozva-cloud --render-index
+</code></pre>
+<p>Require username/password</p>
+<pre><code>ozva-cloud -a admin:123@/:rw
+</code></pre>
+<p>Listen on specific host:ip</p>
+<pre><code>ozva-cloud -b 127.0.0.1 -p 80
+</code></pre>
+<p>Listen on unix socket</p>
+<pre><code>ozva-cloud -b /tmp/ozva-cloud.socket
+</code></pre>
+<p>Use https</p>
+<pre><code>ozva-cloud --tls-cert my.crt --tls-key my.key
+</code></pre>
+<h2>API</h2>
+<p>Upload a file</p>
+<pre><code class="language-sh">curl -T path-to-file http://127.0.0.1:5000/new-path/path-to-file
+</code></pre>
+<p>Download a file</p>
+<pre><code class="language-sh">curl http://127.0.0.1:5000/path-to-file # download the file
+curl http://127.0.0.1:5000/path-to-file?hash # retrieve the sha256 hash of the file
+</code></pre>
+<p>Download a folder as zip file</p>
+<pre><code class="language-sh">curl -o path-to-folder.zip http://127.0.0.1:5000/path-to-folder?zip
+</code></pre>
+<p>Delete a file/folder</p>
+<pre><code class="language-sh">curl -X DELETE http://127.0.0.1:5000/path-to-file-or-folder
+</code></pre>
+<p>Create a directory</p>
+<pre><code class="language-sh">curl -X MKCOL http://127.0.0.1:5000/path-to-folder
+</code></pre>
+<p>Move the file/folder to the new path</p>
+<pre><code class="language-sh">curl -X MOVE http://127.0.0.1:5000/path -H "Destination: http://127.0.0.1:5000/new-path"
+</code></pre>
+<p>List/search directory contents</p>
+<pre><code class="language-sh">curl http://127.0.0.1:5000?q=Dockerfile # search for files, similar to `find -name Dockerfile`
+curl http://127.0.0.1:5000?simple # output names only, similar to `ls -1`
+curl http://127.0.0.1:5000?json # output paths in json format
+</code></pre>
+<p>With authorization (Both basic or digest auth works)</p>
+<pre><code class="language-sh">curl http://127.0.0.1:5000/file --user user:pass # basic auth
+curl http://127.0.0.1:5000/file --user user:pass --digest # digest auth
+</code></pre>
+<p>Resumable downloads</p>
+<pre><code class="language-sh">curl -C- -o file http://127.0.0.1:5000/file
+</code></pre>
+<p>Resumable uploads</p>
+<pre><code class="language-sh">upload_offset=$(curl -I -s http://127.0.0.1:5000/file | tr -d '\r' | sed -n 's/content-length: //p')
+dd skip=$upload_offset if=file status=none ibs=1 | \
+ curl -X PATCH -H "X-Update-Range: append" --data-binary @- http://127.0.0.1:5000/file
+</code></pre>
+<p>Health checks</p>
+<pre><code class="language-sh">curl http://127.0.0.1:5000/__ozva-cloud__/health
+</code></pre>
+<h2>Advanced Topics</h2>
+<h3>Access Control</h3>
+<p>ozva-cloud supports account based access control. You can control who can do what on which path with <code>--auth</code>/<code>-a</code>.</p>
+<pre><code>ozva-cloud -a admin:admin@/:rw -a guest:guest@/
+ozva-cloud -a user:pass@/:rw,/dir1 -a @/
+</code></pre>
+<ol>
+<li>Use <code>@</code> to separate the account and paths. No account means anonymous user.</li>
+<li>Use <code>:</code> to separate the username and password of the account.</li>
+<li>Use <code>,</code> to separate paths.</li>
+<li>Use path suffix <code>:rw</code>/<code>:ro</code> set permissions: <code>read-write</code>/<code>read-only</code>. <code>:ro</code> can be omitted.</li>
+</ol>
+<ul>
+<li><code>-a admin:admin@/:rw</code>: <code>admin</code> has complete permissions for all paths.</li>
+<li><code>-a guest:guest@/</code>: <code>guest</code> has read-only permissions for all paths.</li>
+<li><code>-a user:pass@/:rw,/dir1</code>: <code>user</code> has read-write permissions for <code>/*</code>, has read-only permissions for <code>/dir1/*</code>.</li>
+<li><code>-a @/</code>: All paths is publicly accessible, everyone can view/download it.</li>
+</ul>
+<p><strong>Auth permissions are restricted by ozva-cloud global permissions.</strong> If ozva-cloud does not enable upload permissions via <code>--allow-upload</code>, then the account will not have upload permissions even if it is granted <code>read-write</code>(<code>:rw</code>) permissions.</p>
+<h4>Hashed Password</h4>
+<p>DUFS supports the use of sha-512 hashed password.</p>
+<p>Create hashed password:</p>
+<pre><code class="language-sh">$ openssl passwd -6 123456 # or `mkpasswd -m sha-512 123456`
+$6$tWMB51u6Kb2ui3wd$5gVHP92V9kZcMwQeKTjyTRgySsYJu471Jb1I6iHQ8iZ6s07GgCIO69KcPBRuwPE5tDq05xMAzye0NxVKuJdYs/
+</code></pre>
+<p>Use hashed password:</p>
+<pre><code class="language-sh">ozva-cloud -a 'admin:$6$tWMB51u6Kb2ui3wd$5gVHP92V9kZcMwQeKTjyTRgySsYJu471Jb1I6iHQ8iZ6s07GgCIO69KcPBRuwPE5tDq05xMAzye0NxVKuJdYs/@/:rw'
+</code></pre>
+<blockquote>
+<p>The hashed password contains <code>$6</code>, which can expand to a variable in some shells, so you have to use <strong>single quotes</strong> to wrap it.</p>
+</blockquote>
+<p>Two important things for hashed passwords:</p>
+<ol>
+<li>ozva-cloud only supports sha-512 hashed passwords, so ensure that the password string always starts with <code>$6$</code>.</li>
+<li>Digest authentication does not function properly with hashed passwords.</li>
+</ol>
+<h3>Hide Paths</h3>
+<p>ozva-cloud supports hiding paths from directory listings via option <code>--hidden <glob>,...</code>.</p>
+<pre><code>ozva-cloud --hidden .git,.DS_Store,tmp
+</code></pre>
+<blockquote>
+<p>The glob used in --hidden only matches file and directory names, not paths. So <code>--hidden dir1/file</code> is invalid.</p>
+</blockquote>
+<pre><code class="language-sh">ozva-cloud --hidden '.*' # hidden dotfiles
+ozva-cloud --hidden '*/' # hidden all folders
+ozva-cloud --hidden '*.log,*.lock' # hidden by exts
+ozva-cloud --hidden '*.log' --hidden '*.lock'
+</code></pre>
+<h3>Log Format</h3>
+<p>ozva-cloud supports customize http log format with option <code>--log-format</code>.</p>
+<p>The log format can use following variables.</p>
+<table>
+<thead>
+<tr>
+<th>variable</th>
+<th>description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>$remote_addr</td>
+<td>client address</td>
+</tr>
+<tr>
+<td>$remote_user</td>
+<td>user name supplied with authentication</td>
+</tr>
+<tr>
+<td>$request</td>
+<td>full original request line</td>
+</tr>
+<tr>
+<td>$status</td>
+<td>response status</td>
+</tr>
+<tr>
+<td>$http_</td>
+<td>arbitrary request header field. examples: $http_user_agent, $http_referer</td>
+</tr>
+</tbody>
+</table>
+<p>The default log format is <code>'$remote_addr "$request" $status'</code>.</p>
+<pre><code>2022-08-06T06:59:31+08:00 INFO - 127.0.0.1 "GET /" 200
+</code></pre>
+<p>Disable http log</p>
+<pre><code>ozva-cloud --log-format=''
+</code></pre>
+<p>Log user-agent</p>
+<pre><code>ozva-cloud --log-format '$remote_addr "$request" $status $http_user_agent'
+</code></pre>
+<pre><code>2022-08-06T06:53:55+08:00 INFO - 127.0.0.1 "GET /" 200 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36
+</code></pre>
+<p>Log remote-user</p>
+<pre><code>ozva-cloud --log-format '$remote_addr $remote_user "$request" $status' -a /@admin:admin -a /folder1@user1:pass1
+</code></pre>
+<pre><code>2022-08-06T07:04:37+08:00 INFO - 127.0.0.1 admin "GET /" 200
+</code></pre>
+<h2>Environment variables</h2>
+<p>All options can be set using environment variables prefixed with <code>DUFS_</code>.</p>
+<pre><code>[serve-path] DUFS_SERVE_PATH="."
+ --config <file> DUFS_CONFIG=config.yaml
+-b, --bind <addrs> DUFS_BIND=0.0.0.0
+-p, --port <port> DUFS_PORT=5000
+ --path-prefix <path> DUFS_PATH_PREFIX=/ozva-cloud
+ --hidden <value> DUFS_HIDDEN=tmp,*.log,*.lock
+-a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/"
+-A, --allow-all DUFS_ALLOW_ALL=true
+ --allow-upload DUFS_ALLOW_UPLOAD=true
+ --allow-delete DUFS_ALLOW_DELETE=true
+ --allow-search DUFS_ALLOW_SEARCH=true
+ --allow-symlink DUFS_ALLOW_SYMLINK=true
+ --allow-archive DUFS_ALLOW_ARCHIVE=true
+ --enable-cors DUFS_ENABLE_CORS=true
+ --render-index DUFS_RENDER_INDEX=true
+ --render-try-index DUFS_RENDER_TRY_INDEX=true
+ --render-spa DUFS_RENDER_SPA=true
+ --assets <path> DUFS_ASSETS=./assets
+ --log-format <format> DUFS_LOG_FORMAT=""
+ --log-file <file> DUFS_LOG_FILE=./ozva-cloud.log
+ --compress <compress> DUFS_COMPRESS=low
+ --tls-cert <path> DUFS_TLS_CERT=cert.pem
+ --tls-key <path> DUFS_TLS_KEY=key.pem
+</code></pre>
+<h2>Configuration File</h2>
+<p>You can specify and use the configuration file by selecting the option <code>--config <path-to-config.yaml></code>.</p>
+<p>The following are the configuration items:</p>
+<pre><code class="language-yaml">serve-path: '.'
+bind: 0.0.0.0
+port: 5000
+path-prefix: /ozva-cloud
+hidden:
+ - tmp
+ - '*.log'
+ - '*.lock'
+auth:
+ - admin:admin@/:rw
+ - user:pass@/src:rw,/share
+ - '@/' # According to the YAML spec, quoting is required.
+allow-all: false
+allow-upload: true
+allow-delete: true
+allow-search: true
+allow-symlink: true
+allow-archive: true
+enable-cors: true
+render-index: true
+render-try-index: true
+render-spa: true
+assets: ./assets/
+log-format: '$remote_addr "$request" $status $http_user_agent'
+log-file: ./ozva-cloud.log
+compress: low
+tls-cert: tests/data/cert.pem
+tls-key: tests/data/key_pkcs1.pem
+</code></pre>
+<h3>Customize UI</h3>
+<p>ozva-cloud allows users to customize the UI with your own assets.</p>
+<pre><code>ozva-cloud --assets my-assets-dir/
+</code></pre>
+++ /dev/null
-# Dufs
-
-[](https://github.com/sigoden/dufs/actions/workflows/ci.yaml)
-[](https://crates.io/crates/dufs)
-[](https://hub.docker.com/r/sigoden/dufs)
-
-Dufs is a distinctive utility file server that supports static serving, uploading, searching, accessing control, webdav...
-
-
-
-## Features
-
-- Serve static files
-- Download folder as zip file
-- Upload files and folders (Drag & Drop)
-- Create/Edit/Search files
-- Resumable/partial uploads/downloads
-- Access control
-- Support https
-- Support webdav
-- Easy to use with curl
-
-## Install
-
-### With cargo
-
-```
-cargo install dufs
-```
-
-### With docker
-
-```
-docker run -v `pwd`:/data -p 5000:5000 --rm sigoden/dufs /data -A
-```
-
-### With [Homebrew](https://brew.sh)
-
-```
-brew install dufs
-```
-
-### Binaries on macOS, Linux, Windows
-
-Download from [Github Releases](https://github.com/sigoden/dufs/releases), unzip and add dufs to your $PATH.
-
-## CLI
-
-```
-Dufs is a distinctive utility file server - https://github.com/sigoden/dufs
-
-Usage: dufs [OPTIONS] [serve-path]
-
-Arguments:
- [serve-path] Specific path to serve [default: .]
-
-Options:
- -c, --config <file> Specify configuration file
- -b, --bind <addrs> Specify bind address or unix socket
- -p, --port <port> Specify port to listen on [default: 5000]
- --path-prefix <path> Specify a path prefix
- --hidden <value> Hide paths from directory listings, e.g. tmp,*.log,*.lock
- -a, --auth <rules> Add auth roles, e.g. user:pass@/dir1:rw,/dir2
- -A, --allow-all Allow all operations
- --allow-upload Allow upload files/folders
- --allow-delete Allow delete files/folders
- --allow-search Allow search files/folders
- --allow-symlink Allow symlink to files/folders outside root directory
- --allow-archive Allow download folders as archive file
- --enable-cors Enable CORS, sets `Access-Control-Allow-Origin: *`
- --render-index Serve index.html when requesting a directory, returns 404 if not found index.html
- --render-try-index Serve index.html when requesting a directory, returns directory listing if not found index.html
- --render-spa Serve SPA(Single Page Application)
- --assets <path> Set the path to the assets directory for overriding the built-in assets
- --log-format <format> Customize http log format
- --log-file <file> Specify the file to save logs to, other than stdout/stderr
- --compress <level> Set zip compress level [default: low] [possible values: none, low, medium, high]
- --completions <shell> Print shell completion script for <shell> [possible values: bash, elvish, fish, powershell, zsh]
- --tls-cert <path> Path to an SSL/TLS certificate to serve with HTTPS
- --tls-key <path> Path to the SSL/TLS certificate's private key
- -h, --help Print help
- -V, --version Print version
-```
-
-## Examples
-
-Serve current working directory in read-only mode
-
-```
-dufs
-```
-
-Allow all operations like upload/delete/search/create/edit...
-
-```
-dufs -A
-```
-
-Only allow upload operation
-
-```
-dufs --allow-upload
-```
-
-Serve a specific directory
-
-```
-dufs Downloads
-```
-
-Serve a single file
-
-```
-dufs linux-distro.iso
-```
-
-Serve a single-page application like react/vue
-
-```
-dufs --render-spa
-```
-
-Serve a static website with index.html
-
-```
-dufs --render-index
-```
-
-Require username/password
-
-```
-dufs -a admin:123@/:rw
-```
-
-Listen on specific host:ip
-
-```
-dufs -b 127.0.0.1 -p 80
-```
-
-Listen on unix socket
-```
-dufs -b /tmp/dufs.socket
-```
-
-Use https
-
-```
-dufs --tls-cert my.crt --tls-key my.key
-```
-
-## API
-
-Upload a file
-
-```sh
-curl -T path-to-file http://127.0.0.1:5000/new-path/path-to-file
-```
-
-Download a file
-```sh
-curl http://127.0.0.1:5000/path-to-file # download the file
-curl http://127.0.0.1:5000/path-to-file?hash # retrieve the sha256 hash of the file
-```
-
-Download a folder as zip file
-
-```sh
-curl -o path-to-folder.zip http://127.0.0.1:5000/path-to-folder?zip
-```
-
-Delete a file/folder
-
-```sh
-curl -X DELETE http://127.0.0.1:5000/path-to-file-or-folder
-```
-
-Create a directory
-
-```sh
-curl -X MKCOL http://127.0.0.1:5000/path-to-folder
-```
-
-Move the file/folder to the new path
-
-```sh
-curl -X MOVE http://127.0.0.1:5000/path -H "Destination: http://127.0.0.1:5000/new-path"
-```
-
-List/search directory contents
-
-```sh
-curl http://127.0.0.1:5000?q=Dockerfile # search for files, similar to `find -name Dockerfile`
-curl http://127.0.0.1:5000?simple # output names only, similar to `ls -1`
-curl http://127.0.0.1:5000?json # output paths in json format
-```
-
-With authorization (Both basic or digest auth works)
-
-```sh
-curl http://127.0.0.1:5000/file --user user:pass # basic auth
-curl http://127.0.0.1:5000/file --user user:pass --digest # digest auth
-```
-
-Resumable downloads
-
-```sh
-curl -C- -o file http://127.0.0.1:5000/file
-```
-
-Resumable uploads
-
-```sh
-upload_offset=$(curl -I -s http://127.0.0.1:5000/file | tr -d '\r' | sed -n 's/content-length: //p')
-dd skip=$upload_offset if=file status=none ibs=1 | \
- curl -X PATCH -H "X-Update-Range: append" --data-binary @- http://127.0.0.1:5000/file
-```
-
-Health checks
-
-```sh
-curl http://127.0.0.1:5000/__dufs__/health
-```
-
-<details>
-<summary><h2>Advanced Topics</h2></summary>
-
-### Access Control
-
-Dufs supports account based access control. You can control who can do what on which path with `--auth`/`-a`.
-
-```
-dufs -a admin:admin@/:rw -a guest:guest@/
-dufs -a user:pass@/:rw,/dir1 -a @/
-```
-
-1. Use `@` to separate the account and paths. No account means anonymous user.
-2. Use `:` to separate the username and password of the account.
-3. Use `,` to separate paths.
-4. Use path suffix `:rw`/`:ro` set permissions: `read-write`/`read-only`. `:ro` can be omitted.
-
-- `-a admin:admin@/:rw`: `admin` has complete permissions for all paths.
-- `-a guest:guest@/`: `guest` has read-only permissions for all paths.
-- `-a user:pass@/:rw,/dir1`: `user` has read-write permissions for `/*`, has read-only permissions for `/dir1/*`.
-- `-a @/`: All paths is publicly accessible, everyone can view/download it.
-
-**Auth permissions are restricted by dufs global permissions.** If dufs does not enable upload permissions via `--allow-upload`, then the account will not have upload permissions even if it is granted `read-write`(`:rw`) permissions.
-
-#### Hashed Password
-
-DUFS supports the use of sha-512 hashed password.
-
-Create hashed password:
-
-```sh
-$ openssl passwd -6 123456 # or `mkpasswd -m sha-512 123456`
-$6$tWMB51u6Kb2ui3wd$5gVHP92V9kZcMwQeKTjyTRgySsYJu471Jb1I6iHQ8iZ6s07GgCIO69KcPBRuwPE5tDq05xMAzye0NxVKuJdYs/
-```
-
-Use hashed password:
-
-```sh
-dufs -a 'admin:$6$tWMB51u6Kb2ui3wd$5gVHP92V9kZcMwQeKTjyTRgySsYJu471Jb1I6iHQ8iZ6s07GgCIO69KcPBRuwPE5tDq05xMAzye0NxVKuJdYs/@/:rw'
-```
-> The hashed password contains `$6`, which can expand to a variable in some shells, so you have to use **single quotes** to wrap it.
-
-Two important things for hashed passwords:
-
-1. Dufs only supports sha-512 hashed passwords, so ensure that the password string always starts with `$6$`.
-2. Digest authentication does not function properly with hashed passwords.
-
-
-### Hide Paths
-
-Dufs supports hiding paths from directory listings via option `--hidden <glob>,...`.
-
-```
-dufs --hidden .git,.DS_Store,tmp
-```
-
-> The glob used in --hidden only matches file and directory names, not paths. So `--hidden dir1/file` is invalid.
-
-```sh
-dufs --hidden '.*' # hidden dotfiles
-dufs --hidden '*/' # hidden all folders
-dufs --hidden '*.log,*.lock' # hidden by exts
-dufs --hidden '*.log' --hidden '*.lock'
-```
-
-### Log Format
-
-Dufs supports customize http log format with option `--log-format`.
-
-The log format can use following variables.
-
-| variable | description |
-| ------------ | ------------------------------------------------------------------------- |
-| $remote_addr | client address |
-| $remote_user | user name supplied with authentication |
-| $request | full original request line |
-| $status | response status |
-| $http_ | arbitrary request header field. examples: $http_user_agent, $http_referer |
-
-
-The default log format is `'$remote_addr "$request" $status'`.
-```
-2022-08-06T06:59:31+08:00 INFO - 127.0.0.1 "GET /" 200
-```
-
-Disable http log
-```
-dufs --log-format=''
-```
-
-Log user-agent
-```
-dufs --log-format '$remote_addr "$request" $status $http_user_agent'
-```
-```
-2022-08-06T06:53:55+08:00 INFO - 127.0.0.1 "GET /" 200 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36
-```
-
-Log remote-user
-```
-dufs --log-format '$remote_addr $remote_user "$request" $status' -a /@admin:admin -a /folder1@user1:pass1
-```
-```
-2022-08-06T07:04:37+08:00 INFO - 127.0.0.1 admin "GET /" 200
-```
-
-## Environment variables
-
-All options can be set using environment variables prefixed with `DUFS_`.
-
-```
-[serve-path] DUFS_SERVE_PATH="."
- --config <file> DUFS_CONFIG=config.yaml
--b, --bind <addrs> DUFS_BIND=0.0.0.0
--p, --port <port> DUFS_PORT=5000
- --path-prefix <path> DUFS_PATH_PREFIX=/dufs
- --hidden <value> DUFS_HIDDEN=tmp,*.log,*.lock
--a, --auth <rules> DUFS_AUTH="admin:admin@/:rw|@/"
--A, --allow-all DUFS_ALLOW_ALL=true
- --allow-upload DUFS_ALLOW_UPLOAD=true
- --allow-delete DUFS_ALLOW_DELETE=true
- --allow-search DUFS_ALLOW_SEARCH=true
- --allow-symlink DUFS_ALLOW_SYMLINK=true
- --allow-archive DUFS_ALLOW_ARCHIVE=true
- --enable-cors DUFS_ENABLE_CORS=true
- --render-index DUFS_RENDER_INDEX=true
- --render-try-index DUFS_RENDER_TRY_INDEX=true
- --render-spa DUFS_RENDER_SPA=true
- --assets <path> DUFS_ASSETS=./assets
- --log-format <format> DUFS_LOG_FORMAT=""
- --log-file <file> DUFS_LOG_FILE=./dufs.log
- --compress <compress> DUFS_COMPRESS=low
- --tls-cert <path> DUFS_TLS_CERT=cert.pem
- --tls-key <path> DUFS_TLS_KEY=key.pem
-```
-
-## Configuration File
-
-You can specify and use the configuration file by selecting the option `--config <path-to-config.yaml>`.
-
-The following are the configuration items:
-
-```yaml
-serve-path: '.'
-bind: 0.0.0.0
-port: 5000
-path-prefix: /dufs
-hidden:
- - tmp
- - '*.log'
- - '*.lock'
-auth:
- - admin:admin@/:rw
- - user:pass@/src:rw,/share
- - '@/' # According to the YAML spec, quoting is required.
-allow-all: false
-allow-upload: true
-allow-delete: true
-allow-search: true
-allow-symlink: true
-allow-archive: true
-enable-cors: true
-render-index: true
-render-try-index: true
-render-spa: true
-assets: ./assets/
-log-format: '$remote_addr "$request" $status $http_user_agent'
-log-file: ./dufs.log
-compress: low
-tls-cert: tests/data/cert.pem
-tls-key: tests/data/key_pkcs1.pem
-```
-
-### Customize UI
-
-Dufs allows users to customize the UI with your own assets.
-
-```
-dufs --assets my-assets-dir/
-```
-
-> If you only need to make slight adjustments to the current UI, you copy dufs's [assets](https://github.com/sigoden/dufs/tree/main/assets) directory and modify it accordingly. The current UI doesn't use any frameworks, just plain HTML/JS/CSS. As long as you have some basic knowledge of web development, it shouldn't be difficult to modify.
-
-Your assets folder must contains a `index.html` file.
-
-`index.html` can use the following placeholder variables to retrieve internal data.
-
-- `__INDEX_DATA__`: directory listing data
-- `__ASSETS_PREFIX__`: assets url prefix
-
-</details>
-
-## License
-
-Copyright (c) 2022-2024 dufs-developers.
-
-dufs is made available under the terms of either the MIT License or the Apache License 2.0, at your option.
-
-See the LICENSE-APACHE and LICENSE-MIT files for license details.
+++ /dev/null
-# Security Policy
-
-## Supported Versions
-
-The latest release of *dufs* is supported. The fixes for any security issues found will be included
-in the next release.
-
-
-## Reporting a Vulnerability
-
-Please [use *dufs*'s security advisory reporting tool provided by
-GitHub](https://github.com/sigoden/dufs/security/advisories/new) to report security issues.
-
-We strive to fix security issues as quickly as possible. Across the industry, often the developers'
-slowness in developing and releasing a fix is the biggest delay in the process; we take pride in
-minimizing this delay as much as we practically can. We encourage you to also minimize the delay
-between when you find an issue and when you contact us. You do not need to convince us to take your
-report seriously. You don't need to create a PoC or a patch if that would slow down your reporting.
-You don't need an elaborate write-up. A short, informal note about the issue is good. We can always
-communicate later to fill in any details we need after that first note is shared with us.
-
:root {
--lm-color: #004088;
--dm-color: #004088;
+ --grid-size: 150px;
}
html {
}
.head {
+ z-index:3;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.breadcrumb>a {
- color: #0366d6;
+ color: #0044aa;
text-decoration: none;
}
background-color: rgba(3, 47, 98, 0.2);
}
+.thumbnail-preview {
+ display: inline-block;
+ vertical-align: top;
+ font-style: italic;
+ color: rgba(3, 47, 98, 0.2);
+}
+
+.thumbnail {
+ display: none;
+ position: absolute;
+ margin-left: -170px;
+ margin-top: -200px;
+ width: 200px;
+ height: 200px;
+ object-fit: contain;
+}
+
+.thumbnail-preview:hover > .thumbnail {
+ display: block;
+}
+
+.grid-mode {
+ display: block;
+ width: 100%;
+}
+
+.grid-mode .path a {
+ position: relative;
+ width: calc(var(--grid-size) - 10px);
+ min-width: var(--grid-size);
+ height: 25px;
+ margin-left: 10px;
+ padding-top: var(--grid-size);
+ display: inline-block;
+ z-index: 1;
+}
+
+.grid-mode > thead {
+ display: none;
+}
+.grid-mode > tbody {
+ display: block;
+}
+.grid-mode > tbody > tr {
+ display: inline-block;
+}
+.grid-mode > tbody > tr > td {
+ display: block;
+}
+.grid-mode > tbody > tr {
+ width: var(--grid-size);
+ height: calc(var(--grid-size) + 25px);
+ display: inline-block;
+}
+.grid-mode > tbody > tr > td:not(.cell-name, .cell-icon) {
+ display: none !important;
+}
+.grid-mode > tbody > tr > td.cell-name {
+ width: var(--grid-size);
+ height: calc(var(--grid-size) + 25px);
+ display: block;
+ margin-top: calc(var(--grid-size)*-1);
+}
+.grid-mode > tbody > tr > td.cell-name > * {
+ width: var(--grid-size);
+ height: calc(var(--grid-size) + 25px);
+ margin-bottom: calc(-1 * (var(--grid-size) + 25px));
+ margin-right: calc(var(--grid-size)*-1);
+}
+.grid-mode > tbody > tr > td.cell-name > .dropcopy {
+ display: none;
+}
+.grid-mode > tbody > tr > td.cell-name > .dropcopy {
+ display: none;
+}
+.grid-mode > tbody > tr > td.cell-name > .dropmove {
+ position: relative;
+ height: calc(var(--grid-size) + 25px);
+ color: rgba(3, 47, 98, 0.0);
+ border: 2px solid rgba(3, 47, 98, 0.0);
+ border-radius: 4px;
+ background-color: rgba(3, 47, 98, 0);
+}
+.grid-mode > tbody > tr > td.cell-name > .dropmove.dragging {
+ z-index:2;
+}
+.grid-mode > tbody > tr > td.cell-name > .dropmove.dragging.dragover {
+ background-color: rgba(3, 47, 98, 0.2);
+}
+.grid-mode > tbody > tr > td.cell-icon {
+ display: block;
+ width: var(--grid-size);
+ height: var(--grid-size);
+}
+.grid-mode > tbody > tr > td.cell-icon.has-preview {
+ opacity: 0;
+}
+.grid-mode > tbody > tr > td.cell-icon > svg {
+ width: var(--grid-size);
+ height: var(--grid-size);
+}
+.grid-mode > tbody > tr > td > .thumbnail-preview {
+}
+.grid-mode > tbody > tr > td > .thumbnail-preview > span {
+ display: none;
+}
+.grid-mode > tbody > tr > td > .thumbnail-preview > img {
+ width: var(--grid-size);
+ height: var(--grid-size);
+ display: block;
+ position: static;
+ margin: 0;
+}
+
@media (min-width: 768px) {
.path a {
min-width: 400px;
color: white;
}
}
+
+@media print {
+ .path a {
+ color: #24292e;
+ }
+ .main {
+ padding: 2em;
+ }
+ .paths-table {
+ width: 100%;
+ }
+ .head, #filedrop, .cell-actions, th > a > span {
+ display: none !important;
+ }
+}
d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
</svg>
</div>
+ <div class="grid-btn hidden" title="Grid">
+ <svg
+ width="16"
+ height="16"
+ viewBox="0 0 16 16">
+ <path d="m 1.9042969,0.95703125 c -0.5185163,0 -0.94921878,0.43070255 -0.94921878,0.94921875 v 4.6953125 c 0,0.5185162 0.43070248,0.9492189 0.94921878,0.9492188 h 4.6953125 c 0.5185163,0 0.9492187,-0.4307026 0.9492187,-0.9492188 V 1.90625 c 0,-0.5185162 -0.4307024,-0.94921875 -0.9492187,-0.94921875 z m 7.4960937,0 c -0.5185162,0 -0.9511719,0.43070255 -0.9511719,0.94921875 v 4.6953125 c 0,0.5185162 0.4326557,0.9492189 0.9511719,0.9492188 H 14.09375 c 0.518516,0 0.949219,-0.4307026 0.949219,-0.9492188 V 1.90625 c 0,-0.5185162 -0.430703,-0.94921875 -0.949219,-0.94921875 z M 1.8925781,1.8554687 c 0.00376,-4.875e-4 0.00728,0 0.011719,0 h 4.6953125 c 0.01996,0 0.036866,0.00703 0.044922,0.017578 0.00456,0.00789 0.00586,0.019897 0.00586,0.033203 v 4.6953125 c 0,0.013307 -0.00325,0.02566 -0.00781,0.033203 -0.00806,0.01001 -0.023009,0.015625 -0.042969,0.015625 H 1.9042969 c -0.035484,0 -0.048828,-0.013344 -0.048828,-0.048828 V 1.90625 c 0,-0.01996 0.00757,-0.036866 0.017578,-0.044922 0.00503,-0.00304 0.01201,-0.00488 0.019531,-0.00586 z m 7.4960938,0 c 0.00381,-4.875e-4 0.00728,0 0.011719,0 h 4.6933601 c 0.01996,0 0.03687,0.00703 0.04492,0.017578 0.0046,0.00789 0.0059,0.019897 0.0059,0.033203 v 4.6953125 c 0,0.013307 -0.0033,0.02566 -0.0078,0.033203 -0.0081,0.01001 -0.02301,0.015625 -0.04297,0.015625 H 9.4003906 c -0.01996,0 -0.034913,-0.00757 -0.042969,-0.017578 -0.00456,-0.00754 -0.00781,-0.017943 -0.00781,-0.03125 V 1.90625 c 0,-0.013306 0.00521,-0.027269 0.00977,-0.035156 0.00627,-0.00821 0.015947,-0.013918 0.029297,-0.015625 z m -7.484375,6.5957032 c -0.5185163,0 -0.94921878,0.4307025 -0.94921878,0.9492187 v 4.6953124 c 0,0.518516 0.43070248,0.949219 0.94921878,0.949219 h 4.6953125 c 0.5185163,0 0.9492187,-0.430703 0.9492187,-0.949219 V 9.4003906 c 0,-0.5185162 -0.4307024,-0.9492187 -0.9492187,-0.9492187 z m 7.4960937,0 c -0.5185162,0 -0.9511719,0.4307025 -0.9511719,0.9492187 v 4.6953124 c 0,0.518516 0.4326557,0.949219 0.9511719,0.949219 H 14.09375 c 0.518516,0 0.949219,-0.430703 0.949219,-0.949219 V 9.4003906 c 0,-0.5185162 -0.430703,-0.9492187 -0.949219,-0.9492187 z M 1.8867187,9.3515625 c 0.00515,-9.962e-4 0.010925,0 0.017578,0 h 4.6953125 c 0.035484,0 0.050781,0.013344 0.050781,0.048828 v 4.6953125 c 0,0.0133 -0.00325,0.02566 -0.00781,0.0332 -0.00806,0.01001 -0.023009,0.01563 -0.042969,0.01563 H 1.9042969 c -0.035484,0 -0.048828,-0.01334 -0.048828,-0.04883 V 9.4003906 c 0,-0.01996 0.00757,-0.035454 0.017578,-0.042969 0.00377,-0.00211 0.00852,-0.00486 0.013672,-0.00586 z m 7.4960938,0 c 0.00527,-9.962e-4 0.010925,0 0.017578,0 H 14.09375 c 0.03548,0 0.05078,0.013344 0.05078,0.048828 v 4.6953125 c 0,0.0133 -0.0033,0.02566 -0.0078,0.0332 -0.0081,0.01001 -0.02301,0.01563 -0.04297,0.01563 H 9.4003906 c -0.01996,0 -0.034913,-0.0076 -0.042969,-0.01758 -0.00456,-0.0075 -0.00781,-0.01794 -0.00781,-0.03125 V 9.4003906 c 0,-0.028831 0.010383,-0.044511 0.033203,-0.048828 z" />
+</svg>
+ </div>
</div>
<form class="searchbar hidden">
<div class="icon">
</div>
</div>
</div>
+ <!--
+ <div class="info info-hidden">
+ <p>OzVa Cloud service</p>
+ <p><b>To-do</b></p>
+ <ul>
+ <li>Implement the arbiraty command system safely (important that its safe)</li>
+ </ul>
+ </div>
+ !-->
<div class="main">
<div class="index-page hidden">
<div class="empty-folder hidden"></div>
+++ /dev/null
-/**
-* @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 OZVA_MAX_UPLOADINGS = 1;
-
-/**
-* @type {DATA} DATA
-*/
-var DATA;
-
-/**
-* @type {string}
-*/
-var DIR_EMPTY_NOTE;
-
-/**
-* @type {PARAMS}
-* @typedef {object} PARAMS
-* @property {string} q
-* @property {string} sort
-* @property {string} order
-*/
-const PARAMS = Object.fromEntries(new URLSearchParams(window.location.search).entries());
-
-const IFRAME_FORMATS = [
- ".pdf",
- ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg",
- ".mp4", ".mov", ".avi", ".wmv", ".flv", ".webm",
- ".mp3", ".ogg", ".wav", ".m4a",
-];
-
-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>`,
-}
-
-/**
-* @type Map<string, Uploader>
-*/
-const failUploaders = new Map();
-
-/**
-* @type Element
-*/
-let $pathsTable;
-/**
-* @type Element
-*/
-let $pathsTableHead;
-/**
-* @type Element
-*/
-let $pathsTableBody;
-/**
-* @type Element
-*/
-let $uploadersTable;
-/**
-* @type Element
-*/
-let $emptyFolder;
-/**
-* @type Element
-*/
-let $editor;
-/**
-* @type Element
-*/
-let $loginBtn;
-/**
-* @type Element
-*/
-let $logoutBtn;
-/**
-* @type Element
-*/
-let $userName;
-
-// Produce table when window loads
-window.addEventListener("DOMContentLoaded", async () => {
- const $indexData = document.getElementById('index-data');
- if (!$indexData) {
- alert("No data");
- return;
- }
-
- 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();
-});
-
-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");
- }
-
- console.log("drag start");
- });
-
- e.addEventListener("dragend", (ev) => {
- if (ev.target.classList.contains("dropzone")) {
- ev.target.classList.remove("dragover");
- }
-
- for (zone of document.getElementsByClassName("internaldrop")) {
- zone.classList.remove("dragging");
- zone.classList.remove("dragover");
- }
- },);
-}
-
-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),);
-}
-
-var getDrop = function ( method, e ) {
- return async function doDrop (ev) {
- ev.stopPropagation();
-
- const index = ev.dataTransfer.getData("text/plain");
- const file = DATA.paths[index];
- if (!file) return;
- const fileUrl = newUrl(file.name);
- const fileName = fileUrl.split("/").at(-1)
-
- const dir = DATA.paths[e.dataset.i];
- if (!dir) return;
- const regex = /\/[^\/]+\/\.\./i;
- const dirUrl = newUrl(dir.name).replace(regex, "");
-
- let newFileUrl = `${dirUrl}/${fileName}`;
-
- try {
- await checkAuth();
- const res1 = await fetch(newFileUrl, {
- method: "HEAD",
- });
- if (res1.status === 200) {
- if (!confirm("Override existing file?")) {
- return;
- }
- }
- const res2 = await fetch(fileUrl, {
- method: method,
- headers: {
- "Destination": newFileUrl,
- }
- });
- await assertResOK(res2);
-
- if (method == "MOVE") {
- document.getElementById(`addPath${index}`)?.remove();
- DATA.paths[index] = null;
- if (!DATA.paths.find(v => !!v)) {
- $pathsTable.classList.add("hidden");
- $emptyFolder.textContent = DIR_EMPTY_NOTE;
- $emptyFolder.classList.remove("hidden");
- }
- }
-
- } catch (err) {
- alert(`Cannot ${method.toLowerCase()} \`${fileUrl}\` to \`${newFileUrl}\`, ${err.message}`);
- }
- }
-}
-
-async function ready() {
- $pathsTable = document.querySelector(".paths-table");
- $pathsTableHead = document.querySelector(".paths-table thead");
- $pathsTableBody = document.querySelector(".paths-table tbody");
- $uploadersTable = document.querySelector(".uploaders-table");
- $emptyFolder = document.querySelector(".empty-folder");
- $editor = document.querySelector(".editor");
- $loginBtn = document.querySelector(".login-btn");
- $logoutBtn = document.querySelector(".logout-btn");
- $userName = document.querySelector(".user-name");
-
- addBreadcrumb(DATA.href, DATA.uri_prefix);
-
- if (DATA.kind === "Index") {
- document.title = `Index of ${DATA.href} - OzVa Cloud`;
- document.querySelector(".index-page").classList.remove("hidden");
-
- await setupIndexPage();
-
- // make files draggable
- for (e of document.querySelectorAll('[draggable="true"]')) {
- makeDrag(e);
- }
-
- for (e of document.getElementsByClassName('internaldrop')) {
- makeDrop(e);
- }
-
- } else if (DATA.kind === "Edit") {
- document.title = `Edit ${DATA.href} - OzVa Cloud`;
- document.querySelector(".editor-page").classList.remove("hidden");
-
- await setupEditorPage();
- } else if (DATA.kind === "View") {
- document.title = `View ${DATA.href} - OzVa Cloud`;
- document.querySelector(".editor-page").classList.remove("hidden");
-
- await setupEditorPage();
- }
-}
-
-class Uploader {
- /**
- *
- * @param {File} file
- * @param {string[]} pathParts
- */
- constructor(file, pathParts) {
- /**
- * @type Element
- */
- this.$uploadStatus = null
- this.uploaded = 0;
- this.uploadOffset = 0;
- this.lastUptime = 0;
- this.name = [...pathParts, file.name].join("/");
- this.idx = Uploader.globalIdx++;
- this.file = file;
- this.url = newUrl(this.name);
- }
-
- upload() {
- const { idx, name, url } = this;
- const encodedName = encodedStr(name);
- $uploadersTable.insertAdjacentHTML("beforeend", `
- <tr id="upload${idx}" class="uploader">
- <td class="path cell-icon">
- ${getPathSvg()}
- </td>
- <td class="path cell-name">
- <a href="${url}">${encodedName}</a>
- </td>
- <td class="cell-status upload-status" id="uploadStatus${idx}"></td>
- </tr>`);
- $uploadersTable.classList.remove("hidden");
- $emptyFolder.classList.add("hidden");
- this.$uploadStatus = document.getElementById(`uploadStatus${idx}`);
- this.$uploadStatus.innerHTML = '-';
- this.$uploadStatus.addEventListener("click", e => {
- const nodeId = e.target.id;
- const matches = /^retry(\d+)$/.exec(nodeId);
- if (matches) {
- const id = parseInt(matches[1]);
- let uploader = failUploaders.get(id);
- if (uploader) uploader.retry();
- }
- });
- Uploader.queues.push(this);
- Uploader.runQueue();
- }
-
- ajax() {
- const { url } = this;
-
- this.uploaded = 0;
- this.lastUptime = Date.now();
-
- const ajax = new XMLHttpRequest();
- ajax.upload.addEventListener("progress", e => this.progress(e), false);
- ajax.addEventListener("readystatechange", () => {
- if (ajax.readyState === 4) {
- if (ajax.status >= 200 && ajax.status < 300) {
- this.complete();
- } else {
- if (ajax.status != 0) {
- this.fail(`${ajax.status} ${ajax.statusText}`);
- }
- }
- }
- })
- ajax.addEventListener("error", () => this.fail(), false);
- ajax.addEventListener("abort", () => this.fail(), false);
- if (this.uploadOffset > 0) {
- ajax.open("PATCH", url);
- ajax.setRequestHeader("X-Update-Range", "append");
- ajax.send(this.file.slice(this.uploadOffset));
- } else {
- ajax.open("PUT", url);
- ajax.send(this.file);
- // setTimeout(() => ajax.abort(), 3000);
- }
- }
-
- async retry() {
- const { url } = this;
- let res = await fetch(url, {
- method: "HEAD",
- });
- let uploadOffset = 0;
- if (res.status == 200) {
- let value = res.headers.get("content-length");
- uploadOffset = parseInt(value) || 0;
- }
- this.uploadOffset = uploadOffset;
- this.ajax();
- }
-
- progress(event) {
- const now = Date.now();
- const speed = (event.loaded - this.uploaded) / (now - this.lastUptime) * 1000;
- const [speedValue, speedUnit] = formatFileSize(speed);
- const speedText = `${speedValue} ${speedUnit}/s`;
- const progress = formatPercent(((event.loaded + this.uploadOffset) / this.file.size) * 100);
- const duration = formatDuration((event.total - event.loaded) / speed);
- this.$uploadStatus.innerHTML = `<span style="width: 80px;">${speedText}</span><span>${progress} ${duration}</span>`;
- this.uploaded = event.loaded;
- this.lastUptime = now;
- }
-
- complete() {
- const $uploadStatusNew = this.$uploadStatus.cloneNode(true);
- $uploadStatusNew.innerHTML = `✓`;
- this.$uploadStatus.parentNode.replaceChild($uploadStatusNew, this.$uploadStatus);
- this.$uploadStatus = null;
- failUploaders.delete(this.idx);
- Uploader.runnings--;
- Uploader.runQueue();
- }
-
- fail(reason = "") {
- this.$uploadStatus.innerHTML = `<span style="width: 20px;" title="${reason}">✗</span><span class="retry-btn" id="retry${this.idx}" title="Retry">↻</span>`;
- failUploaders.set(this.idx, this);
- Uploader.runnings--;
- Uploader.runQueue();
- }
-}
-
-Uploader.globalIdx = 0;
-
-Uploader.runnings = 0;
-
-Uploader.auth = false;
-
-/**
-* @type Uploader[]
-*/
-Uploader.queues = [];
-
-
-Uploader.runQueue = async () => {
- 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
-*/
-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>`);
- }
- }
-}
-
-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();
- }
-
- console.log(DATA.paths);
-
- renderPathsTableHead();
- renderPathsTableBody();
-}
-
-/**
-* Render path table thead
-*/
-function renderPathsTableHead() {
- const headerItems = [
- {
- name: "name",
- props: `colspan="2"`,
- text: "Name",
- },
- {
- name: "mtime",
- props: ``,
- text: "Last Modified",
- },
- {
- name: "size",
- props: ``,
- text: "Size",
- }
- ];
- $pathsTableHead.insertAdjacentHTML("beforeend", `
- <tr>
- ${headerItems.map(item => {
- let svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5zm-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5z"/></svg>`;
- let order = "desc";
- if (PARAMS.sort === item.name) {
- if (PARAMS.order === "desc") {
- order = "asc";
- svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/></svg>`
- } else {
- svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"/></svg>`
- }
- }
- const qs = new URLSearchParams({ ...PARAMS, order, sort: item.name }).toString();
- const icon = `<span>${svg}</span>`
- return `<th class="cell-${item.name}" ${item.props}><a href="?${qs}">${item.text}${icon}</a></th>`
- }).join("\n")}
- <th class="cell-actions">Actions</th>
- </tr>
- `);
-}
-
-/**
-* Render path table tbody
-*/
-function renderPathsTableBody() {
- if (DATA.paths && DATA.paths.length > 0) {
- const len = DATA.paths.length;
- if (len > 0) {
- $pathsTable.classList.remove("hidden");
- }
- for (let i = 0; i < len; i++) {
- addPath(DATA.paths[i], i);
- }
- } else {
- $emptyFolder.textContent = DIR_EMPTY_NOTE;
- $emptyFolder.classList.remove("hidden");
- }
-}
-
-/**
-* Add pathitem
-* @param {PathItem} file
-* @param {number} index
-*/
-function addPath(file, index) {
- const encodedName = encodedStr(file.name);
- let url = newUrl(file.name);
- let actionDelete = "";
- let actionDownload = "";
- let actionMove = "";
- let actionEdit = "";
- let actionView = "";
- let isDir = file.path_type.endsWith("Dir");
- let isParent = (file.name == "..");
- if (isDir) {
- url += "/";
- if (DATA.allow_archive) {
- actionDownload = `
- <div class="action-btn">
- <a href="${url}?zip" title="Download folder as a .zip file">${ICONS.download}</a>
- </div>`;
- }
- } else {
- actionDownload = `
- <div class="action-btn" >
- <a href="${url}" title="Download file" download>${ICONS.download}</a>
- </div>`;
- }
- if (DATA.allow_delete) {
- if (DATA.allow_upload) {
- actionMove = `<div onclick="movePath(${index})" class="action-btn" id="moveBtn${index}" title="Move to new path">${ICONS.move}</div>`;
- if (!isDir) {
- actionEdit = `<a class="action-btn" title="Edit file" target="_blank" href="${url}?edit">${ICONS.edit}</a>`;
- }
- }
- actionDelete = `
- <div onclick="deletePath(${index})" class="action-btn" id="deleteBtn${index}" title="Delete">${ICONS.delete}</div>`;
- }
- if (!actionEdit && !isDir) {
- actionView = `<a class="action-btn" title="View file" target="_blank" href="${url}?view">${ICONS.view}</a>`;
- }
- let actionCell = `
- <td class="cell-actions">
- ${actionDownload}
- ${actionView}
- ${actionMove}
- ${actionDelete}
- ${actionEdit}
- </td>`;
-
- let sizeDisplay = isDir ? formatDirSize(file.size) : formatFileSize(file.size).join(" ");
-
- $pathsTableBody.insertAdjacentHTML("beforeend", `
-<tr id="addPath${index}">
- <td class="path cell-icon">
- ${getPathSvg(file.path_type)}
- </td>
- <td class="path cell-name">
- <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() {
- const dropZone = document.getElementById("filedrop");
- dropZone.addEventListener("dragenter", e => {
- e.stopPropagation();
-
- if (!e.dataTransfer.getData("text/plain")) {
- dropZone.classList.add("dragoverfile");
- }
- });
- dropZone.addEventListener("dragleave", e => {
- e.stopPropagation();
-
- dropZone.classList.remove("dragoverfile");
- });
-
- ["dragend", "dragleave"].forEach(name => {
- document.body.addEventListener(name, e => {
- e.preventDefault();
- e.stopPropagation();
-
- if ("dragging" in dropZone.classList) {
- dropZone.classList.remove("dragging");
- }
- });
- });
-
- ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(name => {
- dropZone.addEventListener(name, e => {
- e.preventDefault();
- e.stopPropagation();
- });
- });
- dropZone.addEventListener("drop", async e => {
- dropZone.classList.remove("dragging");
-
- if (!e.dataTransfer.items[0].webkitGetAsEntry) {
- const files = Array.from(e.dataTransfer.files).filter(v => v.size > 0);
- for (const file of files) {
- new Uploader(file, []).upload();
- }
- } else {
- const entries = [];
- const len = e.dataTransfer.items.length;
- for (let i = 0; i < len; i++) {
- entries.push(e.dataTransfer.items[i].webkitGetAsEntry());
- }
- addFileEntries(entries, []);
- }
- });
-}
-
-async function setupAuth() {
- if (DATA.user) {
- $logoutBtn.classList.remove("hidden");
- $logoutBtn.addEventListener("click", logout);
- $userName.textContent = DATA.user;
- } else {
- $loginBtn.classList.remove("hidden");
- $loginBtn.addEventListener("click", async () => {
- try {
- await checkAuth();
- } catch {}
- location.reload();
- });
- }
-}
-
-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;
- }
-}
-
-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();
- }
- });
-}
-
-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);
- });
-}
-
-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);
- });
-}
-
-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}`);
- }
-}
-
-/**
-* Delete path
-* @param {number} index
-* @returns
-*/
-async function deletePath(index) {
- const file = DATA.paths[index];
- if (!file) return;
- await doDeletePath(file.name, newUrl(file.name), () => {
- document.getElementById(`addPath${index}`)?.remove();
- DATA.paths[index] = null;
- if (!DATA.paths.find(v => !!v)) {
- $pathsTable.classList.add("hidden");
- $emptyFolder.textContent = DIR_EMPTY_NOTE;
- $emptyFolder.classList.remove("hidden");
- }
- });
-}
-
-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}`);
- }
-}
-
-/**
-* Move path
-* @param {number} index
-* @returns
-*/
-async function movePath(index) {
- const file = DATA.paths[index];
- if (!file) return;
- const fileUrl = newUrl(file.name);
- const newFileUrl = await doMovePath(fileUrl);
- if (newFileUrl) {
- location.href = newFileUrl.split("/").slice(0, -1).join("/");
- }
-}
-
-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}`);
- }
-}
-
-
-/**
-* Save editor change
-*/
-async function saveChange() {
- try {
- await fetch(baseUrl(), {
- method: "PUT",
- body: $editor.value,
- });
- location.reload();
- } catch (err) {
- alert(`Failed to save file, ${err.message}`);
- }
-}
-
-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();
-}
-
-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();
-}
-
-/**
-* Create a folder
-* @param {string} name
-*/
-async function createFolder(name) {
- const url = newUrl(name);
- try {
- await checkAuth();
- const res = await fetch(url, {
- method: "MKCOL",
- });
- await assertResOK(res);
- location.href = url;
- } catch (err) {
- alert(`Cannot create folder \`${name}\`, ${err.message}`);
- }
-}
-
-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}`);
- }
-}
-
-async function addFileEntries(entries, dirs) {
- for (const entry of entries) {
- if (entry == null) {
- // this is the case if the user has dragged a file into the file upload area
- // this can be made unlikely with fun css
- return
- } else if (entry.isFile) {
- entry.file(file => {
- new Uploader(file, dirs).upload();
- });
- } else if (entry.isDirectory) {
- const dirReader = entry.createReader();
-
- const successCallback = entries => {
- if (entries.length > 0) {
- addFileEntries(entries, [...dirs, entry.name]);
- dirReader.readEntries(successCallback);
- }
- };
-
- dirReader.readEntries(successCallback);
- }
- }
-}
-
-
-function newUrl(name) {
- let url = baseUrl();
- if (!url.endsWith("/")) url += "/";
- url += name.split("/").map(encodeURIComponent).join("/");
- return url;
-}
-
-function baseUrl() {
- return location.href.split(/[?#]/)[0];
-}
-
-function baseName(url) {
- return decodeURIComponent(url.split("/").filter(v => v.length > 0).slice(-1)[0]);
-}
-
-function extName(filename) {
- const dotIndex = filename.lastIndexOf('.');
-
- if (dotIndex === -1 || dotIndex === 0 || dotIndex === filename.length - 1) {
- return '';
- }
-
- return filename.substring(dotIndex);
-}
-
-function getPathSvg(path_type) {
- switch (path_type) {
- case "Dir":
- return ICONS.dir;
- case "SymlinkFile":
- return ICONS.symlinkFile;
- case "SymlinkDir":
- return ICONS.symlinkDir;
- default:
- return ICONS.file;
- }
-}
-
-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}`;
-}
-
-function padZero(value, size) {
- return ("0".repeat(size) + value).slice(-1 * size);
-}
-
-function formatDirSize(size) {
- const unit = size === 1 ? "item" : "items";
- const num = size >= MAX_SUBPATHS_COUNT ? `>${MAX_SUBPATHS_COUNT - 1}` : `${size}`;
- return ` ${num} ${unit}`;
-}
-
-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]];
-}
-
-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)}`;
-}
-
-function formatPercent(percent) {
- if (percent > 10) {
- return percent.toFixed(1) + "%";
- } else {
- return percent.toFixed(2) + "%";
- }
-}
-
-function encodedStr(rawStr) {
- return rawStr.replace(/[\u00A0-\u9999<>\&]/g, function (i) {
- return '&#' + i.charCodeAt(0) + ';';
- });
-}
-
-async function assertResOK(res) {
- if (!(res.status >= 200 && res.status < 300)) {
- throw new Error(await res.text() || `Invalid status ${res.status}`);
- }
-}
-
-function getEncoding(contentType) {
- const charset = contentType?.split(";")[1];
- if (/charset/i.test(charset)) {
- let encoding = charset.split("=")[1];
- if (encoding) {
- return encoding.toLowerCase();
- }
- }
- return 'utf-8';
-}
-
-// 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);
-}
--- /dev/null
+/**
+* @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 OZVA_MAX_UPLOADINGS = 1;
+
+/**
+* @type {DATA} DATA
+*/
+var DATA;
+
+/**
+* @type {string}
+*/
+var DIR_EMPTY_NOTE;
+
+/**
+* @type {PARAMS}
+* @typedef {object} PARAMS
+* @property {string} q
+* @property {string} sort
+* @property {string} order
+*/
+const PARAMS = Object.fromEntries(new URLSearchParams(window.location.search).entries());
+
+const IFRAME_FORMATS = [
+ ".pdf",
+ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg",
+ ".mp4", ".mov", ".avi", ".wmv", ".flv", ".webm",
+ ".mp3", ".ogg", ".wav", ".m4a",
+];
+
+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>`,
+ duplicate: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M 3.6269531 0 C 2.6755659 0 1.9003906 0.78091373 1.9003906 1.7324219 L 1.9003906 12.058594 C 1.9003906 13.010102 2.6755661 13.791016 3.6269531 13.791016 L 3.9667969 13.791016 L 3.9667969 14.267578 C 3.9667969 15.219396 4.7423239 16 5.6933594 16 L 12.554688 16 C 13.505722 16 14.283203 15.219396 14.283203 14.267578 L 14.283203 3.9414062 C 14.283203 2.9895887 13.505722 2.2089844 12.554688 2.2089844 L 12.216797 2.2089844 L 12.216797 1.7324219 C 12.216797 0.7800921 11.439668 -1.4802974e-16 10.488281 0 L 3.6269531 0 z M 3.6269531 0.8984375 L 10.488281 0.8984375 C 10.958544 0.8984375 11.318359 1.2583282 11.318359 1.7324219 L 11.318359 2.1503906 L 5.6503906 2.1503906 C 4.6998277 2.1503906 3.9238281 2.9293513 3.9238281 3.8808594 L 3.9238281 12.892578 L 3.6269531 12.892578 C 3.1566901 12.892578 2.7988281 12.533509 2.7988281 12.058594 L 2.7988281 1.7324219 C 2.7988281 1.2575066 3.1566903 0.8984375 3.6269531 0.8984375 z M 5.6933594 3.109375 L 12.554688 3.109375 C 13.025301 3.109375 13.382812 3.4668003 13.382812 3.9414062 L 13.382812 14.267578 C 13.382812 14.742185 13.025301 15.099609 12.554688 15.099609 L 5.6933594 15.099609 C 5.2227448 15.099609 4.8652344 14.742184 4.8652344 14.267578 L 4.8652344 3.9414062 C 4.8652344 3.4668004 5.2227452 3.109375 5.6933594 3.109375 z"/></svg>`
+}
+
+/**
+* @type Map<string, Uploader>
+*/
+const failUploaders = new Map();
+
+/**
+* @type Element
+*/
+let $pathsTable;
+/**
+* @type Element
+*/
+let $pathsTableHead;
+/**
+* @type Element
+*/
+let $pathsTableBody;
+/**
+* @type Element
+*/
+let $uploadersTable;
+/**
+* @type Element
+*/
+let $emptyFolder;
+/**
+* @type Element
+*/
+let $editor;
+/**
+* @type Element
+*/
+let $loginBtn;
+/**
+* @type Element
+*/
+let $logoutBtn;
+/**
+* @type Element
+*/
+let $userName;
+
+// Produce table when window loads
+window.addEventListener("DOMContentLoaded", async () => {
+ const $indexData = document.getElementById('index-data');
+ if (!$indexData) {
+ alert("No data");
+ return;
+ }
+
+ 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();
+});
+
+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");
+ }
+
+ console.log("drag start");
+ });
+
+ e.addEventListener("dragend", (ev) => {
+ if (ev.target.classList.contains("dropzone")) {
+ ev.target.classList.remove("dragover");
+ }
+
+ for (zone of document.getElementsByClassName("internaldrop")) {
+ zone.classList.remove("dragging");
+ zone.classList.remove("dragover");
+ }
+ },);
+}
+
+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),);
+}
+
+var getDrop = function ( method, e ) {
+ return async function doDrop (ev) {
+ ev.stopPropagation();
+ const index = ev.dataTransfer.getData("text/plain");
+
+ if (index == e.dataset.i) {return};
+
+ 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 duplicate (index) {
+ const file = DATA.paths[index];
+ if (!file) return;
+ const fileUrl = newUrl(file.name);
+
+ let fileParts = fileUrl.split("/");
+ let origionalName = fileParts.pop();
+ let lastPart = `Copy%20of%20${origionalName}`;
+
+ let newFile = file;
+ newFile.name = `Copy of ${origionalName}`;
+
+ fileParts.push(lastPart);
+ newFileUrl = fileParts.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: "COPY",
+ headers: {
+ "Destination": newFileUrl,
+ }
+ });
+ await assertResOK(res2);
+
+ window.location.reload(); // This is a bad idea! i think ! it does work tho so im not sure
+
+ } catch (err) {
+ alert(`Cannot copy \`${fileUrl}\` to \`${newFileUrl}\`, ${err.message}`);
+ }
+}
+
+async function ready() {
+ $pathsTable = document.querySelector(".paths-table");
+ $pathsTableHead = document.querySelector(".paths-table thead");
+ $pathsTableBody = document.querySelector(".paths-table tbody");
+ $uploadersTable = document.querySelector(".uploaders-table");
+ $emptyFolder = document.querySelector(".empty-folder");
+ $editor = document.querySelector(".editor");
+ $loginBtn = document.querySelector(".login-btn");
+ $logoutBtn = document.querySelector(".logout-btn");
+ $userName = document.querySelector(".user-name");
+
+ addBreadcrumb(DATA.href, DATA.uri_prefix);
+
+ if (DATA.kind === "Index") {
+ document.title = `Index of ${DATA.href} - OzVa Cloud`;
+ document.querySelector(".index-page").classList.remove("hidden");
+
+ await setupIndexPage();
+
+ // make files draggable
+ for (e of document.querySelectorAll('[draggable="true"]')) {
+ makeDrag(e);
+ }
+
+ for (e of document.getElementsByClassName('internaldrop')) {
+ makeDrop(e);
+ }
+
+ } else if (DATA.kind === "Edit") {
+ document.title = `Edit ${DATA.href} - OzVa Cloud`;
+ document.querySelector(".editor-page").classList.remove("hidden");
+
+ await setupEditorPage();
+ } else if (DATA.kind === "View") {
+ document.title = `View ${DATA.href} - OzVa Cloud`;
+ document.querySelector(".editor-page").classList.remove("hidden");
+
+ await setupEditorPage();
+ }
+}
+
+class Uploader {
+ /**
+ *
+ * @param {File} file
+ * @param {string[]} pathParts
+ */
+ constructor(file, pathParts) {
+ /**
+ * @type Element
+ */
+ this.$uploadStatus = null
+ this.uploaded = 0;
+ this.uploadOffset = 0;
+ this.lastUptime = 0;
+ this.name = [...pathParts, file.name].join("/");
+ this.idx = Uploader.globalIdx++;
+ this.file = file;
+ this.url = newUrl(this.name);
+ }
+
+ upload() {
+ const { idx, name, url } = this;
+ const encodedName = encodedStr(name);
+ $uploadersTable.insertAdjacentHTML("beforeend", `
+ <tr id="upload${idx}" class="uploader">
+ <td class="path cell-icon">
+ ${getPathSvg()}
+ </td>
+ <td class="path cell-name">
+ <a href="${url}">${encodedName}</a>
+ </td>
+ <td class="cell-status upload-status" id="uploadStatus${idx}"></td>
+ </tr>`);
+ $uploadersTable.classList.remove("hidden");
+ $emptyFolder.classList.add("hidden");
+ this.$uploadStatus = document.getElementById(`uploadStatus${idx}`);
+ this.$uploadStatus.innerHTML = '-';
+ this.$uploadStatus.addEventListener("click", e => {
+ const nodeId = e.target.id;
+ const matches = /^retry(\d+)$/.exec(nodeId);
+ if (matches) {
+ const id = parseInt(matches[1]);
+ let uploader = failUploaders.get(id);
+ if (uploader) uploader.retry();
+ }
+ });
+ Uploader.queues.push(this);
+ Uploader.runQueue();
+ }
+
+ ajax() {
+ const { url } = this;
+
+ this.uploaded = 0;
+ this.lastUptime = Date.now();
+
+ const ajax = new XMLHttpRequest();
+ ajax.upload.addEventListener("progress", e => this.progress(e), false);
+ ajax.addEventListener("readystatechange", () => {
+ if (ajax.readyState === 4) {
+ if (ajax.status >= 200 && ajax.status < 300) {
+ this.complete();
+ } else {
+ if (ajax.status != 0) {
+ this.fail(`${ajax.status} ${ajax.statusText}`);
+ }
+ }
+ }
+ })
+ ajax.addEventListener("error", () => this.fail(), false);
+ ajax.addEventListener("abort", () => this.fail(), false);
+ if (this.uploadOffset > 0) {
+ ajax.open("PATCH", url);
+ ajax.setRequestHeader("X-Update-Range", "append");
+ ajax.send(this.file.slice(this.uploadOffset));
+ } else {
+ ajax.open("PUT", url);
+ ajax.send(this.file);
+ // setTimeout(() => ajax.abort(), 3000);
+ }
+ }
+
+ async retry() {
+ const { url } = this;
+ let res = await fetch(url, {
+ method: "HEAD",
+ });
+ let uploadOffset = 0;
+ if (res.status == 200) {
+ let value = res.headers.get("content-length");
+ uploadOffset = parseInt(value) || 0;
+ }
+ this.uploadOffset = uploadOffset;
+ this.ajax();
+ }
+
+ progress(event) {
+ const now = Date.now();
+ const speed = (event.loaded - this.uploaded) / (now - this.lastUptime) * 1000;
+ const [speedValue, speedUnit] = formatFileSize(speed);
+ const speedText = `${speedValue} ${speedUnit}/s`;
+ const progress = formatPercent(((event.loaded + this.uploadOffset) / this.file.size) * 100);
+ const duration = formatDuration((event.total - event.loaded) / speed);
+ this.$uploadStatus.innerHTML = `<span style="width: 80px;">${speedText}</span><span>${progress} ${duration}</span>`;
+ this.uploaded = event.loaded;
+ this.lastUptime = now;
+ }
+
+ complete() {
+ const $uploadStatusNew = this.$uploadStatus.cloneNode(true);
+ $uploadStatusNew.innerHTML = `✓`;
+ this.$uploadStatus.parentNode.replaceChild($uploadStatusNew, this.$uploadStatus);
+ this.$uploadStatus = null;
+ failUploaders.delete(this.idx);
+ Uploader.runnings--;
+ Uploader.runQueue();
+ }
+
+ fail(reason = "") {
+ this.$uploadStatus.innerHTML = `<span style="width: 20px;" title="${reason}">✗</span><span class="retry-btn" id="retry${this.idx}" title="Retry">↻</span>`;
+ failUploaders.set(this.idx, this);
+ Uploader.runnings--;
+ Uploader.runQueue();
+ }
+}
+
+Uploader.globalIdx = 0;
+
+Uploader.runnings = 0;
+
+Uploader.auth = false;
+
+/**
+* @type Uploader[]
+*/
+Uploader.queues = [];
+
+
+Uploader.runQueue = async () => {
+ 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
+*/
+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>`);
+ }
+ }
+}
+
+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();
+ }
+
+ setupGridButton();
+ renderPathsTableHead();
+ renderPathsTableBody();
+}
+
+/**
+* Render path table thead
+*/
+function renderPathsTableHead() {
+ const headerItems = [
+ {
+ name: "name",
+ props: `colspan="2"`,
+ text: "Name",
+ },
+ {
+ name: "mtime",
+ props: ``,
+ text: "Last Modified",
+ },
+ {
+ name: "size",
+ props: ``,
+ text: "Size",
+ }
+ ];
+ $pathsTableHead.insertAdjacentHTML("beforeend", `
+ <tr>
+ ${headerItems.map(item => {
+ let svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5zm-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5z"/></svg>`;
+ let order = "desc";
+ if (PARAMS.sort === item.name) {
+ if (PARAMS.order === "desc") {
+ order = "asc";
+ svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/></svg>`
+ } else {
+ svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"/></svg>`
+ }
+ }
+ const qs = new URLSearchParams({ ...PARAMS, order, sort: item.name }).toString();
+ const icon = `<span>${svg}</span>`
+ return `<th class="cell-${item.name}" ${item.props}><a href="?${qs}">${item.text}${icon}</a></th>`
+ }).join("\n")}
+ <th class="cell-actions">Actions</th>
+ </tr>
+ `);
+}
+
+/**
+* Render path table tbody
+*/
+function renderPathsTableBody() {
+ if (DATA.paths && DATA.paths.length > 0) {
+ const len = DATA.paths.length;
+ if (len > 0) {
+ $pathsTable.classList.remove("hidden");
+ }
+ for (let i = 0; i < len; i++) {
+ addPath(DATA.paths[i], i);
+ }
+ } else {
+ $emptyFolder.textContent = DIR_EMPTY_NOTE;
+ $emptyFolder.classList.remove("hidden");
+ }
+}
+
+/**
+* Add pathitem
+* @param {PathItem} file
+* @param {number} index
+*/
+function addPath(file, index) {
+ const encodedName = encodedStr(file.name);
+ let url = newUrl(file.name);
+ let actionDelete = "";
+ let actionDownload = "";
+ let actionMove = "";
+ let actionEdit = "";
+ let actionView = "";
+ let isDir = file.path_type.endsWith("Dir");
+ let isParent = (file.name == "..");
+ let isImage = (["png", "jpg", "svg"].includes(url.slice(-3).toLowerCase()));
+ let isDocument = (url.slice(-3).toLowerCase() == "pdf");
+ 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 actionPreview = "";
+ if (isImage && !isDir) {
+ actionPreview = `<div class="thumbnail-preview"><span>[Preview]</span><img class='thumbnail' loading='lazy' src='${url}' /></div>`;
+ }
+ let actionCell = `
+ <td class="cell-actions">
+ ${actionDownload}
+ ${actionView}
+ ${actionMove}
+ <div onclick="duplicate(${index})" class="action-btn" id="duplicateBtn${index}" title="Duplicate">${ICONS.duplicate}</div>
+ ${actionDelete}
+ ${actionEdit}
+ </td>`;
+
+ let sizeDisplay = isDir ? formatDirSize(file.size) : formatFileSize(file.size).join(" ");
+
+ $pathsTableBody.insertAdjacentHTML("beforeend", `
+<tr id="addPath${index}">
+ <td class="path cell-icon ${isImage ? 'has-preview' : ''}">
+ ${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>${actionPreview}
+${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() {
+ const dropZone = document.getElementById("filedrop");
+ dropZone.addEventListener("dragenter", e => {
+ e.stopPropagation();
+
+ if (!e.dataTransfer.getData("text/plain")) {
+ dropZone.classList.add("dragoverfile");
+ }
+ });
+ dropZone.addEventListener("dragleave", e => {
+ e.stopPropagation();
+
+ dropZone.classList.remove("dragoverfile");
+ });
+
+ ["dragend", "dragleave"].forEach(name => {
+ document.body.addEventListener(name, e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ("dragging" in dropZone.classList) {
+ dropZone.classList.remove("dragging");
+ }
+ });
+ });
+
+ ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(name => {
+ dropZone.addEventListener(name, e => {
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ });
+ dropZone.addEventListener("drop", async e => {
+ dropZone.classList.remove("dragging");
+
+ if (!e.dataTransfer.items[0].webkitGetAsEntry) {
+ const files = Array.from(e.dataTransfer.files).filter(v => v.size > 0);
+ for (const file of files) {
+ new Uploader(file, []).upload();
+ }
+ } else {
+ const entries = [];
+ const len = e.dataTransfer.items.length;
+ for (let i = 0; i < len; i++) {
+ entries.push(e.dataTransfer.items[i].webkitGetAsEntry());
+ }
+ addFileEntries(entries, []);
+ }
+ });
+}
+
+async function setupAuth() {
+ if (DATA.user) {
+ $logoutBtn.classList.remove("hidden");
+ $logoutBtn.addEventListener("click", logout);
+ $userName.textContent = DATA.user;
+ } else {
+ $loginBtn.classList.remove("hidden");
+ $loginBtn.addEventListener("click", async () => {
+ try {
+ await checkAuth();
+ } catch {}
+ location.reload();
+ });
+ }
+}
+
+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;
+ }
+}
+
+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();
+ }
+ });
+}
+
+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);
+ });
+}
+
+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);
+ });
+}
+
+function setupGridButton() {
+ const $gridButton = document.querySelector(".grid-btn");
+ $gridButton.classList.remove("hidden");
+ $gridButton.addEventListener("click", () => {
+ const $table = document.querySelector(".paths-table");
+ $table.classList.toggle("grid-mode");
+ });
+}
+
+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}`);
+ }
+}
+
+/**
+* Delete path
+* @param {number} index
+* @returns
+*/
+async function deletePath(index) {
+ const file = DATA.paths[index];
+ if (!file) return;
+ await doDeletePath(file.name, newUrl(file.name), () => {
+ document.getElementById(`addPath${index}`)?.remove();
+ DATA.paths[index] = null;
+ if (!DATA.paths.find(v => !!v)) {
+ $pathsTable.classList.add("hidden");
+ $emptyFolder.textContent = DIR_EMPTY_NOTE;
+ $emptyFolder.classList.remove("hidden");
+ }
+ });
+}
+
+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}`);
+ }
+}
+
+/**
+* Move path
+* @param {number} index
+* @returns
+*/
+async function movePath(index) {
+ const file = DATA.paths[index];
+ if (!file) return;
+ const fileUrl = newUrl(file.name);
+ const newFileUrl = await doMovePath(fileUrl);
+ if (newFileUrl) {
+ location.href = newFileUrl.split("/").slice(0, -1).join("/");
+ }
+}
+
+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}`);
+ }
+}
+
+
+/**
+* Save editor change
+*/
+async function saveChange() {
+ try {
+ await fetch(baseUrl(), {
+ method: "PUT",
+ body: $editor.value,
+ });
+ location.reload();
+ } catch (err) {
+ alert(`Failed to save file, ${err.message}`);
+ }
+}
+
+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();
+}
+
+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();
+}
+
+/**
+* Create a folder
+* @param {string} name
+*/
+async function createFolder(name) {
+ const url = newUrl(name);
+ try {
+ await checkAuth();
+ const res = await fetch(url, {
+ method: "MKCOL",
+ });
+ await assertResOK(res);
+ location.href = url;
+ } catch (err) {
+ alert(`Cannot create folder \`${name}\`, ${err.message}`);
+ }
+}
+
+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}`);
+ }
+}
+
+async function addFileEntries(entries, dirs) {
+ for (const entry of entries) {
+ if (entry == null) {
+ // this is the case if the user has dragged a file into the file upload area
+ // this can be made unlikely with fun css
+ return
+ } else if (entry.isFile) {
+ entry.file(file => {
+ new Uploader(file, dirs).upload();
+ });
+ } else if (entry.isDirectory) {
+ const dirReader = entry.createReader();
+
+ const successCallback = entries => {
+ if (entries.length > 0) {
+ addFileEntries(entries, [...dirs, entry.name]);
+ dirReader.readEntries(successCallback);
+ }
+ };
+
+ dirReader.readEntries(successCallback);
+ }
+ }
+}
+function newUrl(name) {
+ let url = baseUrl();
+ if (!url.endsWith("/")) url += "/";
+ url += name.split("/").map(encodeURIComponent).join("/");
+ return url;
+}
+
+function baseUrl() {
+ return location.href.split(/[?#]/)[0];
+}
+
+function baseName(url) {
+ return decodeURIComponent(url.split("/").filter(v => v.length > 0).slice(-1)[0]);
+}
+
+function extName(filename) {
+ const dotIndex = filename.lastIndexOf('.');
+
+ if (dotIndex === -1 || dotIndex === 0 || dotIndex === filename.length - 1) {
+ return '';
+ }
+
+ return filename.substring(dotIndex);
+}
+
+function getPathSvg(path_type) {
+ switch (path_type) {
+ case "Dir":
+ return ICONS.dir;
+ case "SymlinkFile":
+ return ICONS.symlinkFile;
+ case "SymlinkDir":
+ return ICONS.symlinkDir;
+ default:
+ return ICONS.file;
+ }
+}
+
+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}`;
+}
+
+function padZero(value, size) {
+ return ("0".repeat(size) + value).slice(-1 * size);
+}
+
+function formatDirSize(size) {
+ const unit = size === 1 ? "item" : "items";
+ const num = size >= MAX_SUBPATHS_COUNT ? `>${MAX_SUBPATHS_COUNT - 1}` : `${size}`;
+ return ` ${num} ${unit}`;
+}
+
+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]];
+}
+
+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)}`;
+}
+
+function formatPercent(percent) {
+ if (percent > 10) {
+ return percent.toFixed(1) + "%";
+ } else {
+ return percent.toFixed(2) + "%";
+ }
+}
+
+function encodedStr(rawStr) {
+ return rawStr.replace(/[\u00A0-\u9999<>\&]/g, function (i) {
+ return '&#' + i.charCodeAt(0) + ';';
+ });
+}
+
+async function assertResOK(res) {
+ if (!(res.status >= 200 && res.status < 300)) {
+ throw new Error(await res.text() || `Invalid status ${res.status}`);
+ }
+}
+
+function getEncoding(contentType) {
+ const charset = contentType?.split(";")[1];
+ if (/charset/i.test(charset)) {
+ let encoding = charset.split("=")[1];
+ if (encoding) {
+ return encoding.toLowerCase();
+ }
+ }
+ return 'utf-8';
+}
+
+// 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);
+}
--- /dev/null
+use std::process::Command;
+
+fn main() {
+ Command::new("terser").args(
+ &[
+ "src/js/*.js", "-o", "assets/index.min.js"
+ ]).status().unwrap();
+}
--- /dev/null
+cargo watch -x 'run -- /home/will/Desktop/ -Aa will:test@/:rw' -w src -w assets/index.css -w assets/index.html
--- /dev/null
+/**
+ * Delete path
+ * @param {number} index
+ * @returns
+ */
+async function deletePath(index) {
+ const file = DATA.paths[index];
+ if (!file) return;
+ await doDeletePath(file.name, newUrl(file.name), () => {
+ document.getElementById(`addPath${index}`)?.remove();
+ DATA.paths[index] = null;
+ if (!DATA.paths.find(v => !!v)) {
+ $pathsTable.classList.add("hidden");
+ $emptyFolder.textContent = DIR_EMPTY_NOTE;
+ $emptyFolder.classList.remove("hidden");
+ }
+ });
+}
+
+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}`);
+ }
+}
+
+/**
+ * Move path
+ * @param {number} index
+ * @returns
+ */
+async function movePath(index) {
+ const file = DATA.paths[index];
+ if (!file) return;
+ const fileUrl = newUrl(file.name);
+ const newFileUrl = await doMovePath(fileUrl);
+ if (newFileUrl) {
+ location.href = newFileUrl.split("/").slice(0, -1).join("/");
+ }
+}
+
+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}`);
+ }
+}
+
+async function duplicate (index) {
+ const file = DATA.paths[index];
+ if (!file) return;
+ const fileUrl = newUrl(file.name);
+
+ let fileParts = fileUrl.split("/");
+ let origionalName = fileParts.pop();
+ let lastPart = `Copy%20of%20${origionalName}`;
+
+ let newFile = file;
+ newFile.name = `Copy of ${origionalName}`;
+
+ fileParts.push(lastPart);
+ newFileUrl = fileParts.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: "COPY",
+ headers: {
+ "Destination": newFileUrl,
+ }
+ });
+ await assertResOK(res2);
+
+ window.location.reload(); // This is a bad idea! i think ! it does work tho so im not sure
+
+ } catch (err) {
+ alert(`Cannot copy \`${fileUrl}\` to \`${newFileUrl}\`, ${err.message}`);
+ }
+}
+
+/**
+ * Create a folder
+ * @param {string} name
+ */
+async function createFolder(name) {
+ const url = newUrl(name);
+ try {
+ await checkAuth();
+ const res = await fetch(url, {
+ method: "MKCOL",
+ });
+ await assertResOK(res);
+ location.href = url;
+ } catch (err) {
+ alert(`Cannot create folder \`${name}\`, ${err.message}`);
+ }
+}
+
+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}`);
+ }
+}
--- /dev/null
+const IFRAME_FORMATS = [
+ ".pdf",
+".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg",
+".mp4", ".mov", ".avi", ".wmv", ".flv", ".webm",
+".mp3", ".ogg", ".wav", ".m4a",
+];
+
+const MAX_SUBPATHS_COUNT = 1000;
+
+const ICONS = {
+ dir: `<svg height="16" viewBox="0 0 14 16" width="14"><path fill-rule="evenodd" d="M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM6 4H1V3h5v1z"></path></svg>`,
+ symlinkFile: `<svg height="16" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M8.5 1H1c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V4.5L8.5 1zM11 14H1V2h7l3 3v9zM6 4.5l4 3-4 3v-2c-.98-.02-1.84.22-2.55.7-.71.48-1.19 1.25-1.45 2.3.02-1.64.39-2.88 1.13-3.73.73-.84 1.69-1.27 2.88-1.27v-2H6z"></path></svg>`,
+ symlinkDir: `<svg height="16" viewBox="0 0 14 16" width="14"><path fill-rule="evenodd" d="M13 4H7V3c0-.66-.31-1-1-1H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zM1 3h5v1H1V3zm6 9v-2c-.98-.02-1.84.22-2.55.7-.71.48-1.19 1.25-1.45 2.3.02-1.64.39-2.88 1.13-3.73C4.86 8.43 5.82 8 7.01 8V6l4 3-4 3H7z"></path></svg>`,
+ file: `<svg height="16" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M6 5H2V4h4v1zM2 8h7V7H2v1zm0 2h7V9H2v1zm0 2h7v-1H2v1zm10-7.5V14c0 .55-.45 1-1 1H1c-.55 0-1-.45-1-1V2c0-.55.45-1 1-1h7.5L12 4.5zM11 5L8 2H1v12h10V5z"></path></svg>`,
+ download: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>`,
+ move: `<svg width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"/></svg>`,
+ edit: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>`,
+ delete: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M6.854 7.146a.5.5 0 1 0-.708.708L7.293 9l-1.147 1.146a.5.5 0 0 0 .708.708L8 9.707l1.146 1.147a.5.5 0 0 0 .708-.708L8.707 9l1.147-1.146a.5.5 0 0 0-.708-.708L8 8.293 6.854 7.146z"/><path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/></svg>`,
+ view: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1"/></svg>`,
+ duplicate: `<svg width="16" height="16" viewBox="0 0 16 16"><path d="M 3.6269531 0 C 2.6755659 0 1.9003906 0.78091373 1.9003906 1.7324219 L 1.9003906 12.058594 C 1.9003906 13.010102 2.6755661 13.791016 3.6269531 13.791016 L 3.9667969 13.791016 L 3.9667969 14.267578 C 3.9667969 15.219396 4.7423239 16 5.6933594 16 L 12.554688 16 C 13.505722 16 14.283203 15.219396 14.283203 14.267578 L 14.283203 3.9414062 C 14.283203 2.9895887 13.505722 2.2089844 12.554688 2.2089844 L 12.216797 2.2089844 L 12.216797 1.7324219 C 12.216797 0.7800921 11.439668 -1.4802974e-16 10.488281 0 L 3.6269531 0 z M 3.6269531 0.8984375 L 10.488281 0.8984375 C 10.958544 0.8984375 11.318359 1.2583282 11.318359 1.7324219 L 11.318359 2.1503906 L 5.6503906 2.1503906 C 4.6998277 2.1503906 3.9238281 2.9293513 3.9238281 3.8808594 L 3.9238281 12.892578 L 3.6269531 12.892578 C 3.1566901 12.892578 2.7988281 12.533509 2.7988281 12.058594 L 2.7988281 1.7324219 C 2.7988281 1.2575066 3.1566903 0.8984375 3.6269531 0.8984375 z M 5.6933594 3.109375 L 12.554688 3.109375 C 13.025301 3.109375 13.382812 3.4668003 13.382812 3.9414062 L 13.382812 14.267578 C 13.382812 14.742185 13.025301 15.099609 12.554688 15.099609 L 5.6933594 15.099609 C 5.2227448 15.099609 4.8652344 14.742184 4.8652344 14.267578 L 4.8652344 3.9414062 C 4.8652344 3.4668004 5.2227452 3.109375 5.6933594 3.109375 z"/></svg>`
+}
--- /dev/null
+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");
+ }
+
+ console.log("drag start");
+ });
+
+ e.addEventListener("dragend", (ev) => {
+ if (ev.target.classList.contains("dropzone")) {
+ ev.target.classList.remove("dragover");
+ }
+
+ for (zone of document.getElementsByClassName("internaldrop")) {
+ zone.classList.remove("dragging");
+ zone.classList.remove("dragover");
+ }
+ },);
+}
+
+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),);
+}
+
+var getDrop = function ( method, e ) {
+ return async function doDrop (ev) {
+ ev.stopPropagation();
+ const index = ev.dataTransfer.getData("text/plain");
+
+ if (index == e.dataset.i) {return};
+
+ 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}`);
+ }
+ }
+}
--- /dev/null
+function newUrl(name) {
+ let url = baseUrl();
+ if (!url.endsWith("/")) url += "/";
+ url += name.split("/").map(encodeURIComponent).join("/");
+ return url;
+}
+
+function baseUrl() {
+ return location.href.split(/[?#]/)[0];
+}
+
+function baseName(url) {
+ return decodeURIComponent(url.split("/").filter(v => v.length > 0).slice(-1)[0]);
+}
+
+function extName(filename) {
+ const dotIndex = filename.lastIndexOf('.');
+
+ if (dotIndex === -1 || dotIndex === 0 || dotIndex === filename.length - 1) {
+ return '';
+ }
+
+ return filename.substring(dotIndex);
+}
+
+function getPathSvg(path_type) {
+ switch (path_type) {
+ case "Dir":
+ return ICONS.dir;
+ case "SymlinkFile":
+ return ICONS.symlinkFile;
+ case "SymlinkDir":
+ return ICONS.symlinkDir;
+ default:
+ return ICONS.file;
+ }
+}
+
+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}`;
+}
+
+function padZero(value, size) {
+ return ("0".repeat(size) + value).slice(-1 * size);
+}
+
+function formatDirSize(size) {
+ const unit = size === 1 ? "item" : "items";
+ const num = size >= MAX_SUBPATHS_COUNT ? `>${MAX_SUBPATHS_COUNT - 1}` : `${size}`;
+ return ` ${num} ${unit}`;
+}
+
+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]];
+}
+
+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)}`;
+}
+
+function formatPercent(percent) {
+ if (percent > 10) {
+ return percent.toFixed(1) + "%";
+ } else {
+ return percent.toFixed(2) + "%";
+ }
+}
+
+function encodedStr(rawStr) {
+ return rawStr.replace(/[\u00A0-\u9999<>\&]/g, function (i) {
+ return '&#' + i.charCodeAt(0) + ';';
+ });
+}
+
+async function assertResOK(res) {
+ if (!(res.status >= 200 && res.status < 300)) {
+ throw new Error(await res.text() || `Invalid status ${res.status}`);
+ }
+}
+
+function getEncoding(contentType) {
+ const charset = contentType?.split(";")[1];
+ if (/charset/i.test(charset)) {
+ let encoding = charset.split("=")[1];
+ if (encoding) {
+ return encoding.toLowerCase();
+ }
+ }
+ return 'utf-8';
+}
+
+// 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);
+}
+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();
+}
+
+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();
+}
--- /dev/null
+/**
+* @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 OZVA_MAX_UPLOADINGS = 1;
+
+/**
+* @type {DATA} DATA
+*/
+var DATA;
+
+/**
+* @type {string}
+*/
+var DIR_EMPTY_NOTE;
+
+/**
+* @type {PARAMS}
+* @typedef {object} PARAMS
+* @property {string} q
+* @property {string} sort
+* @property {string} order
+*/
+const PARAMS = Object.fromEntries(new URLSearchParams(window.location.search).entries());
+
+/**
+* @type Map<string, Uploader>
+*/
+const failUploaders = new Map();
+
+/**
+* @type Element
+*/
+let $pathsTable;
+/**
+* @type Element
+*/
+let $pathsTableHead;
+/**
+* @type Element
+*/
+let $pathsTableBody;
+/**
+* @type Element
+*/
+let $uploadersTable;
+/**
+* @type Element
+*/
+let $emptyFolder;
+/**
+* @type Element
+*/
+let $editor;
+/**
+* @type Element
+*/
+let $loginBtn;
+/**
+* @type Element
+*/
+let $logoutBtn;
+/**
+* @type Element
+*/
+let $userName;
+
+// Produce table when window loads
+window.addEventListener("DOMContentLoaded", async () => {
+ const $indexData = document.getElementById('index-data');
+ if (!$indexData) {
+ alert("No data");
+ return;
+ }
+
+ DATA = JSON.parse(decodeBase64($indexData.innerHTML));
+ DIR_EMPTY_NOTE = PARAMS.q ? 'No results' : DATA.dir_exists ? 'Empty folder' : 'Folder will be created when a file is uploaded';
+
+ await ready();
+});
+
+async function ready() {
+ $pathsTable = document.querySelector(".paths-table");
+ $pathsTableHead = document.querySelector(".paths-table thead");
+ $pathsTableBody = document.querySelector(".paths-table tbody");
+ $uploadersTable = document.querySelector(".uploaders-table");
+ $emptyFolder = document.querySelector(".empty-folder");
+ $editor = document.querySelector(".editor");
+ $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 === "Rich") {
+ document.title = `Rich Editor ${DATA.href} - OzVa Cloud`;
+ document.querySelector(".rich-page").classList.remove("hidden");
+
+ await setupRichPage();
+ } else if (DATA.kind === "View") {
+ document.title = `View ${DATA.href} - OzVa Cloud`;
+ document.querySelector(".editor-page").classList.remove("hidden");
+
+ await setupEditorPage();
+ }
+}
+
+/**
+* Add breadcrumb
+* @param {string} href
+* @param {string} uri_prefix
+*/
+function addBreadcrumb(href, uri_prefix) {
+ const $breadcrumb = document.querySelector(".breadcrumb");
+ let parts = [];
+ if (href === "/") {
+ parts = [""];
+ } else {
+ parts = href.split("/");
+ }
+ const len = parts.length;
+ let path = uri_prefix;
+ for (let i = 0; i < len; i++) {
+ const name = parts[i];
+ if (i > 0) {
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
+ path += encodeURIComponent(name);
+ }
+ const encodedName = encodedStr(name);
+ if (i === 0) {
+ $breadcrumb.insertAdjacentHTML("beforeend", `<a href="${path}" title="Root"><svg width="16" height="16" viewBox="0 0 16 16"><path d="M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5z"/></svg></a>`);
+ } else if (i === len - 1) {
+ $breadcrumb.insertAdjacentHTML("beforeend", `<b>${encodedName}</b>`);
+ } else {
+ $breadcrumb.insertAdjacentHTML("beforeend", `<a href="${path}">${encodedName}</a>`);
+ }
+ if (i !== len - 1) {
+ $breadcrumb.insertAdjacentHTML("beforeend", `<span class="separator">/</span>`);
+ }
+ }
+}
+
+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();
+ }
+
+ setupGridButton();
+ renderPathsTableHead();
+ renderPathsTableBody();
+}
+
+/**
+* Render path table thead
+*/
+function renderPathsTableHead() {
+ const headerItems = [
+ {
+ name: "name",
+ props: `colspan="2"`,
+ text: "Name",
+ },
+ {
+ name: "mtime",
+ props: ``,
+ text: "Last Modified",
+ },
+ {
+ name: "size",
+ props: ``,
+ text: "Size",
+ }
+ ];
+ $pathsTableHead.insertAdjacentHTML("beforeend", `
+ <tr>
+ ${headerItems.map(item => {
+ let svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M11.5 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L11 2.707V14.5a.5.5 0 0 0 .5.5zm-7-14a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L4 13.293V1.5a.5.5 0 0 1 .5-.5z"/></svg>`;
+ let order = "desc";
+ if (PARAMS.sort === item.name) {
+ if (PARAMS.order === "desc") {
+ order = "asc";
+ svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/></svg>`
+ } else {
+ svg = `<svg width="12" height="12" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"/></svg>`
+ }
+ }
+ const qs = new URLSearchParams({ ...PARAMS, order, sort: item.name }).toString();
+ const icon = `<span>${svg}</span>`
+ return `<th class="cell-${item.name}" ${item.props}><a href="?${qs}">${item.text}${icon}</a></th>`
+ }).join("\n")}
+ <th class="cell-actions">Actions</th>
+ </tr>
+ `);
+}
+
+/**
+* Render path table tbody
+*/
+function renderPathsTableBody() {
+ if (DATA.paths && DATA.paths.length > 0) {
+ const len = DATA.paths.length;
+ if (len > 0) {
+ $pathsTable.classList.remove("hidden");
+ }
+ for (let i = 0; i < len; i++) {
+ addPath(DATA.paths[i], i);
+ }
+ } else {
+ $emptyFolder.textContent = DIR_EMPTY_NOTE;
+ $emptyFolder.classList.remove("hidden");
+ }
+}
+
+/**
+* Add pathitem
+* @param {PathItem} file
+* @param {number} index
+*/
+function addPath(file, index) {
+ const encodedName = encodedStr(file.name);
+ let url = newUrl(file.name);
+ let actionDelete = "";
+ let actionDownload = "";
+ let actionMove = "";
+ let actionEdit = "";
+ let actionView = "";
+ let isDir = file.path_type.endsWith("Dir");
+ let isParent = (file.name == "..");
+ let isImage = (["png", "jpg", "svg"].includes(url.slice(-3).toLowerCase()));
+ let isDocument = (url.slice(-3).toLowerCase() == "pdf");
+ 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 actionPreview = "";
+ if (isImage && !isDir) {
+ actionPreview = `<div class="thumbnail-preview"><span>[Preview]</span><img class='thumbnail' loading='lazy' src='${url}' /></div>`;
+ }
+ let actionCell = `
+ <td class="cell-actions">
+ ${actionDownload}
+ ${actionView}
+ ${actionMove}
+ <div onclick="duplicate(${index})" class="action-btn" id="duplicateBtn${index}" title="Duplicate">${ICONS.duplicate}</div>
+ ${actionDelete}
+ ${actionEdit}
+ </td>`;
+
+ let sizeDisplay = isDir ? formatDirSize(file.size) : formatFileSize(file.size).join(" ");
+
+ $pathsTableBody.insertAdjacentHTML("beforeend", `
+<tr id="addPath${index}">
+ <td class="path cell-icon ${isImage ? 'has-preview' : ''}">
+ ${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>${actionPreview}
+${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>`);
+}
+
+/**
+* Save editor change
+*/
+async function saveChange() {
+ try {
+ await fetch(baseUrl(), {
+ method: "PUT",
+ body: $editor.value,
+ });
+ location.reload();
+ } catch (err) {
+ alert(`Failed to save file, ${err.message}`);
+ }
+}
+
+async function addFileEntries(entries, dirs) {
+ 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);
+ }
+ }
+}
--- /dev/null
+function setupDropzone() {
+ const dropZone = document.getElementById("filedrop");
+ dropZone.addEventListener("dragenter", e => {
+ e.stopPropagation();
+
+ if (!e.dataTransfer.getData("text/plain")) {
+ dropZone.classList.add("dragoverfile");
+ }
+ });
+ dropZone.addEventListener("dragleave", e => {
+ e.stopPropagation();
+
+ dropZone.classList.remove("dragoverfile");
+ });
+
+ ["dragend", "dragleave"].forEach(name => {
+ document.body.addEventListener(name, e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ("dragging" in dropZone.classList) {
+ dropZone.classList.remove("dragging");
+ }
+ });
+ });
+
+ ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"].forEach(name => {
+ dropZone.addEventListener(name, e => {
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ });
+ dropZone.addEventListener("drop", async e => {
+ dropZone.classList.remove("dragging");
+
+ if (!e.dataTransfer.items[0].webkitGetAsEntry) {
+ const files = Array.from(e.dataTransfer.files).filter(v => v.size > 0);
+ for (const file of files) {
+ new Uploader(file, []).upload();
+ }
+ } else {
+ const entries = [];
+ const len = e.dataTransfer.items.length;
+ for (let i = 0; i < len; i++) {
+ entries.push(e.dataTransfer.items[i].webkitGetAsEntry());
+ }
+ addFileEntries(entries, []);
+ }
+ });
+}
+
+async function setupAuth() {
+ if (DATA.user) {
+ $logoutBtn.classList.remove("hidden");
+ $logoutBtn.addEventListener("click", logout);
+ $userName.textContent = DATA.user;
+ } else {
+ $loginBtn.classList.remove("hidden");
+ $loginBtn.addEventListener("click", async () => {
+ try {
+ await checkAuth();
+ } catch {}
+ location.reload();
+ });
+ }
+}
+
+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;
+ }
+}
+
+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();
+ }
+ });
+}
+
+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);
+ });
+}
+
+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);
+ });
+}
+
+function setupGridButton() {
+ const $gridButton = document.querySelector(".grid-btn");
+ $gridButton.classList.remove("hidden");
+ $gridButton.addEventListener("click", () => {
+ const $table = document.querySelector(".paths-table");
+ $table.classList.toggle("grid-mode");
+ });
+}
+
+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}`);
+ }
+}
--- /dev/null
+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();
+ }
+ }
+
+ Uploader.globalIdx = 0;
+
+ Uploader.runnings = 0;
+
+ Uploader.auth = false;
+
+ /**
+ * @type Uploader[]
+ */
+ Uploader.queues = [];
+
+
+ Uploader.runQueue = async () => {
+ 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();
+ }
const INDEX_HTML: &str = include_str!("../assets/index.html");
const INDEX_CSS: &str = include_str!("../assets/index.css");
-const INDEX_JS: &str = include_str!("../assets/index.js");
+const INDEX_JS: &str = include_str!("../assets/index.min.js");
const FAVICON_ICO: &[u8] = include_bytes!("../assets/favicon.ico");
const INDEX_NAME: &str = "index.html";
const BUF_SIZE: usize = 65536;