feat: filebrowser with encfs

This commit is contained in:
2024-09-03 00:26:36 +08:00
parent 14024c56a3
commit b0b9f70129
330 changed files with 44745 additions and 87 deletions

80
.gitignore vendored
View File

@@ -1,51 +1,43 @@
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
*.db
*.bak
_old
rice-box.go
.idea/
/filebrowser
/filebrowser.exe
/dist
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# ---> macOS
# General
.DS_Store
.AppleDouble
.LSOverride
node_modules
# Icon must end with two \r
Icon
# local env files
.env.local
.env.*.local
# Thumbnails
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
bin/
build/
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
# Vue distributable files
/frontend/dist/*
!/frontend/dist/.gitkeep
# Playwright files
/frontend/test-results/
/frontend/playwright-report/
/frontend/playwright/.cache/
default.nix
Dockerfile.dev

819
CHANGELOG.md Normal file
View File

@@ -0,0 +1,819 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [2.31.1](https://github.com/filebrowser/filebrowser/compare/v2.31.0...v2.31.1) (2024-08-30)
### Bug Fixes
* command not found in shell ([#3438](https://github.com/filebrowser/filebrowser/issues/3438)) ([121d9ab](https://github.com/filebrowser/filebrowser/commit/121d9abecdc7d4e923cfc5023519995938a6ccae))
### Build
* update to alpine 3.20 ([#3447](https://github.com/filebrowser/filebrowser/issues/3447)) ([7de6bc4](https://github.com/filebrowser/filebrowser/commit/7de6bc4a912b5734dd0df02ed8391e78619e2615))
## [2.31.0](https://github.com/filebrowser/filebrowser/compare/v2.30.0...v2.31.0) (2024-08-29)
### Features
* add Czech translation ([#3416](https://github.com/filebrowser/filebrowser/issues/3416)) ([8e67a12](https://github.com/filebrowser/filebrowser/commit/8e67a12f260caefcbe419c2281025b9b15f02bf3))
* Added epub preview. Resolves [#3375](https://github.com/filebrowser/filebrowser/issues/3375) ([#3376](https://github.com/filebrowser/filebrowser/issues/3376)) ([99a6382](https://github.com/filebrowser/filebrowser/commit/99a6382b320874e94f9bd74708f46dd9a7485d3c))
* implement markdown file preview in Ace editor ([#3431](https://github.com/filebrowser/filebrowser/issues/3431)) ([b0f4604](https://github.com/filebrowser/filebrowser/commit/b0f4604f44e6a35e07df3000f106f523cd942cfc))
* support mime type for epub extension ([#3425](https://github.com/filebrowser/filebrowser/issues/3425)) ([f6f7e5f](https://github.com/filebrowser/filebrowser/commit/f6f7e5fea3ff7073ee652008a51cb5445a6f3d5d))
### Bug Fixes
* clipboard copy in safari ([#3261](https://github.com/filebrowser/filebrowser/issues/3261)) ([1fccc5d](https://github.com/filebrowser/filebrowser/commit/1fccc5d649add2a56c55e75cf9dec4851e6d7cbf))
* CSS selectors for listing icons ([#3277](https://github.com/filebrowser/filebrowser/issues/3277)) ([2a90cdf](https://github.com/filebrowser/filebrowser/commit/2a90cdfdaff8655c7cb1167c01994a0978dece8f))
* fix catalan i18n file ([090272e](https://github.com/filebrowser/filebrowser/commit/090272e3b7c56a940c4aa2d28f860c574aa17d53))
* fixing an issue where the upload indicator would "jump" around in the UI ([#3354](https://github.com/filebrowser/filebrowser/issues/3354)) ([7be5644](https://github.com/filebrowser/filebrowser/commit/7be564495226bc6846289a56edb8893511036c6e))
* **frontend:** N files selected hint use i18n ([#3390](https://github.com/filebrowser/filebrowser/issues/3390)) ([10bf3cf](https://github.com/filebrowser/filebrowser/commit/10bf3cffbf8eb7d95fe4e1cc6acf1012329744b9))
* pdf preview header ([#3274](https://github.com/filebrowser/filebrowser/issues/3274)) ([a838868](https://github.com/filebrowser/filebrowser/commit/a8388689f3019083f263845900f683ddc13884dc))
* pull down to refresh within editor ([#3378](https://github.com/filebrowser/filebrowser/issues/3378)) ([21783ed](https://github.com/filebrowser/filebrowser/commit/21783ed91a13ad52afdb411e43faf14fb6ef6e42))
### Build
* bump go libs ([b596567](https://github.com/filebrowser/filebrowser/commit/b596567c6163d57eaefbf3e30d84cfca65c24cdf))
* bump go version to 1.23.0 ([364fdaa](https://github.com/filebrowser/filebrowser/commit/364fdaaf0c1eace82ff8637d337cc1b32e5e9972))
* bump golangci-lint to 1.60.3 ([a6347c8](https://github.com/filebrowser/filebrowser/commit/a6347c88586e584b4565277b0010fa9ff2576b1f))
* **deps-dev:** bump braces from 3.0.2 to 3.0.3 in /frontend ([#3316](https://github.com/filebrowser/filebrowser/issues/3316)) ([e8589be](https://github.com/filebrowser/filebrowser/commit/e8589be6409a2b29edd44ee2edd3fbf6b2d72724))
* **deps-dev:** bump ws from 8.16.0 to 8.17.1 in /frontend ([#3321](https://github.com/filebrowser/filebrowser/issues/3321)) ([c3465f9](https://github.com/filebrowser/filebrowser/commit/c3465f99136506d51b813be4f31b289e708da0ce))
* **deps:** bump golang.org/x/image from 0.15.0 to 0.18.0 ([#3335](https://github.com/filebrowser/filebrowser/issues/3335)) ([30a8ddf](https://github.com/filebrowser/filebrowser/commit/30a8ddf113862e3de2c09547662b7f2af8a30dfe))
* fix goreleaser file ([056cfa8](https://github.com/filebrowser/filebrowser/commit/056cfa8facdca4c397a6b245028d4c9d3f0ca518))
## [2.30.0](https://github.com/filebrowser/filebrowser/compare/v2.29.0...v2.30.0) (2024-05-19)
### Features
* allow multi-select with SHIFT key in singleClick mode ([#3185](https://github.com/filebrowser/filebrowser/issues/3185)) ([2e47a03](https://github.com/filebrowser/filebrowser/commit/2e47a038d63de8f848b070578c1d71f765438a24))
* Enhance MIME Type Detection for Additional File Extensions ([#3183](https://github.com/filebrowser/filebrowser/issues/3183)) ([be62f56](https://github.com/filebrowser/filebrowser/commit/be62f56782551e17d6d5dc23bc29cc56ef961a66))
### Bug Fixes
* add overlay for sidebar on mobile ([#3197](https://github.com/filebrowser/filebrowser/issues/3197)) ([3b48f75](https://github.com/filebrowser/filebrowser/commit/3b48f75301287fe94cbbacff184b4db03f37f7ea))
* current folder name in page title ([#3200](https://github.com/filebrowser/filebrowser/issues/3200)) ([e336a25](https://github.com/filebrowser/filebrowser/commit/e336a25ad29ed8b956169d426992860a877ee551))
* Fixing the inability to play MKV video files online and enhancing the auxiliary features of the VideoPlayer. ([#3181](https://github.com/filebrowser/filebrowser/issues/3181)) ([782375b](https://github.com/filebrowser/filebrowser/commit/782375b1cb4c4f954468c30ec277ce021c82b40d))
* shell window size ([#3198](https://github.com/filebrowser/filebrowser/issues/3198)) ([4c5b612](https://github.com/filebrowser/filebrowser/commit/4c5b612cb2563817f9da50413c7cf9e89b4c4d4a))
* The file type icon in the file list is sensitive to the case of the suffix name ([#3187](https://github.com/filebrowser/filebrowser/issues/3187)) ([a9c327c](https://github.com/filebrowser/filebrowser/commit/a9c327cc0687796a3c7bfafd4ddabf4342859e31))
## [2.29.0](https://github.com/filebrowser/filebrowser/compare/v2.28.0...v2.29.0) (2024-04-30)
### Features
* Display Upload Progress as Percentage and File Size / Total File Size ([#3111](https://github.com/filebrowser/filebrowser/issues/3111)) ([236ca63](https://github.com/filebrowser/filebrowser/commit/236ca637f99e373adfeaaefc5db6af50bd15b6bf))
* migrate to vue 3 ([#2689](https://github.com/filebrowser/filebrowser/issues/2689)) ([5100e58](https://github.com/filebrowser/filebrowser/commit/5100e587d73831ecdb5e3bd35a78fef96ad248a4))
### Bug Fixes
* abort upload behavior to properly handle server-side deletion and frontend state reset ([#3114](https://github.com/filebrowser/filebrowser/issues/3114)) ([434e49b](https://github.com/filebrowser/filebrowser/commit/434e49bf59e4ddf7ec90893fa3fd53faee8c9cbb))
* apply proper zindex to modal dialogs ([#3172](https://github.com/filebrowser/filebrowser/issues/3172)) ([821f51e](https://github.com/filebrowser/filebrowser/commit/821f51ea5ad1f5c2eb72441bc761031cacee43e1))
* correct list item selector ([#3126](https://github.com/filebrowser/filebrowser/issues/3126)) ([#3147](https://github.com/filebrowser/filebrowser/issues/3147)) ([22a05e1](https://github.com/filebrowser/filebrowser/commit/22a05e1f02a083cf7b630e16873dad0de89b7854))
* don't redirect to login when no auth ([#3165](https://github.com/filebrowser/filebrowser/issues/3165)) ([da5a6e0](https://github.com/filebrowser/filebrowser/commit/da5a6e051faa80134c2adf4e621426cbdf046c88))
* Frontend bug, administrators unable to delete users ([#3170](https://github.com/filebrowser/filebrowser/issues/3170)) ([bee71d9](https://github.com/filebrowser/filebrowser/commit/bee71d93fee137cdd807cd8f7716c7da0830fae7))
* handle quotes in healthcheck.sh ([#3130](https://github.com/filebrowser/filebrowser/issues/3130)) ([18f04a7](https://github.com/filebrowser/filebrowser/commit/18f04a7d26186927f51f46354f3b2164a68f1b41))
* the copy method in clipboard.ts ([#3177](https://github.com/filebrowser/filebrowser/issues/3177)) ([4786187](https://github.com/filebrowser/filebrowser/commit/4786187852b8eef07e40aa00cd159ccc1e7e79dc))
### Build
* bump go version to 1.22.1 ([bbd0abb](https://github.com/filebrowser/filebrowser/commit/bbd0abbdfdbb3ddf3326247b7c6d925751dfabcb))
* bump go version to 1.22.2 ([#3158](https://github.com/filebrowser/filebrowser/issues/3158)) ([a9da7fd](https://github.com/filebrowser/filebrowser/commit/a9da7fd56c849b5a13133136b35ef5ebee622962))
* **deps:** bump golang.org/x/net from 0.22.0 to 0.23.0 ([#3133](https://github.com/filebrowser/filebrowser/issues/3133)) ([6b77b8d](https://github.com/filebrowser/filebrowser/commit/6b77b8d683f7357ef71af678550e78910c10ddeb))
## [2.28.0](https://github.com/filebrowser/filebrowser/compare/v2.27.0...v2.28.0) (2024-04-01)
### Features
* allow to configure if home directory is automatically created from cli ([#2963](https://github.com/filebrowser/filebrowser/issues/2963)) ([a4b089a](https://github.com/filebrowser/filebrowser/commit/a4b089a6dbf9821ecede428cd7d13e69c8b85231))
* auto hiding header bar in preview to enlarge the preview window ([#3024](https://github.com/filebrowser/filebrowser/issues/3024)) ([d706506](https://github.com/filebrowser/filebrowser/commit/d70650689c34ce9f631fda6a453fd521faef22fa))
* close editor when click escape key ([#2947](https://github.com/filebrowser/filebrowser/issues/2947)) ([70c8261](https://github.com/filebrowser/filebrowser/commit/70c826133b8578b8712e6db8f762a15a076cd9a9))
* enable preview in shared folder ([#3055](https://github.com/filebrowser/filebrowser/issues/3055)) ([4c233c3](https://github.com/filebrowser/filebrowser/commit/4c233c3db39ea5a00d6e602ec0ecbddecb590877))
* focus editor when opened ([#2946](https://github.com/filebrowser/filebrowser/issues/2946)) ([b19710e](https://github.com/filebrowser/filebrowser/commit/b19710efca6daa7af56dc211d0051d500d2eea22))
* freezing the list in the backgroud while previewing a file ([#3004](https://github.com/filebrowser/filebrowser/issues/3004)) ([e167c3e](https://github.com/filebrowser/filebrowser/commit/e167c3e1efed8b16be45d994a8d443fda1d8cf49))
* prompt to confirm discard editor changes ([#2948](https://github.com/filebrowser/filebrowser/issues/2948)) ([fb1a09c](https://github.com/filebrowser/filebrowser/commit/fb1a09c7c172b913c12b30975ca545e505df0c05))
* select multiple files with ctrl even with singleClick option ([#2953](https://github.com/filebrowser/filebrowser/issues/2953)) ([d49c3df](https://github.com/filebrowser/filebrowser/commit/d49c3dfacfc0ff07e620b3ad2700e64927b06235))
### Bug Fixes
* dashboard buttons position in rtl layout ([#2949](https://github.com/filebrowser/filebrowser/issues/2949)) ([2cfee21](https://github.com/filebrowser/filebrowser/commit/2cfee2183c98d0cb67fc4e9788644ed4278e25bc))
* editor discard prompt ([#2990](https://github.com/filebrowser/filebrowser/issues/2990)) ([34a0817](https://github.com/filebrowser/filebrowser/commit/34a08170c894321d49bb843e259a0e59e2245998))
* files and directories are created with the correct permissions ([#2966](https://github.com/filebrowser/filebrowser/issues/2966)) ([5c5ab6b](https://github.com/filebrowser/filebrowser/commit/5c5ab6b8750a5168f0ae2a26bd5de41e0b6d9637))
* fix lint warnings ([#2976](https://github.com/filebrowser/filebrowser/issues/2976)) ([fe5ca74](https://github.com/filebrowser/filebrowser/commit/fe5ca74aa1e4257e5cb36f1de58daa0c3548319f))
* **healthcheck:** use address configured if not empty ([#2938](https://github.com/filebrowser/filebrowser/issues/2938)) ([81cd8fc](https://github.com/filebrowser/filebrowser/commit/81cd8fc6d307b00af278beefcdbad4158a128fea))
* keyboard shortcut to confirm prompts ([#2932](https://github.com/filebrowser/filebrowser/issues/2932)) ([ff9502f](https://github.com/filebrowser/filebrowser/commit/ff9502ff34790c46f31d175911cd51c9b62804fb))
* moment locale ([#2952](https://github.com/filebrowser/filebrowser/issues/2952)) ([883383a](https://github.com/filebrowser/filebrowser/commit/883383a5715d82883c51138dfb547805dfad2a3c))
* shell direction ([#2980](https://github.com/filebrowser/filebrowser/issues/2980)) ([6d7ba65](https://github.com/filebrowser/filebrowser/commit/6d7ba65faf576ee4ed095f3d0c41775b21e498de))
* stay in the same position after renaming or deleting ([#3039](https://github.com/filebrowser/filebrowser/issues/3039)) ([cdf8def](https://github.com/filebrowser/filebrowser/commit/cdf8def3304315bef261da7f52f8599d90b1f0f0))
### Build
* **deps-dev:** bump vite from 4.4.12 to 4.5.2 in /frontend ([#2951](https://github.com/filebrowser/filebrowser/issues/2951)) ([bf36cc0](https://github.com/filebrowser/filebrowser/commit/bf36cc00f1369dd10a422f230ccabcbeefae1517))
* **deps:** bump google.golang.org/protobuf from 1.31.0 to 1.33.0 ([#3045](https://github.com/filebrowser/filebrowser/issues/3045)) ([05bfae2](https://github.com/filebrowser/filebrowser/commit/05bfae264a7a477d1b7db582f06f4efb24d26ec9))
* **deps:** bump google.golang.org/protobuf in /tools ([#3044](https://github.com/filebrowser/filebrowser/issues/3044)) ([7797a4e](https://github.com/filebrowser/filebrowser/commit/7797a4ef18038a877df31bd34f2ebf70d18823f8))
## [2.27.0](https://github.com/filebrowser/filebrowser/compare/v2.26.0...v2.27.0) (2024-01-02)
### Features
* allow setting theme via cli ([#2881](https://github.com/filebrowser/filebrowser/issues/2881)) ([748af71](https://github.com/filebrowser/filebrowser/commit/748af7172ce96f0b66c394e88839bd57c194ffc7))
* display image resolutions in file details ([#2830](https://github.com/filebrowser/filebrowser/issues/2830)) ([a09dfa8](https://github.com/filebrowser/filebrowser/commit/a09dfa8d9f190243d811a841de44c4abb4403d87))
* make user session timeout configurable by flags ([#2845](https://github.com/filebrowser/filebrowser/issues/2845)) ([391a078](https://github.com/filebrowser/filebrowser/commit/391a078cd486e618c95a0c5850326076cbc025b6))
### Bug Fixes
* delete message when delete file from preview ([3264cea](https://github.com/filebrowser/filebrowser/commit/3264cea8307dca9ab5463dc81f2a10a817eb3d54))
* fix typo ([#2843](https://github.com/filebrowser/filebrowser/issues/2843)) ([4dbc802](https://github.com/filebrowser/filebrowser/commit/4dbc802972c930f5f42fc27507fac35c28c42afd))
* set correct port in docker healthcheck ([#2812](https://github.com/filebrowser/filebrowser/issues/2812)) ([d59ad59](https://github.com/filebrowser/filebrowser/commit/d59ad594b8649f57f61453b0dfbc350c57b690a2))
* typo in build error [#2903](https://github.com/filebrowser/filebrowser/issues/2903) ([#2904](https://github.com/filebrowser/filebrowser/issues/2904)) ([c4e955a](https://github.com/filebrowser/filebrowser/commit/c4e955acf4a1a8f8e8e94f697ffc838515e69a60))
### Build
* **deps-dev:** bump vite from 4.4.9 to 4.4.12 in /frontend ([#2862](https://github.com/filebrowser/filebrowser/issues/2862)) ([fc2ee37](https://github.com/filebrowser/filebrowser/commit/fc2ee373536584d024f7def62f350bdbb712d927))
* **deps:** bump golang.org/x/crypto from 0.14.0 to 0.17.0 ([#2890](https://github.com/filebrowser/filebrowser/issues/2890)) ([821fba4](https://github.com/filebrowser/filebrowser/commit/821fba41a25ba99d47641f01b10ac51960157888))
## [2.26.0](https://github.com/filebrowser/filebrowser/compare/v2.25.0...v2.26.0) (2023-11-02)
### Features
* add modern greek translation ([#2778](https://github.com/filebrowser/filebrowser/issues/2778)) ([c3079d3](https://github.com/filebrowser/filebrowser/commit/c3079d30e22385d7e677f172324cd9cbab6487ce))
* make user session timeout configurable ([#2753](https://github.com/filebrowser/filebrowser/issues/2753)) ([7fabadc](https://github.com/filebrowser/filebrowser/commit/7fabadc871ea91ea22fe9454e2ca4b33e5c211be))
### Bug Fixes
* avoid the front-end calling api/renew loop ([#2792](https://github.com/filebrowser/filebrowser/issues/2792)) ([edd808f](https://github.com/filebrowser/filebrowser/commit/edd808f124f4ada99bcbe4bca98ddbe20e5a424c))
* disable static resource files listing ([da1fe7c](https://github.com/filebrowser/filebrowser/commit/da1fe7c9d76a9c6a25bfa19ebd6cf8023eff5d62))
* display file size as base 2 (KiB instead of KB) ([#2779](https://github.com/filebrowser/filebrowser/issues/2779)) ([cdcd9a3](https://github.com/filebrowser/filebrowser/commit/cdcd9a313aa50c2e6806a182b6838462d42dcafe))
* goreleaser yaml ([4d0a68e](https://github.com/filebrowser/filebrowser/commit/4d0a68e7875274f4c939f2bfa15739a9b0ecf70a))
* revert fetchURL changes in auth (Fixes [#2729](https://github.com/filebrowser/filebrowser/issues/2729)) ([#2739](https://github.com/filebrowser/filebrowser/issues/2739)) ([bd3c194](https://github.com/filebrowser/filebrowser/commit/bd3c1941ff8289a5dae877e08f7e25fa9b2a92c5))
* solve docker build failed issue ([#2797](https://github.com/filebrowser/filebrowser/issues/2797)) ([6a31af6](https://github.com/filebrowser/filebrowser/commit/6a31af6c0a144128af865d802c8039fa5250e946))
### Build
* **deps-dev:** bump postcss from 8.4.27 to 8.4.31 in /frontend ([#2749](https://github.com/filebrowser/filebrowser/issues/2749)) ([21d361a](https://github.com/filebrowser/filebrowser/commit/21d361ad308d109d2a6b323597019aaa09ce1781))
* **deps:** bump @babel/traverse in /frontend ([#2775](https://github.com/filebrowser/filebrowser/issues/2775)) ([bb4bb50](https://github.com/filebrowser/filebrowser/commit/bb4bb508a9d71516e8fa80b3a6285fe002a059d2))
* **deps:** bump golang.org/x/image from 0.5.0 to 0.10.0 ([#2800](https://github.com/filebrowser/filebrowser/issues/2800)) ([a744bd2](https://github.com/filebrowser/filebrowser/commit/a744bd224f0ff1efc53ab94481fa76ef68788df1))
* **deps:** bump golang.org/x/net from 0.11.0 to 0.17.0 ([#2758](https://github.com/filebrowser/filebrowser/issues/2758)) ([d574fb6](https://github.com/filebrowser/filebrowser/commit/d574fb6d1af41ec31778b0f402674e5111a7875d))
* fix deprecated goreleaser config options ([38f7788](https://github.com/filebrowser/filebrowser/commit/38f77882559133b9ff330cfb955a9d4ea4728cf8))
## [2.25.0](https://github.com/filebrowser/filebrowser/compare/v2.24.2...v2.25.0) (2023-09-14)
### Features
* add new folder button to move/create dialogs ([#2667](https://github.com/filebrowser/filebrowser/issues/2667)) ([5994224](https://github.com/filebrowser/filebrowser/commit/599422446849fa37d5ab448bbf464afb7304b99d))
* added shell resizing ([#2648](https://github.com/filebrowser/filebrowser/issues/2648)) ([584b706](https://github.com/filebrowser/filebrowser/commit/584b706b1e310297acc2580c60442ff5c11ae432))
* implement abort upload functionality ([#2673](https://github.com/filebrowser/filebrowser/issues/2673)) ([a404fb0](https://github.com/filebrowser/filebrowser/commit/a404fb043da2573bf04385863b2d34b1f918b8e1))
* implement upload speed calculation and ETA estimation ([#2677](https://github.com/filebrowser/filebrowser/issues/2677)) ([ecdd684](https://github.com/filebrowser/filebrowser/commit/ecdd684bf1d537a4591caa38348102b61dd51e5d))
### Bug Fixes
* refactor path resolution logic for project root ([#2674](https://github.com/filebrowser/filebrowser/issues/2674)) ([95fec7f](https://github.com/filebrowser/filebrowser/commit/95fec7f69430c108e5cf95c428db9d671cd97a94))
* tus upload with cloudflare proxy ([36af01d](https://github.com/filebrowser/filebrowser/commit/36af01daa6e04005ce3d18985eebaeef06f7393d)), closes [#2593](https://github.com/filebrowser/filebrowser/issues/2593)
### Refactorings
* migrate frontend tooling to vite 4 ([#2645](https://github.com/filebrowser/filebrowser/issues/2645)) ([8838a09](https://github.com/filebrowser/filebrowser/commit/8838a09cf5104deac22b6143050588040c6825e6))
### Build
* bump go version to 1.21.0 ([#2672](https://github.com/filebrowser/filebrowser/issues/2672)) ([2c97573](https://github.com/filebrowser/filebrowser/commit/2c97573301a1b13179678fb7f9bd8316539ecdff))
* bump node version to 18 ([#2671](https://github.com/filebrowser/filebrowser/issues/2671)) ([70eba7e](https://github.com/filebrowser/filebrowser/commit/70eba7ecc9d19545c0899ae40eb3897a7c48562f))
### Performance improvements
* **backend:** optimize subtitles detection performance ([#2637](https://github.com/filebrowser/filebrowser/issues/2637)) ([374bbd3](https://github.com/filebrowser/filebrowser/commit/374bbd3ec199fddbe491ab2b74e520a10a73e54b))
### [2.24.2](https://github.com/filebrowser/filebrowser/compare/v2.24.1...v2.24.2) (2023-08-08)
### Bug Fixes
* 403 error error when uploading ([#2598](https://github.com/filebrowser/filebrowser/issues/2598)) ([289c8e6](https://github.com/filebrowser/filebrowser/commit/289c8e6f32eb520cc711389f6b6a4ed94a73ecd4))
* config init for branding.disableUsedPercentage ([#2576](https://github.com/filebrowser/filebrowser/issues/2576)) ([#2596](https://github.com/filebrowser/filebrowser/issues/2596)) ([ff1e0b8](https://github.com/filebrowser/filebrowser/commit/ff1e0b8185faf14b1f8e91830ca5e71e68ab672e))
### Build
* add riscv64 binary releases ([#2587](https://github.com/filebrowser/filebrowser/issues/2587)) ([0ac3968](https://github.com/filebrowser/filebrowser/commit/0ac39684f175487314e97403406f4d2c482e3d79))
### [2.24.1](https://github.com/filebrowser/filebrowser/compare/v2.24.0...v2.24.1) (2023-07-31)
### Bug Fixes
* add directory creation code to partial upload handler ([#2575](https://github.com/filebrowser/filebrowser/issues/2575)) ([#2580](https://github.com/filebrowser/filebrowser/issues/2580)) ([912f27a](https://github.com/filebrowser/filebrowser/commit/912f27a9e3286ee4bf2a27b366a1d35b3b55799c))
* resolved CSS rendering issue in Chrome browser ([#2582](https://github.com/filebrowser/filebrowser/issues/2582)) ([2a4a46c](https://github.com/filebrowser/filebrowser/commit/2a4a46c61a5d5376bea65b28d0eb6a7ec2fdf4e5))
### Build
* **backend:** upgrade golangci-lint to v1.53.3 ([efd41cc](https://github.com/filebrowser/filebrowser/commit/efd41cc4c147b8d2d5e61fb2642df8d934f49362))
## [2.24.0](https://github.com/filebrowser/filebrowser/compare/v2.23.0...v2.24.0) (2023-07-29)
### Features
* add a healthcheck script that works with a dynamic port ([#2510](https://github.com/filebrowser/filebrowser/issues/2510)) ([ff4375c](https://github.com/filebrowser/filebrowser/commit/ff4375cf6ce849459889f892dd91304703c52dcd))
* add a new setting that disables the display of the disk usage ([#2136](https://github.com/filebrowser/filebrowser/issues/2136)) ([428c1c6](https://github.com/filebrowser/filebrowser/commit/428c1c606d1b858ed0eb58b7c31f570bc6a9b792))
* add Hungarian translation ([#2232](https://github.com/filebrowser/filebrowser/issues/2232)) ([11e9202](https://github.com/filebrowser/filebrowser/commit/11e92021607e12efff9fb2d5c8728483eee31199))
* add option to copy download links from shares ([#2442](https://github.com/filebrowser/filebrowser/issues/2442)) ([a4ef02a](https://github.com/filebrowser/filebrowser/commit/a4ef02a47b53742a0ac1f639563b0c67116619c8))
* integrate tus.io for resumable and chunked uploads ([#2145](https://github.com/filebrowser/filebrowser/issues/2145)) ([7b35815](https://github.com/filebrowser/filebrowser/commit/7b35815754690540f76e3ffe114eedb47cfd5c7e))
### Bug Fixes
* added an early return on non-existent items ([#2571](https://github.com/filebrowser/filebrowser/issues/2571)) ([2744f7d](https://github.com/filebrowser/filebrowser/commit/2744f7d5b9106c7c2eec69010e550e0939c23d80))
* build on FreeBSD and non-Linux platforms ([#2332](https://github.com/filebrowser/filebrowser/issues/2332)) ([60d1e2d](https://github.com/filebrowser/filebrowser/commit/60d1e2d2913cce591fbee97337bd58310480269f))
* error while using fallback of dir move ([#2349](https://github.com/filebrowser/filebrowser/issues/2349)) ([853ec90](https://github.com/filebrowser/filebrowser/commit/853ec906efbdee9013c5d34ed1d9b8fee88a6b29))
* filter ANSI color for shell ([#2529](https://github.com/filebrowser/filebrowser/issues/2529)) ([9bcfa90](https://github.com/filebrowser/filebrowser/commit/9bcfa900f904fe683c8d9085947f57932bfe22a0))
* goreleaser docker build ([051104b](https://github.com/filebrowser/filebrowser/commit/051104bfa061720d4402c612e61bb0fc80a946bf))
* solve broken Docker build with alpine image ([#2486](https://github.com/filebrowser/filebrowser/issues/2486)) ([b8ee340](https://github.com/filebrowser/filebrowser/commit/b8ee3404ee480ef1fd439543ab6d46f318ff3647))
* video preview click next or prev button subtitles not update ([#2423](https://github.com/filebrowser/filebrowser/issues/2423)) ([6744cd4](https://github.com/filebrowser/filebrowser/commit/6744cd47cef87e3a76a2190bdf123b6c2197fe6f))
* xss vulnerability in /api/raw ([#2570](https://github.com/filebrowser/filebrowser/issues/2570)) ([#2572](https://github.com/filebrowser/filebrowser/issues/2572)) ([b508ac3](https://github.com/filebrowser/filebrowser/commit/b508ac3d4f7f0f75d6b49c99bdc661a6d2173f30))
### Refactorings
* replace username old focus logic with the autofocus attribute ([#2223](https://github.com/filebrowser/filebrowser/issues/2223)) ([2b2c108](https://github.com/filebrowser/filebrowser/commit/2b2c1085fb50ad68612ad438e527fd316d8aafee))
### Build
* **backend:** bump go version to 1.20.1 ([fa95299](https://github.com/filebrowser/filebrowser/commit/fa95299df4aa7e4c54d872e786a91ded5bdb01c1))
* **backend:** bump go version to 1.20.6 ([9bf6b85](https://github.com/filebrowser/filebrowser/commit/9bf6b856e5411e635ba9102ff53dfe927183848e))
* **deps-dev:** bump word-wrap from 1.2.3 to 1.2.4 in /frontend ([#2556](https://github.com/filebrowser/filebrowser/issues/2556)) ([bb34862](https://github.com/filebrowser/filebrowser/commit/bb3486286c0da112ad97456ad258ddcdfe17c154))
* **deps:** bump minimatch from 3.0.4 to 3.1.2 in /tools ([#2561](https://github.com/filebrowser/filebrowser/issues/2561)) ([a664ba1](https://github.com/filebrowser/filebrowser/commit/a664ba1f9df45c7f6d03492c85466c5aa07c740e))
* **deps:** bump semver from 5.7.1 to 5.7.2 in /tools ([#2546](https://github.com/filebrowser/filebrowser/issues/2546)) ([c2f1423](https://github.com/filebrowser/filebrowser/commit/c2f1423c02e4736f4c243c3164dc671879e065f3))
* remove armv6-s6 docker target ([66dfbb3](https://github.com/filebrowser/filebrowser/commit/66dfbb303cf792b7b01650d0125d948ab8d81ddd))
* remove armv7-s6 docker target ([4d77ce0](https://github.com/filebrowser/filebrowser/commit/4d77ce0955644551f891af3e4098c37e9cc37e40))
## [2.23.0](https://github.com/filebrowser/filebrowser/compare/v2.22.4...v2.23.0) (2022-11-05)
### Features
* add rtl support ([#2178](https://github.com/filebrowser/filebrowser/issues/2178)) ([2c14146](https://github.com/filebrowser/filebrowser/commit/2c14146a314bb271be66a36c63b64852a2848e26))
* hebrew translation ([#2168](https://github.com/filebrowser/filebrowser/issues/2168)) ([a49105d](https://github.com/filebrowser/filebrowser/commit/a49105db1d5f0d8f3d6641940ea86da959ffe006))
* hook authentication method ([dda9a38](https://github.com/filebrowser/filebrowser/commit/dda9a389f387e94643a9a2ae56027260b210152a))
* update Polish translation ([#2089](https://github.com/filebrowser/filebrowser/issues/2089)) ([57c99e0](https://github.com/filebrowser/filebrowser/commit/57c99e0e261b4ed4c2cf468ce3ab09f1a440b359))
### Bug Fixes
* missing video controls on mobile ([#2180](https://github.com/filebrowser/filebrowser/issues/2180)) ([a5757b9](https://github.com/filebrowser/filebrowser/commit/a5757b94e8ed492d454b9e427b7f45824cc56c5c))
* modify the delete confirmation interface logic. ([#2138](https://github.com/filebrowser/filebrowser/issues/2138)) ([0401adf](https://github.com/filebrowser/filebrowser/commit/0401adf7f4dd76760fe26b5baee02ebc726b51a9))
### Build
* **deps:** bump ansi-html and webpack-dev-server in /frontend ([#2184](https://github.com/filebrowser/filebrowser/issues/2184)) ([3a0dace](https://github.com/filebrowser/filebrowser/commit/3a0dace9a93f9d57855801de548891010cf0830e))
* **deps:** bump terser from 4.8.0 to 4.8.1 in /frontend ([#2054](https://github.com/filebrowser/filebrowser/issues/2054)) ([aaed985](https://github.com/filebrowser/filebrowser/commit/aaed985699b3c63092ecb02c8bc07634123360ab))
### [2.22.4](https://github.com/filebrowser/filebrowser/compare/v2.22.3...v2.22.4) (2022-07-18)
### Bug Fixes
* disable cookie auth for non GET requests ([80030de](https://github.com/filebrowser/filebrowser/commit/80030dee32d161043766d57ba4e0ad0b0d99290b))
### Build
* **deps:** bump moment from 2.29.2 to 2.29.4 in /frontend ([#2036](https://github.com/filebrowser/filebrowser/issues/2036)) ([cb43770](https://github.com/filebrowser/filebrowser/commit/cb437700255e41ff559b9f5a99ab4290b2f8df87))
* **deps:** bump shell-quote from 1.7.2 to 1.7.3 in /frontend ([#2025](https://github.com/filebrowser/filebrowser/issues/2025)) ([eaba7e5](https://github.com/filebrowser/filebrowser/commit/eaba7e5255f960141e0fc1557f87073df9f6d66a))
### [2.22.3](https://github.com/filebrowser/filebrowser/compare/v2.22.2...v2.22.3) (2022-07-05)
### Bug Fixes
* use correct field name in user put api ([#2026](https://github.com/filebrowser/filebrowser/issues/2026)) ([d94acdd](https://github.com/filebrowser/filebrowser/commit/d94acdd89a0069fe87107024fd332a0d59a112fc))
### [2.22.2](https://github.com/filebrowser/filebrowser/compare/v2.22.1...v2.22.2) (2022-07-01)
### Bug Fixes
* display disk capacity in a correct format ([#2013](https://github.com/filebrowser/filebrowser/issues/2013)) ([dec3d62](https://github.com/filebrowser/filebrowser/commit/dec3d629d42de567aa708154ebc4e03b5223608c))
* don't calculate usage for files ([#1973](https://github.com/filebrowser/filebrowser/issues/1973)) ([577c0ef](https://github.com/filebrowser/filebrowser/commit/577c0efa9cff13628d5e3bac710ef568a00949e0)), closes [#1972](https://github.com/filebrowser/filebrowser/issues/1972) [#1967](https://github.com/filebrowser/filebrowser/issues/1967)
* preview url building fix ([#1976](https://github.com/filebrowser/filebrowser/issues/1976)) ([dcf0bc6](https://github.com/filebrowser/filebrowser/commit/dcf0bc65bfcfc7df3804d7392598a92019468cf7))
### Build
* **backend:** upgrade golangci-lint to 1.46.2 ([#1991](https://github.com/filebrowser/filebrowser/issues/1991)) ([8118afd](https://github.com/filebrowser/filebrowser/commit/8118afd0ac0d25f4503c98879369764c35e7408e))
### [2.22.1](https://github.com/filebrowser/filebrowser/compare/v2.22.0...v2.22.1) (2022-06-06)
### Bug Fixes
* use correct basepath prefix for preview urls ([#1971](https://github.com/filebrowser/filebrowser/issues/1971)) ([1e7d3b2](https://github.com/filebrowser/filebrowser/commit/1e7d3b25c283c556d98c65f1c2f46db4e4178995))
### Build
* **backend:** bump go version to 1.8.3 ([b16982d](https://github.com/filebrowser/filebrowser/commit/b16982df0f7da9eedb678455298b42ac55c86666))
## [2.22.0](https://github.com/filebrowser/filebrowser/compare/v2.21.1...v2.22.0) (2022-06-03)
### Features
* add branding to the window title ([#1850](https://github.com/filebrowser/filebrowser/issues/1850)) ([f8dfbf7](https://github.com/filebrowser/filebrowser/commit/f8dfbf7eeecf3ee99ce906276777676f44e81e34))
* add disk usage information to the sidebar ([d1d8e3e](https://github.com/filebrowser/filebrowser/commit/d1d8e3e3405381b01317fe07ae729d70219415a7))
* automatically focus username field on login page ([596c732](https://github.com/filebrowser/filebrowser/commit/596c73288f5b53bd7e79ab8046136dc75ff078b9))
* invalid symlink icon ([b14b911](https://github.com/filebrowser/filebrowser/commit/b14b9114f837cacf9f7788e88c503142a81585be))
* page title localization ([8a43413](https://github.com/filebrowser/filebrowser/commit/8a43413f888440dc11b11c509abff45f706033d8))
### Bug Fixes
* allow CSP inline styling ([5da9d74](https://github.com/filebrowser/filebrowser/commit/5da9d74da62c69c431361bcaf0c07dc1da237ea8))
* disable autocapitalize of login input (closes [#1910](https://github.com/filebrowser/filebrowser/issues/1910)) ([aed3af5](https://github.com/filebrowser/filebrowser/commit/aed3af58384697dc3de30f1450b837b0b74e4fa6))
* drag-and-drop folder upload ([e677c78](https://github.com/filebrowser/filebrowser/commit/e677c78471f09f8d2c21d63d7388e908924aa6d9))
* expired token error ([c3bd118](https://github.com/filebrowser/filebrowser/commit/c3bd1188aa396cbf00c593d259a9da0eddeeea3b))
* folder info on upload list ([d1d7b23](https://github.com/filebrowser/filebrowser/commit/d1d7b23da6cc0c9a2f2f3e17021ec4f13ea557dd))
* network error object message ([fc209f6](https://github.com/filebrowser/filebrowser/commit/fc209f64deff7a2793980d11ee738f7140c444cf))
* set correct scope when user home creation is enabled ([02730bb](https://github.com/filebrowser/filebrowser/commit/02730bb9bfa3bfbfa251bb4736fc4c08d33609ab))
### Build
* **backend:** bump dependency versions ([7c9a75e](https://github.com/filebrowser/filebrowser/commit/7c9a75e72588f92d58fb58d32cdac352bce73b20))
* **deps:** bump async from 2.6.3 to 2.6.4 in /frontend ([#1933](https://github.com/filebrowser/filebrowser/issues/1933)) ([e5fa96b](https://github.com/filebrowser/filebrowser/commit/e5fa96b666eac2e46a02bde832488baca5f2cd6d))
* **deps:** bump eventsource from 1.1.0 to 1.1.1 in /frontend ([dd50369](https://github.com/filebrowser/filebrowser/commit/dd503695a1a8119a631643414d3a9070890f3f3c))
* **deps:** bump minimist from 1.2.5 to 1.2.6 in /frontend ([#1889](https://github.com/filebrowser/filebrowser/issues/1889)) ([a74c72d](https://github.com/filebrowser/filebrowser/commit/a74c72db451207e1275988f3d208fa6d6f0468a9))
* **deps:** bump minimist from 1.2.5 to 1.2.6 in /tools ([#1891](https://github.com/filebrowser/filebrowser/issues/1891)) ([f5b1e10](https://github.com/filebrowser/filebrowser/commit/f5b1e106183fb2192063a72fd195fc8c181ba8f9))
* **deps:** bump moment from 2.29.1 to 2.29.2 in /frontend ([#1900](https://github.com/filebrowser/filebrowser/issues/1900)) ([040584c](https://github.com/filebrowser/filebrowser/commit/040584c86563d869c7a05887ef1f781bce653033))
* **deps:** bump url-parse from 1.5.7 to 1.5.10 in /frontend ([#1841](https://github.com/filebrowser/filebrowser/issues/1841)) ([b2ad3f7](https://github.com/filebrowser/filebrowser/commit/b2ad3f73686a2abaa4fc62963fba6f83c9da9b5e))
* **frontend:** bump node version from 14 to 16 ([ac3ead8](https://github.com/filebrowser/filebrowser/commit/ac3ead8dcef9c64c6be8b5cbbceee143b2cc77a8))
* upgrade go version to 1.18.1 ([6bd34c7](https://github.com/filebrowser/filebrowser/commit/6bd34c76324780c1edd8625d5b22f5a84990852b))
### [2.21.1](https://github.com/filebrowser/filebrowser/compare/v2.21.0...v2.21.1) (2022-02-22)
### Bug Fixes
* display user scope for admin users ([#1834](https://github.com/filebrowser/filebrowser/issues/1834)) ([6366cf0](https://github.com/filebrowser/filebrowser/commit/6366cf0b181f13eac38f69f1760d6f6f0586a5d1))
## [2.21.0](https://github.com/filebrowser/filebrowser/compare/v2.20.1...v2.21.0) (2022-02-21)
### Features
* add colorized file type icons ([2948589](https://github.com/filebrowser/filebrowser/commit/2948589fcde6d1dca7f3ea52a621d8213fa3300c))
* add gallery view mode ([8888b9f](https://github.com/filebrowser/filebrowser/commit/8888b9f44640394df9e3583db4392472d7027a4b))
* add Ukrainian translation / update Russian translation ([#1753](https://github.com/filebrowser/filebrowser/issues/1753)) ([665e458](https://github.com/filebrowser/filebrowser/commit/665e45889cd333f1e3500e4bf38d15d229c9fe2a))
* add upload file list with progress ([#1825](https://github.com/filebrowser/filebrowser/issues/1825)) ([cf85404](https://github.com/filebrowser/filebrowser/commit/cf85404dd25cd7fdd73aa32878b4dc5f85ee3e96))
* smaller column width to fit 2 columns in landscape mobiles ([7870e89](https://github.com/filebrowser/filebrowser/commit/7870e89bc04f1494f2705795476b5f1c9d621e38))
* use real image path to calculate cache key ([c198723](https://github.com/filebrowser/filebrowser/commit/c1987237d05adcce77c614e5247a181ae5cdfacd))
### Bug Fixes
* correctly handle non-ascii passwords for shared resources ([c782f21](https://github.com/filebrowser/filebrowser/commit/c782f21b0fa4511a15e7015117d075eaf5ea332c))
* don't expose scope for non-admin users ([0942fc7](https://github.com/filebrowser/filebrowser/commit/0942fc7042fd949cce91855169d0bcf16eb75771))
* open all the pdf files correctly ([#1742](https://github.com/filebrowser/filebrowser/issues/1742)) ([949f0f2](https://github.com/filebrowser/filebrowser/commit/949f0f277f6004904b3edfa716a8365ec93fa0fa))
### Build
* **deps:** bump browserslist from 4.16.3 to 4.19.1 in /frontend ([8089007](https://github.com/filebrowser/filebrowser/commit/80890075e802e2a4217edbb01d6417122d702f5e))
* **deps:** bump dns-packet from 1.3.1 to 1.3.4 in /frontend ([a73d7f1](https://github.com/filebrowser/filebrowser/commit/a73d7f14b787935c6ebe525dba64b65f8ed733e2))
* **deps:** bump follow-redirects from 1.13.3 to 1.14.8 in /frontend ([f1f7f17](https://github.com/filebrowser/filebrowser/commit/f1f7f17ade8d40fc6cfb22c79960bce299876b56))
* **deps:** bump hosted-git-info from 2.8.8 to 2.8.9 in /frontend ([e7659ea](https://github.com/filebrowser/filebrowser/commit/e7659ea36bdf780ce17005f7170a2fef02a2d5e5))
* **deps:** bump path-parse from 1.0.6 to 1.0.7 in /frontend ([c014966](https://github.com/filebrowser/filebrowser/commit/c01496624a7ebfc8a7c256bd919a400367281cbb))
* **deps:** bump postcss from 7.0.35 to 7.0.39 in /frontend ([9182d33](https://github.com/filebrowser/filebrowser/commit/9182d33e1cc375473fb18989a92d20252884f096))
* **deps:** bump ssri from 6.0.1 to 6.0.2 in /frontend ([3717186](https://github.com/filebrowser/filebrowser/commit/371718634b11f32e68165f31c51b6b1139c829ec))
* **deps:** bump tar from 6.1.0 to 6.1.11 in /frontend ([010d16f](https://github.com/filebrowser/filebrowser/commit/010d16fc1d8f0200e5662943aef17ee89c5877b7))
* **deps:** bump url-parse from 1.5.1 to 1.5.4 in /frontend ([8906408](https://github.com/filebrowser/filebrowser/commit/8906408a8f0ed86d1e11ea90fc573b36815c9c0d))
* **deps:** bump url-parse from 1.5.4 to 1.5.7 in /frontend ([228ebea](https://github.com/filebrowser/filebrowser/commit/228ebea66cc871b33459406590a80ef906298e7d))
* **deps:** bump ws from 6.2.1 to 6.2.2 in /frontend ([73c8073](https://github.com/filebrowser/filebrowser/commit/73c80732d934bc8802a6d7c7a559cad37df405f0))
### [2.20.1](https://github.com/filebrowser/filebrowser/compare/v2.20.0...v2.20.1) (2021-12-21)
### Build
* revert to using the default alpine based docker image ([46d8046](https://github.com/filebrowser/filebrowser/commit/46d80464d2a67927b06a11b83fb137ad364a90ed))
## [2.20.0](https://github.com/filebrowser/filebrowser/compare/v2.19.0...v2.20.0) (2021-12-20)
### Features
* detect multiple subtitle languages ([#1723](https://github.com/filebrowser/filebrowser/issues/1723)) ([c2e03bb](https://github.com/filebrowser/filebrowser/commit/c2e03bbfab97fc6716bcdd59158e9d5129bf0ea7))
* use linuxserver based docker image ([b8f35ce](https://github.com/filebrowser/filebrowser/commit/b8f35ce9322c2b0dbf954cfd3ff584bc9f742fdd))
### Bug Fixes
* set correct default database path in the config ([988d3e5](https://github.com/filebrowser/filebrowser/commit/988d3e5bdd224509ddc2f08444560e3087e9c67d))
* upgrade vulnerable versions of the library ([6eb3ab0](https://github.com/filebrowser/filebrowser/commit/6eb3ab063509a015ad630ab704ae3791461d0982))
### Build
* refactor makefile ([f81857a](https://github.com/filebrowser/filebrowser/commit/f81857acce25936a700945db5ef4af545eaeb1cf))
* remove deprecated goreleaser use_buildx param ([4d1b9dd](https://github.com/filebrowser/filebrowser/commit/4d1b9dd2112002a93bb26cece07dcfd81c31dc2c))
## [2.19.0](https://github.com/filebrowser/filebrowser/compare/v2.18.0...v2.19.0) (2021-11-24)
### Features
* prefetch previous and next images in preview. ([#1627](https://github.com/filebrowser/filebrowser/issues/1627)) ([7401d16](https://github.com/filebrowser/filebrowser/commit/7401d16e457bb232fd7dd7ef427e8960d465705c))
### Bug Fixes
* empty file listing on share ([e082397](https://github.com/filebrowser/filebrowser/commit/e08239781f61e7bb25d9b8c5c6cce90f34621a76))
* relative font sizes ([c29698d](https://github.com/filebrowser/filebrowser/commit/c29698dffac769077ab7c7869569a902979ee3d7))
## [2.18.0](https://github.com/filebrowser/filebrowser/compare/v2.17.2...v2.18.0) (2021-10-31)
### Features
* add ability to select file modified time format ([#1536](https://github.com/filebrowser/filebrowser/issues/1536)) ([0426629](https://github.com/filebrowser/filebrowser/commit/0426629a59c712849570d3e29956948ae7725a4a))
* add manifest theme color param ([#1542](https://github.com/filebrowser/filebrowser/issues/1542)) ([0358e42](https://github.com/filebrowser/filebrowser/commit/0358e42d2c206732fffa77714f5a66f4fe50a69d))
### Bug Fixes
* back button behaviour in preview ([#1573](https://github.com/filebrowser/filebrowser/issues/1573)) ([deabc80](https://github.com/filebrowser/filebrowser/commit/deabc80fd7670983039dfcd29531b45002ca5d9e))
* fix sidebar navigation on mobile devices ([#1618](https://github.com/filebrowser/filebrowser/issues/1618)) ([f09bf3e](https://github.com/filebrowser/filebrowser/commit/f09bf3e1d076b27d29ba8a91cf448a99993bc444))
* search box is misaligned when the browser preferred font size is other than 16px ([#1613](https://github.com/filebrowser/filebrowser/issues/1613)) ([6f345be](https://github.com/filebrowser/filebrowser/commit/6f345be3e47ba57ecc1eb9a62587ab949078c125))
* security issue in command runner (closes [#1621](https://github.com/filebrowser/filebrowser/issues/1621)) ([74b7cd8](https://github.com/filebrowser/filebrowser/commit/74b7cd8e81840537a8206317344f118093153e8d))
* set correct editor height regardless of preferred font size ([#1614](https://github.com/filebrowser/filebrowser/issues/1614)) ([ddd4ffa](https://github.com/filebrowser/filebrowser/commit/ddd4ffa4caa6b292a3a644ecd897aba1237c7503))
* zoom pics when dlclick at first time ([#1561](https://github.com/filebrowser/filebrowser/issues/1561)) ([b6a51be](https://github.com/filebrowser/filebrowser/commit/b6a51bed516814944f8aa41440652242d57824c5))
### [2.17.2](https://github.com/filebrowser/filebrowser/compare/v2.17.1...v2.17.2) (2021-08-27)
### Bug Fixes
* bug with inlineLink not creating url properly ([#1515](https://github.com/filebrowser/filebrowser/issues/1515)) ([43a4609](https://github.com/filebrowser/filebrowser/commit/43a460993c3f0d158b876db4b20caa7963e9f361))
### [2.17.1](https://github.com/filebrowser/filebrowser/compare/v2.17.0...v2.17.1) (2021-08-23)
### Bug Fixes
* internal server error if --disable-preview-resize flag is set (closes [#1510](https://github.com/filebrowser/filebrowser/issues/1510)) ([4c3099a](https://github.com/filebrowser/filebrowser/commit/4c3099a086c206dcb3bc70ee8c8da02eee61c30b))
## [2.17.0](https://github.com/filebrowser/filebrowser/compare/v2.16.1...v2.17.0) (2021-08-21)
### Features
* open file option on preview ([76add9e](https://github.com/filebrowser/filebrowser/commit/76add9e5274b0373c6b983e3b20e387a14ea6c9e))
### Bug Fixes
* 401 error in share view open file button ([#1495](https://github.com/filebrowser/filebrowser/issues/1495)) ([25c8788](https://github.com/filebrowser/filebrowser/commit/25c87883908babde073390a2e2320a8e5880a87c))
* escape quote on index template ([23d646c](https://github.com/filebrowser/filebrowser/commit/23d646c456876d06cf48e71c1e57b69de99511f0)), closes [#1501](https://github.com/filebrowser/filebrowser/issues/1501)
* file caching directive ([c63cc5a](https://github.com/filebrowser/filebrowser/commit/c63cc5a2d25909cc4e2f2e7235f276ec66c32bf2))
### [2.16.1](https://github.com/filebrowser/filebrowser/compare/v2.16.0...v2.16.1) (2021-08-04)
### Bug Fixes
* check symlink target type (closes [#1488](https://github.com/filebrowser/filebrowser/issues/1488)) ([76b466f](https://github.com/filebrowser/filebrowser/commit/76b466f6492e74cf13e66a33e7e5f597ac92b240))
## [2.16.0](https://github.com/filebrowser/filebrowser/compare/v2.15.0...v2.16.0) (2021-07-26)
### Features
* browser cache directives ([190cb99](https://github.com/filebrowser/filebrowser/commit/190cb99a79a0d438eca2da13539f8c6449ad73ac))
* display error messages on settings ([6032038](https://github.com/filebrowser/filebrowser/commit/603203848a8b2221158088b6d849609db4c0c46c))
* file name on page title ([16a34de](https://github.com/filebrowser/filebrowser/commit/16a34defc02554a77c6ac47b9e17e69d098a09fe))
* gzip encoding for static js files ([aa172b8](https://github.com/filebrowser/filebrowser/commit/aa172b8bb5f17d5f5cb9666bfb5ee650d8091fb5))
* loading spinner on views navigation ([976eb55](https://github.com/filebrowser/filebrowser/commit/976eb5583dae474125fd7ddec5dc19b6c291f98f))
* message for connection error ([5e6f14b](https://github.com/filebrowser/filebrowser/commit/5e6f14b5dcb9c5efdf526f1346e09c2d0b2f6974))
* mod time title on file info ([7d1e030](https://github.com/filebrowser/filebrowser/commit/7d1e03075d2c27148f60813defa0f68403d1d3c2))
* open file option on share ([1c25f6e](https://github.com/filebrowser/filebrowser/commit/1c25f6ee69bd71eed82af7020006d0e27537a967))
* show more button on share ([ba8c09f](https://github.com/filebrowser/filebrowser/commit/ba8c09f454feeadf4a1e97547a34151a81b389d5))
* support for IE11 browser ([7ec24d9](https://github.com/filebrowser/filebrowser/commit/7ec24d9d7794fa37825f64ca2d1575f568fb1362))
### Bug Fixes
* break resource create/update handlers on error (closes [#1464](https://github.com/filebrowser/filebrowser/issues/1464)) ([5072bbb](https://github.com/filebrowser/filebrowser/commit/5072bbb2cbf5b29d041629faa8367f15e4d145a2))
* copying files with special characters ([20ebbf6](https://github.com/filebrowser/filebrowser/commit/20ebbf6611b734371426fb1b9cb5e388be90bf7e))
* delete image cache when moving ([8973c45](https://github.com/filebrowser/filebrowser/commit/8973c4598ff817647f1f1ad6ee36480054cd2776))
* don't remove files on unsuccessful updates (closes [#1456](https://github.com/filebrowser/filebrowser/issues/1456)) ([6b19ab6](https://github.com/filebrowser/filebrowser/commit/6b19ab6613b12be7f075299cd98f4b41d43827c7))
* failure on broken symlink deletion ([8650d2f](https://github.com/filebrowser/filebrowser/commit/8650d2ffe7a29cbafa800efcecbf6a61598a9f0c))
* inconsistent double click on listing item ([ba7e71a](https://github.com/filebrowser/filebrowser/commit/ba7e71a7c3b0cc71012e5adf94b1c642e554972e))
* no items displayed on file listing ([18889ad](https://github.com/filebrowser/filebrowser/commit/18889ad725f7f7e5a7e3f7abcf156487556dbeaf))
* omit file content ([209f9fa](https://github.com/filebrowser/filebrowser/commit/209f9fa77f751054512355f2b74b9b7258465d0b))
* short commit sha and typo fix in Makefile ([#1411](https://github.com/filebrowser/filebrowser/issues/1411)) ([46ee595](https://github.com/filebrowser/filebrowser/commit/46ee59538914dc2859f0da6b32e2d062d0a01b10))
## [2.15.0](https://github.com/filebrowser/filebrowser/compare/v2.14.1...v2.15.0) (2021-04-06)
### Features
* add EXIF thumbnail support for JPEG files ([#1234](https://github.com/filebrowser/filebrowser/issues/1234)) ([7dd5b34](https://github.com/filebrowser/filebrowser/commit/7dd5b34d425dfbc2782152310483cbecf85c800a))
* dynamic autoplay on previewer ([a76e01d](https://github.com/filebrowser/filebrowser/commit/a76e01d2b78a785f3665a8b3532c7cc566bfabce))
* dynamic item count on file listing ([6c8ee96](https://github.com/filebrowser/filebrowser/commit/6c8ee96e6a21fae5d4608bdc7a5c5a161d7dafd3))
* dynamic zoom limit on previewer ([e410272](https://github.com/filebrowser/filebrowser/commit/e410272e6be6a0b660efe8d4eee6c6e9dd834cc5))
### Bug Fixes
* buttons without permission on header ([1516d99](https://github.com/filebrowser/filebrowser/commit/1516d9932bf9926ac8b4cb3e738a5f51e80d5b1d))
* check modify permission on file overwrite ([59f9964](https://github.com/filebrowser/filebrowser/commit/59f9964e80c8233775f27be33a4c16a31bfe848a))
* empty archive name on directory download ([2697093](https://github.com/filebrowser/filebrowser/commit/2697093ac151f74eea3022951d128acfe04d1dcf))
* empty text file on editor ([e9baf0c](https://github.com/filebrowser/filebrowser/commit/e9baf0c4b688fab291cdc842ec464c7a7a816499))
* error causes panic on upload ([e1a6f59](https://github.com/filebrowser/filebrowser/commit/e1a6f593e1824e7fa4345a61dff5b1bb8cd22d05))
* hidden editor header on Safari ([b521dec](https://github.com/filebrowser/filebrowser/commit/b521dec8f9b14dd92248c429e902ebc639046389))
* image quality switch on previewer ([c0d85f3](https://github.com/filebrowser/filebrowser/commit/c0d85f3d85926c8790757bf142140d19455ae8ca))
* list item interactions on share ([87f1881](https://github.com/filebrowser/filebrowser/commit/87f1881b429877a740ea84a8e783ad4103248289))
* missing bold variation for Roboto font ([98d79b8](https://github.com/filebrowser/filebrowser/commit/98d79b8ed955df5691a306d709b4ab60d91da408))
* mouse wheel zoom on previewer ([fcb115f](https://github.com/filebrowser/filebrowser/commit/fcb115f42d33db2be7a4d428ec53d65d6050320b))
* no header button animations on file listing ([fe80730](https://github.com/filebrowser/filebrowser/commit/fe80730bb135b38e4d9de470c75cbe10b1aec201))
### [2.14.1](https://github.com/filebrowser/filebrowser/compare/v2.14.0...v2.14.1) (2021-03-21)
### Bug Fixes
* display public routes with header proxy auth ([da54bd6](https://github.com/filebrowser/filebrowser/commit/da54bd6c214d7ee39b71d710ddfe6dd25fc4e5d6))
## [2.14.0](https://github.com/filebrowser/filebrowser/compare/v2.13.0...v2.14.0) (2021-03-21)
### Features
* add health check handler ([a721dc1](https://github.com/filebrowser/filebrowser/commit/a721dc1f314732e60d331a1a7da97d06e0e8b613))
### Bug Fixes
* hide dotfile error on share ([5f4a031](https://github.com/filebrowser/filebrowser/commit/5f4a0317ab5685fe4a558df74e604c12e04a1c10))
* prefix handling on http router ([93a35ad](https://github.com/filebrowser/filebrowser/commit/93a35ad2516accdcb9735db509550979d01de2c3))
* qr code url on share ([22f4be8](https://github.com/filebrowser/filebrowser/commit/22f4be8f54162b7cf494177705ffb8b09117bd01))
* text file detection on editor ([eeadc53](https://github.com/filebrowser/filebrowser/commit/eeadc532fe6057969b3c1a4726f236851b154cfa))
## [2.13.0](https://github.com/filebrowser/filebrowser/compare/v2.12.1...v2.13.0) (2021-03-14)
### Features
* dual pane settings view ([db5aad8](https://github.com/filebrowser/filebrowser/commit/db5aad8eb679cfe1b1ace5142cf342951217f0f7))
* improved settings navbar ([5b28aa0](https://github.com/filebrowser/filebrowser/commit/5b28aa0848710b9d3ee02a2aa912856395f48bd2))
* improved sharing prompt ([1819377](https://github.com/filebrowser/filebrowser/commit/18193778971e27d18b5a35df8c2d0e2953b48111))
* increased header button counter size ([4fb832c](https://github.com/filebrowser/filebrowser/commit/4fb832c0422107e16f22b7aa928224f36de4978f))
* larger previewer content ([62fff5c](https://github.com/filebrowser/filebrowser/commit/62fff5ca60da1f887c1f95fa4808d3753596dab2))
### Bug Fixes
* archive contains parent path on Windows ([54f3570](https://github.com/filebrowser/filebrowser/commit/54f35701a2bd5cb7ec0628ca9789047072c073db))
* check rules on http resource handlers ([5bf1554](https://github.com/filebrowser/filebrowser/commit/5bf15548d0ad147acfad5000277531be2671f7ce))
* download current dir on file listing ([488d980](https://github.com/filebrowser/filebrowser/commit/488d98045e7476ed11e53c13d9498a9db3165bbc))
* encoded file path on share ([7955e07](https://github.com/filebrowser/filebrowser/commit/7955e0720baef3710106c7e69bbbf078d5489220))
* full file path on share ([e017a19](https://github.com/filebrowser/filebrowser/commit/e017a199850e19dd51b960ba59402c215fd8f1af))
* header dropdown icon color on previewer ([f8df76f](https://github.com/filebrowser/filebrowser/commit/f8df76f52684f10722ce123fec2c90e321ddf103))
* item dragging on file listing ([326b35a](https://github.com/filebrowser/filebrowser/commit/326b35a7ac7871afcdf892ca150349665b7f6379))
* modified time on info prompt ([11ebaec](https://github.com/filebrowser/filebrowser/commit/11ebaec5f0671ec02ebe55d4a73a514bce3a6713))
* root path name on archive ([426b38b](https://github.com/filebrowser/filebrowser/commit/426b38bb3362d2d477d0d8aa27d880664d537431))
* stuck icon on header button ([6a734c0](https://github.com/filebrowser/filebrowser/commit/6a734c01391b437c2842f5d97fb63f29a0017510))
* update image cache when replacing ([81b6f4d](https://github.com/filebrowser/filebrowser/commit/81b6f4d6f6a01886583016f61f4f1951a59f244d))
* wait for async command exit ([#1326](https://github.com/filebrowser/filebrowser/issues/1326)) ([6d5ceae](https://github.com/filebrowser/filebrowser/commit/6d5ceae8b454edd749b3b65c88aacc0a31ce9215))
### Refactorings
* migrate from rice to embed.FS ([fc55061](https://github.com/filebrowser/filebrowser/commit/fc5506179a64e9e2f57f7b6d6cce4b95f5ebc235))
### [2.12.1](https://github.com/filebrowser/filebrowser/compare/v2.12.0...v2.12.1) (2021-03-07)
### Bug Fixes
* add missing default config into the docker image ([7358b3f](https://github.com/filebrowser/filebrowser/commit/7358b3fe3178c20007b4b5ef5c03705badd538c4))
## [2.12.0](https://github.com/filebrowser/filebrowser/compare/v2.11.0...v2.12.0) (2021-03-04)
### Features
* add homebrew tap ([2d2c598](https://github.com/filebrowser/filebrowser/commit/2d2c598fa6bd1ecaf39c542182890c8dd9b1cad0))
* added tiff files preview support ([#1222](https://github.com/filebrowser/filebrowser/issues/1222)) ([e8c9d1c](https://github.com/filebrowser/filebrowser/commit/e8c9d1c53989b4b52f6fba2a8ac41ae612c03a7c))
* allow disabling file detections by reading header ([#1175](https://github.com/filebrowser/filebrowser/issues/1175)) ([6914063](https://github.com/filebrowser/filebrowser/commit/6914063853a8a3f3cecfa4b21f223820c2a0b7df))
* allow to password protect shares ([#1252](https://github.com/filebrowser/filebrowser/issues/1252)) ([d8f415f](https://github.com/filebrowser/filebrowser/commit/d8f415f8abd0c4301803bd968c54429dd3fe4b59))
* build multi-arch docker images ([cf4836d](https://github.com/filebrowser/filebrowser/commit/cf4836dc757ef79ad615179bb7a6c7bbd3b09c2c))
* share management delete confirm ([#1212](https://github.com/filebrowser/filebrowser/issues/1212)) ([b600b11](https://github.com/filebrowser/filebrowser/commit/b600b11415fd1fb90ff2f5136be95a9c737ae1cb))
### Bug Fixes
* don't allow to remove root user ([019ce80](https://github.com/filebrowser/filebrowser/commit/019ce80fc529a0437984fdc3d1ab6916f34dd594))
* double click to zoom pics in phone's browser ([#1274](https://github.com/filebrowser/filebrowser/issues/1274)) ([f1b7bd5](https://github.com/filebrowser/filebrowser/commit/f1b7bd59f67e719b7bfd203b0d7ec016fd21ab49))
* environmental variables not expanded in command ([#1241](https://github.com/filebrowser/filebrowser/issues/1241)) ([f3afd5c](https://github.com/filebrowser/filebrowser/commit/f3afd5cb79d6ad8b9cc8d54cb8fc2344b7c07d3d))
* fetch resource api once when sorting (closes [#1172](https://github.com/filebrowser/filebrowser/issues/1172)) ([#1202](https://github.com/filebrowser/filebrowser/issues/1202)) ([05bb7c8](https://github.com/filebrowser/filebrowser/commit/05bb7c85531349f3e9d1d8a523bb1243587b2ebc))
### Build
* use make for building the project ([#1304](https://github.com/filebrowser/filebrowser/issues/1304)) ([23f8464](https://github.com/filebrowser/filebrowser/commit/23f84642e6c1e07f89f98d2c1bb6fc9da36cc71c))
## [2.11.0](https://github.com/filebrowser/filebrowser/compare/v2.10.0...v2.11.0) (2020-12-28)
### Features
* add sharing management ([#1178](https://github.com/filebrowser/filebrowser/issues/1178)) (closes [#1000](https://github.com/filebrowser/filebrowser/issues/1000)) ([677bce3](https://github.com/filebrowser/filebrowser/commit/677bce376b024d9ff38f34e74243034fe5a1ec3c))
* download shared subdirectory ([#1184](https://github.com/filebrowser/filebrowser/issues/1184)) ([fb5b28d](https://github.com/filebrowser/filebrowser/commit/fb5b28d9cbdee10d38fcd719b9fd832121be58ef))
### Bug Fixes
* check user input to prevent permission elevation ([#1196](https://github.com/filebrowser/filebrowser/issues/1196)) (closes [#1195](https://github.com/filebrowser/filebrowser/issues/1195)) ([f62806f](https://github.com/filebrowser/filebrowser/commit/f62806f6c9e9c7f392d1b747d65b8fe40b313e89))
* delete extra remove prefix ([#1186](https://github.com/filebrowser/filebrowser/issues/1186)) ([7a5298a](https://github.com/filebrowser/filebrowser/commit/7a5298a7556f7dcc52f59b8ea76d040d3ddc3d12))
* move files between different volumes (closes [#1177](https://github.com/filebrowser/filebrowser/issues/1177)) ([58835b7](https://github.com/filebrowser/filebrowser/commit/58835b7e535cc96e1c8a5d85821c1545743ca757))
* recaptcha race condition ([#1176](https://github.com/filebrowser/filebrowser/issues/1176)) ([ac3673e](https://github.com/filebrowser/filebrowser/commit/ac3673e111afac6616af9650ca07028b6c27e6cd))
## [2.10.0](https://github.com/filebrowser/filebrowser/compare/v2.9.0...v2.10.0) (2020-11-24)
### Features
* add hide dotfiles param ([#1148](https://github.com/filebrowser/filebrowser/issues/1148)) ([10e399b](https://github.com/filebrowser/filebrowser/commit/10e399b3c3dbdcfb4465a9d4138e1da6bae0873d))
* add single click mode ([#1139](https://github.com/filebrowser/filebrowser/issues/1139)) ([e8b4e9a](https://github.com/filebrowser/filebrowser/commit/e8b4e9af46d6e99dbeb965dd9727d9ed017d52a2))
* automatically jump to the next photo when deleting while previewing ([#1143](https://github.com/filebrowser/filebrowser/issues/1143)) ([9515cee](https://github.com/filebrowser/filebrowser/commit/9515ceeb42e5ef5267400220a2082dec775e843d))
* shared folder file listing ([e119bc5](https://github.com/filebrowser/filebrowser/commit/e119bc55ea82cefcbcc0571650107dfd5d73f570))
* shared item information ([36cacdf](https://github.com/filebrowser/filebrowser/commit/36cacdf598e4e09f064c8ace0ca7a6c24b23028e))
### Bug Fixes
* empty folder in archive ([7096b3d](https://github.com/filebrowser/filebrowser/commit/7096b3dab92441981c9964e4a6175af0a255d2be))
* fix hanging when reading a named pipe file (closes [#1155](https://github.com/filebrowser/filebrowser/issues/1155)) ([586d198](https://github.com/filebrowser/filebrowser/commit/586d198d47b525eeccc6fe587573a3ad83adb4f6))
* previewer title overflow ([4e48ffc](https://github.com/filebrowser/filebrowser/commit/4e48ffc14d09dabeea12dc495144277db62b5b7d))
* resource rename action invalid path ([1ce3068](https://github.com/filebrowser/filebrowser/commit/1ce3068a99c80c153fd41359255d173bce6e79e8))
## [2.9.0](https://github.com/filebrowser/filebrowser/compare/v2.8.0...v2.9.0) (2020-10-21)
### Features
* support WKWebview custom protocol ([#1113](https://github.com/filebrowser/filebrowser/issues/1113)) ([0ac80e8](https://github.com/filebrowser/filebrowser/commit/0ac80e8387a69924284259bde448af2813d84ed1))
### Bug Fixes
* allow start from Windows explorer ([f2c4e78](https://github.com/filebrowser/filebrowser/commit/f2c4e78381610879eda5316d38a999c89df6c14a))
* file upload missing path slash ([5e27ba5](https://github.com/filebrowser/filebrowser/commit/5e27ba5c8c1be603c6ae7fec8de48e3532dea1f7))
* preview case sensitive file extension ([05bff54](https://github.com/filebrowser/filebrowser/commit/05bff54b71543fd232f1089c40504d0cbfd106be))
* search missing path slash ([2bd163d](https://github.com/filebrowser/filebrowser/commit/2bd163d92a856d65c8d4615e37898470c1edf2f4))
## [2.8.0](https://github.com/filebrowser/filebrowser/compare/v2.7.0...v2.8.0) (2020-10-05)
### Features
* add disable exec flag ([#1090](https://github.com/filebrowser/filebrowser/issues/1090)) ([97693cc](https://github.com/filebrowser/filebrowser/commit/97693cc6117ce1c956baede91de5dd48b904e175))
### Bug Fixes
* empty commands setting ([c6d4fcd](https://github.com/filebrowser/filebrowser/commit/c6d4fcd08f5f1531c2cef514dc86019e23e7289f))
* file upload path encoding ([babd778](https://github.com/filebrowser/filebrowser/commit/babd7783afe85b790e1c558375d7b5013b2d366f))
* fix empty command name ([#1106](https://github.com/filebrowser/filebrowser/issues/1106)) ([36fb9f5](https://github.com/filebrowser/filebrowser/commit/36fb9f562a2c005ca4390fdebde0b4690201dff9))
* fix panic when accessing nonexistent .js file in static path ([#1105](https://github.com/filebrowser/filebrowser/issues/1105)) ([ad99bf1](https://github.com/filebrowser/filebrowser/commit/ad99bf180197e0e6d82231a86457585de16366a8))
* preview key shortcut conflict ([dd7b9dd](https://github.com/filebrowser/filebrowser/commit/dd7b9ddd8546361060ef99e838a691b2fc6c495a))
* search results absolute url ([26d62e4](https://github.com/filebrowser/filebrowser/commit/26d62e411716a5eb9a5a703e47484cfb3fbf3bd0))
## [2.7.0](https://github.com/filebrowser/filebrowser/compare/v2.6.2...v2.7.0) (2020-09-11)
### Features
* add --socket-perm flag to control unix socket file permissions (closes [#1060](https://github.com/filebrowser/filebrowser/issues/1060)) ([65ac734](https://github.com/filebrowser/filebrowser/commit/65ac73414fadc4686c94803a93ff319e8f7ce9d1))
* preview mobile dropdown ([7787344](https://github.com/filebrowser/filebrowser/commit/778734419de314d4cb64d07109bbab73f8e2e42a))
* preview size button ([3d2cb83](https://github.com/filebrowser/filebrowser/commit/3d2cb838d111ee61047599f49e76de80c821f341))
* put selected files in the root of the archive (closes [#1065](https://github.com/filebrowser/filebrowser/issues/1065)) ([8142b32](https://github.com/filebrowser/filebrowser/commit/8142b32f3865eccd3331328e0d087f805d186ed5))
### [2.6.2](https://github.com/filebrowser/filebrowser/compare/v2.6.1...v2.6.2) (2020-08-05)
### [2.6.1](https://github.com/filebrowser/filebrowser/compare/v2.6.0...v2.6.1) (2020-07-28)
### Bug Fixes
* delete cached previews when deleting file ([f5d02cd](https://github.com/filebrowser/filebrowser/commit/f5d02cdde97923b963878abf5a300393b9feb348))
* escape special characters in preview url (closes [#1002](https://github.com/filebrowser/filebrowser/issues/1002)) ([c9340af](https://github.com/filebrowser/filebrowser/commit/c9340af8d045671ad3338c5d2d887c335ab92de4))
## [2.6.0](https://github.com/filebrowser/filebrowser/compare/v2.5.0...v2.6.0) (2020-07-27)
### Features
* add lazy load of image thumbnails ([bc00165](https://github.com/filebrowser/filebrowser/commit/bc001650944ae963b12b5b2538a68de7cd0d8f82))
* add param to disable img resizing ([aa78e3a](https://github.com/filebrowser/filebrowser/commit/aa78e3ab1fcae6f618e811ba4e315a7a209f9df2))
* cache resized images ([95bc929](https://github.com/filebrowser/filebrowser/commit/95bc92955f391ece22c40d9592f2a3e6e26907b9))
* limit image resize workers ([94ef596](https://github.com/filebrowser/filebrowser/commit/94ef59602fb50fc21b1164feda90a3b9aeb5e972))
### Bug Fixes
* conflict handling on upload button ([f228fa5](https://github.com/filebrowser/filebrowser/commit/f228fa55408824618e9f0879da67c86d22b0d324))
* drop feedback ([f2d2c1c](https://github.com/filebrowser/filebrowser/commit/f2d2c1cbf85fba3edffb7b079f121ed3f0bc1e02))
* missing error message ([d9be370](https://github.com/filebrowser/filebrowser/commit/d9be370e2474b8070fa58db920c9481270cc4a48))
* parent verification on copy ([727c63b](https://github.com/filebrowser/filebrowser/commit/727c63b98e2964d0960d25914c296570f6c79478))
* path separator inconsistency on rename ([34dfb49](https://github.com/filebrowser/filebrowser/commit/34dfb49b719c948e709a4639b4af2c5cb73b3887))
## [2.5.0](https://github.com/filebrowser/filebrowser/compare/v2.4.0...v2.5.0) (2020-07-17)
### Features
* add previewer title and loading indicator ([716396a](https://github.com/filebrowser/filebrowser/commit/716396a726329f0ba42fc34167dd07497c5bf47c))
* duplicate files in the same directory ([43526d9](https://github.com/filebrowser/filebrowser/commit/43526d9d1a8c837245e3f5059e0b4737583eeaeb))
* file copy, move and paste conflict checking ([eed9da1](https://github.com/filebrowser/filebrowser/commit/eed9da1471723ed3fbe6c00b1d6362b1c5fd8b04))
* rename option on replace prompt ([2636f87](https://github.com/filebrowser/filebrowser/commit/2636f876ab8f88eea6d9548de524ca2339eb0843))
* upload queue ([6ec6a23](https://github.com/filebrowser/filebrowser/commit/6ec6a2386173410f5cab9941dbf1bacb6b70ddd2))
### Bug Fixes
* blinking previewer ([9a2ebba](https://github.com/filebrowser/filebrowser/commit/9a2ebbabe2e9f0c292701d33f36f9b7a457b1164))
* dark theme colors ([b3b6445](https://github.com/filebrowser/filebrowser/commit/b3b644527d5673e16e61d404ff58a3c7bd6b6637))
* directory conflict checking ([7e5beef](https://github.com/filebrowser/filebrowser/commit/7e5beeff464e75ab185c430cd96e7cc67209ccc1))
* prompt before closing window ([194030f](https://github.com/filebrowser/filebrowser/commit/194030fcfcf54a2cf5e2f8ececcbb4754474d8f8))
* remove incomplete uploaded files ([0727496](https://github.com/filebrowser/filebrowser/commit/0727496601a9918c8131c56f62419bfac7ac589a))
* reset clipboard after pasting cutted files ([10570ad](https://github.com/filebrowser/filebrowser/commit/10570ade442b573ebe00af08369e28b1b0688df6))
## [2.4.0](https://github.com/filebrowser/filebrowser/compare/v2.3.0...v2.4.0) (2020-07-07)
### Features
* full screen editor ([0d665e5](https://github.com/filebrowser/filebrowser/commit/0d665e528f880ceda0976ceed66070ac34de7969))
### Bug Fixes
* add preview bypass for .gif files ([#1012](https://github.com/filebrowser/filebrowser/issues/1012)) ([453636d](https://github.com/filebrowser/filebrowser/commit/453636dfe2bbf177c74617862eb763485d4774bf))
* prompt key shortcut conflict ([0d69fbd](https://github.com/filebrowser/filebrowser/commit/0d69fbd9a342aa2695859021df0c423e3ae4a4fa))
## [2.3.0](https://github.com/filebrowser/filebrowser/compare/v2.2.0...v2.3.0) (2020-06-26)
### Features
* add image thumbnails support ([#980](https://github.com/filebrowser/filebrowser/issues/980)) ([6b0d49b](https://github.com/filebrowser/filebrowser/commit/6b0d49b1fc8bdce89576ba91cc0b8ec594fcd625))
### Bug Fixes
* typo in image_templates (apline -> alpine) ([#1005](https://github.com/filebrowser/filebrowser/issues/1005)) ([84da110](https://github.com/filebrowser/filebrowser/commit/84da11008516a371fc0446d97863dc14d337aa25))
## [2.2.0](https://github.com/filebrowser/filebrowser/compare/v2.1.2...v2.2.0) (2020-06-22)
### Features
* add alpine and debian docker images ([66863b7](https://github.com/filebrowser/filebrowser/commit/66863b72f7664e6cb9417f7da542a92fa77ca635))
* add folder upload ([#981](https://github.com/filebrowser/filebrowser/issues/981)) ([8977344](https://github.com/filebrowser/filebrowser/commit/89773447a56675b298394149d7a05c5df4039f14)), closes [filebrowser/filebrowser#741](https://github.com/filebrowser/filebrowser/issues/741)
* add key shortcuts ([95316cb](https://github.com/filebrowser/filebrowser/commit/95316cbe8c8ac3dbb28310bc11ec347c0caf699b))
* upload progress based on total size ([#993](https://github.com/filebrowser/filebrowser/issues/993)) ([cd454ba](https://github.com/filebrowser/filebrowser/commit/cd454bae51f40b1249e6fa6133c2949970eb3018))
### Bug Fixes
* add a workaround to fix window freezing when viewing a large file [#992](https://github.com/filebrowser/filebrowser/issues/992) ([2412016](https://github.com/filebrowser/filebrowser/commit/241201657c2bf01806d02a297eb846b26102a479))
* apply all fs user rulles ([68f8348](https://github.com/filebrowser/filebrowser/commit/68f8348ddeecba570a361e7aba4546052cc3e356))
* frontend token validation ([dd40b0d](https://github.com/filebrowser/filebrowser/commit/dd40b0d9b9cc6268a611306ac4684a1af852b79d)), closes [filebrowser/filebrowser#638](https://github.com/filebrowser/filebrowser/issues/638)
* multiple selection count ([963837e](https://github.com/filebrowser/filebrowser/commit/963837ef1dc6e2e84fcf924606ce388ac30f3891))
* save event hook ([82c883f](https://github.com/filebrowser/filebrowser/commit/82c883f95eead9eebe215e230f74773c945f864a)), closes [filebrowser/filebrowser#696](https://github.com/filebrowser/filebrowser/issues/696)

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM alpine:latest
RUN apk --update add ca-certificates \
mailcap \
curl \
jq
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh # Make the script executable
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD /healthcheck.sh || exit 1
VOLUME /srv
EXPOSE 80
COPY docker_config.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

16
Dockerfile.s6 Normal file
View File

@@ -0,0 +1,16 @@
FROM ghcr.io/linuxserver/baseimage-alpine:3.20
RUN apk --update add ca-certificates \
mailcap \
curl
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
# copy local files
COPY docker/root/ /
COPY filebrowser /usr/bin/filebrowser
# ports and volumes
VOLUME /srv /config /database
EXPOSE 80

16
Dockerfile.s6.aarch64 Normal file
View File

@@ -0,0 +1,16 @@
FROM ghcr.io/linuxserver/baseimage-alpine:arm64v8-3.20
RUN apk --update add ca-certificates \
mailcap \
curl
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
# copy local files
COPY docker/root/ /
COPY filebrowser /usr/bin/filebrowser
# ports and volumes
VOLUME /srv /config /database
EXPOSE 80

211
LICENSE
View File

@@ -1,73 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
1. Definitions.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
1. Definitions.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"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.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this 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.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"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.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"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).
"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.
"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.
"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).
"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."
"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.
"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.
"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."
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.
"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.
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.
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.
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:
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.
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
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:
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; 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
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; 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.
(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
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.
(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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
END OF TERMS AND CONDITIONS
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.
APPENDIX: How to apply the Apache License to your work.
END OF TERMS AND CONDITIONS
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.
APPENDIX: How to apply the Apache License to your work.
Copyright [yyyy] [name of copyright owner]
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.
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
Copyright 2018 File Browser contributors
http://www.apache.org/licenses/LICENSE-2.0
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
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.
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.

68
Makefile Normal file
View File

@@ -0,0 +1,68 @@
include common.mk
include tools.mk
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
## Build:
.PHONY: build
build: | build-frontend build-backend ## Build binary
.PHONY: build-frontend
build-frontend: ## Build frontend
$Q cd frontend && npm ci && npm run build
.PHONY: build-backend
build-backend: ## Build backend
$Q $(go) build -ldflags '$(LDFLAGS)' -o .
.PHONY: test
test: | test-frontend test-backend ## Run all tests
.PHONY: test-frontend
test-frontend: ## Run frontend tests
.PHONY: test-backend
test-backend: ## Run backend tests
$Q $(go) test -v ./...
.PHONY: lint
lint: lint-frontend lint-backend ## Run all linters
.PHONY: lint-frontend
lint-frontend: ## Run frontend linters
$Q cd frontend && npm ci && npm run lint
.PHONY: lint-backend
lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(golangci-lint) run -v
.PHONY: lint-commits
lint-commits: $(commitlint) ## Run commit linters
$Q ./scripts/commitlint.sh
fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
clean: clean-tools ## Clean
## Release:
.PHONY: bump-version
bump-version: $(standard-version) ## Bump app version
$Q ./scripts/bump_version.sh
## Help:
help: ## Show this help
@echo ''
@echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target> [options]${RESET}'
@echo ''
@echo 'Options:'
@$(call global_option, "V [0|1]", "enable verbose mode (default:0)")
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)

View File

@@ -1,3 +1,39 @@
# filebrowser
<p align="center">
<img src="https://raw.githubusercontent.com/filebrowser/logo/master/banner.png" width="550"/>
</p>
Cloned from: https://github.com/filebrowser/filebrowser
![Preview](https://user-images.githubusercontent.com/5447088/50716739-ebd26700-107a-11e9-9817-14230c53efd2.gif)
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser?style=flat-square)](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/filebrowser/filebrowser)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg?style=flat-square)](https://github.com/filebrowser/filebrowser/releases/latest)
[![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23filebrowser)
filebrowser provides a file managing interface within a specified directory and it can be used to upload, delete, preview, rename and edit your files. It allows the creation of multiple users and each user can have its own directory. It can be used as a standalone app.
## Demo
url: https://demo.filebrowser.org/
credentials: `demo`/`demo`
## Features
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)
## Install
For installation instructions please refer to our docs at [https://filebrowser.org/installation](https://filebrowser.org/installation).
## Configuration
[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
[Command Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want.
## Contributing
If you're interested in contributing to this project, our docs are best places to start [https://filebrowser.org/contributing](https://filebrowser.org/contributing).

26
SECURITY.md Normal file
View File

@@ -0,0 +1,26 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| < 2.0 | :x: |
## Reporting a Vulnerability
Vulnerabilities should be reported to filebrowser@googlegroups.com - which is a private, maintainer-only group. Maintainers will attempt to respond to/confirm reports within 2-3 days, but if you believe your report to be "critical" to user safety and security, please note as such in the subject. We have tens of thousands of users using our software, and take security vulnerabilities seriously.
When reporting an issue, where possible, please provide at least:
* The commit version the issue was identified at
* A proof of concept (plaintext; no binaries)
* Steps to reproduce
* Your recommended remediation(s), if any.
The FileBrowser team is a volunteer-only effort, and may reach back out for clarification.
> Note: Please do not open public issues for security issues, as GitHub does not provide facility for private issues, and deleting the issue makes it hard to triage/respond back to the reporter.

16
auth/auth.go Normal file
View File

@@ -0,0 +1,16 @@
package auth
import (
"net/http"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// Auther is the authentication interface.
type Auther interface {
// Auth is called to authenticate a request.
Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error)
// LoginPage indicates if this auther needs a login page.
LoginPage() bool
}

303
auth/hook.go Normal file
View File

@@ -0,0 +1,303 @@
package auth
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"strings"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/files"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodHookAuth is used to identify hook auth.
const MethodHookAuth settings.AuthMethod = "hook"
type hookCred struct {
Password string `json:"password"`
Username string `json:"username"`
}
// HookAuth is a hook implementation of an Auther.
type HookAuth struct {
Users users.Store `json:"-"`
Settings *settings.Settings `json:"-"`
Server *settings.Server `json:"-"`
Cred hookCred `json:"-"`
Fields hookFields `json:"-"`
Command string `json:"command"`
}
// Auth authenticates the user via a json in content body.
func (a *HookAuth) Auth(r *http.Request, usr users.Store, stg *settings.Settings, srv *settings.Server) (*users.User, error) {
var cred hookCred
if r.Body == nil {
return nil, os.ErrPermission
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return nil, os.ErrPermission
}
a.Users = usr
a.Settings = stg
a.Server = srv
a.Cred = cred
action, err := a.RunCommand()
if err != nil {
return nil, err
}
switch action {
case "auth":
u, err := a.SaveUser()
if err != nil {
return nil, err
}
return u, nil
case "block":
return nil, os.ErrPermission
case "pass":
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil || !users.CheckPwd(a.Cred.Password, u.Password) {
return nil, os.ErrPermission
}
return u, nil
default:
return nil, fmt.Errorf("invalid hook action: %s", action)
}
}
// LoginPage tells that hook auth requires a login page.
func (a *HookAuth) LoginPage() bool {
return true
}
// RunCommand starts the hook command and returns the action
func (a *HookAuth) RunCommand() (string, error) {
command := strings.Split(a.Command, " ")
envMapping := func(key string) string {
switch key {
case "USERNAME":
return a.Cred.Username
case "PASSWORD":
return a.Cred.Password
default:
return os.Getenv(key)
}
}
for i, arg := range command {
if i == 0 {
continue
}
command[i] = os.Expand(arg, envMapping)
}
cmd := exec.Command(command[0], command[1:]...) //nolint:gosec
cmd.Env = append(os.Environ(), fmt.Sprintf("USERNAME=%s", a.Cred.Username))
cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", a.Cred.Password))
out, err := cmd.Output()
if err != nil {
return "", err
}
a.GetValues(string(out))
return a.Fields.Values["hook.action"], nil
}
// GetValues creates a map with values from the key-value format string
func (a *HookAuth) GetValues(s string) {
m := map[string]string{}
// make line breaks consistent on Windows platform
s = strings.ReplaceAll(s, "\r\n", "\n")
// iterate input lines
for _, val := range strings.Split(s, "\n") {
v := strings.SplitN(val, "=", 2)
// skips non key and value format
if len(v) != 2 {
continue
}
fieldKey := strings.TrimSpace(v[0])
fieldValue := strings.TrimSpace(v[1])
if a.Fields.IsValid(fieldKey) {
m[fieldKey] = fieldValue
}
}
a.Fields.Values = m
}
// SaveUser updates the existing user or creates a new one when not found
func (a *HookAuth) SaveUser() (*users.User, error) {
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
return nil, err
}
if u == nil {
pass, err := users.HashPwd(a.Cred.Password)
if err != nil {
return nil, err
}
// create user with the provided credentials
d := &users.User{
Username: a.Cred.Username,
Password: pass,
Scope: a.Settings.Defaults.Scope,
Locale: a.Settings.Defaults.Locale,
ViewMode: a.Settings.Defaults.ViewMode,
SingleClick: a.Settings.Defaults.SingleClick,
Sorting: a.Settings.Defaults.Sorting,
Perm: a.Settings.Defaults.Perm,
Commands: a.Settings.Defaults.Commands,
HideDotfiles: a.Settings.Defaults.HideDotfiles,
}
u = a.GetUser(d)
userHome, err := a.Settings.MakeUserDir(u.Username, u.Scope, a.Server.Root)
if err != nil {
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
}
u.Scope = userHome
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
err = a.Users.Save(u)
if err != nil {
return nil, err
}
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
u = a.GetUser(u)
// update the password when it doesn't match the current
if p {
pass, err := users.HashPwd(a.Cred.Password)
if err != nil {
return nil, err
}
u.Password = pass
}
// update user with provided fields
err := a.Users.Update(u)
if err != nil {
return nil, err
}
}
return u, nil
}
// GetUser returns a User filled with hook values or provided defaults
func (a *HookAuth) GetUser(d *users.User) *users.User {
// adds all permissions when user is admin
isAdmin := a.Fields.GetBoolean("user.perm.admin", d.Perm.Admin)
perms := users.Permissions{
Admin: isAdmin,
Execute: isAdmin || a.Fields.GetBoolean("user.perm.execute", d.Perm.Execute),
Create: isAdmin || a.Fields.GetBoolean("user.perm.create", d.Perm.Create),
Rename: isAdmin || a.Fields.GetBoolean("user.perm.rename", d.Perm.Rename),
Modify: isAdmin || a.Fields.GetBoolean("user.perm.modify", d.Perm.Modify),
Delete: isAdmin || a.Fields.GetBoolean("user.perm.delete", d.Perm.Delete),
Share: isAdmin || a.Fields.GetBoolean("user.perm.share", d.Perm.Share),
Download: isAdmin || a.Fields.GetBoolean("user.perm.download", d.Perm.Download),
}
user := users.User{
ID: d.ID,
Username: d.Username,
Password: d.Password,
Scope: a.Fields.GetString("user.scope", d.Scope),
Locale: a.Fields.GetString("user.locale", d.Locale),
ViewMode: users.ViewMode(a.Fields.GetString("user.viewMode", string(d.ViewMode))),
SingleClick: a.Fields.GetBoolean("user.singleClick", d.SingleClick),
Sorting: files.Sorting{
Asc: a.Fields.GetBoolean("user.sorting.asc", d.Sorting.Asc),
By: a.Fields.GetString("user.sorting.by", d.Sorting.By),
},
Commands: a.Fields.GetArray("user.commands", d.Commands),
HideDotfiles: a.Fields.GetBoolean("user.hideDotfiles", d.HideDotfiles),
Perm: perms,
LockPassword: true,
}
return &user
}
// hookFields is used to access fields from the hook
type hookFields struct {
Values map[string]string
}
// validHookFields contains names of the fields that can be used
var validHookFields = []string{
"hook.action",
"user.scope",
"user.locale",
"user.viewMode",
"user.singleClick",
"user.sorting.by",
"user.sorting.asc",
"user.commands",
"user.hideDotfiles",
"user.perm.admin",
"user.perm.execute",
"user.perm.create",
"user.perm.rename",
"user.perm.modify",
"user.perm.delete",
"user.perm.share",
"user.perm.download",
}
// IsValid checks if the provided field is on the valid fields list
func (hf *hookFields) IsValid(field string) bool {
for _, val := range validHookFields {
if field == val {
return true
}
}
return false
}
// GetString returns the string value or provided default
func (hf *hookFields) GetString(k, dv string) string {
val, ok := hf.Values[k]
if ok {
return val
}
return dv
}
// GetBoolean returns the bool value or provided default
func (hf *hookFields) GetBoolean(k string, dv bool) bool {
val, ok := hf.Values[k]
if ok {
return val == "true"
}
return dv
}
// GetArray returns the array value or provided default
func (hf *hookFields) GetArray(k string, dv []string) []string {
val, ok := hf.Values[k]
if ok && strings.TrimSpace(val) != "" {
return strings.Split(val, " ")
}
return dv
}

108
auth/json.go Normal file
View File

@@ -0,0 +1,108 @@
package auth
import (
"encoding/json"
"net/http"
"net/url"
"os"
"strings"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodJSONAuth is used to identify json auth.
const MethodJSONAuth settings.AuthMethod = "json"
type jsonCred struct {
Password string `json:"password"`
Username string `json:"username"`
ReCaptcha string `json:"recaptcha"`
}
// JSONAuth is a json implementation of an Auther.
type JSONAuth struct {
ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"`
}
// Auth authenticates the user via a json in content body.
func (a JSONAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
var cred jsonCred
if r.Body == nil {
return nil, os.ErrPermission
}
err := json.NewDecoder(r.Body).Decode(&cred)
if err != nil {
return nil, os.ErrPermission
}
// If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && a.ReCaptcha.Secret != "" {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
if err != nil {
return nil, err
}
if !ok {
return nil, os.ErrPermission
}
}
u, err := usr.Get(srv.Root, cred.Username)
if err != nil || !users.CheckPwd(cred.Password, u.Password) {
return nil, os.ErrPermission
}
return u, nil
}
// LoginPage tells that json auth doesn't require a login page.
func (a JSONAuth) LoginPage() bool {
return true
}
const reCaptchaAPI = "/recaptcha/api/siteverify"
// ReCaptcha identifies a recaptcha connection.
type ReCaptcha struct {
Host string `json:"host"`
Key string `json:"key"`
Secret string `json:"secret"`
}
// Ok checks if a reCaptcha responde is correct.
func (r *ReCaptcha) Ok(response string) (bool, error) {
body := url.Values{}
body.Set("secret", r.Secret)
body.Add("response", response)
client := &http.Client{}
resp, err := client.Post(
r.Host+reCaptchaAPI,
"application/x-www-form-urlencoded",
strings.NewReader(body.Encode()),
)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return false, nil
}
var data struct {
Success bool `json:"success"`
}
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return false, err
}
return data.Success, nil
}

24
auth/none.go Normal file
View File

@@ -0,0 +1,24 @@
package auth
import (
"net/http"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodNoAuth is used to identify no auth.
const MethodNoAuth settings.AuthMethod = "noauth"
// NoAuth is no auth implementation of auther.
type NoAuth struct{}
// Auth uses authenticates user 1.
func (a NoAuth) Auth(_ *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
return usr.Get(srv.Root, uint(1))
}
// LoginPage tells that no auth doesn't require a login page.
func (a NoAuth) LoginPage() bool {
return false
}

35
auth/proxy.go Normal file
View File

@@ -0,0 +1,35 @@
package auth
import (
"errors"
"net/http"
"os"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// MethodProxyAuth is used to identify no auth.
const MethodProxyAuth settings.AuthMethod = "proxy"
// ProxyAuth is a proxy implementation of an auther.
type ProxyAuth struct {
Header string `json:"header"`
}
// Auth authenticates the user via an HTTP header.
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, _ *settings.Settings, srv *settings.Server) (*users.User, error) {
username := r.Header.Get(a.Header)
user, err := usr.Get(srv.Root, username)
if errors.Is(err, fbErrors.ErrNotExist) {
return nil, os.ErrPermission
}
return user, err
}
// LoginPage tells that proxy auth doesn't require a login page.
func (a ProxyAuth) LoginPage() bool {
return false
}

33
auth/storage.go Normal file
View File

@@ -0,0 +1,33 @@
package auth
import (
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
// StorageBackend is a storage backend for auth storage.
type StorageBackend interface {
Get(settings.AuthMethod) (Auther, error)
Save(Auther) error
}
// Storage is a auth storage.
type Storage struct {
back StorageBackend
users *users.Storage
}
// NewStorage creates a auth storage from a backend.
func NewStorage(back StorageBackend, userStore *users.Storage) *Storage {
return &Storage{back: back, users: userStore}
}
// Get wraps a StorageBackend.Get.
func (s *Storage) Get(t settings.AuthMethod) (Auther, error) {
return s.back.Get(t)
}
// Save wraps a StorageBackend.Save.
func (s *Storage) Save(a Auther) error {
return s.back.Save(a)
}

12
cmd/cmd.go Normal file
View File

@@ -0,0 +1,12 @@
package cmd
import (
"log"
)
// Execute executes the commands.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}

26
cmd/cmds.go Normal file
View File

@@ -0,0 +1,26 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(cmdsCmd)
}
var cmdsCmd = &cobra.Command{
Use: "cmds",
Short: "Command runner management utility",
Long: `Command runner management utility.`,
Args: cobra.NoArgs,
}
func printEvents(m map[string][]string) {
for evt, cmds := range m {
for i, cmd := range cmds {
fmt.Printf("%s(%d): %s\n", evt, i, cmd)
}
}
}

27
cmd/cmds_add.go Normal file
View File

@@ -0,0 +1,27 @@
package cmd
import (
"strings"
"github.com/spf13/cobra"
)
func init() {
cmdsCmd.AddCommand(cmdsAddCmd)
}
var cmdsAddCmd = &cobra.Command{
Use: "add <event> <command>",
Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2),
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
command := strings.Join(args[1:], " ")
s.Commands[args[0]] = append(s.Commands[args[0]], command)
err = d.store.Settings.Save(s)
checkErr(err)
printEvents(s.Commands)
}, pythonConfig{}),
}

31
cmd/cmds_ls.go Normal file
View File

@@ -0,0 +1,31 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
cmdsCmd.AddCommand(cmdsLsCmd)
cmdsLsCmd.Flags().StringP("event", "e", "", "event name, without 'before' or 'after'")
}
var cmdsLsCmd = &cobra.Command{
Use: "ls",
Short: "List all commands for each event",
Long: `List all commands for each event.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
evt := mustGetString(cmd.Flags(), "event")
if evt == "" {
printEvents(s.Commands)
} else {
show := map[string][]string{}
show["before_"+evt] = s.Commands["before_"+evt]
show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show)
}
}, pythonConfig{}),
}

56
cmd/cmds_rm.go Normal file
View File

@@ -0,0 +1,56 @@
package cmd
import (
"strconv"
"github.com/spf13/cobra"
)
func init() {
cmdsCmd.AddCommand(cmdsRmCmd)
}
var cmdsRmCmd = &cobra.Command{
Use: "rm <event> <index> [index_end]",
Short: "Removes a command from an event hooker",
Long: `Removes a command from an event hooker. The provided index
is the same that's printed when you run 'cmds ls'. Note
that after each removal/addition, the index of the
commands change. So be careful when removing them after each
other.
You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end',
including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil {
return err
}
for _, arg := range args[1:] {
if _, err := strconv.Atoi(arg); err != nil {
return err
}
}
return nil
},
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
evt := args[0]
i, err := strconv.Atoi(args[1])
checkErr(err)
f := i
if len(args) == 3 {
f, err = strconv.Atoi(args[2])
checkErr(err)
}
s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...)
err = d.store.Settings.Save(s)
checkErr(err)
printEvents(s.Commands)
}, pythonConfig{}),
}

189
cmd/config.go Normal file
View File

@@ -0,0 +1,189 @@
package cmd
import (
"encoding/json"
nerrors "errors"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/settings"
)
func init() {
rootCmd.AddCommand(configCmd)
}
var configCmd = &cobra.Command{
Use: "config",
Short: "Configuration management utility",
Long: `Configuration management utility.`,
Args: cobra.NoArgs,
}
func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags)
addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.String("shell", "", "shell command to which other commands should be appended")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
flags.String("auth.command", "", "command for auth.method=hook")
flags.String("recaptcha.host", "https://www.google.com", "use another host for ReCAPTCHA. recaptcha.net might be useful in China")
flags.String("recaptcha.key", "", "ReCaptcha site key")
flags.String("recaptcha.secret", "", "ReCaptcha secret")
flags.String("branding.name", "", "replace 'File Browser' by this name")
flags.String("branding.theme", "", "set the theme")
flags.String("branding.color", "", "set the theme color")
flags.String("branding.files", "", "path to directory with images and custom styles")
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
}
//nolint:gocyclo
func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, auth.Auther) {
method := settings.AuthMethod(mustGetString(flags, "auth.method"))
var defaultAuther map[string]interface{}
if len(defaults) > 0 {
if hasAuth := defaults[0]; hasAuth != true {
for _, arg := range defaults {
switch def := arg.(type) {
case *settings.Settings:
method = def.AuthMethod
case auth.Auther:
ms, err := json.Marshal(def)
checkErr(err)
err = json.Unmarshal(ms, &defaultAuther)
checkErr(err)
}
}
}
}
var auther auth.Auther
if method == auth.MethodProxyAuth {
header := mustGetString(flags, "auth.header")
if header == "" {
header = defaultAuther["header"].(string)
}
if header == "" {
checkErr(nerrors.New("you must set the flag 'auth.header' for method 'proxy'"))
}
auther = &auth.ProxyAuth{Header: header}
}
if method == auth.MethodNoAuth {
auther = &auth.NoAuth{}
}
if method == auth.MethodJSONAuth {
jsonAuth := &auth.JSONAuth{}
host := mustGetString(flags, "recaptcha.host")
key := mustGetString(flags, "recaptcha.key")
secret := mustGetString(flags, "recaptcha.secret")
if key == "" {
if kmap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
key = kmap["key"].(string)
}
}
if secret == "" {
if smap, ok := defaultAuther["recaptcha"].(map[string]interface{}); ok {
secret = smap["secret"].(string)
}
}
if key != "" && secret != "" {
jsonAuth.ReCaptcha = &auth.ReCaptcha{
Host: host,
Key: key,
Secret: secret,
}
}
auther = jsonAuth
}
if method == auth.MethodHookAuth {
command := mustGetString(flags, "auth.command")
if command == "" {
command = defaultAuther["command"].(string)
}
if command == "" {
checkErr(nerrors.New("you must set the flag 'auth.command' for method 'hook'"))
}
auther = &auth.HookAuth{Command: command}
}
if auther == nil {
panic(errors.ErrInvalidAuthMethod)
}
return method, auther
}
func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Auther) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "Sign up:\t%t\n", set.Signup)
fmt.Fprintf(w, "Create User Dir:\t%t\n", set.CreateUserDir)
fmt.Fprintf(w, "Auth method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:")
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
fmt.Fprintf(w, "\tDisable external links:\t%t\n", set.Branding.DisableExternal)
fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
fmt.Fprintln(w, "\nServer:")
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
fmt.Fprintf(w, "\tBase URL:\t%s\n", ser.BaseURL)
fmt.Fprintf(w, "\tRoot:\t%s\n", ser.Root)
fmt.Fprintf(w, "\tSocket:\t%s\n", ser.Socket)
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
fmt.Fprintf(w, "\tPermissions:\n")
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
fmt.Fprintf(w, "\t\tCreate:\t%t\n", set.Defaults.Perm.Create)
fmt.Fprintf(w, "\t\tRename:\t%t\n", set.Defaults.Perm.Rename)
fmt.Fprintf(w, "\t\tModify:\t%t\n", set.Defaults.Perm.Modify)
fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete)
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
checkErr(err)
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
}

25
cmd/config_cat.go Normal file
View File

@@ -0,0 +1,25 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
configCmd.AddCommand(configCatCmd)
}
var configCatCmd = &cobra.Command{
Use: "cat",
Short: "Prints the configuration",
Long: `Prints the configuration.`,
Args: cobra.NoArgs,
Run: python(func(_ *cobra.Command, _ []string, d pythonData) {
set, err := d.store.Settings.Get()
checkErr(err)
ser, err := d.store.Settings.GetServer()
checkErr(err)
auther, err := d.store.Auth.Get(set.AuthMethod)
checkErr(err)
printSettings(ser, set, auther)
}, pythonConfig{}),
}

37
cmd/config_export.go Normal file
View File

@@ -0,0 +1,37 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
configCmd.AddCommand(configExportCmd)
}
var configExportCmd = &cobra.Command{
Use: "export <path>",
Short: "Export the configuration to a file",
Long: `Export the configuration to a file. The path must be for a
json or yaml file. This exported configuration can be changed,
and imported again with 'config import' command.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
settings, err := d.store.Settings.Get()
checkErr(err)
server, err := d.store.Settings.GetServer()
checkErr(err)
auther, err := d.store.Auth.Get(settings.AuthMethod)
checkErr(err)
data := &settingsFile{
Settings: settings,
Auther: auther,
Server: server,
}
err = marshal(args[0], data)
checkErr(err)
}, pythonConfig{}),
}

94
cmd/config_import.go Normal file
View File

@@ -0,0 +1,94 @@
package cmd
import (
"encoding/json"
"errors"
"path/filepath"
"reflect"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
)
func init() {
configCmd.AddCommand(configImportCmd)
}
type settingsFile struct {
Settings *settings.Settings `json:"settings"`
Server *settings.Server `json:"server"`
Auther interface{} `json:"auther"`
}
var configImportCmd = &cobra.Command{
Use: "import <path>",
Short: "Import a configuration file",
Long: `Import a configuration file. This will replace all the existing
configuration. Can be used with or without unexisting databases.
If used with a nonexisting database, a key will be generated
automatically. Otherwise the key will be kept the same as in the
database.
The path must be for a json or yaml file.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
var key []byte
if d.hadDB {
settings, err := d.store.Settings.Get()
checkErr(err)
key = settings.Key
} else {
key = generateKey()
}
file := settingsFile{}
err := unmarshal(args[0], &file)
checkErr(err)
file.Settings.Key = key
err = d.store.Settings.Save(file.Settings)
checkErr(err)
err = d.store.Settings.SaveServer(file.Server)
checkErr(err)
var rawAuther interface{}
if filepath.Ext(args[0]) != ".json" { //nolint:goconst
rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{}))
} else {
rawAuther = file.Auther
}
var auther auth.Auther
switch file.Settings.AuthMethod {
case auth.MethodJSONAuth:
auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth)
case auth.MethodNoAuth:
auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth)
case auth.MethodProxyAuth:
auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth)
case auth.MethodHookAuth:
auther = getAuther(&auth.HookAuth{}, rawAuther).(*auth.HookAuth)
default:
checkErr(errors.New("invalid auth method"))
}
err = d.store.Auth.Save(auther)
checkErr(err)
printSettings(file.Server, file.Settings, auther)
}, pythonConfig{allowNoDB: true}),
}
func getAuther(sample auth.Auther, data interface{}) interface{} {
authType := reflect.TypeOf(sample)
auther := reflect.New(authType).Interface()
bytes, err := json.Marshal(data)
checkErr(err)
err = json.Unmarshal(bytes, &auther)
checkErr(err)
return auther
}

72
cmd/config_init.go Normal file
View File

@@ -0,0 +1,72 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/settings"
)
func init() {
configCmd.AddCommand(configInitCmd)
addConfigFlags(configInitCmd.Flags())
}
var configInitCmd = &cobra.Command{
Use: "init",
Short: "Initialize a new database",
Long: `Initialize a new database to use with File Browser. All of
this options can be changed in the future with the command
'filebrowser config set'. The user related flags apply
to the defaults when creating new users and you don't
override the options.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
defaults := settings.UserDefaults{}
flags := cmd.Flags()
getUserDefaults(flags, &defaults, true)
authMethod, auther := getAuthentication(flags)
s := &settings.Settings{
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
CreateUserDir: mustGetBool(flags, "create-user-dir"),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: mustGetString(flags, "branding.name"),
DisableExternal: mustGetBool(flags, "branding.disableExternal"),
DisableUsedPercentage: mustGetBool(flags, "branding.disableUsedPercentage"),
Theme: mustGetString(flags, "branding.theme"),
Files: mustGetString(flags, "branding.files"),
},
}
ser := &settings.Server{
Address: mustGetString(flags, "address"),
Socket: mustGetString(flags, "socket"),
Root: mustGetString(flags, "root"),
BaseURL: mustGetString(flags, "baseurl"),
TLSKey: mustGetString(flags, "key"),
TLSCert: mustGetString(flags, "cert"),
Port: mustGetString(flags, "port"),
Log: mustGetString(flags, "log"),
}
err := d.store.Settings.Save(s)
checkErr(err)
err = d.store.Settings.SaveServer(ser)
checkErr(err)
err = d.store.Auth.Save(auther)
checkErr(err)
fmt.Printf(`
Congratulations! You've set up your database to use with File Browser.
Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
printSettings(ser, s, auther)
}, pythonConfig{noDB: true}),
}

86
cmd/config_set.go Normal file
View File

@@ -0,0 +1,86 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
configCmd.AddCommand(configSetCmd)
addConfigFlags(configSetCmd.Flags())
}
var configSetCmd = &cobra.Command{
Use: "set",
Short: "Updates the configuration",
Long: `Updates the configuration. Set the flags for the options
you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
flags := cmd.Flags()
set, err := d.store.Settings.Get()
checkErr(err)
ser, err := d.store.Settings.GetServer()
checkErr(err)
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
switch flag.Name {
case "baseurl":
ser.BaseURL = mustGetString(flags, flag.Name)
case "root":
ser.Root = mustGetString(flags, flag.Name)
case "socket":
ser.Socket = mustGetString(flags, flag.Name)
case "cert":
ser.TLSCert = mustGetString(flags, flag.Name)
case "key":
ser.TLSKey = mustGetString(flags, flag.Name)
case "address":
ser.Address = mustGetString(flags, flag.Name)
case "port":
ser.Port = mustGetString(flags, flag.Name)
case "log":
ser.Log = mustGetString(flags, flag.Name)
case "signup":
set.Signup = mustGetBool(flags, flag.Name)
case "auth.method":
hasAuth = true
case "shell":
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
case "create-user-dir":
set.CreateUserDir = mustGetBool(flags, flag.Name)
case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name)
case "branding.color":
set.Branding.Color = mustGetString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme = mustGetString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal = mustGetBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage = mustGetBool(flags, flag.Name)
case "branding.files":
set.Branding.Files = mustGetString(flags, flag.Name)
}
})
getUserDefaults(flags, &set.Defaults, false)
// read the defaults
auther, err := d.store.Auth.Get(set.AuthMethod)
checkErr(err)
// check if there are new flags for existing auth method
set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther)
err = d.store.Auth.Save(auther)
checkErr(err)
err = d.store.Settings.Save(set)
checkErr(err)
err = d.store.Settings.SaveServer(ser)
checkErr(err)
printSettings(ser, set, auther)
}, pythonConfig{}),
}

139
cmd/docs.go Normal file
View File

@@ -0,0 +1,139 @@
package cmd
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
rootCmd.AddCommand(docsCmd)
docsCmd.Flags().StringP("path", "p", "./docs", "path to save the docs")
}
func printToc(names []string) {
for i, name := range names {
name = strings.TrimSuffix(name, filepath.Ext(name))
name = strings.Replace(name, "-", " ", -1)
names[i] = name
}
sort.Strings(names)
toc := ""
for _, name := range names {
toc += "* [" + name + "](cli/" + strings.Replace(name, " ", "-", -1) + ".md)\n"
}
fmt.Println(toc)
}
var docsCmd = &cobra.Command{
Use: "docs",
Hidden: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
dir := mustGetString(cmd.Flags(), "path")
generateDocs(rootCmd, dir)
names := []string{}
err := filepath.Walk(dir, func(_ string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
if !strings.HasPrefix(info.Name(), "filebrowser") {
return nil
}
names = append(names, info.Name())
return nil
})
checkErr(err)
printToc(names)
},
}
func generateDocs(cmd *cobra.Command, dir string) {
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
generateDocs(c, dir)
}
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + ".md"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
checkErr(err)
defer f.Close()
generateMarkdown(cmd, f)
}
func generateMarkdown(cmd *cobra.Command, w io.Writer) {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()
buf := new(bytes.Buffer)
name := cmd.CommandPath()
short := cmd.Short
long := cmd.Long
if long == "" {
long = short
}
buf.WriteString("---\ndescription: " + short + "\n---\n\n")
buf.WriteString("# " + name + "\n\n")
buf.WriteString("## Synopsis\n\n")
buf.WriteString(long + "\n\n")
if cmd.Runnable() {
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
}
if cmd.Example != "" {
buf.WriteString("## Examples\n\n")
_, _ = fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
}
printOptions(buf, cmd)
_, err := buf.WriteTo(w)
checkErr(err)
}
func generateFlagsTable(fs *pflag.FlagSet, buf io.StringWriter) {
_, _ = buf.WriteString("| Name | Shorthand | Usage |\n")
_, _ = buf.WriteString("|------|-----------|-------|\n")
fs.VisitAll(func(f *pflag.Flag) {
_, _ = buf.WriteString("|" + f.Name + "|" + f.Shorthand + "|" + f.Usage + "|\n")
})
}
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasAvailableFlags() {
buf.WriteString("## Options\n\n")
generateFlagsTable(flags, buf)
buf.WriteString("\n")
}
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasAvailableFlags() {
buf.WriteString("### Inherited\n\n")
generateFlagsTable(parentFlags, buf)
buf.WriteString("\n")
}
}

25
cmd/hash.go Normal file
View File

@@ -0,0 +1,25 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
rootCmd.AddCommand(hashCmd)
}
var hashCmd = &cobra.Command{
Use: "hash <password>",
Short: "Hashes a password",
Long: `Hashes a password using bcrypt algorithm.`,
Args: cobra.ExactArgs(1),
Run: func(_ *cobra.Command, args []string) {
pwd, err := users.HashPwd(args[0])
checkErr(err)
fmt.Println(pwd)
},
}

433
cmd/root.go Normal file
View File

@@ -0,0 +1,433 @@
package cmd
import (
"crypto/tls"
"errors"
"io"
"io/fs"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
"github.com/filebrowser/filebrowser/v2/encfs"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
)
var (
cfgFile string
)
func init() {
cobra.OnInitialize(initConfig)
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
flags := rootCmd.Flags()
persistent := rootCmd.PersistentFlags()
persistent.StringVarP(&cfgFile, "config", "c", "", "config file path")
persistent.StringP("database", "d", "./filebrowser.db", "database path")
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick config (default \"admin\")")
addServerFlags(flags)
}
func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
flags.StringP("log", "l", "stdout", "log output")
flags.StringP("port", "p", "8080", "port to listen on")
flags.StringP("cert", "t", "", "tls certificate")
flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
flags.Uint32("socket-perm", 0666, "unix socket file permissions") //nolint:gomnd
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count") //nolint:gomnd
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", false, "disables Command Runner feature")
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
}
var rootCmd = &cobra.Command{
Use: "filebrowser",
Short: "A stylish web-based file browser",
Long: `File Browser CLI lets you create the database to use with File Browser,
manage your users and all the configurations without acessing the
web interface.
If you've never run File Browser, you'll need to have a database for
it. Don't worry: you don't need to setup a separate database server.
We're using Bolt DB which is a single file database and all managed
by ourselves.
For this specific command, all the flags you have available (except
"config" for the configuration file), can be given either through
environment variables or configuration files.
If you don't set "config", it will look for a configuration file called
.filebrowser.{json, toml, yaml, yml} in the following directories:
- ./
- $HOME/
- /etc/filebrowser/
The precedence of the configuration values are as follows:
- flags
- environment variables
- configuration file
- database values
- defaults
The environment variables are prefixed by "FB_" followed by the option
name in caps. So to set "database" via an env variable, you should
set FB_DATABASE.
Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be bootstraped and a new
user created with the credentials from options "username" and "password".`,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
log.Println(cfgFile)
if !d.hadDB {
quickSetup(cmd.Flags(), d)
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
checkErr(err)
if workersCount < 1 {
log.Fatal("Image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err)
if cacheDir != "" {
log.Println("Cache is on:", cacheDir)
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet,gomnd
log.Fatalf("can't make directory %s: %s", cacheDir, err)
}
// fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
encryptionMasterKey, err := encfs.GetCachedEncryptionMasterKey()
if err != nil {
panic(err)
}
fileCache = diskcache.New(encfs.NewEncFs(encryptionMasterKey), cacheDir)
}
server := getRunParams(cmd.Flags(), d.store)
setupLog(server.Log)
root, err := filepath.Abs(server.Root)
checkErr(err)
server.Root = root
adr := server.Address + ":" + server.Port
var listener net.Listener
switch {
case server.Socket != "":
listener, err = net.Listen("unix", server.Socket)
checkErr(err)
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
checkErr(err)
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
checkErr(err)
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
checkErr(err)
listener, err = tls.Listen("tcp", adr, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
checkErr(err)
default:
listener, err = net.Listen("tcp", adr)
checkErr(err)
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc)
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
if err != nil {
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
checkErr(err)
defer listener.Close()
log.Println("Listening on", listener.Addr().String())
//nolint: gosec
if err := http.Serve(listener, handler); err != nil {
log.Fatal(err)
}
}, pythonConfig{allowNoDB: true}),
}
func cleanupHandler(listener net.Listener, c chan os.Signal) { //nolint:interfacer
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
listener.Close()
os.Exit(0)
}
//nolint:gocyclo
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server, err := st.Settings.GetServer()
checkErr(err)
if val, set := getParamB(flags, "root"); set {
server.Root = val
}
if val, set := getParamB(flags, "baseurl"); set {
server.BaseURL = val
}
if val, set := getParamB(flags, "log"); set {
server.Log = val
}
isSocketSet := false
isAddrSet := false
if val, set := getParamB(flags, "address"); set {
server.Address = val
isAddrSet = isAddrSet || set
}
if val, set := getParamB(flags, "port"); set {
server.Port = val
isAddrSet = isAddrSet || set
}
if val, set := getParamB(flags, "key"); set {
server.TLSKey = val
isAddrSet = isAddrSet || set
}
if val, set := getParamB(flags, "cert"); set {
server.TLSCert = val
isAddrSet = isAddrSet || set
}
if val, set := getParamB(flags, "socket"); set {
server.Socket = val
isSocketSet = isSocketSet || set
}
if isAddrSet && isSocketSet {
checkErr(errors.New("--socket flag cannot be used with --address, --port, --key nor --cert"))
}
// Do not use saved Socket if address was manually set.
if isAddrSet && server.Socket != "" {
server.Socket = ""
}
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
server.EnableThumbnails = !disableThumbnails
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
_, disableTypeDetectionByHeader := getParamB(flags, "disable-type-detection-by-header")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
_, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec
if val, set := getParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
return server
}
// getParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getParamB(flags *pflag.FlagSet, key string) (string, bool) {
value, _ := flags.GetString(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value, true
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetString(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getParam(flags *pflag.FlagSet, key string) string {
val, _ := getParamB(flags, key)
return val
}
func setupLog(logMethod string) {
switch logMethod {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
log.SetOutput(io.Discard)
default:
log.SetOutput(&lumberjack.Logger{
Filename: logMethod,
MaxSize: 100,
MaxAge: 14,
MaxBackups: 10,
})
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) {
set := &settings.Settings{
Key: generateKey(),
Signup: false,
CreateUserDir: false,
UserHomeBasePath: settings.DefaultUsersHomeBasePath,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
SingleClick: false,
Perm: users.Permissions{
Admin: false,
Execute: true,
Create: true,
Rename: true,
Modify: true,
Delete: true,
Share: true,
Download: true,
},
},
AuthMethod: "",
Branding: settings.Branding{},
Tus: settings.Tus{
ChunkSize: settings.DefaultTusChunkSize,
RetryCount: settings.DefaultTusRetryCount,
},
Commands: nil,
Shell: nil,
Rules: nil,
}
var err error
if _, noauth := getParamB(flags, "noauth"); noauth {
set.AuthMethod = auth.MethodNoAuth
err = d.store.Auth.Save(&auth.NoAuth{})
} else {
set.AuthMethod = auth.MethodJSONAuth
err = d.store.Auth.Save(&auth.JSONAuth{})
}
checkErr(err)
err = d.store.Settings.Save(set)
checkErr(err)
ser := &settings.Server{
BaseURL: getParam(flags, "baseurl"),
Port: getParam(flags, "port"),
Log: getParam(flags, "log"),
TLSKey: getParam(flags, "key"),
TLSCert: getParam(flags, "cert"),
Address: getParam(flags, "address"),
Root: getParam(flags, "root"),
}
err = d.store.Settings.SaveServer(ser)
checkErr(err)
username := getParam(flags, "username")
password := getParam(flags, "password")
if password == "" {
password, err = users.HashPwd("admin")
checkErr(err)
}
if username == "" || password == "" {
log.Fatal("username and password cannot be empty during quick setup")
}
user := &users.User{
Username: username,
Password: password,
LockPassword: false,
}
set.Defaults.Apply(user)
user.Perm.Admin = true
err = d.store.Users.Save(user)
checkErr(err)
}
func initConfig() {
if cfgFile == "" {
home, err := homedir.Dir()
checkErr(err)
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
if err := v.ReadInConfig(); err != nil {
var configParseError v.ConfigParseError
if errors.As(err, &configParseError) {
panic(err)
}
cfgFile = "No config file used"
} else {
cfgFile = "Using config file: " + v.ConfigFileUsed()
}
}

66
cmd/rule_rm.go Normal file
View File

@@ -0,0 +1,66 @@
package cmd
import (
"strconv"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
rulesCmd.AddCommand(rulesRmCommand)
rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove")
_ = rulesRmCommand.MarkFlagRequired("index")
}
var rulesRmCommand = &cobra.Command{
Use: "rm <index> [index_end]",
Short: "Remove a global rule or user rule",
Long: `Remove a global rule or user rule. The provided index
is the same that's printed when you run 'rules ls'. Note
that after each removal/addition, the index of the
commands change. So be careful when removing them after each
other.
You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end',
including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil {
return err
}
for _, arg := range args {
if _, err := strconv.Atoi(arg); err != nil {
return err
}
}
return nil
},
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
i, err := strconv.Atoi(args[0])
checkErr(err)
f := i
if len(args) == 2 {
f, err = strconv.Atoi(args[1])
checkErr(err)
}
user := func(u *users.User) {
u.Rules = append(u.Rules[:i], u.Rules[f+1:]...)
err := d.store.Users.Save(u)
checkErr(err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules[:i], s.Rules[f+1:]...)
err := d.store.Settings.Save(s)
checkErr(err)
}
runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

92
cmd/rules.go Normal file
View File

@@ -0,0 +1,92 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
rootCmd.AddCommand(rulesCmd)
rulesCmd.PersistentFlags().StringP("username", "u", "", "username of user to which the rules apply")
rulesCmd.PersistentFlags().UintP("id", "i", 0, "id of user to which the rules apply")
}
var rulesCmd = &cobra.Command{
Use: "rules",
Short: "Rules management utility",
Long: `On each subcommand you'll have available at least two flags:
"username" and "id". You must either set only one of them
or none. If you set one of them, the command will apply to
an user, otherwise it will be applied to the global set or
rules.`,
Args: cobra.NoArgs,
}
func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User), globalFn func(*settings.Settings)) {
id := getUserIdentifier(cmd.Flags())
if id != nil {
user, err := st.Users.Get("", id)
checkErr(err)
if usersFn != nil {
usersFn(user)
}
printRules(user.Rules, id)
return
}
s, err := st.Settings.Get()
checkErr(err)
if globalFn != nil {
globalFn(s)
}
printRules(s.Rules, id)
}
func getUserIdentifier(flags *pflag.FlagSet) interface{} {
id := mustGetUint(flags, "id")
username := mustGetString(flags, "username")
if id != 0 {
return id
} else if username != "" {
return username
}
return nil
}
func printRules(rulez []rules.Rule, id interface{}) {
if id == nil {
fmt.Printf("Global Rules:\n\n")
} else {
fmt.Printf("Rules for user %v:\n\n", id)
}
for id, rule := range rulez {
fmt.Printf("(%d) ", id)
if rule.Regex {
if rule.Allow {
fmt.Printf("Allow Regex: \t%s\n", rule.Regexp.Raw)
} else {
fmt.Printf("Disallow Regex: \t%s\n", rule.Regexp.Raw)
}
} else {
if rule.Allow {
fmt.Printf("Allow Path: \t%s\n", rule.Path)
} else {
fmt.Printf("Disallow Path: \t%s\n", rule.Path)
}
}
}
}

58
cmd/rules_add.go Normal file
View File

@@ -0,0 +1,58 @@
package cmd
import (
"regexp"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/rules"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
rulesCmd.AddCommand(rulesAddCmd)
rulesAddCmd.Flags().BoolP("allow", "a", false, "indicates this is an allow rule")
rulesAddCmd.Flags().BoolP("regex", "r", false, "indicates this is a regex rule")
}
var rulesAddCmd = &cobra.Command{
Use: "add <path|expression>",
Short: "Add a global rule or user rule",
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
allow := mustGetBool(cmd.Flags(), "allow")
regex := mustGetBool(cmd.Flags(), "regex")
exp := args[0]
if regex {
regexp.MustCompile(exp)
}
rule := rules.Rule{
Allow: allow,
Regex: regex,
}
if regex {
rule.Regexp = &rules.Regexp{Raw: exp}
} else {
rule.Path = exp
}
user := func(u *users.User) {
u.Rules = append(u.Rules, rule)
err := d.store.Users.Save(u)
checkErr(err)
}
global := func(s *settings.Settings) {
s.Rules = append(s.Rules, rule)
err := d.store.Settings.Save(s)
checkErr(err)
}
runRules(d.store, cmd, user, global)
}, pythonConfig{}),
}

19
cmd/rules_ls.go Normal file
View File

@@ -0,0 +1,19 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
rulesCmd.AddCommand(rulesLsCommand)
}
var rulesLsCommand = &cobra.Command{
Use: "ls",
Short: "List global rules or user specific rules",
Long: `List global rules or user specific rules.`,
Args: cobra.NoArgs,
Run: python(func(cmd *cobra.Command, _ []string, d pythonData) {
runRules(d.store, cmd, nil, nil)
}, pythonConfig{}),
}

31
cmd/upgrade.go Normal file
View File

@@ -0,0 +1,31 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/storage/bolt/importer"
)
func init() {
rootCmd.AddCommand(upgradeCmd)
upgradeCmd.Flags().String("old.database", "", "")
upgradeCmd.Flags().String("old.config", "", "")
_ = upgradeCmd.MarkFlagRequired("old.database")
}
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "Upgrades an old configuration",
Long: `Upgrades an old configuration. This command DOES NOT
import share links because they are incompatible with
this version.`,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
flags := cmd.Flags()
oldDB := mustGetString(flags, "old.database")
oldConf := mustGetString(flags, "old.config")
err := importer.Import(oldDB, oldConf, getParam(flags, "database"))
checkErr(err)
},
}

134
cmd/users.go Normal file
View File

@@ -0,0 +1,134 @@
package cmd
import (
"errors"
"fmt"
"os"
"strconv"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
rootCmd.AddCommand(usersCmd)
}
var usersCmd = &cobra.Command{
Use: "users",
Short: "Users management utility",
Long: `Users management utility.`,
Args: cobra.NoArgs,
}
func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
u.ID,
u.Username,
u.Scope,
u.Locale,
u.ViewMode,
u.SingleClick,
u.Perm.Admin,
u.Perm.Execute,
u.Perm.Create,
u.Perm.Rename,
u.Perm.Modify,
u.Perm.Delete,
u.Perm.Share,
u.Perm.Download,
u.LockPassword,
)
}
w.Flush()
}
func parseUsernameOrID(arg string) (username string, id uint) {
id64, err := strconv.ParseUint(arg, 10, 64)
if err != nil {
return arg, 0
}
return "", uint(id64)
}
func addUserFlags(flags *pflag.FlagSet) {
flags.Bool("perm.admin", false, "admin perm for users")
flags.Bool("perm.execute", true, "execute perm for users")
flags.Bool("perm.create", true, "create perm for users")
flags.Bool("perm.rename", true, "rename perm for users")
flags.Bool("perm.modify", true, "modify perm for users")
flags.Bool("perm.delete", true, "delete perm for users")
flags.Bool("perm.share", true, "share perm for users")
flags.Bool("perm.download", true, "download perm for users")
flags.String("sorting.by", "name", "sorting mode (name, size or modified)")
flags.Bool("sorting.asc", false, "sorting by ascending order")
flags.Bool("lockPassword", false, "lock password")
flags.StringSlice("commands", nil, "a list of the commands a user can execute")
flags.String("scope", ".", "scope for users")
flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only")
}
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
viewMode := users.ViewMode(mustGetString(flags, "viewMode"))
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
checkErr(errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\""))
}
return viewMode
}
//nolint:gocyclo
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) {
visit := func(flag *pflag.Flag) {
switch flag.Name {
case "scope":
defaults.Scope = mustGetString(flags, flag.Name)
case "locale":
defaults.Locale = mustGetString(flags, flag.Name)
case "viewMode":
defaults.ViewMode = getViewMode(flags)
case "singleClick":
defaults.SingleClick = mustGetBool(flags, flag.Name)
case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
case "perm.execute":
defaults.Perm.Execute = mustGetBool(flags, flag.Name)
case "perm.create":
defaults.Perm.Create = mustGetBool(flags, flag.Name)
case "perm.rename":
defaults.Perm.Rename = mustGetBool(flags, flag.Name)
case "perm.modify":
defaults.Perm.Modify = mustGetBool(flags, flag.Name)
case "perm.delete":
defaults.Perm.Delete = mustGetBool(flags, flag.Name)
case "perm.share":
defaults.Perm.Share = mustGetBool(flags, flag.Name)
case "perm.download":
defaults.Perm.Download = mustGetBool(flags, flag.Name)
case "commands":
commands, err := flags.GetStringSlice(flag.Name)
checkErr(err)
defaults.Commands = commands
case "sorting.by":
defaults.Sorting.By = mustGetString(flags, flag.Name)
case "sorting.asc":
defaults.Sorting.Asc = mustGetBool(flags, flag.Name)
}
}
if all {
flags.VisitAll(visit)
} else {
flags.Visit(visit)
}
}

51
cmd/users_add.go Normal file
View File

@@ -0,0 +1,51 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
usersCmd.AddCommand(usersAddCmd)
addUserFlags(usersAddCmd.Flags())
}
var usersAddCmd = &cobra.Command{
Use: "add <username> <password>",
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)
getUserDefaults(cmd.Flags(), &s.Defaults, false)
password, err := users.HashPwd(args[1])
checkErr(err)
user := &users.User{
Username: args[0],
Password: password,
LockPassword: mustGetBool(cmd.Flags(), "lockPassword"),
}
s.Defaults.Apply(user)
servSettings, err := d.store.Settings.GetServer()
checkErr(err)
// since getUserDefaults() polluted s.Defaults.Scope
// which makes the Scope not the one saved in the db
// we need the right s.Defaults.Scope here
s2, err := d.store.Settings.Get()
checkErr(err)
userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root)
checkErr(err)
user.Scope = userHome
err = d.store.Users.Save(user)
checkErr(err)
printUsers([]*users.User{user})
}, pythonConfig{}),
}

24
cmd/users_export.go Normal file
View File

@@ -0,0 +1,24 @@
package cmd
import (
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersExportCmd)
}
var usersExportCmd = &cobra.Command{
Use: "export <path>",
Short: "Export all users to a file.",
Long: `Export all users to a json or yaml file. Please indicate the
path to the file where you want to write the users.`,
Args: jsonYamlArg,
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
list, err := d.store.Users.Gets("")
checkErr(err)
err = marshal(args[0], list)
checkErr(err)
}, pythonConfig{}),
}

51
cmd/users_find.go Normal file
View File

@@ -0,0 +1,51 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
usersCmd.AddCommand(usersFindCmd)
usersCmd.AddCommand(usersLsCmd)
}
var usersFindCmd = &cobra.Command{
Use: "find <id|username>",
Short: "Find a user by username or id",
Long: `Find a user by username or id. If no flag is set, all users will be printed.`,
Args: cobra.ExactArgs(1),
Run: findUsers,
}
var usersLsCmd = &cobra.Command{
Use: "ls",
Short: "List all users.",
Args: cobra.NoArgs,
Run: findUsers,
}
var findUsers = python(func(_ *cobra.Command, args []string, d pythonData) {
var (
list []*users.User
user *users.User
err error
)
if len(args) == 1 {
username, id := parseUsernameOrID(args[0])
if username != "" {
user, err = d.store.Users.Get("", username)
} else {
user, err = d.store.Users.Get("", id)
}
list = []*users.User{user}
} else {
list, err = d.store.Users.Gets("")
}
checkErr(err)
printUsers(list)
}, pythonConfig{})

89
cmd/users_import.go Normal file
View File

@@ -0,0 +1,89 @@
package cmd
import (
"errors"
"fmt"
"os"
"strconv"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
usersCmd.AddCommand(usersImportCmd)
usersImportCmd.Flags().Bool("overwrite", false, "overwrite users with the same id/username combo")
usersImportCmd.Flags().Bool("replace", false, "replace the entire user base")
}
var usersImportCmd = &cobra.Command{
Use: "import <path>",
Short: "Import users from a file",
Long: `Import users from a file. The path must be for a json or yaml
file. You can use this command to import new users to your
installation. For that, just don't place their ID on the files
list or set it to 0.`,
Args: jsonYamlArg,
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
fd, err := os.Open(args[0])
checkErr(err)
defer fd.Close()
list := []*users.User{}
err = unmarshal(args[0], &list)
checkErr(err)
for _, user := range list {
err = user.Clean("")
checkErr(err)
}
if mustGetBool(cmd.Flags(), "replace") {
oldUsers, err := d.store.Users.Gets("")
checkErr(err)
err = marshal("users.backup.json", list)
checkErr(err)
for _, user := range oldUsers {
err = d.store.Users.Delete(user.ID)
checkErr(err)
}
}
overwrite := mustGetBool(cmd.Flags(), "overwrite")
for _, user := range list {
onDB, err := d.store.Users.Get("", user.ID)
// User exists in DB.
if err == nil {
if !overwrite {
checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registered"))
}
// If the usernames mismatch, check if there is another one in the DB
// with the new username. If there is, print an error and cancel the
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
}
}
} else {
// If it doesn't exist, set the ID to 0 to automatically get a new
// one that make sense in this DB.
user.ID = 0
}
err = d.store.Users.Save(user)
checkErr(err)
}
}, pythonConfig{}),
}
func usernameConflictError(username string, originalID, newID uint) error {
return fmt.Errorf(`can't import user with ID %d and username "%s" because the username is already registered with the user %d`,
newID, username, originalID)
}

31
cmd/users_rm.go Normal file
View File

@@ -0,0 +1,31 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
usersCmd.AddCommand(usersRmCmd)
}
var usersRmCmd = &cobra.Command{
Use: "rm <id|username>",
Short: "Delete a user by username or id",
Long: `Delete a user by username or id`,
Args: cobra.ExactArgs(1),
Run: python(func(_ *cobra.Command, args []string, d pythonData) {
username, id := parseUsernameOrID(args[0])
var err error
if username != "" {
err = d.store.Users.Delete(username)
} else {
err = d.store.Users.Delete(id)
}
checkErr(err)
fmt.Println("user deleted successfully")
}, pythonConfig{}),
}

75
cmd/users_update.go Normal file
View File

@@ -0,0 +1,75 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)
func init() {
usersCmd.AddCommand(usersUpdateCmd)
usersUpdateCmd.Flags().StringP("password", "p", "", "new password")
usersUpdateCmd.Flags().StringP("username", "u", "", "new username")
addUserFlags(usersUpdateCmd.Flags())
}
var usersUpdateCmd = &cobra.Command{
Use: "update <id|username>",
Short: "Updates an existing user",
Long: `Updates an existing user. Set the flags for the
options you want to change.`,
Args: cobra.ExactArgs(1),
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
username, id := parseUsernameOrID(args[0])
flags := cmd.Flags()
password := mustGetString(flags, "password")
newUsername := mustGetString(flags, "username")
var (
err error
user *users.User
)
if id != 0 {
user, err = d.store.Users.Get("", id)
} else {
user, err = d.store.Users.Get("", username)
}
checkErr(err)
defaults := settings.UserDefaults{
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
SingleClick: user.SingleClick,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
}
getUserDefaults(flags, &defaults, false)
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
user.SingleClick = defaults.SingleClick
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.LockPassword = mustGetBool(flags, "lockPassword")
if newUsername != "" {
user.Username = newUsername
}
if password != "" {
user.Password, err = users.HashPwd(password)
checkErr(err)
}
err = d.store.Users.Update(user)
checkErr(err)
printUsers([]*users.User{user})
}, pythonConfig{}),
}

200
cmd/utils.go Normal file
View File

@@ -0,0 +1,200 @@
package cmd
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/asdine/storm/v3"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
yaml "gopkg.in/yaml.v2"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
func mustGetString(flags *pflag.FlagSet, flag string) string {
s, err := flags.GetString(flag)
checkErr(err)
return s
}
func mustGetBool(flags *pflag.FlagSet, flag string) bool {
b, err := flags.GetBool(flag)
checkErr(err)
return b
}
func mustGetUint(flags *pflag.FlagSet, flag string) uint {
b, err := flags.GetUint(flag)
checkErr(err)
return b
}
func generateKey() []byte {
k, err := settings.GenerateKey()
checkErr(err)
return k
}
type cobraFunc func(cmd *cobra.Command, args []string)
type pythonFunc func(cmd *cobra.Command, args []string, data pythonData)
type pythonConfig struct {
noDB bool
allowNoDB bool
}
type pythonData struct {
hadDB bool
store *storage.Storage
}
func dbExists(path string) (bool, error) {
stat, err := os.Stat(path)
if err == nil {
return stat.Size() != 0, nil
}
if os.IsNotExist(err) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet,gomnd
return false, err
}
return false, nil
}
}
return false, err
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) {
data := pythonData{hadDB: true}
path := getParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
}
exists, err := dbExists(path)
if err != nil {
panic(err)
} else if exists && cfg.noDB {
log.Fatal(absPath + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
} else if !exists && !cfg.noDB {
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
}
log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path)
checkErr(err)
defer db.Close()
data.store, err = bolt.NewStorage(db)
checkErr(err)
fn(cmd, args, data)
}
}
func marshal(filename string, data interface{}) error {
fd, err := os.Create(filename)
checkErr(err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
case ".json":
encoder := json.NewEncoder(fd)
encoder.SetIndent("", " ")
return encoder.Encode(data)
case ".yml", ".yaml": //nolint:goconst
encoder := yaml.NewEncoder(fd)
return encoder.Encode(data)
default:
return errors.New("invalid format: " + ext)
}
}
func unmarshal(filename string, data interface{}) error {
fd, err := os.Open(filename)
checkErr(err)
defer fd.Close()
switch ext := filepath.Ext(filename); ext {
case ".json":
return json.NewDecoder(fd).Decode(data)
case ".yml", ".yaml":
return yaml.NewDecoder(fd).Decode(data)
default:
return errors.New("invalid format: " + ext)
}
}
func jsonYamlArg(cmd *cobra.Command, args []string) error {
if err := cobra.ExactArgs(1)(cmd, args); err != nil {
return err
}
switch ext := filepath.Ext(args[0]); ext {
case ".json", ".yml", ".yaml":
return nil
default:
return errors.New("invalid format: " + ext)
}
}
func cleanUpInterfaceMap(in map[interface{}]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range in {
result[fmt.Sprintf("%v", k)] = cleanUpMapValue(v)
}
return result
}
func cleanUpInterfaceArray(in []interface{}) []interface{} {
result := make([]interface{}, len(in))
for i, v := range in {
result[i] = cleanUpMapValue(v)
}
return result
}
func cleanUpMapValue(v interface{}) interface{} {
switch v := v.(type) {
case []interface{}:
return cleanUpInterfaceArray(v)
case map[interface{}]interface{}:
return cleanUpInterfaceMap(v)
default:
return v
}
}
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
// then returns empty string array, else returns the splitted word array of cmd.
// This is to ensure the result will never be []string{""}
func convertCmdStrToCmdArray(cmd string) []string {
var cmdArray []string
trimmedCmdStr := strings.TrimSpace(cmd)
if trimmedCmdStr != "" {
cmdArray = strings.Split(trimmedCmdStr, " ")
}
return cmdArray
}

21
cmd/version.go Normal file
View File

@@ -0,0 +1,21 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/filebrowser/filebrowser/v2/version"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number",
Run: func(_ *cobra.Command, _ []string) {
fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
},
}

34
commitlint.config.js Normal file
View File

@@ -0,0 +1,34 @@
module.exports = {
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'revert',
'refactor',
'build',
'ci',
'test',
'chore',
'docs',
],
],
},
};

28
common.mk Normal file
View File

@@ -0,0 +1,28 @@
SHELL := /usr/bin/env bash
DATE ?= $(shell date +%FT%T%z)
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
go = GOGC=off go
MODULE = $(shell env GO111MODULE=on go list -m)
# printing
# $Q (quiet) is used in the targets as a replacer for @.
# This macro helps to print the command for debugging by setting V to 1. Example `make test-unit V=1`
V = 0
Q = $(if $(filter 1,$V),,@)
# $M is a macro to print a colored ▶ character. Example `$(info $(M) running coverage tests…)` will print "▶ running coverage tests…"
M = $(shell printf "\033[34;1m▶\033[0m")
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)
define global_option
printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n" $(1) $(2)
endef

11
diskcache/cache.go Normal file
View File

@@ -0,0 +1,11 @@
package diskcache
import (
"context"
)
type Interface interface {
Store(ctx context.Context, key string, value []byte) error
Load(ctx context.Context, key string) (value []byte, exist bool, err error)
Delete(ctx context.Context, key string) error
}

110
diskcache/file_cache.go Normal file
View File

@@ -0,0 +1,110 @@
package diskcache
import (
"context"
"crypto/sha1" //nolint:gosec
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sync"
"github.com/spf13/afero"
)
type FileCache struct {
fs afero.Fs
// granular locks
scopedLocks struct {
sync.Mutex
sync.Once
locks map[string]sync.Locker
}
}
func New(fs afero.Fs, root string) *FileCache {
return &FileCache{
fs: afero.NewBasePathFs(fs, root),
}
}
func (f *FileCache) Store(_ context.Context, key string, value []byte) error {
mu := f.getScopedLocks(key)
mu.Lock()
defer mu.Unlock()
fileName := f.getFileName(key)
if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { //nolint:gomnd
return err
}
if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { //nolint:gomnd
return err
}
return nil
}
func (f *FileCache) Load(_ context.Context, key string) (value []byte, exist bool, err error) {
r, ok, err := f.open(key)
if err != nil || !ok {
return nil, ok, err
}
defer r.Close()
value, err = io.ReadAll(r)
if err != nil {
return nil, false, err
}
return value, true, nil
}
func (f *FileCache) Delete(_ context.Context, key string) error {
mu := f.getScopedLocks(key)
mu.Lock()
defer mu.Unlock()
fileName := f.getFileName(key)
if err := f.fs.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return nil
}
func (f *FileCache) open(key string) (afero.File, bool, error) {
fileName := f.getFileName(key)
file, err := f.fs.Open(fileName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
}
return nil, false, err
}
return file, true, nil
}
// getScopedLocks pull lock from the map if found or create a new one
func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) {
f.scopedLocks.Do(func() { f.scopedLocks.locks = map[string]sync.Locker{} })
f.scopedLocks.Lock()
lock, ok := f.scopedLocks.locks[key]
if !ok {
lock = &sync.Mutex{}
f.scopedLocks.locks[key] = lock
}
f.scopedLocks.Unlock()
return lock
}
func (f *FileCache) getFileName(key string) string {
hasher := sha1.New() //nolint:gosec
_, _ = hasher.Write([]byte(key))
hash := hex.EncodeToString(hasher.Sum(nil))
return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash)
}

View File

@@ -0,0 +1,55 @@
package diskcache
import (
"context"
"path/filepath"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
func TestFileCache(t *testing.T) {
ctx := context.Background()
const (
key = "key"
value = "some text"
newValue = "new text"
cacheRoot = "/cache"
cachedFilePath = "a/62/a62f2225bf70bfaccbc7f1ef2a397836717377de"
)
fs := afero.NewMemMapFs()
cache := New(fs, "/cache")
// store new key
err := cache.Store(ctx, key, []byte(value))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, value)
// update existing key
err = cache.Store(ctx, key, []byte(newValue))
require.NoError(t, err)
checkValue(t, ctx, fs, filepath.Join(cacheRoot, cachedFilePath), cache, key, newValue)
// delete key
err = cache.Delete(ctx, key)
require.NoError(t, err)
exists, err := afero.Exists(fs, filepath.Join(cacheRoot, cachedFilePath))
require.NoError(t, err)
require.False(t, exists)
}
func checkValue(t *testing.T, ctx context.Context, fs afero.Fs, fileFullPath string, cache *FileCache, key, wantValue string) { //nolint:revive
t.Helper()
// check actual file content
b, err := afero.ReadFile(fs, fileFullPath)
require.NoError(t, err)
require.Equal(t, wantValue, string(b))
// check cache content
b, ok, err := cache.Load(ctx, key)
require.NoError(t, err)
require.True(t, ok)
require.Equal(t, wantValue, string(b))
}

24
diskcache/noop_cache.go Normal file
View File

@@ -0,0 +1,24 @@
package diskcache
import (
"context"
)
type NoOp struct {
}
func NewNoOp() *NoOp {
return &NoOp{}
}
func (n *NoOp) Store(_ context.Context, _ string, _ []byte) error {
return nil
}
func (n *NoOp) Load(_ context.Context, _ string) (value []byte, exist bool, err error) {
return nil, false, nil
}
func (n *NoOp) Delete(_ context.Context, _ string) error {
return nil
}

View File

@@ -0,0 +1,8 @@
{
"port": 80,
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database/filebrowser.db",
"root": "/srv"
}

View File

@@ -0,0 +1,15 @@
#!/usr/bin/with-contenv bash
# make folders
mkdir -p /database
# copy config
if [ ! -f "/config/settings.json" ]; then
cp -a /defaults/settings.json /config/settings.json
fi
# permissions
chown abc:abc \
/config/settings.json \
/database \
/srv

View File

@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bash
exec s6-setuidgid abc filebrowser -c /config/settings.json -d /database/filebrowser.db;

8
docker_config.json Normal file
View File

@@ -0,0 +1,8 @@
{
"port": 80,
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database.db",
"root": "/srv"
}

370
encfs/file.go Normal file
View File

@@ -0,0 +1,370 @@
package encfs
import (
"crypto/aes"
"crypto/rand"
"encoding/binary"
"encoding/json"
"errors"
"io"
"io/fs"
"math"
"os"
"strings"
"syscall"
"github.com/spf13/afero"
)
const EncFileExt = ".__encfile"
var (
ErrFileForbiddenFileExt = errors.New("file ext is forbidden")
)
type EncFileMeta struct {
Name string `json:"name"`
Iv []byte `json:"iv"`
}
func openOrNewEncFileMeta(name string) (*EncFileMeta, error) {
oldEncFileMeta, err := openEncFileMeta(name)
if err == nil && oldEncFileMeta != nil {
return oldEncFileMeta, nil
}
iv := make([]byte, 16)
_, err = rand.Read(iv)
if err != nil {
return nil, err
}
encFileMeta := &EncFileMeta{
Name: name,
Iv: iv,
}
encFileMetaName := name + EncFileExt
encFileMetaFile, err := os.Create(encFileMetaName)
if err != nil {
return nil, err
}
defer func() {
_ = encFileMetaFile.Close()
}()
encFileMetaBytes, err := marshalEncFileMeta(encFileMeta)
if err != nil {
return nil, err
}
_, err = encFileMetaFile.Write(encFileMetaBytes)
if err != nil {
return nil, err
}
return encFileMeta, nil
}
func openEncFileMeta(name string) (*EncFileMeta, error) {
encFileMetaName := name + EncFileExt
encFileMetaFile, err := os.Open(encFileMetaName)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer func() {
_ = encFileMetaFile.Close()
}()
encFileMetaBytes, err := io.ReadAll(encFileMetaFile)
if err != nil {
return nil, err
}
encFileMeta, err := unmarchalEncFileMeta(encFileMetaBytes)
if err != nil {
return nil, err
}
return encFileMeta, nil
}
func marshalEncFileMeta(encFileMeata *EncFileMeta) ([]byte, error) {
return json.Marshal(encFileMeata)
}
func unmarchalEncFileMeta(data []byte) (*EncFileMeta, error) {
var encFileMeta EncFileMeta
err := json.Unmarshal(data, &encFileMeta)
if err != nil {
return nil, err
}
return &encFileMeta, nil
}
type EncFile struct {
isDir bool
closed bool
encFileMeta *EncFileMeta
encFs *EncFs
filePos int64
file *os.File
}
func NewEncFile(name string, file *os.File, encFs *EncFs, isCreate bool) (*EncFile, error) {
fileInfo, err := file.Stat()
if err != nil {
return nil, err
}
isDir := fileInfo.IsDir()
var encFileMeta *EncFileMeta = nil
fileState, err := os.Stat(name)
if err == nil && !fileState.IsDir() && fileState.Size() == 0 {
isCreate = true
}
if !isDir {
if isCreate {
encFileMeta, err = openOrNewEncFileMeta(name)
} else {
encFileMeta, err = openEncFileMeta(name)
}
if err != nil {
return nil, err
}
}
return &EncFile{
isDir: isDir,
closed: false,
encFileMeta: encFileMeta,
encFs: encFs,
filePos: 0,
file: file,
}, nil
}
func (f *EncFile) Close() error {
if f.closed {
return afero.ErrFileClosed
}
f.closed = true
return f.file.Close()
}
func (f *EncFile) Read(p []byte) (n int, err error) {
checkIsFileErr := f.checkIsFile()
if checkIsFileErr != nil {
return 0, checkIsFileErr
}
beforeReadFilePos := f.filePos
readLen, err := f.file.Read(p)
if err == nil {
f.filePos += int64(readLen)
if f.encFs != nil && f.encFs.key != nil && f.encFileMeta != nil {
encryptedBytes, err := generateCtrEncryptBytes(f.encFs.key.key, f.encFileMeta.Iv, beforeReadFilePos, int64(readLen))
if err != nil {
return 0, err
}
for i := 0; i < readLen; i++ {
p[i] = p[i] ^ encryptedBytes[i]
}
}
}
return readLen, err
}
func (f *EncFile) ReadAt(p []byte, off int64) (n int, err error) {
checkIsFileErr := f.checkIsFile()
if checkIsFileErr != nil {
return 0, checkIsFileErr
}
readLen, err := f.file.ReadAt(p, off)
if err == nil {
if f.encFs != nil && f.encFs.key != nil && f.encFileMeta != nil {
encryptedBytes, err := generateCtrEncryptBytes(f.encFs.key.key, f.encFileMeta.Iv, off, int64(readLen))
if err != nil {
return 0, err
}
for i := 0; i < readLen; i++ {
p[i] = p[i] ^ encryptedBytes[i]
}
}
}
return readLen, err
}
func (f *EncFile) Seek(offset int64, whence int) (int64, error) {
checkIsFileErr := f.checkIsFile()
if checkIsFileErr != nil {
return 0, checkIsFileErr
}
ret, err := f.file.Seek(offset, whence)
if err == nil {
f.filePos = ret
}
return ret, err
}
func (f *EncFile) Write(p []byte) (n int, err error) {
checkIsFileErr := f.checkIsFile()
if checkIsFileErr != nil {
return 0, checkIsFileErr
}
writeBuff := p
if f.encFs != nil && f.encFs.key != nil && f.encFileMeta != nil {
buff := make([]byte, len(p))
encryptedBytes, err := generateCtrEncryptBytes(f.encFs.key.key, f.encFileMeta.Iv, f.filePos, int64(len(p)))
if err != nil {
return 0, err
}
for i := 0; i < len(p); i++ {
buff[i] = p[i] ^ encryptedBytes[i]
}
writeBuff = buff
}
writeLen, err := f.file.Write(writeBuff)
if err == nil {
f.filePos += int64(writeLen)
}
return writeLen, err
}
func (f *EncFile) WriteAt(p []byte, off int64) (n int, err error) {
checkIsFileErr := f.checkIsFile()
if checkIsFileErr != nil {
return 0, checkIsFileErr
}
writeBuff := p
if f.encFs != nil && f.encFs.key != nil && f.encFileMeta != nil {
buff := make([]byte, len(p))
encryptedBytes, err := generateCtrEncryptBytes(f.encFs.key.key, f.encFileMeta.Iv, off, int64(len(p)))
if err != nil {
return 0, err
}
for i := 0; i < len(p); i++ {
buff[i] = p[i] ^ encryptedBytes[i]
}
writeBuff = buff
}
writeLen, err := f.file.WriteAt(writeBuff, off)
return writeLen, err
}
func (f *EncFile) Name() string {
return f.file.Name()
}
func (f *EncFile) Readdir(count int) ([]os.FileInfo, error) {
if f.closed {
return nil, afero.ErrFileClosed
}
// FIXME is count * 2 just ok?
fileInfos, err := f.file.Readdir(count * 2)
if err != nil {
return nil, err
}
if count == 0 {
return make([]fs.FileInfo, 0), nil
}
filterFileInfos := make([]os.FileInfo, 0)
for _, fileInfo := range fileInfos {
isEncFileMetaFile := strings.HasSuffix(fileInfo.Name(), EncFileExt)
if !isEncFileMetaFile {
filterFileInfos = append(filterFileInfos, fileInfo)
}
if count >= 0 && len(filterFileInfos) >= count {
break
}
}
return filterFileInfos, nil
}
func (f *EncFile) Readdirnames(n int) ([]string, error) {
fi, err := f.Readdir(n)
if err != nil {
return nil, err
}
var names []string
for _, f := range fi {
names = append(names, f.Name())
}
return names, nil
}
func (f *EncFile) Stat() (os.FileInfo, error) {
return f.file.Stat()
}
func (f *EncFile) Sync() error {
return f.file.Sync()
}
func (f *EncFile) Truncate(size int64) error {
return f.file.Truncate(size)
}
func (f *EncFile) WriteString(s string) (ret int, err error) {
return f.Write([]byte(s))
}
func (f *EncFile) checkIsFile() error {
if f.closed {
return afero.ErrFileClosed
}
if f.isDir {
return syscall.EISDIR
}
return nil
}
func generateCtrEncryptBytes(key, iv []byte, offset, len int64) ([]byte, error) {
endOffset := offset + len
blockOffset := offset / 16
encryptStartOffset := blockOffset * 16
encryptEndOffset := (endOffset / 16) * 16
if endOffset%16 > 0 {
encryptEndOffset += 16
}
blocksCount := int((encryptEndOffset - encryptStartOffset) / 16)
cipher, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
encryptBytes := make([]byte, blocksCount*16)
for i := 0; i < blocksCount; i++ {
encNonce := nonceAdd(iv, uint64(i)+uint64(blockOffset))
cipher.Encrypt(encryptBytes[i*16:(i+1)*16], encNonce)
}
encryptedBytes := encryptBytes[offset-encryptStartOffset : offset-encryptStartOffset+len]
//fmt.Println("XX", hex.EncodeToString(key), hex.EncodeToString(iv), offset, len, hex.EncodeToString(encryptedBytes))
return encryptedBytes, nil
}
func nonceAdd(nonce []byte, incrementValue uint64) []byte {
n1 := binary.BigEndian.Uint64(nonce[:8])
n2 := binary.BigEndian.Uint64(nonce[8:])
leftToMax := math.MaxUint64 - n2
if leftToMax <= incrementValue {
incrementValue -= leftToMax + 1
n2 = incrementValue
n1 += 1
} else {
n2 += incrementValue
}
newNonce := make([]byte, 16)
binary.BigEndian.PutUint64(newNonce, n1)
binary.BigEndian.PutUint64(newNonce[8:], n2)
return newNonce
}

142
encfs/fs.go Normal file
View File

@@ -0,0 +1,142 @@
package encfs
import (
"os"
"strings"
"time"
"github.com/spf13/afero"
)
// copied from afero/os.go
type EncryptionMasterKey struct {
key []byte
}
type EncFs struct {
key *EncryptionMasterKey
}
func NewEncFs(key *EncryptionMasterKey) afero.Fs {
return &EncFs{
key: key,
}
}
func (*EncFs) Name() string { return "EncFs" }
func (encFs *EncFs) Create(name string) (afero.File, error) {
if err := checkFileExt(name); err != nil {
return nil, err
}
f, e := os.Create(name)
if f == nil {
// while this looks strange, we need to return a bare nil (of type nil) not
// a nil value of type *os.File or nil won't be nil
return nil, e
}
return convertOsFileToEncFile(name, f, e, encFs, true)
}
func (*EncFs) Mkdir(name string, perm os.FileMode) error {
return os.Mkdir(name, perm)
}
func (*EncFs) MkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}
func (encFs *EncFs) Open(name string) (afero.File, error) {
if err := checkFileExt(name); err != nil {
return nil, err
}
f, e := os.Open(name)
if f == nil {
// while this looks strange, we need to return a bare nil (of type nil) not
// a nil value of type *os.File or nil won't be nil
return nil, e
}
return convertOsFileToEncFile(name, f, e, encFs, false)
}
func (encFs *EncFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
if err := checkFileExt(name); err != nil {
return nil, err
}
f, e := os.OpenFile(name, flag, perm)
if f == nil {
// while this looks strange, we need to return a bare nil (of type nil) not
// a nil value of type *os.File or nil won't be nil
return nil, e
}
return convertOsFileToEncFile(name, f, e, encFs, false)
}
func (*EncFs) Remove(name string) error {
encFileMetaName := name + EncFileExt
_ = os.Remove(encFileMetaName)
return os.Remove(name)
}
func (encFs *EncFs) RemoveAll(path string) error {
fileInfo, err := os.Stat(path)
if err == nil && !fileInfo.IsDir() {
return encFs.Remove(path)
}
return os.RemoveAll(path)
}
func (*EncFs) Rename(oldname, newname string) error {
oldEncFileMetaName := oldname + EncFileExt
newEncFileMetaName := newname + EncFileExt
_ = os.Rename(oldEncFileMetaName, newEncFileMetaName)
return os.Rename(oldname, newname)
}
func (*EncFs) Stat(name string) (os.FileInfo, error) {
return os.Stat(name)
}
func (*EncFs) Chmod(name string, mode os.FileMode) error {
return os.Chmod(name, mode)
}
func (*EncFs) Chown(name string, uid, gid int) error {
return os.Chown(name, uid, gid)
}
func (*EncFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
return os.Chtimes(name, atime, mtime)
}
func (*EncFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
fi, err := os.Lstat(name)
return fi, true, err
}
func (*EncFs) SymlinkIfPossible(oldname, newname string) error {
return os.Symlink(oldname, newname)
}
func (*EncFs) ReadlinkIfPossible(name string) (string, error) {
return os.Readlink(name)
}
func checkFileExt(name string) error {
if strings.HasSuffix(name, EncFileExt) {
return ErrFileForbiddenFileExt
}
return nil
}
func convertOsFileToEncFile(name string, file *os.File, e error, encFs *EncFs, isCreate bool) (afero.File, error) {
if e != nil {
return nil, e
}
encFile, err := NewEncFile(name, file, encFs, isCreate)
if err != nil {
return nil, err
}
return encFile, nil
}

129
encfs/kms.go Normal file
View File

@@ -0,0 +1,129 @@
package encfs
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"time"
)
const LOCAL_MINI_KMS_ADDRESS = "LOCAL_MINI_KMS_ADDRESS"
const DISABLE_ENCRYPTION_MASTER_KEY = "DISABLE_ENCRYPTION_MASTER_KEY"
const ENCRYPTED_ENCRYPTION_MASTER_KEY = "ENCRYPTED_ENCRYPTION_MASTER_KEY"
type MultiViewValue struct {
ValueHex string `json:"value_hex"`
ValueBase64 string `json:"value_base64"`
}
type EncryptRequest struct {
EncryptedValue string `json:"encrypted_value"`
}
var cachedcEncryptionMasterKey *EncryptionMasterKey = nil
var cachedcEncryptionMasterKeyLock sync.Mutex
func GetCachedEncryptionMasterKey() (*EncryptionMasterKey, error) {
cachedcEncryptionMasterKeyLock.Lock()
defer cachedcEncryptionMasterKeyLock.Unlock()
if cachedcEncryptionMasterKey == nil {
var err error
cachedcEncryptionMasterKey, err = GetEncryptionMasterKey()
if err != nil {
return nil, err
}
}
return cachedcEncryptionMasterKey, nil
}
func GetEncryptionMasterKey() (*EncryptionMasterKey, error) {
disableEncryptionMasterKey := os.Getenv(DISABLE_ENCRYPTION_MASTER_KEY)
if disableEncryptionMasterKey == "1" ||
disableEncryptionMasterKey == "on" ||
disableEncryptionMasterKey == "yes" ||
disableEncryptionMasterKey == "true" {
return nil, nil
}
encryptedEncryptionMasterKey := os.Getenv(ENCRYPTED_ENCRYPTION_MASTER_KEY)
if encryptedEncryptionMasterKey == "" {
fmt.Println("[ERROR] encrypted encryption master key is not present")
return nil, errors.New("encrypted encryption master key is not present")
}
key, err := DecryptBytes(encryptedEncryptionMasterKey)
if err != nil {
return nil, err
}
return &EncryptionMasterKey{
key: key,
}, nil
}
func DecryptBytes(encryptedValue string) ([]byte, error) {
localMiniKmsAddress := os.Getenv(LOCAL_MINI_KMS_ADDRESS)
if localMiniKmsAddress == "" {
localMiniKmsAddress = "127.0.0.1:5567"
}
if !strings.HasPrefix(strings.ToLower(localMiniKmsAddress), "http://") {
localMiniKmsAddress = fmt.Sprintf("http://%s", localMiniKmsAddress)
}
multiViewValue, err := Decrypt(localMiniKmsAddress, encryptedValue)
if err != nil {
fmt.Println("[ERROR] Decrypt from", localMiniKmsAddress, "failed, error:", err)
return nil, err
}
valueBytes, err := hex.DecodeString(multiViewValue.ValueHex)
if err != nil {
return nil, err
}
return valueBytes, nil
}
func Decrypt(endpoint, encryptedValue string) (*MultiViewValue, error) {
encryptRequest := EncryptRequest{
EncryptedValue: encryptedValue,
}
encryptRequestBytes, err := json.Marshal(&encryptRequest)
if err != nil {
return nil, err
}
encryptRequestReader := bytes.NewReader(encryptRequestBytes)
client := http.Client{
Timeout: 5 * time.Second,
}
encryptResponse, err := client.Post(joinEnpointPath(endpoint, "/decrypt"), "application/json", encryptRequestReader)
if err != nil {
return nil, err
}
if encryptResponse.StatusCode != 200 {
return nil, fmt.Errorf("decrypt failed, http status: %d", encryptResponse.StatusCode)
}
encryptResponseBodyBytes, err := io.ReadAll(encryptResponse.Body)
if err != nil {
return nil, err
}
var multiViewValue MultiViewValue
err = json.Unmarshal(encryptResponseBodyBytes, &multiViewValue)
if err != nil {
return nil, err
}
return &multiViewValue, nil
}
func joinEnpointPath(endpoint, path string) string {
endpointEndsWithSlash := strings.HasSuffix(endpoint, "/")
pathStartsWithSlash := strings.HasPrefix(path, "/")
if endpointEndsWithSlash && pathStartsWithSlash {
return fmt.Sprintf("%s%s", endpoint, path[1:])
} else if endpointEndsWithSlash || pathStartsWithSlash {
return fmt.Sprintf("%s%s", endpoint, path)
} else {
return fmt.Sprintf("%s/%s", endpoint, path)
}
}

21
errors/errors.go Normal file
View File

@@ -0,0 +1,21 @@
package errors
import "errors"
var (
ErrEmptyKey = errors.New("empty key")
ErrExist = errors.New("the resource already exists")
ErrNotExist = errors.New("the resource does not exist")
ErrEmptyPassword = errors.New("password is empty")
ErrEmptyUsername = errors.New("username is empty")
ErrEmptyRequest = errors.New("empty request")
ErrScopeIsRelative = errors.New("scope is a relative path")
ErrInvalidDataType = errors.New("invalid data type")
ErrIsDirectory = errors.New("file is directory")
ErrInvalidOption = errors.New("invalid option")
ErrInvalidAuthMethod = errors.New("invalid auth method")
ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
)

466
files/file.go Normal file
View File

@@ -0,0 +1,466 @@
package files
import (
"crypto/md5" //nolint:gosec
"crypto/sha1" //nolint:gosec
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"errors"
"hash"
"image"
"io"
"io/fs"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/spf13/afero"
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/rules"
)
const PermFile = 0644
const PermDir = 0755
var (
reSubDirs = regexp.MustCompile("(?i)^sub(s|titles)$")
reSubExts = regexp.MustCompile("(?i)(.vtt|.srt|.ass|.ssa)$")
)
// FileInfo describes a file.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
currentDir []os.FileInfo `json:"-"`
Resolution *ImageResolution `json:"resolution,omitempty"`
}
// FileOptions are the options when getting a file info.
type FileOptions struct {
Fs afero.Fs
Path string
Modify bool
Expand bool
ReadHeader bool
Token string
Checker rules.Checker
Content bool
}
type ImageResolution struct {
Width int `json:"width"`
Height int `json:"height"`
}
// NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles.
func NewFileInfo(opts *FileOptions) (*FileInfo, error) {
if !opts.Checker.Check(opts.Path) {
return nil, os.ErrPermission
}
file, err := stat(opts)
if err != nil {
return nil, err
}
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
return nil, err
}
return file, nil
}
err = file.detectType(opts.Modify, opts.Content, true)
if err != nil {
return nil, err
}
}
return file, err
}
func stat(opts *FileOptions) (*FileInfo, error) {
var file *FileInfo
if lstaterFs, ok := opts.Fs.(afero.Lstater); ok {
info, _, err := lstaterFs.LstatIfPossible(opts.Path)
if err != nil {
return nil, err
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
IsSymlink: IsSymlink(info.Mode()),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
}
// regular file
if file != nil && !file.IsSymlink {
return file, nil
}
// fs doesn't support afero.Lstater interface or the file is a symlink
info, err := opts.Fs.Stat(opts.Path)
if err != nil {
// can't follow symlink
if file != nil && file.IsSymlink {
return file, nil
}
return nil, err
}
// set correct file size in case of symlink
if file != nil && file.IsSymlink {
file.Size = info.Size()
file.IsDir = info.IsDir()
return file, nil
}
file = &FileInfo{
Fs: opts.Fs,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
Mode: info.Mode(),
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
return file, nil
}
// Checksum checksums a given File for a given User, using a specific
// algorithm. The checksums data is saved on File object.
func (i *FileInfo) Checksum(algo string) error {
if i.IsDir {
return fbErrors.ErrIsDirectory
}
if i.Checksums == nil {
i.Checksums = map[string]string{}
}
reader, err := i.Fs.Open(i.Path)
if err != nil {
return err
}
defer reader.Close()
var h hash.Hash
//nolint:gosec
switch algo {
case "md5":
h = md5.New()
case "sha1":
h = sha1.New()
case "sha256":
h = sha256.New()
case "sha512":
h = sha512.New()
default:
return fbErrors.ErrInvalidOption
}
_, err = io.Copy(h, reader)
if err != nil {
return err
}
i.Checksums[algo] = hex.EncodeToString(h.Sum(nil))
return nil
}
func (i *FileInfo) RealPath() string {
if realPathFs, ok := i.Fs.(interface {
RealPath(name string) (fPath string, err error)
}); ok {
realPath, err := realPathFs.RealPath(i.Path)
if err == nil {
return realPath
}
}
return i.Path
}
//nolint:goconst
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) {
i.Type = "blob"
return nil
}
// failing to detect the type should not return error.
// imagine the situation where a file in a dir with thousands
// of files couldn't be opened: we'd have immediately
// a 500 even though it doesn't matter. So we just log it.
mimetype := mime.TypeByExtension(i.Extension)
var buffer []byte
if readHeader {
buffer = i.readFirstBytes()
if mimetype == "" {
mimetype = http.DetectContentType(buffer)
}
}
switch {
case strings.HasPrefix(mimetype, "video"):
i.Type = "video"
i.detectSubtitles()
return nil
case strings.HasPrefix(mimetype, "audio"):
i.Type = "audio"
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
resolution, err := calculateImageResolution(i.Fs, i.Path)
if err != nil {
log.Printf("Error calculating image resolution: %v", err)
} else {
i.Resolution = resolution
}
return nil
case strings.HasSuffix(mimetype, "pdf"):
i.Type = "pdf"
return nil
case (strings.HasPrefix(mimetype, "text") || !isBinary(buffer)) && i.Size <= 10*1024*1024: // 10 MB
i.Type = "text"
if !modify {
i.Type = "textImmutable"
}
if saveContent {
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(i.Path)
if err != nil {
return err
}
i.Content = string(content)
}
return nil
default:
i.Type = "blob"
}
return nil
}
func calculateImageResolution(fSys afero.Fs, filePath string) (*ImageResolution, error) {
file, err := fSys.Open(filePath)
if err != nil {
return nil, err
}
defer func() {
if cErr := file.Close(); cErr != nil {
log.Printf("Failed to close file: %v", cErr)
}
}()
config, _, err := image.DecodeConfig(file)
if err != nil {
return nil, err
}
return &ImageResolution{
Width: config.Width,
Height: config.Height,
}, nil
}
func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path)
if err != nil {
log.Print(err)
i.Type = "blob"
return nil
}
defer reader.Close()
buffer := make([]byte, 512) //nolint:gomnd
n, err := reader.Read(buffer)
if err != nil && !errors.Is(err, io.EOF) {
log.Print(err)
i.Type = "blob"
return nil
}
return buffer[:n]
}
func (i *FileInfo) detectSubtitles() {
if i.Type != "video" {
return
}
i.Subtitles = []string{}
ext := filepath.Ext(i.Path)
// detect multiple languages. Base*.vtt
parentDir := strings.TrimRight(i.Path, i.Name)
var dir []os.FileInfo
if len(i.currentDir) > 0 {
dir = i.currentDir
} else {
var err error
dir, err = afero.ReadDir(i.Fs, parentDir)
if err != nil {
return
}
}
base := strings.TrimSuffix(i.Name, ext)
for _, f := range dir {
// load all supported subtitles from subs directories
// should cover all instances of subtitle distributions
// like tv-shows with multiple episodes in single dir
if f.IsDir() && reSubDirs.MatchString(f.Name()) {
subsDir := path.Join(parentDir, f.Name())
i.loadSubtitles(subsDir, base, true)
} else if isSubtitleMatch(f, base) {
i.addSubtitle(path.Join(parentDir, f.Name()))
}
}
}
func (i *FileInfo) loadSubtitles(subsPath, baseName string, recursive bool) {
dir, err := afero.ReadDir(i.Fs, subsPath)
if err == nil {
for _, f := range dir {
if isSubtitleMatch(f, "") {
i.addSubtitle(path.Join(subsPath, f.Name()))
} else if f.IsDir() && recursive && strings.HasPrefix(f.Name(), baseName) {
subsDir := path.Join(subsPath, f.Name())
i.loadSubtitles(subsDir, baseName, false)
}
}
}
}
func IsSupportedSubtitle(fileName string) bool {
return reSubExts.MatchString(fileName)
}
func isSubtitleMatch(f fs.FileInfo, baseName string) bool {
return !f.IsDir() && strings.HasPrefix(f.Name(), baseName) &&
IsSupportedSubtitle(f.Name())
}
func (i *FileInfo) addSubtitle(fPath string) {
i.Subtitles = append(i.Subtitles, fPath)
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
if err != nil {
return err
}
listing := &Listing{
Items: []*FileInfo{},
NumDirs: 0,
NumFiles: 0,
}
for _, f := range dir {
name := f.Name()
fPath := path.Join(i.Path, name)
if !checker.Check(fPath) {
continue
}
isSymlink, isInvalidLink := false, false
if IsSymlink(f.Mode()) {
isSymlink = true
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead of the target's.
info, err := i.Fs.Stat(fPath)
if err == nil {
f = info
} else {
isInvalidLink = true
}
}
file := &FileInfo{
Fs: i.Fs,
Name: name,
Size: f.Size(),
ModTime: f.ModTime(),
Mode: f.Mode(),
IsDir: f.IsDir(),
IsSymlink: isSymlink,
Extension: filepath.Ext(name),
Path: fPath,
currentDir: dir,
}
if !file.IsDir && strings.HasPrefix(mime.TypeByExtension(file.Extension), "image/") {
resolution, err := calculateImageResolution(file.Fs, file.Path)
if err != nil {
log.Printf("Error calculating resolution for image %s: %v", file.Path, err)
} else {
file.Resolution = resolution
}
}
if file.IsDir {
listing.NumDirs++
} else {
listing.NumFiles++
if isInvalidLink {
file.Type = "invalid_link"
} else {
err := file.detectType(true, false, readHeader)
if err != nil {
return err
}
}
}
listing.Items = append(listing.Items, file)
}
i.Listing = listing
return nil
}

110
files/listing.go Normal file
View File

@@ -0,0 +1,110 @@
package files
import (
"sort"
"strings"
"github.com/maruel/natural"
)
// Listing is a collection of files.
type Listing struct {
Items []*FileInfo `json:"items"`
NumDirs int `json:"numDirs"`
NumFiles int `json:"numFiles"`
Sorting Sorting `json:"sorting"`
}
// ApplySort applies the sort order using .Order and .Sort
//
//nolint:goconst
func (l Listing) ApplySort() {
// Check '.Order' to know how to sort
if !l.Sorting.Asc {
switch l.Sorting.By {
case "name":
sort.Sort(sort.Reverse(byName(l)))
case "size":
sort.Sort(sort.Reverse(bySize(l)))
case "modified":
sort.Sort(sort.Reverse(byModified(l)))
default:
// If not one of the above, do nothing
return
}
} else { // If we had more Orderings we could add them here
switch l.Sorting.By {
case "name":
sort.Sort(byName(l))
case "size":
sort.Sort(bySize(l))
case "modified":
sort.Sort(byModified(l))
default:
sort.Sort(byName(l))
return
}
}
}
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byModified Listing
// By Name
func (l byName) Len() int {
return len(l.Items)
}
func (l byName) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
// Treat upper and lower case equally
func (l byName) Less(i, j int) bool {
if l.Items[i].IsDir && !l.Items[j].IsDir {
return l.Sorting.Asc
}
if !l.Items[i].IsDir && l.Items[j].IsDir {
return !l.Sorting.Asc
}
return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name))
}
// By Size
func (l bySize) Len() int {
return len(l.Items)
}
func (l bySize) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
const directoryOffset = -1 << 31 // = math.MinInt32
func (l bySize) Less(i, j int) bool {
iSize, jSize := l.Items[i].Size, l.Items[j].Size
if l.Items[i].IsDir {
iSize = directoryOffset + iSize
}
if l.Items[j].IsDir {
jSize = directoryOffset + jSize
}
return iSize < jSize
}
// By Modified
func (l byModified) Len() int {
return len(l.Items)
}
func (l byModified) Swap(i, j int) {
l.Items[i], l.Items[j] = l.Items[j], l.Items[i]
}
func (l byModified) Less(i, j int) bool {
iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime
return iModified.Sub(jModified) < 0
}

609
files/mime.go Normal file
View File

@@ -0,0 +1,609 @@
package files
// This file contains code primarily sourced from::
// github.com/kataras/iris
import (
"mime"
)
const (
// ContentBinaryHeaderValue header value for binary data.
ContentBinaryHeaderValue = "application/octet-stream"
// ContentWebassemblyHeaderValue header value for web assembly files.
ContentWebassemblyHeaderValue = "application/wasm"
// ContentHTMLHeaderValue is the string of text/html response header's content type value.
ContentHTMLHeaderValue = "text/html"
// ContentJSONHeaderValue header value for JSON data.
ContentJSONHeaderValue = "application/json"
// ContentJSONProblemHeaderValue header value for JSON API problem error.
// Read more at: https://tools.ietf.org/html/rfc7807
ContentJSONProblemHeaderValue = "application/problem+json"
// ContentXMLProblemHeaderValue header value for XML API problem error.
// Read more at: https://tools.ietf.org/html/rfc7807
ContentXMLProblemHeaderValue = "application/problem+xml"
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
ContentJavascriptHeaderValue = "text/javascript"
// ContentTextHeaderValue header value for Text data.
ContentTextHeaderValue = "text/plain"
// ContentXMLHeaderValue header value for XML data.
ContentXMLHeaderValue = "text/xml"
// ContentXMLUnreadableHeaderValue obselete header value for XML.
ContentXMLUnreadableHeaderValue = "application/xml"
// ContentMarkdownHeaderValue custom key/content type, the real is the text/html.
ContentMarkdownHeaderValue = "text/markdown"
// ContentYAMLHeaderValue header value for YAML data.
ContentYAMLHeaderValue = "application/x-yaml"
// ContentYAMLTextHeaderValue header value for YAML plain text.
ContentYAMLTextHeaderValue = "text/yaml"
// ContentProtobufHeaderValue header value for Protobuf messages data.
ContentProtobufHeaderValue = "application/x-protobuf"
// ContentMsgPackHeaderValue header value for MsgPack data.
ContentMsgPackHeaderValue = "application/msgpack"
// ContentMsgPack2HeaderValue alternative header value for MsgPack data.
ContentMsgPack2HeaderValue = "application/x-msgpack"
// ContentFormHeaderValue header value for post form data.
ContentFormHeaderValue = "application/x-www-form-urlencoded"
// ContentFormMultipartHeaderValue header value for post multipart form data.
ContentFormMultipartHeaderValue = "multipart/form-data"
// ContentMultipartRelatedHeaderValue header value for multipart related data.
ContentMultipartRelatedHeaderValue = "multipart/related"
// ContentGRPCHeaderValue Content-Type header value for gRPC.
ContentGRPCHeaderValue = "application/grpc"
)
var types = map[string]string{
".3dm": "x-world/x-3dmf",
".3dmf": "x-world/x-3dmf",
".7z": "application/x-7z-compressed",
".a": "application/octet-stream",
".aab": "application/x-authorware-bin",
".aam": "application/x-authorware-map",
".aas": "application/x-authorware-seg",
".abc": "text/vndabc",
".ace": "application/x-ace-compressed",
".acgi": "text/html",
".afl": "video/animaflex",
".ai": "application/postscript",
".aif": "audio/aiff",
".aifc": "audio/aiff",
".aiff": "audio/aiff",
".aim": "application/x-aim",
".aip": "text/x-audiosoft-intra",
".alz": "application/x-alz-compressed",
".ani": "application/x-navi-animation",
".aos": "application/x-nokia-9000-communicator-add-on-software",
".aps": "application/mime",
".apk": "application/vnd.android.package-archive",
".arc": "application/x-arc-compressed",
".arj": "application/arj",
".art": "image/x-jg",
".asf": "video/x-ms-asf",
".asm": "text/x-asm",
".asp": "text/asp",
".asx": "application/x-mplayer2",
".au": "audio/basic",
".avi": "video/x-msvideo",
".avs": "video/avs-video",
".bcpio": "application/x-bcpio",
".bin": "application/mac-binary",
".bmp": "image/bmp",
".boo": "application/book",
".book": "application/book",
".boz": "application/x-bzip2",
".bsh": "application/x-bsh",
".bz2": "application/x-bzip2",
".bz": "application/x-bzip",
".c++": ContentTextHeaderValue,
".c": "text/x-c",
".cab": "application/vnd.ms-cab-compressed",
".cat": "application/vndms-pkiseccat",
".cc": "text/x-c",
".ccad": "application/clariscad",
".cco": "application/x-cocoa",
".cdf": "application/cdf",
".cer": "application/pkix-cert",
".cha": "application/x-chat",
".chat": "application/x-chat",
".chrt": "application/vnd.kde.kchart",
".class": "application/java",
".com": ContentTextHeaderValue,
".conf": ContentTextHeaderValue,
".cpio": "application/x-cpio",
".cpp": "text/x-c",
".cpt": "application/mac-compactpro",
".crl": "application/pkcs-crl",
".crt": "application/pkix-cert",
".crx": "application/x-chrome-extension",
".csh": "text/x-scriptcsh",
".css": "text/css",
".csv": "text/csv",
".cxx": ContentTextHeaderValue,
".dar": "application/x-dar",
".dcr": "application/x-director",
".deb": "application/x-debian-package",
".deepv": "application/x-deepv",
".def": ContentTextHeaderValue,
".der": "application/x-x509-ca-cert",
".dif": "video/x-dv",
".dir": "application/x-director",
".divx": "video/divx",
".dl": "video/dl",
".dmg": "application/x-apple-diskimage",
".doc": "application/msword",
".dot": "application/msword",
".dp": "application/commonground",
".drw": "application/drafting",
".dump": "application/octet-stream",
".dv": "video/x-dv",
".dvi": "application/x-dvi",
".dwf": "drawing/x-dwf=(old)",
".dwg": "application/acad",
".dxf": "application/dxf",
".dxr": "application/x-director",
".el": "text/x-scriptelisp",
".elc": "application/x-bytecodeelisp=(compiled=elisp)",
".eml": "message/rfc822",
".env": "application/x-envoy",
".eps": "application/postscript",
".es": "application/x-esrehber",
".etx": "text/x-setext",
".evy": "application/envoy",
".exe": "application/octet-stream",
".f77": "text/x-fortran",
".f90": "text/x-fortran",
".f": "text/x-fortran",
".fdf": "application/vndfdf",
".fif": "application/fractals",
".fli": "video/fli",
".flo": "image/florian",
".flv": "video/x-flv",
".flx": "text/vndfmiflexstor",
".fmf": "video/x-atomic3d-feature",
".for": "text/x-fortran",
".fpx": "image/vndfpx",
".frl": "application/freeloader",
".funk": "audio/make",
".g3": "image/g3fax",
".g": ContentTextHeaderValue,
".gif": "image/gif",
".gl": "video/gl",
".gsd": "audio/x-gsm",
".gsm": "audio/x-gsm",
".gsp": "application/x-gsp",
".gss": "application/x-gss",
".gtar": "application/x-gtar",
".gz": "application/x-compressed",
".gzip": "application/x-gzip",
".h": "text/x-h",
".hdf": "application/x-hdf",
".help": "application/x-helpfile",
".hgl": "application/vndhp-hpgl",
".hh": "text/x-h",
".hlb": "text/x-script",
".hlp": "application/hlp",
".hpg": "application/vndhp-hpgl",
".hpgl": "application/vndhp-hpgl",
".hqx": "application/binhex",
".hta": "application/hta",
".htc": "text/x-component",
".htm": "text/html",
".html": "text/html",
".htmls": "text/html",
".htt": "text/webviewhtml",
".htx": "text/html",
".ice": "x-conference/x-cooltalk",
".ico": "image/x-icon",
".ics": "text/calendar",
".icz": "text/calendar",
".idc": ContentTextHeaderValue,
".ief": "image/ief",
".iefs": "image/ief",
".iges": "application/iges",
".igs": "application/iges",
".ima": "application/x-ima",
".imap": "application/x-httpd-imap",
".inf": "application/inf",
".ins": "application/x-internett-signup",
".ip": "application/x-ip2",
".isu": "video/x-isvideo",
".it": "audio/it",
".iv": "application/x-inventor",
".ivr": "i-world/i-vrml",
".ivy": "application/x-livescreen",
".jam": "audio/x-jam",
".jav": "text/x-java-source",
".java": "text/x-java-source",
".jcm": "application/x-java-commerce",
".jfif-tbnl": "image/jpeg",
".jfif": "image/jpeg",
".jnlp": "application/x-java-jnlp-file",
".jpe": "image/jpeg",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".jps": "image/x-jps",
".js": ContentJavascriptHeaderValue,
".mjs": ContentJavascriptHeaderValue,
".json": ContentJSONHeaderValue,
".vue": ContentJavascriptHeaderValue,
".jut": "image/jutvision",
".kar": "audio/midi",
".karbon": "application/vnd.kde.karbon",
".kfo": "application/vnd.kde.kformula",
".flw": "application/vnd.kde.kivio",
".kml": "application/vnd.google-earth.kml+xml",
".kmz": "application/vnd.google-earth.kmz",
".kon": "application/vnd.kde.kontour",
".kpr": "application/vnd.kde.kpresenter",
".kpt": "application/vnd.kde.kpresenter",
".ksp": "application/vnd.kde.kspread",
".kwd": "application/vnd.kde.kword",
".kwt": "application/vnd.kde.kword",
".ksh": "text/x-scriptksh",
".la": "audio/nspaudio",
".lam": "audio/x-liveaudio",
".latex": "application/x-latex",
".lha": "application/lha",
".lhx": "application/octet-stream",
".list": ContentTextHeaderValue,
".lma": "audio/nspaudio",
".log": ContentTextHeaderValue,
".lsp": "text/x-scriptlisp",
".lst": ContentTextHeaderValue,
".lsx": "text/x-la-asf",
".ltx": "application/x-latex",
".lzh": "application/octet-stream",
".lzx": "application/lzx",
".m1v": "video/mpeg",
".m2a": "audio/mpeg",
".m2v": "video/mpeg",
".m3u": "audio/x-mpegurl",
".m": "text/x-m",
".man": "application/x-troff-man",
".manifest": "text/cache-manifest",
".map": "application/x-navimap",
".mar": ContentTextHeaderValue,
".mbd": "application/mbedlet",
".mc$": "application/x-magic-cap-package-10",
".mcd": "application/mcad",
".mcf": "text/mcf",
".mcp": "application/netmc",
".me": "application/x-troff-me",
".mht": "message/rfc822",
".mhtml": "message/rfc822",
".mid": "application/x-midi",
".midi": "application/x-midi",
".mif": "application/x-frame",
".mime": "message/rfc822",
".mjf": "audio/x-vndaudioexplosionmjuicemediafile",
".mjpg": "video/x-motion-jpeg",
".mm": "application/base64",
".mme": "application/base64",
".mod": "audio/mod",
".moov": "video/quicktime",
".mov": "video/quicktime",
".movie": "video/x-sgi-movie",
".mp2": "audio/mpeg",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".mpa": "audio/mpeg",
".mpc": "application/x-project",
".mpe": "video/mpeg",
".mpeg": "video/mpeg",
".mpg": "video/mpeg",
".mpga": "audio/mpeg",
".mpp": "application/vndms-project",
".mpt": "application/x-project",
".mpv": "application/x-project",
".mpx": "application/x-project",
".mrc": "application/marc",
".ms": "application/x-troff-ms",
".mv": "video/x-sgi-movie",
".my": "audio/make",
".mzz": "application/x-vndaudioexplosionmzz",
".nap": "image/naplps",
".naplps": "image/naplps",
".nc": "application/x-netcdf",
".ncm": "application/vndnokiaconfiguration-message",
".nif": "image/x-niff",
".niff": "image/x-niff",
".nix": "application/x-mix-transfer",
".nsc": "application/x-conference",
".nvd": "application/x-navidoc",
".o": "application/octet-stream",
".oda": "application/oda",
".odb": "application/vnd.oasis.opendocument.database",
".odc": "application/vnd.oasis.opendocument.chart",
".odf": "application/vnd.oasis.opendocument.formula",
".odg": "application/vnd.oasis.opendocument.graphics",
".odi": "application/vnd.oasis.opendocument.image",
".odm": "application/vnd.oasis.opendocument.text-master",
".odp": "application/vnd.oasis.opendocument.presentation",
".ods": "application/vnd.oasis.opendocument.spreadsheet",
".odt": "application/vnd.oasis.opendocument.text",
".oga": "audio/ogg",
".ogg": "audio/ogg",
".ogv": "video/ogg",
".omc": "application/x-omc",
".omcd": "application/x-omcdatamaker",
".omcr": "application/x-omcregerator",
".otc": "application/vnd.oasis.opendocument.chart-template",
".otf": "application/vnd.oasis.opendocument.formula-template",
".otg": "application/vnd.oasis.opendocument.graphics-template",
".oth": "application/vnd.oasis.opendocument.text-web",
".oti": "application/vnd.oasis.opendocument.image-template",
".otm": "application/vnd.oasis.opendocument.text-master",
".otp": "application/vnd.oasis.opendocument.presentation-template",
".ots": "application/vnd.oasis.opendocument.spreadsheet-template",
".ott": "application/vnd.oasis.opendocument.text-template",
".p10": "application/pkcs10",
".p12": "application/pkcs-12",
".p7a": "application/x-pkcs7-signature",
".p7c": "application/pkcs7-mime",
".p7m": "application/pkcs7-mime",
".p7r": "application/x-pkcs7-certreqresp",
".p7s": "application/pkcs7-signature",
".p": "text/x-pascal",
".part": "application/pro_eng",
".pas": "text/pascal",
".pbm": "image/x-portable-bitmap",
".pcl": "application/vndhp-pcl",
".pct": "image/x-pict",
".pcx": "image/x-pcx",
".pdb": "chemical/x-pdb",
".pdf": "application/pdf",
".pfunk": "audio/make",
".pgm": "image/x-portable-graymap",
".pic": "image/pict",
".pict": "image/pict",
".pkg": "application/x-newton-compatible-pkg",
".pko": "application/vndms-pkipko",
".pl": "text/x-scriptperl",
".plx": "application/x-pixclscript",
".pm4": "application/x-pagemaker",
".pm5": "application/x-pagemaker",
".pm": "text/x-scriptperl-module",
".png": "image/png",
".pnm": "application/x-portable-anymap",
".pot": "application/mspowerpoint",
".pov": "model/x-pov",
".ppa": "application/vndms-powerpoint",
".ppm": "image/x-portable-pixmap",
".pps": "application/mspowerpoint",
".ppt": "application/mspowerpoint",
".ppz": "application/mspowerpoint",
".pre": "application/x-freelance",
".prt": "application/pro_eng",
".ps": "application/postscript",
".psd": "application/octet-stream",
".pvu": "paleovu/x-pv",
".pwz": "application/vndms-powerpoint",
".py": "text/x-scriptphyton",
".pyc": "application/x-bytecodepython",
".qcp": "audio/vndqcelp",
".qd3": "x-world/x-3dmf",
".qd3d": "x-world/x-3dmf",
".qif": "image/x-quicktime",
".qt": "video/quicktime",
".qtc": "video/x-qtc",
".qti": "image/x-quicktime",
".qtif": "image/x-quicktime",
".ra": "audio/x-pn-realaudio",
".ram": "audio/x-pn-realaudio",
".rar": "application/x-rar-compressed",
".ras": "application/x-cmu-raster",
".rast": "image/cmu-raster",
".rexx": "text/x-scriptrexx",
".rf": "image/vndrn-realflash",
".rgb": "image/x-rgb",
".rm": "application/vndrn-realmedia",
".rmi": "audio/mid",
".rmm": "audio/x-pn-realaudio",
".rmp": "audio/x-pn-realaudio",
".rng": "application/ringing-tones",
".rnx": "application/vndrn-realplayer",
".roff": "application/x-troff",
".rp": "image/vndrn-realpix",
".rpm": "audio/x-pn-realaudio-plugin",
".rt": "text/vndrn-realtext",
".rtf": "text/richtext",
".rtx": "text/richtext",
".rv": "video/vndrn-realvideo",
".s": "text/x-asm",
".s3m": "audio/s3m",
".s7z": "application/x-7z-compressed",
".saveme": "application/octet-stream",
".sbk": "application/x-tbook",
".scm": "text/x-scriptscheme",
".sdml": ContentTextHeaderValue,
".sdp": "application/sdp",
".sdr": "application/sounder",
".sea": "application/sea",
".set": "application/set",
".sgm": "text/x-sgml",
".sgml": "text/x-sgml",
".sh": "text/x-scriptsh",
".shar": "application/x-bsh",
".shtml": "text/x-server-parsed-html",
".sid": "audio/x-psid",
".skd": "application/x-koan",
".skm": "application/x-koan",
".skp": "application/x-koan",
".skt": "application/x-koan",
".sit": "application/x-stuffit",
".sitx": "application/x-stuffitx",
".sl": "application/x-seelogo",
".smi": "application/smil",
".smil": "application/smil",
".snd": "audio/basic",
".sol": "application/solids",
".spc": "text/x-speech",
".spl": "application/futuresplash",
".spr": "application/x-sprite",
".sprite": "application/x-sprite",
".spx": "audio/ogg",
".src": "application/x-wais-source",
".ssi": "text/x-server-parsed-html",
".ssm": "application/streamingmedia",
".sst": "application/vndms-pkicertstore",
".step": "application/step",
".stl": "application/sla",
".stp": "application/step",
".sv4cpio": "application/x-sv4cpio",
".sv4crc": "application/x-sv4crc",
".svf": "image/vnddwg",
".svg": "image/svg+xml",
".svr": "application/x-world",
".swf": "application/x-shockwave-flash",
".t": "application/x-troff",
".talk": "text/x-speech",
".tar": "application/x-tar",
".tbk": "application/toolbook",
".tcl": "text/x-scripttcl",
".tcsh": "text/x-scripttcsh",
".tex": "application/x-tex",
".texi": "application/x-texinfo",
".texinfo": "application/x-texinfo",
".text": ContentTextHeaderValue,
".tgz": "application/gnutar",
".tif": "image/tiff",
".tiff": "image/tiff",
".tr": "application/x-troff",
".tsi": "audio/tsp-audio",
".tsp": "application/dsptype",
".tsv": "text/tab-separated-values",
".turbot": "image/florian",
".txt": ContentTextHeaderValue,
".uil": "text/x-uil",
".uni": "text/uri-list",
".unis": "text/uri-list",
".unv": "application/i-deas",
".uri": "text/uri-list",
".uris": "text/uri-list",
".ustar": "application/x-ustar",
".uu": "text/x-uuencode",
".uue": "text/x-uuencode",
".vcd": "application/x-cdlink",
".vcf": "text/x-vcard",
".vcard": "text/x-vcard",
".vcs": "text/x-vcalendar",
".vda": "application/vda",
".vdo": "video/vdo",
".vew": "application/groupwise",
".viv": "video/vivo",
".vivo": "video/vivo",
".vmd": "application/vocaltec-media-desc",
".vmf": "application/vocaltec-media-file",
".voc": "audio/voc",
".vos": "video/vosaic",
".vox": "audio/voxware",
".vqe": "audio/x-twinvq-plugin",
".vqf": "audio/x-twinvq",
".vql": "audio/x-twinvq-plugin",
".vrml": "application/x-vrml",
".vrt": "x-world/x-vrt",
".vsd": "application/x-visio",
".vst": "application/x-visio",
".vsw": "application/x-visio",
".w60": "application/wordperfect60",
".w61": "application/wordperfect61",
".w6w": "application/msword",
".wav": "audio/wav",
".wb1": "application/x-qpro",
".wbmp": "image/vnd.wap.wbmp",
".web": "application/vndxara",
".wiz": "application/msword",
".wk1": "application/x-123",
".wmf": "windows/metafile",
".wml": "text/vnd.wap.wml",
".wmlc": "application/vnd.wap.wmlc",
".wmls": "text/vnd.wap.wmlscript",
".wmlsc": "application/vnd.wap.wmlscriptc",
".word": "application/msword",
".wp5": "application/wordperfect",
".wp6": "application/wordperfect",
".wp": "application/wordperfect",
".wpd": "application/wordperfect",
".wq1": "application/x-lotus",
".wri": "application/mswrite",
".wrl": "application/x-world",
".wrz": "model/vrml",
".wsc": "text/scriplet",
".wsrc": "application/x-wais-source",
".wtk": "application/x-wintalk",
".x-png": "image/png",
".xbm": "image/x-xbitmap",
".xdr": "video/x-amt-demorun",
".xgz": "xgl/drawing",
".xif": "image/vndxiff",
".xl": "application/excel",
".xla": "application/excel",
".xlb": "application/excel",
".xlc": "application/excel",
".xld": "application/excel",
".xlk": "application/excel",
".xll": "application/excel",
".xlm": "application/excel",
".xls": "application/excel",
".xlt": "application/excel",
".xlv": "application/excel",
".xlw": "application/excel",
".xm": "audio/xm",
".xml": ContentXMLHeaderValue,
".xmz": "xgl/movie",
".xpix": "application/x-vndls-xpix",
".xpm": "image/x-xpixmap",
".xsr": "video/x-amt-showrun",
".xwd": "image/x-xwd",
".xyz": "chemical/x-pdb",
".z": "application/x-compress",
".zip": "application/zip",
".zoo": "application/octet-stream",
".zsh": "text/x-scriptzsh",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".docm": "application/vnd.ms-word.document.macroEnabled.12",
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
".xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
".ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
".sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
".thmx": "application/vnd.ms-officetheme",
".onetoc": "application/onenote",
".onetoc2": "application/onenote",
".onetmp": "application/onenote",
".onepkg": "application/onenote",
".xpi": "application/x-xpinstall",
".wasm": "application/wasm",
".m4a": "audio/mp4",
".flac": "audio/x-flac",
".amr": "audio/amr",
".aac": "audio/aac",
".opus": "video/ogg",
".m4v": "video/mp4",
".mkv": "video/x-matroska",
".caf": "audio/x-caf",
".m3u8": "application/x-mpegURL",
".mpd": "application/dash+xml",
".webp": "image/webp",
".epub": "application/epub+zip",
}
//nolint:gochecknoinits
func init() {
for ext, typ := range types {
// skip errors
_ = mime.AddExtensionType(ext, typ)
}
}

7
files/sorting.go Normal file
View File

@@ -0,0 +1,7 @@
package files
// Sorting contains a sorting order.
type Sorting struct {
By string `json:"by"`
Asc bool `json:"asc"`
}

59
files/utils.go Normal file
View File

@@ -0,0 +1,59 @@
package files
import (
"os"
"unicode/utf8"
)
func isBinary(content []byte) bool {
maybeStr := string(content)
runeCnt := utf8.RuneCount(content)
runeIndex := 0
gotRuneErrCnt := 0
firstRuneErrIndex := -1
const (
// 8 and below are control chars (e.g. backspace, null, eof, etc)
maxControlCharsCode = 8
// 0xFFFD(65533) is the "error" Rune or "Unicode replacement character"
// see https://golang.org/pkg/unicode/utf8/#pkg-constants
unicodeReplacementChar = 0xFFFD
)
for _, b := range maybeStr {
if b <= maxControlCharsCode {
return true
}
if b == unicodeReplacementChar {
// if it is not the last (utf8.UTFMax - x) rune
if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax {
return true
}
// else it is the last (utf8.UTFMax - x) rune
// there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (assume V is the byte we got)
// for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune
gotRuneErrCnt++
// mark the first time
if firstRuneErrIndex == -1 {
firstRuneErrIndex = runeIndex
}
}
runeIndex++
}
// if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all
if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex {
return true
}
return false
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}

39
fileutils/copy.go Normal file
View File

@@ -0,0 +1,39 @@
package fileutils
import (
"os"
"path"
"github.com/spf13/afero"
)
// Copy copies a file or folder from one place to another.
func Copy(fs afero.Fs, src, dst string) error {
if src = path.Clean("/" + src); src == "" {
return os.ErrNotExist
}
if dst = path.Clean("/" + dst); dst == "" {
return os.ErrNotExist
}
if src == "/" || dst == "/" {
// Prohibit copying from or to the virtual root directory.
return os.ErrInvalid
}
if dst == src {
return os.ErrInvalid
}
info, err := fs.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(fs, src, dst)
}
return CopyFile(fs, src, dst)
}

62
fileutils/dir.go Normal file
View File

@@ -0,0 +1,62 @@
package fileutils
import (
"errors"
"github.com/spf13/afero"
)
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source, dest string) error {
// Get properties of source.
srcinfo, err := fs.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = fs.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := fs.Open(source)
obs, err := dir.Readdir(-1)
if err != nil {
return err
}
var errs []error
for _, obj := range obs {
fsource := source + "/" + obj.Name()
fdest := dest + "/" + obj.Name()
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(fs, fsource, fdest)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(fs, fsource, fdest)
if err != nil {
errs = append(errs, err)
}
}
}
var errString string
for _, err := range errs {
errString += err.Error() + "\n"
}
if errString != "" {
return errors.New(errString)
}
return nil
}

130
fileutils/file.go Normal file
View File

@@ -0,0 +1,130 @@
package fileutils
import (
"io"
"os"
"path"
"path/filepath"
"github.com/spf13/afero"
"github.com/filebrowser/filebrowser/v2/files"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(fs afero.Fs, src, dst string) error {
if fs.Rename(src, dst) == nil {
return nil
}
// fallback
err := Copy(fs, src, dst)
if err != nil {
_ = fs.Remove(dst)
return err
}
if err := fs.RemoveAll(src); err != nil {
return err
}
return nil
}
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(fs afero.Fs, source, dest string) error {
// Open the source file.
src, err := fs.Open(source)
if err != nil {
return err
}
defer src.Close()
// Makes the directory needed to create the dst
// file.
err = fs.MkdirAll(filepath.Dir(dest), files.PermDir)
if err != nil {
return err
}
// Create the destination file.
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, files.PermFile)
if err != nil {
return err
}
defer dst.Close()
// Copy the contents of the file.
_, err = io.Copy(dst, src)
if err != nil {
return err
}
// Copy the mode
info, err := fs.Stat(source)
if err != nil {
return err
}
err = fs.Chmod(dest, info.Mode())
if err != nil {
return err
}
return nil
}
// CommonPrefix returns common directory path of provided files
func CommonPrefix(sep byte, paths ...string) string {
// Handle special cases.
switch len(paths) {
case 0:
return ""
case 1:
return path.Clean(paths[0])
}
// Note, we treat string as []byte, not []rune as is often
// done in Go. (And sep as byte, not rune). This is because
// most/all supported OS' treat paths as string of non-zero
// bytes. A filename may be displayed as a sequence of Unicode
// runes (typically encoded as UTF-8) but paths are
// not required to be valid UTF-8 or in any normalized form
// (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different
// file names.
c := []byte(path.Clean(paths[0]))
// We add a trailing sep to handle the case where the
// common prefix directory is included in the path list
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
// path.Clean will have cleaned off trailing / separators with
// the exception of the root directory, "/" (in which case we
// make it "//", but this will get fixed up to "/" bellow).
c = append(c, sep)
// Ignore the first path since it's already in c
for _, v := range paths[1:] {
// Clean up each path before testing it
v = path.Clean(v) + string(sep)
// Find the first non-common byte and truncate c
if len(v) < len(c) {
c = c[:len(v)]
}
for i := 0; i < len(c); i++ {
if v[i] != c[i] {
c = c[:i]
break
}
}
}
// Remove trailing non-separator characters and the final separator
for i := len(c) - 1; i >= 0; i-- {
if c[i] == sep {
c = c[:i]
break
}
}
return string(c)
}

46
fileutils/file_test.go Normal file
View File

@@ -0,0 +1,46 @@
package fileutils
import "testing"
func TestCommonPrefix(t *testing.T) {
testCases := map[string]struct {
paths []string
want string
}{
"same lvl": {
paths: []string{
"/home/user/file1",
"/home/user/file2",
},
want: "/home/user",
},
"sub folder": {
paths: []string{
"/home/user/folder",
"/home/user/folder/file",
},
want: "/home/user/folder",
},
"relative path": {
paths: []string{
"/home/user/folder",
"/home/user/folder/../folder2",
},
want: "/home/user",
},
"no common path": {
paths: []string{
"/home/user/folder",
"/etc/file",
},
want: "",
},
}
for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
if got := CommonPrefix('/', tt.paths...); got != tt.want {
t.Errorf("CommonPrefix() = %v, want %v", got, tt.want)
}
})
}
}

27
frontend/.eslintrc.json Normal file
View File

@@ -0,0 +1,27 @@
{
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier"
],
"rules": {
"vue/multi-word-component-names": "off",
"vue/no-mutating-props": [
"error",
{
"shallowOnly": true
}
]
// no-undef is already included in
// @vue/eslint-config-typescript
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
}
}

2
frontend/.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
# Ignore artifacts:
dist

View File

@@ -0,0 +1,3 @@
{
"trailingComma": "es5"
}

13
frontend/assets.go Normal file
View File

@@ -0,0 +1,13 @@
//go:build !dev
// +build !dev
package frontend
import "embed"
//go:embed dist/*
var assets embed.FS
func Assets() embed.FS {
return assets
}

15
frontend/assets_dev.go Normal file
View File

@@ -0,0 +1,15 @@
//go:build dev
// +build dev
package frontend
import (
"io/fs"
"os"
)
var assets fs.FS = os.DirFS("frontend")
func Assets() fs.FS {
return assets
}

0
frontend/dist/.gitkeep vendored Normal file
View File

192
frontend/index.html Normal file
View File

@@ -0,0 +1,192 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
<title>File Browser</title>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta name="theme-color" content="#2979ff" />
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="/img/icons/mstile-144x144.png"
/>
<meta name="msapplication-TileColor" content="#2979ff" />
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = {
AuthMethod: "json",
BaseURL: "",
CSS: false,
Color: "",
DisableExternal: false,
DisableUsedPercentage: false,
EnableExec: true,
EnableThumbs: true,
LoginPage: true,
Name: "",
NoAuth: false,
ReCaptcha: false,
ResizePreview: true,
Signup: false,
StaticURL: "",
Theme: "",
TusSettings: { chunkSize: 10485760, retryCount: 5 },
Version: "(untracked)",
};
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-192x192.png"
),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl(
"/img/icons/android-chrome-512x512.png"
),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

10
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

7962
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

73
frontend/package.json Normal file
View File

@@ -0,0 +1,73 @@
{
"name": "filebrowser-frontend",
"version": "3.0.0",
"private": true,
"type": "module",
"engines": {
"npm": ">=7.0.0",
"node": ">=18.0.0"
},
"scripts": {
"dev": "vite dev",
"build": "npm run typecheck && vite build",
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
"lint": "npm run typecheck && eslint --ext .vue,.ts src/",
"lint:fix": "eslint --ext .vue,.ts --fix src/",
"format": "prettier --write .",
"test": "playwright test"
},
"dependencies": {
"@chenfengyuan/vue-number-input": "^2.0.1",
"@vueuse/core": "^10.9.0",
"@vueuse/integrations": "^10.9.0",
"ace-builds": "^1.32.9",
"core-js": "^3.36.1",
"dayjs": "^1.11.10",
"filesize": "^10.1.1",
"js-base64": "^3.7.7",
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"material-icons": "^1.13.12",
"marked": "^14.1.0",
"normalize.css": "^8.0.1",
"pinia": "^2.1.7",
"pretty-bytes": "^6.1.1",
"qrcode.vue": "^3.4.1",
"tus-js-client": "^4.1.0",
"utif": "^3.1.0",
"video.js": "^8.10.0",
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.4.21",
"vue-final-modal": "^4.5.4",
"vue-i18n": "^9.10.2",
"vue-lazyload": "^3.0.0",
"vue-reader": "^1.2.14",
"vue-router": "^4.3.0",
"vue-toastification": "^2.0.0-rc.5"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@playwright/test": "^1.42.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.12.2",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@vitejs/plugin-legacy": "^5.3.2",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"autoprefixer": "^10.4.19",
"concurrently": "^8.2.2",
"eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.24.0",
"jsdom": "^24.0.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"terser": "^5.30.0",
"vite": "^5.2.7",
"vite-plugin-compression2": "^1.0.0",
"vue-tsc": "^2.0.7"
}
}

View File

@@ -0,0 +1,80 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
// {
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev",
url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};

0
frontend/public/.gitkeep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#455a64</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M3245 6989 c-522 -39 -1042 -197 -1480 -449 -849 -488 -1459 -1308
-1673 -2250 -177 -776 -89 -1582 250 -2301 368 -778 1052 -1418 1857 -1739
903 -359 1927 -325 2812 92 778 368 1418 1052 1739 1857 359 903 325 1927 -92
2812 -296 627 -806 1175 -1423 1529 -587 338 -1308 500 -1990 449z m555 -580
c519 -51 1018 -245 1446 -565 788 -588 1229 -1526 1174 -2496 -16 -277 -58
-500 -145 -763 -144 -440 -378 -819 -710 -1150 -452 -452 -1005 -730 -1655
-832 -91 -14 -175 -18 -405 -18 -304 0 -369 6 -595 51 -1105 223 -1999 1092
-2259 2197 -52 221 -73 412 -73 667 0 397 64 732 204 1080 304 752 886 1334
1638 1638 431 174 895 238 1380 191z"/>
<path d="M2670 5215 c0 -13 -44 -15 -335 -15 -352 0 -383 -3 -399 -45 -3 -9
-6 -758 -6 -1663 0 -1168 -3 -1643 -11 -1632 -8 11 -9 8 -4 -15 3 -16 17 -41
31 -55 l24 -25 1530 0 1530 0 24 25 c14 14 26 36 27 50 1 14 1 711 1 1550 l-2
1526 -228 142 -229 142 -136 0 -137 0 0 -600 0 -600 -705 0 -705 0 0 615 0
615 -135 0 c-113 0 -135 -2 -135 -15z m-264 -190 c57 -29 89 -71 103 -137 35
-154 -98 -282 -258 -247 -55 12 -122 62 -148 113 -36 69 -12 186 49 243 62 58
170 70 254 28z m2316 -1702 c17 -15 18 -49 18 -670 l0 -653 -1245 0 -1245 0 0
654 c0 582 2 656 16 670 14 14 139 16 1226 16 1113 0 1213 -1 1230 -17z
m-2602 -1363 c40 -40 13 -100 -43 -100 -60 0 -88 59 -47 100 11 11 31 20 45
20 14 0 34 -9 45 -20z m2840 0 c41 -41 11 -100 -52 -100 -35 0 -58 24 -58 60
0 54 71 79 110 40z"/>
<path d="M2431 3091 c-7 -13 -7 -23 2 -35 11 -15 97 -16 1067 -14 l1055 3 0
30 0 30 -1057 3 c-1023 2 -1058 1 -1067 -17z"/>
<path d="M2436 2675 c-19 -19 -11 -41 17 -49 41 -11 2067 -7 2088 4 23 13 25
46 3 54 -9 3 -483 6 -1054 6 -919 0 -1040 -2 -1054 -15z"/>
<path d="M2447 2273 c-14 -4 -17 -13 -15 -36 l3 -32 1049 -3 c767 -1 1052 1
1062 9 20 16 17 47 -5 59 -20 10 -2055 13 -2094 3z"/>
<path d="M3822 5027 c-21 -23 -22 -30 -22 -293 0 -258 1 -271 20 -292 27 -29
56 -35 140 -30 56 3 75 8 93 26 22 22 22 26 22 298 l0 276 -24 19 c-19 16 -40
19 -115 19 -84 0 -95 -2 -114 -23z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xml:space="preserve"
width="560"
height="560"
version="1.1"
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
viewBox="0 0 560 560"
id="svg44"
sodipodi:docname="icon_raw.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
inkscape:export-filename="/home/umarcor/filebrowser/logo/icon_raw.svg.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><metadata
id="metadata48"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="711"
id="namedview46"
showgrid="false"
inkscape:zoom="0.33714286"
inkscape:cx="-172.33051"
inkscape:cy="280"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="svg44" />
<defs
id="defs4">
<style
type="text/css"
id="style2">
<![CDATA[
.fil1 {fill:#FEFEFE}
.fil6 {fill:#006498}
.fil7 {fill:#0EA5EB}
.fil8 {fill:#2979FF}
.fil3 {fill:#2BBCFF}
.fil0 {fill:#455A64}
.fil4 {fill:#53C6FC}
.fil5 {fill:#BDEAFF}
.fil2 {fill:#332C2B;fill-opacity:0.149020}
]]>
</style>
</defs>
<g
id="g85"
transform="translate(-70,-70)"><path
class="fil1"
d="M 350,71 C 504,71 629,196 629,350 629,504 504,629 350,629 196,629 71,504 71,350 71,196 196,71 350,71 Z"
id="path9"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil2"
d="M 475,236 593,387 C 596,503 444,639 301,585 L 225,486 339,330 c 0,0 138,-95 136,-94 z"
id="path11"
inkscape:connector-curvature="0"
style="fill:#332c2b;fill-opacity:0.14902003" /><path
class="fil3"
d="m 231,211 h 208 l 38,24 v 246 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 V 219 c 0,-5 3,-8 8,-8 z"
id="path13"
inkscape:connector-curvature="0"
style="fill:#2bbcff" /><path
class="fil4"
d="m 231,211 h 208 l 38,24 v 2 L 440,214 H 231 c -4,0 -7,3 -7,7 v 263 c -1,-1 -1,-2 -1,-3 V 219 c 0,-5 3,-8 8,-8 z"
id="path15"
inkscape:connector-curvature="0"
style="fill:#53c6fc" /><polygon
class="fil5"
points="305,212 418,212 418,310 305,310 "
id="polygon17"
style="fill:#bdeaff" /><path
class="fil5"
d="m 255,363 h 189 c 3,0 5,2 5,4 V 483 H 250 V 367 c 0,-2 2,-4 5,-4 z"
id="path19"
inkscape:connector-curvature="0"
style="fill:#bdeaff" /><polygon
class="fil6"
points="250,470 449,470 449,483 250,483 "
id="polygon21"
style="fill:#006498" /><path
class="fil6"
d="m 380,226 h 10 c 3,0 6,2 6,5 v 40 c 0,3 -3,6 -6,6 h -10 c -3,0 -6,-3 -6,-6 v -40 c 0,-3 3,-5 6,-5 z"
id="path23"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil1"
d="m 254,226 c 10,0 17,7 17,17 0,9 -7,16 -17,16 -9,0 -17,-7 -17,-16 0,-10 8,-17 17,-17 z"
id="path25"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil6"
d="m 267,448 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,3 -3,3 H 267 c -2,0 -3,-2 -3,-3 v 0 c 0,-2 1,-3 3,-3 z"
id="path27"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil6"
d="m 267,415 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,2 -3,2 H 267 c -2,0 -3,-1 -3,-2 v 0 c 0,-2 1,-3 3,-3 z"
id="path29"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil6"
d="m 267,381 h 165 c 2,0 3,2 3,3 v 0 c 0,2 -1,3 -3,3 H 267 c -2,0 -3,-1 -3,-3 v 0 c 0,-1 1,-3 3,-3 z"
id="path31"
inkscape:connector-curvature="0"
style="fill:#006498" /><path
class="fil1"
d="m 236,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
id="path33"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><path
class="fil1"
d="m 463,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
id="path35"
inkscape:connector-curvature="0"
style="fill:#fefefe" /><polygon
class="fil6"
points="305,212 284,212 284,310 305,310 "
id="polygon37"
style="fill:#006498" /><path
class="fil7"
d="m 477,479 v 2 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 v -2 c 0,4 3,8 8,8 h 238 c 5,0 8,-4 8,-8 z"
id="path39"
inkscape:connector-curvature="0"
style="fill:#0ea5eb" /><path
class="fil8"
d="M 350,70 C 505,70 630,195 630,350 630,505 505,630 350,630 195,630 70,505 70,350 70,195 195,70 350,70 Z m 0,46 C 479,116 584,221 584,350 584,479 479,584 350,584 221,584 116,479 116,350 116,221 221,116 350,116 Z"
id="path41"
inkscape:connector-curvature="0"
style="fill:#2979ff" /></g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

190
frontend/public/index.html Normal file
View File

@@ -0,0 +1,190 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no"
/>
[{[ if .ReCaptcha -]}]
<script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit"></script>
[{[ end ]}]
<title>
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
</title>
<meta name="robots" content="noindex,nofollow" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png"
/>
<!-- Add to home screen for Android and modern mobile browsers -->
<link
rel="manifest"
id="manifestPlaceholder"
crossorigin="use-credentials"
/>
<meta
name="theme-color"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="assets" />
<link
rel="apple-touch-icon"
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
/>
<!-- Add to home screen for Windows -->
<meta
name="msapplication-TileImage"
content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png"
/>
<meta
name="msapplication-TileColor"
content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]"
/>
<!-- Inject Some Variables and generate the manifest json -->
<script>
// We can assign JSON directly
window.FileBrowser = [{[ .Json ]}];
// Global function to prepend static url
window.__prependStaticUrl = (url) => {
return `${window.FileBrowser.StaticURL}/${url.replace(/^\/+/, "")}`;
};
var dynamicManifest = {
name: window.FileBrowser.Name || "File Browser",
short_name: window.FileBrowser.Name || "File Browser",
icons: [
{
src: window.__prependStaticUrl("/img/icons/android-chrome-192x192.png"),
sizes: "192x192",
type: "image/png",
},
{
src: window.__prependStaticUrl("/img/icons/android-chrome-512x512.png"),
sizes: "512x512",
type: "image/png",
},
],
start_url: window.location.origin + window.FileBrowser.BaseURL,
display: "standalone",
background_color: "#ffffff",
theme_color: window.FileBrowser.Color || "#455a64",
};
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], { type: "application/json" });
const manifestURL = URL.createObjectURL(blob);
document
.querySelector("#manifestPlaceholder")
.setAttribute("href", manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: 0.1s ease opacity;
-webkit-transition: 0.1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div id="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
[{[ if .CSS -]}]
<link rel="stylesheet" href="[{[ .StaticURL ]}]/custom.css" />
[{[ end ]}]
</body>
</html>

View File

@@ -0,0 +1,20 @@
{
"name": "File Browser",
"short_name": "File Browser",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./static/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#455a64"
}

33
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,33 @@
<template>
<div>
<router-view></router-view>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
import { useI18n } from "vue-i18n";
import { setHtmlLocale } from "./i18n";
import { getMediaPreference, getTheme, setTheme } from "./utils/theme";
const { locale } = useI18n();
const userTheme = ref<UserTheme>(getTheme() || getMediaPreference());
onMounted(() => {
setTheme(userTheme.value);
setHtmlLocale(locale.value);
// this might be null during HMR
const loading = document.getElementById("loading");
loading?.classList.add("done");
setTimeout(function () {
loading?.parentNode?.removeChild(loading);
}, 200);
});
// handles ltr/rtl changes
watch(locale, (newValue) => {
newValue && setHtmlLocale(newValue);
});
</script>

View File

@@ -0,0 +1,23 @@
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
export default function command(
url: string,
command: string,
onmessage: WebSocket["onmessage"],
onclose: WebSocket["onclose"]
) {
const authStore = useAuthStore();
url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${authStore.jwt}`;
const conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command);
conn.onmessage = onmessage;
conn.onclose = onclose;
}

217
frontend/src/api/files.ts Normal file
View File

@@ -0,0 +1,217 @@
import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import { useAuthStore } from "@/stores/auth";
import { upload as postTus, useTus } from "./tus";
export async function fetch(url: string) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {});
const data = (await res.json()) as Resource;
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
// Perhaps change the any
data.items = data.items.map((item: any, index: any) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
}
async function resourceAction(url: string, method: ApiMethod, content?: any) {
url = removePrefix(url);
const opts: ApiOpts = {
method,
};
if (content) {
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts);
return res;
}
export async function remove(url: string) {
return resourceAction(url, "DELETE");
}
export async function put(url: string, content = "") {
return resourceAction(url, "PUT", content);
}
export function download(format: any, ...files: string[]) {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + "?";
} else {
let arg = "";
for (const file of files) {
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
const authStore = useAuthStore();
if (authStore.jwt) {
url += `auth=${authStore.jwt}&`;
}
window.open(url);
}
export async function post(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any = () => {}
) {
// Use the pre-existing API if:
const useResourcesApi =
// a folder is being created
url.endsWith("/") ||
// We're not using http(s)
(content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)) ||
// Tus is disabled / not applicable
!(await useTus(content));
return useResourcesApi
? postResources(url, content, overwrite, onupload)
: postTus(url, content, overwrite, onupload);
}
async function postResources(
url: string,
content: ApiContent = "",
overwrite = false,
onupload: any
) {
url = removePrefix(url);
let bufferContent: ArrayBuffer;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
const authStore = useAuthStore();
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", authStore.jwt);
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status);
} else {
reject(request.responseText);
}
};
request.onerror = () => {
reject(new Error("001 Connection aborted"));
};
request.send(bufferContent || content);
});
}
function moveCopy(
items: any[],
copy = false,
overwrite = false,
rename = false
) {
const promises = [];
for (const item of items) {
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to ?? ""));
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises);
}
export function move(items: any[], overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename);
}
export function copy(items: any[], overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename);
}
export async function checksum(url: string, algo: ChecksumAlg) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
}
export function getDownloadURL(file: ResourceItem, inline: any) {
const params = {
...(inline && { inline: "true" }),
};
return createURL("api/raw" + file.path, params);
}
export function getPreviewURL(file: ResourceItem, size: string) {
const params = {
inline: "true",
key: Date.parse(file.modified),
};
return createURL("api/preview/" + size + file.path, params);
}
export function getSubtitlesURL(file: ResourceItem) {
const params = {
inline: "true",
};
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
}
export async function usage(url: string) {
url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {});
return await res.json();
}

View File

@@ -0,0 +1,9 @@
import * as files from "./files";
import * as share from "./share";
import * as users from "./users";
import * as settings from "./settings";
import * as pub from "./pub";
import search from "./search";
import commands from "./commands";
export { files, share, users, settings, pub, commands, search };

75
frontend/src/api/pub.ts Normal file
View File

@@ -0,0 +1,75 @@
import { fetchURL, removePrefix, createURL } from "./utils";
import { baseURL } from "@/utils/constants";
export async function fetch(url: string, password: string = "") {
url = removePrefix(url);
const res = await fetchURL(
`/api/public/share${url}`,
{
headers: { "X-SHARE-PASSWORD": encodeURIComponent(password) },
},
false
);
const data = (await res.json()) as Resource;
data.url = `/share${url}`;
if (data.isDir) {
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item: any, index: any) => {
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += "/";
}
return item;
});
}
return data;
}
export function download(
format: DownloadFormat,
hash: string,
token: string,
...files: string[]
) {
let url = `${baseURL}/api/public/dl/${hash}`;
if (files.length === 1) {
url += encodeURIComponent(files[0]) + "?";
} else {
let arg = "";
for (const file of files) {
arg += encodeURIComponent(file) + ",";
}
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format) {
url += `algo=${format}&`;
}
if (token) {
url += `token=${token}&`;
}
window.open(url);
}
export function getDownloadURL(res: Resource, inline = false) {
const params = {
...(inline && { inline: "true" }),
...(res.token && { token: res.token }),
};
return createURL("api/public/dl/" + res.hash + res.path, params, false);
}

Some files were not shown because too many files have changed in this diff Show More