├── .circleci └── config.yml ├── .docker.json ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build-dirty.yml │ └── build-release.yml ├── .gitignore ├── .goreleaser.yml ├── .tx └── config ├── Dockerfile ├── LICENSE ├── README.md ├── auth ├── auth.go ├── json.go ├── none.go ├── proxy.go └── storage.go ├── cmd ├── cmd.go ├── cmds.go ├── cmds_add.go ├── cmds_ls.go ├── cmds_rm.go ├── config.go ├── config_cat.go ├── config_export.go ├── config_import.go ├── config_init.go ├── config_set.go ├── docs.go ├── hash.go ├── root.go ├── rule_rm.go ├── rules.go ├── rules_add.go ├── rules_ls.go ├── upgrade.go ├── users.go ├── users_add.go ├── users_export.go ├── users_find.go ├── users_import.go ├── users_rm.go ├── users_update.go ├── utils.go └── version.go ├── errors └── errors.go ├── files ├── file.go ├── listing.go ├── sorting.go └── utils.go ├── fileutils ├── copy.go ├── dir.go └── file.go ├── frontend ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── img │ │ ├── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-144x144.png │ │ │ ├── mstile-150x150.png │ │ │ ├── mstile-310x150.png │ │ │ ├── mstile-310x310.png │ │ │ ├── mstile-70x70.png │ │ │ └── safari-pinned-tab.svg │ │ └── logo.svg │ ├── index.html │ ├── manifest.json │ └── themes │ │ └── dark.css ├── src │ ├── App.vue │ ├── api │ │ ├── commands.js │ │ ├── files.js │ │ ├── index.js │ │ ├── search.js │ │ ├── settings.js │ │ ├── share.js │ │ ├── users.js │ │ └── utils.js │ ├── assets │ │ └── fonts │ │ │ └── roboto │ │ │ ├── medium-cyrillic-ext.woff2 │ │ │ ├── medium-cyrillic.woff2 │ │ │ ├── medium-greek-ext.woff2 │ │ │ ├── medium-greek.woff2 │ │ │ ├── medium-latin-ext.woff2 │ │ │ ├── medium-latin.woff2 │ │ │ ├── medium-vietnamese.woff2 │ │ │ ├── normal-cyrillic-ext.woff2 │ │ │ ├── normal-cyrillic.woff2 │ │ │ ├── normal-greek-ext.woff2 │ │ │ ├── normal-greek.woff2 │ │ │ ├── normal-latin-ext.woff2 │ │ │ ├── normal-latin.woff2 │ │ │ └── normal-vietnamese.woff2 │ ├── components │ │ ├── Header.vue │ │ ├── Search.vue │ │ ├── Shell.vue │ │ ├── Sidebar.vue │ │ ├── buttons │ │ │ ├── Copy.vue │ │ │ ├── Delete.vue │ │ │ ├── Download.vue │ │ │ ├── Info.vue │ │ │ ├── Move.vue │ │ │ ├── Rename.vue │ │ │ ├── Share.vue │ │ │ ├── Shell.vue │ │ │ ├── SwitchView.vue │ │ │ └── Upload.vue │ │ ├── files │ │ │ ├── Editor.vue │ │ │ ├── ExtendedImage.vue │ │ │ ├── Listing.vue │ │ │ ├── ListingItem.vue │ │ │ └── Preview.vue │ │ ├── prompts │ │ │ ├── Copy.vue │ │ │ ├── Delete.vue │ │ │ ├── Download.vue │ │ │ ├── FileList.vue │ │ │ ├── Help.vue │ │ │ ├── Info.vue │ │ │ ├── Move.vue │ │ │ ├── NewDir.vue │ │ │ ├── NewFile.vue │ │ │ ├── Prompts.vue │ │ │ ├── Rename.vue │ │ │ ├── Replace.vue │ │ │ └── Share.vue │ │ └── settings │ │ │ ├── Commands.vue │ │ │ ├── Languages.vue │ │ │ ├── Permissions.vue │ │ │ ├── Rules.vue │ │ │ ├── Themes.vue │ │ │ └── UserForm.vue │ ├── css │ │ ├── _buttons.css │ │ ├── _inputs.css │ │ ├── _share.css │ │ ├── _shell.css │ │ ├── _variables.css │ │ ├── base.css │ │ ├── dashboard.css │ │ ├── fonts.css │ │ ├── header.css │ │ ├── listing.css │ │ ├── login.css │ │ ├── mobile.css │ │ └── styles.css │ ├── i18n │ │ ├── ar.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── index.js │ │ ├── is.json │ │ ├── it.json │ │ ├── ja.json │ │ ├── ko.json │ │ ├── nl-be.json │ │ ├── pl.json │ │ ├── pt-br.json │ │ ├── pt.json │ │ ├── ro.json │ │ ├── ru.json │ │ ├── sv-se.json │ │ ├── zh-cn.json │ │ └── zh-tw.json │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── mutations.js │ ├── utils │ │ ├── auth.js │ │ ├── buttons.js │ │ ├── constants.js │ │ ├── cookie.js │ │ ├── css.js │ │ ├── url.js │ │ └── vue.js │ └── views │ │ ├── Files.vue │ │ ├── Layout.vue │ │ ├── Login.vue │ │ ├── Settings.vue │ │ ├── Share.vue │ │ ├── errors │ │ ├── 403.vue │ │ ├── 404.vue │ │ └── 500.vue │ │ └── settings │ │ ├── Global.vue │ │ ├── Profile.vue │ │ ├── User.vue │ │ └── Users.vue └── vue.config.js ├── go.mod ├── go.sum ├── http ├── auth.go ├── commands.go ├── data.go ├── http.go ├── public.go ├── raw.go ├── resource.go ├── search.go ├── settings.go ├── share.go ├── static.go ├── users.go └── utils.go ├── main.go ├── rules └── rules.go ├── runner ├── parser.go └── runner.go ├── search ├── conditions.go └── search.go ├── settings ├── branding.go ├── defaults.go ├── dir.go ├── settings.go └── storage.go ├── share ├── share.go └── storage.go ├── storage ├── bolt │ ├── auth.go │ ├── bolt.go │ ├── config.go │ ├── importer │ │ ├── conf.go │ │ ├── importer.go │ │ └── users.go │ ├── share.go │ ├── users.go │ └── utils.go └── storage.go ├── users ├── password.go ├── permissions.go ├── storage.go └── users.go ├── version └── version.go └── wizard.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | lint: 4 | docker: 5 | - image: golangci/golangci-lint:v1.16 6 | steps: 7 | - checkout 8 | - run: golangci-lint run -v -D errcheck 9 | build-node: 10 | docker: 11 | - image: circleci/node 12 | steps: 13 | - checkout 14 | - run: 15 | name: "Build" 16 | command: ./wizard.sh -a 17 | - run: 18 | name: "Cleanup" 19 | command: rm -rf frontend/node_modules 20 | - persist_to_workspace: 21 | root: . 22 | paths: 23 | - '*' 24 | build-go: 25 | docker: 26 | - image: circleci/golang:1.12 27 | steps: 28 | - attach_workspace: 29 | at: '~/project' 30 | - run: 31 | name: "Compile" 32 | command: GOOS=linux GOARCH=amd64 ./wizard.sh -c 33 | - run: 34 | name: "Cleanup" 35 | command: | 36 | rm -rf frontend/build 37 | git checkout -- go.sum # TODO: why is it being changed? 38 | - persist_to_workspace: 39 | root: . 40 | paths: 41 | - '*' 42 | release: 43 | docker: 44 | - image: circleci/golang:1.12 45 | steps: 46 | - attach_workspace: 47 | at: '~/project' 48 | - setup_remote_docker 49 | - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 50 | - run: curl -sL https://git.io/goreleaser | bash 51 | - run: docker logout 52 | workflows: 53 | version: 2 54 | build-workflow: 55 | jobs: 56 | - lint: 57 | filters: 58 | tags: 59 | only: /.*/ 60 | - build-node: 61 | filters: 62 | tags: 63 | only: /.*/ 64 | - build-go: 65 | filters: 66 | tags: 67 | only: /.*/ 68 | requires: 69 | - build-node 70 | - lint 71 | - release: 72 | context: deploy 73 | requires: 74 | - build-go 75 | filters: 76 | tags: 77 | only: /^v.*/ 78 | branches: 79 | ignore: /.*/ -------------------------------------------------------------------------------- /.docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 80, 3 | "baseURL": "", 4 | "address": "", 5 | "log": "stdout", 6 | "database": "/database.db", 7 | "root": "/srv" 8 | } 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | testdata/ 2 | .github/ 3 | **.git 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Description** 7 | A clear and concise description of what the issue is about. What are you trying to do? 8 | 9 | **Expected behaviour** 10 | What did you expect to happen? 11 | 12 | **What is happening instead?** 13 | Please, give full error messages and/or log. 14 | 15 | **Additional context** 16 | Add any other context about the problem here. If applicable, add screenshots to help explain your problem. 17 | 18 | **How to reproduce?** 19 | Tell us how to reproduce this issue. How can someone who is starting from scratch reproduce this behaviour as minimally as possible? 20 | 21 | **Files** 22 | A list of relevant files for this issue. Large files can be uploaded one-by-one or in a tarball/zipfile. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | Add a clear and concise description of what the problem is. E.g. *I'm always frustrated when [...]* 8 | 9 | **Describe the solution you'd like** 10 | Add a clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | Add a clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Description** 2 | Please explain the changes you made here. 3 | If the feature changes current behaviour, explain why your solution is better. 4 | 5 | :rotating_light: Before submitting your PR, please read [community](https://github.com/filebrowser/community), and indicate which issues (in any of the repos) are either fixed or closed by this PR. See [GitHub Help: Closing issues using keywords](https://help.github.com/articles/closing-issues-via-commit-messages/). 6 | 7 | - [ ] DO make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master! 8 | - [ ] DO make sure you are making a pull request against the **master branch** (left side). Also you should start *your branch* off *our master*. 9 | - [ ] DO make sure that File Browser can be successfully built. See [builds](https://github.com/filebrowser/community/blob/master/builds.md) and [development](https://github.com/filebrowser/community/blob/master/development.md). 10 | - [ ] DO make sure that related issues are opened in other repositories. I.e., the frontend, caddy plugins or the web page need to be updated accordingly. 11 | - [ ] AVOID breaking the continuous integration build. 12 | 13 | **Further comments** 14 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did, what alternatives you considered, etc. 15 | 16 | :heart: Thank you! 17 | -------------------------------------------------------------------------------- /.github/workflows/build-dirty.yml: -------------------------------------------------------------------------------- 1 | name: build-dirty 2 | on: [ push ] 3 | 4 | jobs: 5 | dirty-build-frontend: 6 | name: dirty-build-frontend 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v1 11 | - name: Build frontend 12 | run: ./wizard.sh -a 13 | - name: Persisting frontend dist 14 | uses: actions/upload-artifact@v1 15 | with: 16 | name: frontend_dist 17 | path: frontend/dist 18 | 19 | dirty-build-go: 20 | name: dirty-build-go 21 | runs-on: ubuntu-18.04 22 | needs: dirty-build-frontend 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Preparing go build env 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: '1.12.17' 29 | - name: Download frontend dist 30 | uses: actions/download-artifact@v1 31 | with: 32 | name: frontend_dist 33 | path: frontend/dist 34 | - name: Compile Go 35 | run: GOOS=linux GOARCH=amd64 ./wizard.sh -c 36 | - name: Cleanup 37 | run: | 38 | git checkout -- go.sum -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: build-release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | 9 | build-frontend: 10 | name: build-node 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | - run: ./wizard.sh -a 16 | - run: rm -rf frontend/node_modules 17 | - name: Persisting frontend dist 18 | uses: actions/upload-artifact@v1 19 | with: 20 | name: frontend_dist 21 | path: frontend/dist 22 | 23 | build-go: 24 | name: build-go 25 | runs-on: ubuntu-18.04 26 | needs: build-frontend 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Preparing go build env 30 | uses: actions/setup-go@v2 31 | with: 32 | go-version: '1.12.17' 33 | - name: Create frontend dist directory 34 | run: mkdir -p frontend/dist 35 | - name: Download frontend dist 36 | uses: actions/download-artifact@v1 37 | with: 38 | name: frontend_dist 39 | path: frontend/dist 40 | - name: Compile Go 41 | run: GOOS=linux GOARCH=amd64 ./wizard.sh -c 42 | - name: Cleanup 43 | run: | 44 | git checkout -- go.sum 45 | - name: Persisting go dist 46 | uses: actions/upload-artifact@v1 47 | with: 48 | name: go_dist 49 | path: . 50 | 51 | release: 52 | name: release 53 | runs-on: ubuntu-18.04 54 | needs: build-go 55 | steps: 56 | - name: Set up Go 57 | uses: actions/setup-go@v2 58 | with: 59 | go-version: 1.12.17 60 | - name: Download go dist 61 | uses: actions/download-artifact@v1 62 | with: 63 | name: go_dist 64 | path: . 65 | - name: Run goreleaser 66 | uses: goreleaser/goreleaser-action@v2 67 | with: 68 | version: latest 69 | args: release --rm-dist --skip-validate 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.lock 3 | *.bak 4 | _old 5 | rice-box.go 6 | .idea/ 7 | filebrowser 8 | 9 | .DS_Store 10 | node_modules 11 | /frontend/dist 12 | 13 | # local env files 14 | .env.local 15 | .env.*.local 16 | 17 | # Log files 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | # Editor directories and files 23 | .idea 24 | .vscode 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw* 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: filebrowser 2 | 3 | env: 4 | - GO111MODULE=on 5 | 6 | before: 7 | hooks: 8 | - go mod download 9 | 10 | build: 11 | env: 12 | - CGO_ENABLED=0 13 | ldflags: 14 | - -s -w -X github.com/dev-techmoe/filebrowser/v2/version.Version={{ .Version }} -X github.com/dev-techmoe/filebrowser/v2/version.CommitSHA={{ .ShortCommit }} 15 | main: main.go 16 | binary: filebrowser 17 | goos: 18 | - darwin 19 | - linux 20 | - windows 21 | - freebsd 22 | - netbsd 23 | - openbsd 24 | - dragonfly 25 | - solaris 26 | goarch: 27 | - amd64 28 | - 386 29 | - arm 30 | - arm64 31 | goarm: 32 | - 5 33 | - 6 34 | - 7 35 | ignore: 36 | - goos: darwin 37 | goarch: 386 38 | - goos: openbsd 39 | goarch: arm 40 | - goos: openbsd 41 | goarch: arm64 42 | - goos: freebsd 43 | goarch: arm 44 | - goos: freebsd 45 | goarch: arm64 46 | - goos: netbsd 47 | goarch: arm 48 | - goos: netbsd 49 | goarch: arm64 50 | - goos: solaris 51 | goarch: arm 52 | - goos: solaris 53 | goarch: arm64 54 | 55 | archives: 56 | - 57 | name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}" 58 | format: tar.gz 59 | format_overrides: 60 | - goos: windows 61 | format: zip 62 | 63 | release: 64 | prerelease: auto 65 | 66 | # dockers: 67 | # - 68 | # goos: linux 69 | # goarch: amd64 70 | # goarm: '' 71 | # image_templates: 72 | # - "dev-techmoe/filebrowser:latest" 73 | # - "dev-techmoe/filebrowser:{{ .Tag }}" 74 | # - "dev-techmoe/filebrowser:v{{ .Major }}" 75 | # extra_files: 76 | # - .docker.json 77 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = pt_BR: pt-br, zh_CN: zh-cn, zh_HK: zh-hk, zh_TW: zh-tw, nl_BE: nl-be, sv_SE: sv-se 4 | 5 | [file-browser.file-browser] 6 | file_filter = frontend/src/i18n/.json 7 | minimum_perc = 50 8 | source_file = frontend/src/i18n/en.json 9 | source_lang = en 10 | type = KEYVALUEJSON 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as alpine 2 | RUN apk --update add ca-certificates 3 | RUN apk --update add mailcap 4 | 5 | FROM scratch 6 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 7 | COPY --from=alpine /etc/mime.types /etc/mime.types 8 | 9 | VOLUME /srv 10 | EXPOSE 80 11 | 12 | COPY .docker.json /.filebrowser.json 13 | COPY filebrowser /filebrowser 14 | 15 | ENTRYPOINT [ "/filebrowser" ] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Notice: This project is fork from [https://github.com/filebrowser/filebrowser](filebrowser/filebrowser) and the original author is no longer maintenance this project. Now this repo is maintenance by [@dev-techmoe](https://github.com/dev-techmoe) and it will 2 | get necessary security update and some bug fix only, new features will not be added in principle. Feel free to fork this repo and PR is welcome! 3 | 4 | ⚠️ WARN: **This project will not be developed anymore. If you're willing to take over this project, please read [#532](https://github.com/filebrowser/filebrowser/issues/532) for more info and feel free to fork the project!** 5 | 6 | 7 |

8 | 9 |

10 | 11 | ![Preview](https://user-images.githubusercontent.com/5447088/50716739-ebd26700-107a-11e9-9817-14230c53efd2.gif) 12 | 13 | [![Travis](https://img.shields.io/travis/com/filebrowser/filebrowser.svg?style=flat-square)](https://travis-ci.com/filebrowser/filebrowser) 14 | [![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser?style=flat-square)](https://goreportcard.com/report/github.com/filebrowser/filebrowser) 15 | [![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/filebrowser/filebrowser) 16 | [![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg?style=flat-square)](https://github.com/filebrowser/filebrowser/releases/latest) 17 | [![Chat IRC](https://img.shields.io/badge/freenode-%23filebrowser-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23filebrowser) 18 | 19 | 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 or as a middleware. 20 | 21 | ## Features 22 | 23 | Please refer to our docs at [filebrowser.xyz/features](https://github.com/filebrowser/docs/tree/master/features) 24 | 25 | ## Install 26 | 27 | Please refer to our docs at [filebrowser.xyz](https://github.com/filebrowser/docs/tree/master/). 28 | 29 | ## Usage 30 | 31 | Please refer to our docs at [filebrowser.xyz/usage](https://github.com/filebrowser/docs/tree/master/usage). 32 | 33 | ## Contributing 34 | 35 | Please refer to our docs at [filebrowser.xyz/contributing](https://github.com/filebrowser/docs/tree/master/contributing). 36 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/users" 7 | ) 8 | 9 | // Auther is the authentication interface. 10 | type Auther interface { 11 | // Auth is called to authenticate a request. 12 | Auth(r *http.Request, s *users.Storage, root string) (*users.User, error) 13 | // LoginPage indicates if this auther needs a login page. 14 | LoginPage() bool 15 | } 16 | -------------------------------------------------------------------------------- /auth/json.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "strings" 9 | 10 | "github.com/dev-techmoe/filebrowser/v2/settings" 11 | "github.com/dev-techmoe/filebrowser/v2/users" 12 | ) 13 | 14 | // MethodJSONAuth is used to identify json auth. 15 | const MethodJSONAuth settings.AuthMethod = "json" 16 | 17 | type jsonCred struct { 18 | Password string `json:"password"` 19 | Username string `json:"username"` 20 | ReCaptcha string `json:"recaptcha"` 21 | } 22 | 23 | // JSONAuth is a json implementaion of an Auther. 24 | type JSONAuth struct { 25 | ReCaptcha *ReCaptcha `json:"recaptcha" yaml:"recaptcha"` 26 | } 27 | 28 | // Auth authenticates the user via a json in content body. 29 | func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { 30 | var cred jsonCred 31 | 32 | if r.Body == nil { 33 | return nil, os.ErrPermission 34 | } 35 | 36 | err := json.NewDecoder(r.Body).Decode(&cred) 37 | if err != nil { 38 | return nil, os.ErrPermission 39 | } 40 | 41 | // If ReCaptcha is enabled, check the code. 42 | if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 { 43 | ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) 44 | 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | if !ok { 50 | return nil, os.ErrPermission 51 | } 52 | } 53 | 54 | u, err := sto.Get(root, cred.Username) 55 | if err != nil || !users.CheckPwd(cred.Password, u.Password) { 56 | return nil, os.ErrPermission 57 | } 58 | 59 | return u, nil 60 | } 61 | 62 | // LoginPage tells that json auth doesn't require a login page. 63 | func (a JSONAuth) LoginPage() bool { 64 | return true 65 | } 66 | 67 | const reCaptchaAPI = "/recaptcha/api/siteverify" 68 | 69 | // ReCaptcha identifies a recaptcha conenction. 70 | type ReCaptcha struct { 71 | Host string `json:"host"` 72 | Key string `json:"key"` 73 | Secret string `json:"secret"` 74 | } 75 | 76 | // Ok checks if a reCaptcha responde is correct. 77 | func (r *ReCaptcha) Ok(response string) (bool, error) { 78 | body := url.Values{} 79 | body.Set("secret", r.Secret) 80 | body.Add("response", response) 81 | 82 | client := &http.Client{} 83 | 84 | resp, err := client.Post( 85 | r.Host+reCaptchaAPI, 86 | "application/x-www-form-urlencoded", 87 | strings.NewReader(body.Encode()), 88 | ) 89 | if err != nil { 90 | return false, err 91 | } 92 | 93 | if resp.StatusCode != http.StatusOK { 94 | return false, nil 95 | } 96 | 97 | var data struct { 98 | Success bool `json:"success"` 99 | } 100 | 101 | err = json.NewDecoder(resp.Body).Decode(&data) 102 | if err != nil { 103 | return false, err 104 | } 105 | 106 | return data.Success, nil 107 | } 108 | -------------------------------------------------------------------------------- /auth/none.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/settings" 7 | "github.com/dev-techmoe/filebrowser/v2/users" 8 | ) 9 | 10 | // MethodNoAuth is used to identify no auth. 11 | const MethodNoAuth settings.AuthMethod = "noauth" 12 | 13 | // NoAuth is no auth implementation of auther. 14 | type NoAuth struct{} 15 | 16 | // Auth uses authenticates user 1. 17 | func (a NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { 18 | return sto.Get(root, uint(1)) 19 | } 20 | 21 | // LoginPage tells that no auth doesn't require a login page. 22 | func (a NoAuth) LoginPage() bool { 23 | return false 24 | } 25 | -------------------------------------------------------------------------------- /auth/proxy.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/errors" 8 | "github.com/dev-techmoe/filebrowser/v2/settings" 9 | "github.com/dev-techmoe/filebrowser/v2/users" 10 | ) 11 | 12 | // MethodProxyAuth is used to identify no auth. 13 | const MethodProxyAuth settings.AuthMethod = "proxy" 14 | 15 | // ProxyAuth is a proxy implementation of an auther. 16 | type ProxyAuth struct { 17 | Header string `json:"header"` 18 | } 19 | 20 | // Auth authenticates the user via an HTTP header. 21 | func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) { 22 | username := r.Header.Get(a.Header) 23 | user, err := sto.Get(root, username) 24 | if err == errors.ErrNotExist { 25 | return nil, os.ErrPermission 26 | } 27 | 28 | return user, err 29 | } 30 | 31 | // LoginPage tells that proxy auth doesn't require a login page. 32 | func (a ProxyAuth) LoginPage() bool { 33 | return false 34 | } 35 | -------------------------------------------------------------------------------- /auth/storage.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/dev-techmoe/filebrowser/v2/settings" 5 | "github.com/dev-techmoe/filebrowser/v2/users" 6 | ) 7 | 8 | // StorageBackend is a storage backend for auth storage. 9 | type StorageBackend interface { 10 | Get(settings.AuthMethod) (Auther, error) 11 | Save(Auther) error 12 | } 13 | 14 | // Storage is a auth storage. 15 | type Storage struct { 16 | back StorageBackend 17 | users *users.Storage 18 | } 19 | 20 | // NewStorage creates a auth storage from a backend. 21 | func NewStorage(back StorageBackend, users *users.Storage) *Storage { 22 | return &Storage{back: back, users: users} 23 | } 24 | 25 | // Get wraps a StorageBackend.Get. 26 | func (s *Storage) Get(t settings.AuthMethod) (Auther, error) { 27 | return s.back.Get(t) 28 | } 29 | 30 | // Save wraps a StorageBackend.Save. 31 | func (s *Storage) Save(a Auther) error { 32 | return s.back.Save(a) 33 | } 34 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // Execute executes the commands. 8 | func Execute() { 9 | if err := rootCmd.Execute(); err != nil { 10 | log.Fatal(err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cmd/cmds.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | rootCmd.AddCommand(cmdsCmd) 11 | } 12 | 13 | var cmdsCmd = &cobra.Command{ 14 | Use: "cmds", 15 | Short: "Command runner management utility", 16 | Long: `Command runner management utility.`, 17 | Args: cobra.NoArgs, 18 | } 19 | 20 | func printEvents(m map[string][]string) { 21 | for evt, cmds := range m { 22 | for i, cmd := range cmds { 23 | fmt.Printf("%s(%d): %s\n", evt, i, cmd) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/cmds_add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | cmdsCmd.AddCommand(cmdsAddCmd) 11 | } 12 | 13 | var cmdsAddCmd = &cobra.Command{ 14 | Use: "add ", 15 | Short: "Add a command to run on a specific event", 16 | Long: `Add a command to run on a specific event.`, 17 | Args: cobra.MinimumNArgs(2), 18 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 19 | s, err := d.store.Settings.Get() 20 | checkErr(err) 21 | command := strings.Join(args[1:], " ") 22 | s.Commands[args[0]] = append(s.Commands[args[0]], command) 23 | err = d.store.Settings.Save(s) 24 | checkErr(err) 25 | printEvents(s.Commands) 26 | }, pythonConfig{}), 27 | } 28 | -------------------------------------------------------------------------------- /cmd/cmds_ls.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | cmdsCmd.AddCommand(cmdsLsCmd) 9 | cmdsLsCmd.Flags().StringP("event", "e", "", "event name, without 'before' or 'after'") 10 | } 11 | 12 | var cmdsLsCmd = &cobra.Command{ 13 | Use: "ls", 14 | Short: "List all commands for each event", 15 | Long: `List all commands for each event.`, 16 | Args: cobra.NoArgs, 17 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 18 | s, err := d.store.Settings.Get() 19 | checkErr(err) 20 | evt := mustGetString(cmd.Flags(), "event") 21 | 22 | if evt == "" { 23 | printEvents(s.Commands) 24 | } else { 25 | show := map[string][]string{} 26 | show["before_"+evt] = s.Commands["before_"+evt] 27 | show["after_"+evt] = s.Commands["after_"+evt] 28 | printEvents(show) 29 | } 30 | }, pythonConfig{}), 31 | } 32 | -------------------------------------------------------------------------------- /cmd/cmds_rm.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | cmdsCmd.AddCommand(cmdsRmCmd) 11 | } 12 | 13 | var cmdsRmCmd = &cobra.Command{ 14 | Use: "rm [index_end]", 15 | Short: "Removes a command from an event hooker", 16 | Long: `Removes a command from an event hooker. The provided index 17 | is the same that's printed when you run 'cmds ls'. Note 18 | that after each removal/addition, the index of the 19 | commands change. So be careful when removing them after each 20 | other. 21 | 22 | You can also specify an optional parameter (index_end) so 23 | you can remove all commands from 'index' to 'index_end', 24 | including 'index_end'.`, 25 | Args: func(cmd *cobra.Command, args []string) error { 26 | if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { 27 | return err 28 | } 29 | 30 | for _, arg := range args[1:] { 31 | if _, err := strconv.Atoi(arg); err != nil { 32 | return err 33 | } 34 | } 35 | 36 | return nil 37 | }, 38 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 39 | s, err := d.store.Settings.Get() 40 | checkErr(err) 41 | evt := args[0] 42 | 43 | i, err := strconv.Atoi(args[1]) 44 | checkErr(err) 45 | f := i 46 | if len(args) == 3 { 47 | f, err = strconv.Atoi(args[2]) 48 | checkErr(err) 49 | } 50 | 51 | s.Commands[evt] = append(s.Commands[evt][:i], s.Commands[evt][f+1:]...) 52 | err = d.store.Settings.Save(s) 53 | checkErr(err) 54 | printEvents(s.Commands) 55 | }, pythonConfig{}), 56 | } 57 | -------------------------------------------------------------------------------- /cmd/config_cat.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | configCmd.AddCommand(configCatCmd) 9 | } 10 | 11 | var configCatCmd = &cobra.Command{ 12 | Use: "cat", 13 | Short: "Prints the configuration", 14 | Long: `Prints the configuration.`, 15 | Args: cobra.NoArgs, 16 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 17 | set, err := d.store.Settings.Get() 18 | checkErr(err) 19 | ser, err := d.store.Settings.GetServer() 20 | checkErr(err) 21 | auther, err := d.store.Auth.Get(set.AuthMethod) 22 | checkErr(err) 23 | printSettings(ser, set, auther) 24 | }, pythonConfig{}), 25 | } 26 | -------------------------------------------------------------------------------- /cmd/config_export.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | configCmd.AddCommand(configExportCmd) 9 | } 10 | 11 | var configExportCmd = &cobra.Command{ 12 | Use: "export ", 13 | Short: "Export the configuration to a file", 14 | Long: `Export the configuration to a file. The path must be for a 15 | json or yaml file. This exported configuration can be changed, 16 | and imported again with 'config import' command.`, 17 | Args: jsonYamlArg, 18 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 19 | settings, err := d.store.Settings.Get() 20 | checkErr(err) 21 | 22 | server, err := d.store.Settings.GetServer() 23 | checkErr(err) 24 | 25 | auther, err := d.store.Auth.Get(settings.AuthMethod) 26 | checkErr(err) 27 | 28 | data := &settingsFile{ 29 | Settings: settings, 30 | Auther: auther, 31 | Server: server, 32 | } 33 | 34 | err = marshal(args[0], data) 35 | checkErr(err) 36 | }, pythonConfig{}), 37 | } 38 | -------------------------------------------------------------------------------- /cmd/config_import.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "path/filepath" 7 | "reflect" 8 | 9 | "github.com/dev-techmoe/filebrowser/v2/auth" 10 | "github.com/dev-techmoe/filebrowser/v2/settings" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func init() { 15 | configCmd.AddCommand(configImportCmd) 16 | } 17 | 18 | type settingsFile struct { 19 | Settings *settings.Settings `json:"settings"` 20 | Server *settings.Server `json:"server"` 21 | Auther interface{} `json:"auther"` 22 | } 23 | 24 | var configImportCmd = &cobra.Command{ 25 | Use: "import ", 26 | Short: "Import a configuration file", 27 | Long: `Import a configuration file. This will replace all the existing 28 | configuration. Can be used with or without unexisting databases. 29 | 30 | If used with a nonexisting database, a key will be generated 31 | automatically. Otherwise the key will be kept the same as in the 32 | database. 33 | 34 | The path must be for a json or yaml file.`, 35 | Args: jsonYamlArg, 36 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 37 | var key []byte 38 | if d.hadDB { 39 | settings, err := d.store.Settings.Get() 40 | checkErr(err) 41 | key = settings.Key 42 | } else { 43 | key = generateKey() 44 | } 45 | 46 | file := settingsFile{} 47 | err := unmarshal(args[0], &file) 48 | checkErr(err) 49 | 50 | file.Settings.Key = key 51 | err = d.store.Settings.Save(file.Settings) 52 | checkErr(err) 53 | 54 | err = d.store.Settings.SaveServer(file.Server) 55 | checkErr(err) 56 | 57 | var rawAuther interface{} 58 | if filepath.Ext(args[0]) != ".json" { 59 | rawAuther = cleanUpInterfaceMap(file.Auther.(map[interface{}]interface{})) 60 | } else { 61 | rawAuther = file.Auther 62 | } 63 | 64 | var auther auth.Auther 65 | switch file.Settings.AuthMethod { 66 | case auth.MethodJSONAuth: 67 | auther = getAuther(auth.JSONAuth{}, rawAuther).(*auth.JSONAuth) 68 | case auth.MethodNoAuth: 69 | auther = getAuther(auth.NoAuth{}, rawAuther).(*auth.NoAuth) 70 | case auth.MethodProxyAuth: 71 | auther = getAuther(auth.ProxyAuth{}, rawAuther).(*auth.ProxyAuth) 72 | default: 73 | checkErr(errors.New("invalid auth method")) 74 | } 75 | 76 | err = d.store.Auth.Save(auther) 77 | checkErr(err) 78 | 79 | printSettings(file.Server, file.Settings, auther) 80 | }, pythonConfig{allowNoDB: true}), 81 | } 82 | 83 | func getAuther(sample auth.Auther, data interface{}) interface{} { 84 | authType := reflect.TypeOf(sample) 85 | auther := reflect.New(authType).Interface() 86 | bytes, err := json.Marshal(data) 87 | checkErr(err) 88 | err = json.Unmarshal(bytes, &auther) 89 | checkErr(err) 90 | return auther 91 | } 92 | -------------------------------------------------------------------------------- /cmd/config_init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/settings" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func init() { 12 | configCmd.AddCommand(configInitCmd) 13 | addConfigFlags(configInitCmd.Flags()) 14 | } 15 | 16 | var configInitCmd = &cobra.Command{ 17 | Use: "init", 18 | Short: "Initialize a new database", 19 | Long: `Initialize a new database to use with File Browser. All of 20 | this options can be changed in the future with the command 21 | 'filebrowser config set'. The user related flags apply 22 | to the defaults when creating new users and you don't 23 | override the options.`, 24 | Args: cobra.NoArgs, 25 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 26 | defaults := settings.UserDefaults{} 27 | flags := cmd.Flags() 28 | getUserDefaults(flags, &defaults, true) 29 | authMethod, auther := getAuthentication(flags) 30 | 31 | s := &settings.Settings{ 32 | Key: generateKey(), 33 | Signup: mustGetBool(flags, "signup"), 34 | Shell: strings.Split(strings.TrimSpace(mustGetString(flags, "shell")), " "), 35 | AuthMethod: authMethod, 36 | Defaults: defaults, 37 | Branding: settings.Branding{ 38 | Name: mustGetString(flags, "branding.name"), 39 | DisableExternal: mustGetBool(flags, "branding.disableExternal"), 40 | Files: mustGetString(flags, "branding.files"), 41 | }, 42 | } 43 | 44 | ser := &settings.Server{ 45 | Address: mustGetString(flags, "address"), 46 | Socket: mustGetString(flags, "socket"), 47 | Root: mustGetString(flags, "root"), 48 | BaseURL: mustGetString(flags, "baseurl"), 49 | TLSKey: mustGetString(flags, "key"), 50 | TLSCert: mustGetString(flags, "cert"), 51 | Port: mustGetString(flags, "port"), 52 | Log: mustGetString(flags, "log"), 53 | } 54 | 55 | err := d.store.Settings.Save(s) 56 | checkErr(err) 57 | err = d.store.Settings.SaveServer(ser) 58 | checkErr(err) 59 | err = d.store.Auth.Save(auther) 60 | checkErr(err) 61 | 62 | fmt.Printf(` 63 | Congratulations! You've set up your database to use with File Browser. 64 | Now add your first user via 'filebrowser users new' and then you just 65 | need to call the main command to boot up the server. 66 | `) 67 | printSettings(ser, s, auther) 68 | }, pythonConfig{noDB: true}), 69 | } 70 | -------------------------------------------------------------------------------- /cmd/config_set.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/pflag" 8 | ) 9 | 10 | func init() { 11 | configCmd.AddCommand(configSetCmd) 12 | addConfigFlags(configSetCmd.Flags()) 13 | } 14 | 15 | var configSetCmd = &cobra.Command{ 16 | Use: "set", 17 | Short: "Updates the configuration", 18 | Long: `Updates the configuration. Set the flags for the options 19 | you want to change. Other options will remain unchanged.`, 20 | Args: cobra.NoArgs, 21 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 22 | flags := cmd.Flags() 23 | set, err := d.store.Settings.Get() 24 | checkErr(err) 25 | 26 | ser, err := d.store.Settings.GetServer() 27 | checkErr(err) 28 | 29 | hasAuth := false 30 | flags.Visit(func(flag *pflag.Flag) { 31 | switch flag.Name { 32 | case "baseurl": 33 | ser.BaseURL = mustGetString(flags, flag.Name) 34 | case "root": 35 | ser.Root = mustGetString(flags, flag.Name) 36 | case "socket": 37 | ser.Socket = mustGetString(flags, flag.Name) 38 | case "cert": 39 | ser.TLSCert = mustGetString(flags, flag.Name) 40 | case "key": 41 | ser.TLSKey = mustGetString(flags, flag.Name) 42 | case "address": 43 | ser.Address = mustGetString(flags, flag.Name) 44 | case "port": 45 | ser.Port = mustGetString(flags, flag.Name) 46 | case "log": 47 | ser.Log = mustGetString(flags, flag.Name) 48 | case "signup": 49 | set.Signup = mustGetBool(flags, flag.Name) 50 | case "auth.method": 51 | hasAuth = true 52 | case "shell": 53 | set.Shell = strings.Split(strings.TrimSpace(mustGetString(flags, flag.Name)), " ") 54 | case "branding.name": 55 | set.Branding.Name = mustGetString(flags, flag.Name) 56 | case "branding.disableExternal": 57 | set.Branding.DisableExternal = mustGetBool(flags, flag.Name) 58 | case "branding.files": 59 | set.Branding.Files = mustGetString(flags, flag.Name) 60 | } 61 | }) 62 | 63 | getUserDefaults(flags, &set.Defaults, false) 64 | 65 | // read the defaults 66 | auther, err := d.store.Auth.Get(set.AuthMethod) 67 | checkErr(err) 68 | 69 | // check if there are new flags for existing auth method 70 | set.AuthMethod, auther = getAuthentication(flags, hasAuth, set, auther) 71 | 72 | err = d.store.Auth.Save(auther) 73 | checkErr(err) 74 | err = d.store.Settings.Save(set) 75 | checkErr(err) 76 | err = d.store.Settings.SaveServer(ser) 77 | checkErr(err) 78 | printSettings(ser, set, auther) 79 | }, pythonConfig{}), 80 | } 81 | -------------------------------------------------------------------------------- /cmd/hash.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/users" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | rootCmd.AddCommand(hashCmd) 12 | } 13 | 14 | var hashCmd = &cobra.Command{ 15 | Use: "hash ", 16 | Short: "Hashes a password", 17 | Long: `Hashes a password using bcrypt algorithm.`, 18 | Args: cobra.ExactArgs(1), 19 | Run: func(cmd *cobra.Command, args []string) { 20 | pwd, err := users.HashPwd(args[0]) 21 | checkErr(err) 22 | fmt.Println(pwd) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /cmd/rule_rm.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/settings" 7 | "github.com/dev-techmoe/filebrowser/v2/users" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func init() { 12 | rulesCmd.AddCommand(rulesRmCommand) 13 | rulesRmCommand.Flags().Uint("index", 0, "index of rule to remove") 14 | rulesRmCommand.MarkFlagRequired("index") 15 | } 16 | 17 | var rulesRmCommand = &cobra.Command{ 18 | Use: "rm [index_end]", 19 | Short: "Remove a global rule or user rule", 20 | Long: `Remove a global rule or user rule. The provided index 21 | is the same that's printed when you run 'rules ls'. Note 22 | that after each removal/addition, the index of the 23 | commands change. So be careful when removing them after each 24 | other. 25 | 26 | You can also specify an optional parameter (index_end) so 27 | you can remove all commands from 'index' to 'index_end', 28 | including 'index_end'.`, 29 | Args: func(cmd *cobra.Command, args []string) error { 30 | if err := cobra.RangeArgs(1, 2)(cmd, args); err != nil { 31 | return err 32 | } 33 | 34 | for _, arg := range args { 35 | if _, err := strconv.Atoi(arg); err != nil { 36 | return err 37 | } 38 | } 39 | 40 | return nil 41 | }, 42 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 43 | i, err := strconv.Atoi(args[0]) 44 | checkErr(err) 45 | f := i 46 | if len(args) == 2 { 47 | f, err = strconv.Atoi(args[1]) 48 | checkErr(err) 49 | } 50 | 51 | user := func(u *users.User) { 52 | u.Rules = append(u.Rules[:i], u.Rules[f+1:]...) 53 | err := d.store.Users.Save(u) 54 | checkErr(err) 55 | } 56 | 57 | global := func(s *settings.Settings) { 58 | s.Rules = append(s.Rules[:i], s.Rules[f+1:]...) 59 | err := d.store.Settings.Save(s) 60 | checkErr(err) 61 | } 62 | 63 | runRules(d.store, cmd, user, global) 64 | }, pythonConfig{}), 65 | } 66 | -------------------------------------------------------------------------------- /cmd/rules.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/rules" 7 | "github.com/dev-techmoe/filebrowser/v2/settings" 8 | "github.com/dev-techmoe/filebrowser/v2/storage" 9 | "github.com/dev-techmoe/filebrowser/v2/users" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/pflag" 12 | ) 13 | 14 | func init() { 15 | rootCmd.AddCommand(rulesCmd) 16 | rulesCmd.PersistentFlags().StringP("username", "u", "", "username of user to which the rules apply") 17 | rulesCmd.PersistentFlags().UintP("id", "i", 0, "id of user to which the rules apply") 18 | } 19 | 20 | var rulesCmd = &cobra.Command{ 21 | Use: "rules", 22 | Short: "Rules management utility", 23 | Long: `On each subcommand you'll have available at least two flags: 24 | "username" and "id". You must either set only one of them 25 | or none. If you set one of them, the command will apply to 26 | an user, otherwise it will be applied to the global set or 27 | rules.`, 28 | Args: cobra.NoArgs, 29 | } 30 | 31 | func runRules(st *storage.Storage, cmd *cobra.Command, users func(*users.User), global func(*settings.Settings)) { 32 | id := getUserIdentifier(cmd.Flags()) 33 | if id != nil { 34 | user, err := st.Users.Get("", id) 35 | checkErr(err) 36 | 37 | if users != nil { 38 | users(user) 39 | } 40 | 41 | printRules(user.Rules, id) 42 | return 43 | } 44 | 45 | s, err := st.Settings.Get() 46 | checkErr(err) 47 | 48 | if global != nil { 49 | global(s) 50 | } 51 | 52 | printRules(s.Rules, id) 53 | } 54 | 55 | func getUserIdentifier(flags *pflag.FlagSet) interface{} { 56 | id := mustGetUint(flags, "id") 57 | username := mustGetString(flags, "username") 58 | 59 | if id != 0 { 60 | return id 61 | } else if username != "" { 62 | return username 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func printRules(rules []rules.Rule, id interface{}) { 69 | if id == nil { 70 | fmt.Printf("Global Rules:\n\n") 71 | } else { 72 | fmt.Printf("Rules for user %v:\n\n", id) 73 | } 74 | 75 | for id, rule := range rules { 76 | fmt.Printf("(%d) ", id) 77 | if rule.Regex { 78 | if rule.Allow { 79 | fmt.Printf("Allow Regex: \t%s\n", rule.Regexp.Raw) 80 | } else { 81 | fmt.Printf("Disallow Regex: \t%s\n", rule.Regexp.Raw) 82 | } 83 | } else { 84 | if rule.Allow { 85 | fmt.Printf("Allow Path: \t%s\n", rule.Path) 86 | } else { 87 | fmt.Printf("Disallow Path: \t%s\n", rule.Path) 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/rules_add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/rules" 7 | "github.com/dev-techmoe/filebrowser/v2/settings" 8 | "github.com/dev-techmoe/filebrowser/v2/users" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | rulesCmd.AddCommand(rulesAddCmd) 14 | rulesAddCmd.Flags().BoolP("allow", "a", false, "indicates this is an allow rule") 15 | rulesAddCmd.Flags().BoolP("regex", "r", false, "indicates this is a regex rule") 16 | } 17 | 18 | var rulesAddCmd = &cobra.Command{ 19 | Use: "add ", 20 | Short: "Add a global rule or user rule", 21 | Long: `Add a global rule or user rule.`, 22 | Args: cobra.ExactArgs(1), 23 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 24 | allow := mustGetBool(cmd.Flags(), "allow") 25 | regex := mustGetBool(cmd.Flags(), "regex") 26 | exp := args[0] 27 | 28 | if regex { 29 | regexp.MustCompile(exp) 30 | } 31 | 32 | rule := rules.Rule{ 33 | Allow: allow, 34 | Regex: regex, 35 | } 36 | 37 | if regex { 38 | rule.Regexp = &rules.Regexp{Raw: exp} 39 | } else { 40 | rule.Path = exp 41 | } 42 | 43 | user := func(u *users.User) { 44 | u.Rules = append(u.Rules, rule) 45 | err := d.store.Users.Save(u) 46 | checkErr(err) 47 | } 48 | 49 | global := func(s *settings.Settings) { 50 | s.Rules = append(s.Rules, rule) 51 | err := d.store.Settings.Save(s) 52 | checkErr(err) 53 | } 54 | 55 | runRules(d.store, cmd, user, global) 56 | }, pythonConfig{}), 57 | } 58 | -------------------------------------------------------------------------------- /cmd/rules_ls.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | rulesCmd.AddCommand(rulesLsCommand) 9 | } 10 | 11 | var rulesLsCommand = &cobra.Command{ 12 | Use: "ls", 13 | Short: "List global rules or user specific rules", 14 | Long: `List global rules or user specific rules.`, 15 | Args: cobra.NoArgs, 16 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 17 | runRules(d.store, cmd, nil, nil) 18 | }, pythonConfig{}), 19 | } 20 | -------------------------------------------------------------------------------- /cmd/upgrade.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/dev-techmoe/filebrowser/v2/storage/bolt/importer" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func init() { 9 | rootCmd.AddCommand(upgradeCmd) 10 | 11 | upgradeCmd.Flags().String("old.database", "", "") 12 | upgradeCmd.Flags().String("old.config", "", "") 13 | upgradeCmd.MarkFlagRequired("old.database") 14 | } 15 | 16 | var upgradeCmd = &cobra.Command{ 17 | Use: "upgrade", 18 | Short: "Upgrades an old configuration", 19 | Long: `Upgrades an old configuration. This command DOES NOT 20 | import share links because they are incompatible with 21 | this version.`, 22 | Args: cobra.NoArgs, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | flags := cmd.Flags() 25 | oldDB := mustGetString(flags, "old.database") 26 | oldConf := mustGetString(flags, "old.config") 27 | err := importer.Import(oldDB, oldConf, getParam(flags, "database")) 28 | checkErr(err) 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /cmd/users_add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/dev-techmoe/filebrowser/v2/users" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func init() { 9 | usersCmd.AddCommand(usersAddCmd) 10 | addUserFlags(usersAddCmd.Flags()) 11 | } 12 | 13 | var usersAddCmd = &cobra.Command{ 14 | Use: "add ", 15 | Short: "Create a new user", 16 | Long: `Create a new user and add it to the database.`, 17 | Args: cobra.ExactArgs(2), 18 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 19 | s, err := d.store.Settings.Get() 20 | checkErr(err) 21 | getUserDefaults(cmd.Flags(), &s.Defaults, false) 22 | 23 | password, err := users.HashPwd(args[1]) 24 | checkErr(err) 25 | 26 | user := &users.User{ 27 | Username: args[0], 28 | Password: password, 29 | LockPassword: mustGetBool(cmd.Flags(), "lockPassword"), 30 | } 31 | 32 | s.Defaults.Apply(user) 33 | 34 | servSettings, err := d.store.Settings.GetServer() 35 | checkErr(err) 36 | //since getUserDefaults() polluted s.Defaults.Scope 37 | //which makes the Scope not the one saved in the db 38 | //we need the right s.Defaults.Scope here 39 | s2, err := d.store.Settings.Get() 40 | checkErr(err) 41 | 42 | userHome, err := s2.MakeUserDir(user.Username, user.Scope, servSettings.Root) 43 | checkErr(err) 44 | user.Scope = userHome 45 | 46 | err = d.store.Users.Save(user) 47 | checkErr(err) 48 | printUsers([]*users.User{user}) 49 | }, pythonConfig{}), 50 | } 51 | -------------------------------------------------------------------------------- /cmd/users_export.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | usersCmd.AddCommand(usersExportCmd) 9 | } 10 | 11 | var usersExportCmd = &cobra.Command{ 12 | Use: "export ", 13 | Short: "Export all users to a file.", 14 | Long: `Export all users to a json or yaml file. Please indicate the 15 | path to the file where you want to write the users.`, 16 | Args: jsonYamlArg, 17 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 18 | list, err := d.store.Users.Gets("") 19 | checkErr(err) 20 | 21 | err = marshal(args[0], list) 22 | checkErr(err) 23 | }, pythonConfig{}), 24 | } 25 | -------------------------------------------------------------------------------- /cmd/users_find.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/dev-techmoe/filebrowser/v2/users" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func init() { 9 | usersCmd.AddCommand(usersFindCmd) 10 | usersCmd.AddCommand(usersLsCmd) 11 | } 12 | 13 | var usersFindCmd = &cobra.Command{ 14 | Use: "find ", 15 | Short: "Find a user by username or id", 16 | Long: `Find a user by username or id. If no flag is set, all users will be printed.`, 17 | Args: cobra.ExactArgs(1), 18 | Run: findUsers, 19 | } 20 | 21 | var usersLsCmd = &cobra.Command{ 22 | Use: "ls", 23 | Short: "List all users.", 24 | Args: cobra.NoArgs, 25 | Run: findUsers, 26 | } 27 | 28 | var findUsers = python(func(cmd *cobra.Command, args []string, d pythonData) { 29 | var ( 30 | list []*users.User 31 | user *users.User 32 | err error 33 | ) 34 | 35 | if len(args) == 1 { 36 | username, id := parseUsernameOrID(args[0]) 37 | if username != "" { 38 | user, err = d.store.Users.Get("", username) 39 | } else { 40 | user, err = d.store.Users.Get("", id) 41 | } 42 | 43 | list = []*users.User{user} 44 | } else { 45 | list, err = d.store.Users.Gets("") 46 | } 47 | 48 | checkErr(err) 49 | printUsers(list) 50 | }, pythonConfig{}) 51 | -------------------------------------------------------------------------------- /cmd/users_import.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/dev-techmoe/filebrowser/v2/users" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | usersCmd.AddCommand(usersImportCmd) 14 | usersImportCmd.Flags().Bool("overwrite", false, "overwrite users with the same id/username combo") 15 | usersImportCmd.Flags().Bool("replace", false, "replace the entire user base") 16 | } 17 | 18 | var usersImportCmd = &cobra.Command{ 19 | Use: "import ", 20 | Short: "Import users from a file", 21 | Long: `Import users from a file. The path must be for a json or yaml 22 | file. You can use this command to import new users to your 23 | installation. For that, just don't place their ID on the files 24 | list or set it to 0.`, 25 | Args: jsonYamlArg, 26 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 27 | fd, err := os.Open(args[0]) 28 | checkErr(err) 29 | defer fd.Close() 30 | 31 | list := []*users.User{} 32 | err = unmarshal(args[0], &list) 33 | checkErr(err) 34 | 35 | for _, user := range list { 36 | err = user.Clean("") 37 | checkErr(err) 38 | } 39 | 40 | if mustGetBool(cmd.Flags(), "replace") { 41 | oldUsers, err := d.store.Users.Gets("") 42 | checkErr(err) 43 | 44 | err = marshal("users.backup.json", list) 45 | checkErr(err) 46 | 47 | for _, user := range oldUsers { 48 | err = d.store.Users.Delete(user.ID) 49 | checkErr(err) 50 | } 51 | } 52 | 53 | overwrite := mustGetBool(cmd.Flags(), "overwrite") 54 | 55 | for _, user := range list { 56 | onDB, err := d.store.Users.Get("", user.ID) 57 | 58 | // User exists in DB. 59 | if err == nil { 60 | if !overwrite { 61 | checkErr(errors.New("user " + strconv.Itoa(int(user.ID)) + " is already registred")) 62 | } 63 | 64 | // If the usernames mismatch, check if there is another one in the DB 65 | // with the new username. If there is, print an error and cancel the 66 | // operation 67 | if user.Username != onDB.Username { 68 | conflictuous, err := d.store.Users.Get("", user.Username) 69 | if err == nil { 70 | checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID)) 71 | } 72 | } 73 | } else { 74 | // If it doesn't exist, set the ID to 0 to automatically get a new 75 | // one that make sense in this DB. 76 | user.ID = 0 77 | } 78 | 79 | err = d.store.Users.Save(user) 80 | checkErr(err) 81 | } 82 | }, pythonConfig{}), 83 | } 84 | 85 | func usernameConflictError(username string, original, new uint) error { 86 | return errors.New("can't import user with ID " + strconv.Itoa(int(new)) + " and username \"" + username + "\" because the username is already registred with the user " + strconv.Itoa(int(original))) 87 | } 88 | -------------------------------------------------------------------------------- /cmd/users_rm.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | usersCmd.AddCommand(usersRmCmd) 11 | } 12 | 13 | var usersRmCmd = &cobra.Command{ 14 | Use: "rm ", 15 | Short: "Delete a user by username or id", 16 | Long: `Delete a user by username or id`, 17 | Args: cobra.ExactArgs(1), 18 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 19 | username, id := parseUsernameOrID(args[0]) 20 | var err error 21 | 22 | if username != "" { 23 | err = d.store.Users.Delete(username) 24 | } else { 25 | err = d.store.Users.Delete(id) 26 | } 27 | 28 | checkErr(err) 29 | fmt.Println("user deleted successfully") 30 | }, pythonConfig{}), 31 | } 32 | -------------------------------------------------------------------------------- /cmd/users_update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/dev-techmoe/filebrowser/v2/settings" 5 | "github.com/dev-techmoe/filebrowser/v2/users" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | usersCmd.AddCommand(usersUpdateCmd) 11 | 12 | usersUpdateCmd.Flags().StringP("password", "p", "", "new password") 13 | usersUpdateCmd.Flags().StringP("username", "u", "", "new username") 14 | addUserFlags(usersUpdateCmd.Flags()) 15 | } 16 | 17 | var usersUpdateCmd = &cobra.Command{ 18 | Use: "update ", 19 | Short: "Updates an existing user", 20 | Long: `Updates an existing user. Set the flags for the 21 | options you want to change.`, 22 | Args: cobra.ExactArgs(1), 23 | Run: python(func(cmd *cobra.Command, args []string, d pythonData) { 24 | username, id := parseUsernameOrID(args[0]) 25 | flags := cmd.Flags() 26 | password := mustGetString(flags, "password") 27 | newUsername := mustGetString(flags, "username") 28 | 29 | var ( 30 | err error 31 | user *users.User 32 | ) 33 | 34 | if id != 0 { 35 | user, err = d.store.Users.Get("", id) 36 | } else { 37 | user, err = d.store.Users.Get("", username) 38 | } 39 | 40 | checkErr(err) 41 | 42 | defaults := settings.UserDefaults{ 43 | Scope: user.Scope, 44 | Locale: user.Locale, 45 | ViewMode: user.ViewMode, 46 | Perm: user.Perm, 47 | Sorting: user.Sorting, 48 | Commands: user.Commands, 49 | } 50 | getUserDefaults(flags, &defaults, false) 51 | user.Scope = defaults.Scope 52 | user.Locale = defaults.Locale 53 | user.ViewMode = defaults.ViewMode 54 | user.Perm = defaults.Perm 55 | user.Commands = defaults.Commands 56 | user.Sorting = defaults.Sorting 57 | user.LockPassword = mustGetBool(flags, "lockPassword") 58 | 59 | if newUsername != "" { 60 | user.Username = newUsername 61 | } 62 | 63 | if password != "" { 64 | user.Password, err = users.HashPwd(password) 65 | checkErr(err) 66 | } 67 | 68 | err = d.store.Users.Update(user) 69 | checkErr(err) 70 | printUsers([]*users.User{user}) 71 | }, pythonConfig{}), 72 | } 73 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/version" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | rootCmd.AddCommand(versionCmd) 12 | } 13 | 14 | var versionCmd = &cobra.Command{ 15 | Use: "version", 16 | Short: "Print the version number", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrEmptyKey = errors.New("empty key") 7 | ErrExist = errors.New("the resource already exists") 8 | ErrNotExist = errors.New("the resource does not exist") 9 | ErrEmptyPassword = errors.New("password is empty") 10 | ErrEmptyUsername = errors.New("username is empty") 11 | ErrEmptyRequest = errors.New("empty request") 12 | ErrScopeIsRelative = errors.New("scope is a relative path") 13 | ErrInvalidDataType = errors.New("invalid data type") 14 | ErrIsDirectory = errors.New("file is directory") 15 | ErrInvalidOption = errors.New("invalid option") 16 | ErrInvalidAuthMethod = errors.New("invalid auth method") 17 | ) 18 | -------------------------------------------------------------------------------- /files/listing.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/maruel/natural" 8 | ) 9 | 10 | // Listing is a collection of files. 11 | type Listing struct { 12 | Items []*FileInfo `json:"items"` 13 | NumDirs int `json:"numDirs"` 14 | NumFiles int `json:"numFiles"` 15 | Sorting Sorting `json:"sorting"` 16 | } 17 | 18 | // ApplySort applies the sort order using .Order and .Sort 19 | func (l Listing) ApplySort() { 20 | // Check '.Order' to know how to sort 21 | if !l.Sorting.Asc { 22 | switch l.Sorting.By { 23 | case "name": 24 | sort.Sort(sort.Reverse(byName(l))) 25 | case "size": 26 | sort.Sort(sort.Reverse(bySize(l))) 27 | case "modified": 28 | sort.Sort(sort.Reverse(byModified(l))) 29 | default: 30 | // If not one of the above, do nothing 31 | return 32 | } 33 | } else { // If we had more Orderings we could add them here 34 | switch l.Sorting.By { 35 | case "name": 36 | sort.Sort(byName(l)) 37 | case "size": 38 | sort.Sort(bySize(l)) 39 | case "modified": 40 | sort.Sort(byModified(l)) 41 | default: 42 | sort.Sort(byName(l)) 43 | return 44 | } 45 | } 46 | } 47 | 48 | // Implement sorting for Listing 49 | type byName Listing 50 | type bySize Listing 51 | type byModified Listing 52 | 53 | // By Name 54 | func (l byName) Len() int { 55 | return len(l.Items) 56 | } 57 | 58 | func (l byName) Swap(i, j int) { 59 | l.Items[i], l.Items[j] = l.Items[j], l.Items[i] 60 | } 61 | 62 | // Treat upper and lower case equally 63 | func (l byName) Less(i, j int) bool { 64 | if l.Items[i].IsDir && !l.Items[j].IsDir { 65 | return l.Sorting.Asc 66 | } 67 | 68 | if !l.Items[i].IsDir && l.Items[j].IsDir { 69 | return !l.Sorting.Asc 70 | } 71 | 72 | return natural.Less(strings.ToLower(l.Items[j].Name), strings.ToLower(l.Items[i].Name)) 73 | } 74 | 75 | // By Size 76 | func (l bySize) Len() int { 77 | return len(l.Items) 78 | } 79 | 80 | func (l bySize) Swap(i, j int) { 81 | l.Items[i], l.Items[j] = l.Items[j], l.Items[i] 82 | } 83 | 84 | const directoryOffset = -1 << 31 // = math.MinInt32 85 | func (l bySize) Less(i, j int) bool { 86 | iSize, jSize := l.Items[i].Size, l.Items[j].Size 87 | if l.Items[i].IsDir { 88 | iSize = directoryOffset + iSize 89 | } 90 | if l.Items[j].IsDir { 91 | jSize = directoryOffset + jSize 92 | } 93 | return iSize < jSize 94 | } 95 | 96 | // By Modified 97 | func (l byModified) Len() int { 98 | return len(l.Items) 99 | } 100 | 101 | func (l byModified) Swap(i, j int) { 102 | l.Items[i], l.Items[j] = l.Items[j], l.Items[i] 103 | } 104 | 105 | func (l byModified) Less(i, j int) bool { 106 | iModified, jModified := l.Items[i].ModTime, l.Items[j].ModTime 107 | return iModified.Sub(jModified) < 0 108 | } 109 | -------------------------------------------------------------------------------- /files/sorting.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | // Sorting contains a sorting order. 4 | type Sorting struct { 5 | By string `json:"by"` 6 | Asc bool `json:"asc"` 7 | } 8 | -------------------------------------------------------------------------------- /files/utils.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "unicode/utf8" 5 | ) 6 | 7 | func isBinary(content []byte, n int) bool { 8 | maybeStr := string(content) 9 | runeCnt := utf8.RuneCount(content) 10 | runeIndex := 0 11 | gotRuneErrCnt := 0 12 | firstRuneErrIndex := -1 13 | 14 | for _, b := range maybeStr { 15 | // 8 and below are control chars (e.g. backspace, null, eof, etc) 16 | if b <= 8 { 17 | return true 18 | } 19 | 20 | // 0xFFFD(65533) is the "error" Rune or "Unicode replacement character" 21 | // see https://golang.org/pkg/unicode/utf8/#pkg-constants 22 | if b == 0xFFFD { 23 | //if it is not the last (utf8.UTFMax - x) rune 24 | if runeCnt > utf8.UTFMax && runeIndex < runeCnt-utf8.UTFMax { 25 | return true 26 | } else { 27 | //else it is the last (utf8.UTFMax - x) rune 28 | //there maybe Vxxx, VVxx, VVVx, thus, we may got max 3 0xFFFD rune (asume V is the byte we got) 29 | //for Chinese, it can only be Vxx, VVx, we may got max 2 0xFFFD rune 30 | gotRuneErrCnt++ 31 | 32 | //mark the first time 33 | if firstRuneErrIndex == -1 { 34 | firstRuneErrIndex = runeIndex 35 | } 36 | } 37 | } 38 | runeIndex++ 39 | } 40 | 41 | //if last (utf8.UTFMax - x ) rune has the "error" Rune, but not all 42 | if firstRuneErrIndex != -1 && gotRuneErrCnt != runeCnt-firstRuneErrIndex { 43 | return true 44 | } 45 | return false 46 | } 47 | -------------------------------------------------------------------------------- /fileutils/copy.go: -------------------------------------------------------------------------------- 1 | package fileutils 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/spf13/afero" 8 | ) 9 | 10 | // Copy copies a file or folder from one place to another. 11 | func Copy(fs afero.Fs, src, dst string) error { 12 | if src = path.Clean("/" + src); src == "" { 13 | return os.ErrNotExist 14 | } 15 | 16 | if dst = path.Clean("/" + dst); dst == "" { 17 | return os.ErrNotExist 18 | } 19 | 20 | if src == "/" || dst == "/" { 21 | // Prohibit copying from or to the virtual root directory. 22 | return os.ErrInvalid 23 | } 24 | 25 | if dst == src { 26 | return os.ErrInvalid 27 | } 28 | 29 | info, err := fs.Stat(src) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if info.IsDir() { 35 | return CopyDir(fs, src, dst) 36 | } 37 | 38 | return CopyFile(fs, src, dst) 39 | } 40 | -------------------------------------------------------------------------------- /fileutils/dir.go: -------------------------------------------------------------------------------- 1 | package fileutils 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/spf13/afero" 7 | ) 8 | 9 | // CopyDir copies a directory from source to dest and all 10 | // of its sub-directories. It doesn't stop if it finds an error 11 | // during the copy. Returns an error if any. 12 | func CopyDir(fs afero.Fs, source string, dest string) error { 13 | // Get properties of source. 14 | srcinfo, err := fs.Stat(source) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | // Create the destination directory. 20 | err = fs.MkdirAll(dest, srcinfo.Mode()) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | dir, _ := fs.Open(source) 26 | obs, err := dir.Readdir(-1) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | var errs []error 32 | 33 | for _, obj := range obs { 34 | fsource := source + "/" + obj.Name() 35 | fdest := dest + "/" + obj.Name() 36 | 37 | if obj.IsDir() { 38 | // Create sub-directories, recursively. 39 | err = CopyDir(fs, fsource, fdest) 40 | if err != nil { 41 | errs = append(errs, err) 42 | } 43 | } else { 44 | // Perform the file copy. 45 | err = CopyFile(fs, fsource, fdest) 46 | if err != nil { 47 | errs = append(errs, err) 48 | } 49 | } 50 | } 51 | 52 | var errString string 53 | for _, err := range errs { 54 | errString += err.Error() + "\n" 55 | } 56 | 57 | if errString != "" { 58 | return errors.New(errString) 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /fileutils/file.go: -------------------------------------------------------------------------------- 1 | package fileutils 2 | 3 | import ( 4 | "io" 5 | "path/filepath" 6 | 7 | "github.com/spf13/afero" 8 | ) 9 | 10 | // CopyFile copies a file from source to dest and returns 11 | // an error if any. 12 | func CopyFile(fs afero.Fs, source string, dest string) error { 13 | // Open the source file. 14 | src, err := fs.Open(source) 15 | if err != nil { 16 | return err 17 | } 18 | defer src.Close() 19 | 20 | // Makes the directory needed to create the dst 21 | // file. 22 | err = fs.MkdirAll(filepath.Dir(dest), 0666) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | // Create the destination file. 28 | dst, err := fs.Create(dest) 29 | if err != nil { 30 | return err 31 | } 32 | defer dst.Close() 33 | 34 | // Copy the contents of the file. 35 | _, err = io.Copy(dst, src) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // Copy the mode if the user can't 41 | // open the file. 42 | info, err := fs.Stat(source) 43 | if err != nil { 44 | err = fs.Chmod(dest, info.Mode()) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filebrowser-frontend", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "watch": "vue-cli-service build --watch", 9 | "lint": "vue-cli-service lint --fix" 10 | }, 11 | "dependencies": { 12 | "ace-builds": "^1.4.7", 13 | "clipboard": "^2.0.4", 14 | "js-base64": "^2.5.1", 15 | "lodash.clonedeep": "^4.5.0", 16 | "material-design-icons": "^3.0.1", 17 | "moment": "^2.24.0", 18 | "normalize.css": "^8.0.1", 19 | "noty": "^3.2.0-beta", 20 | "qrcode.vue": "^1.7.0", 21 | "vue": "^2.6.10", 22 | "vue-i18n": "^8.15.3", 23 | "vue-router": "^3.1.3", 24 | "vuex": "^3.1.2", 25 | "vuex-router-sync": "^5.0.0" 26 | }, 27 | "devDependencies": { 28 | "@vue/cli-plugin-babel": "^4.1.2", 29 | "@vue/cli-plugin-eslint": "^4.1.1", 30 | "@vue/cli-service": "^4.1.2", 31 | "babel-eslint": "^10.0.3", 32 | "eslint": "^6.7.2", 33 | "eslint-plugin-vue": "^6.1.2", 34 | "vue-template-compiler": "^2.6.10" 35 | }, 36 | "eslintConfig": { 37 | "root": true, 38 | "env": { 39 | "node": true 40 | }, 41 | "extends": [ 42 | "plugin:vue/essential", 43 | "eslint:recommended" 44 | ], 45 | "rules": {}, 46 | "parserOptions": { 47 | "parser": "babel-eslint" 48 | } 49 | }, 50 | "postcss": { 51 | "plugins": { 52 | "autoprefixer": {} 53 | } 54 | }, 55 | "browserslist": [ 56 | "> 1%", 57 | "last 2 versions", 58 | "not ie <= 8" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /frontend/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/img/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #455a64 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/favicon.ico -------------------------------------------------------------------------------- /frontend/public/img/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/mstile-144x144.png -------------------------------------------------------------------------------- /frontend/public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/public/img/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/mstile-310x150.png -------------------------------------------------------------------------------- /frontend/public/img/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/mstile-310x310.png -------------------------------------------------------------------------------- /frontend/public/img/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/public/img/icons/mstile-70x70.png -------------------------------------------------------------------------------- /frontend/public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 21 | 32 | 34 | 36 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "File Browser", 3 | "short_name": "File Browser", 4 | "icons": [ 5 | { 6 | "src": "./img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./static/img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/", 17 | "display": "standalone", 18 | "background_color": "#ffffff", 19 | "theme_color": "#455a64" 20 | } 21 | -------------------------------------------------------------------------------- /frontend/public/themes/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #121212; 3 | --surfacePrimary: #171819; 4 | --surfaceSecondary: #212528; 5 | --divider: rgba(255, 255, 255, 0.12); 6 | --icon: #ffffff; 7 | --textPrimary: rgba(255, 255, 255, 0.87); 8 | --textSecondary: rgba(255, 255, 255, 0.6); 9 | } 10 | 11 | body { 12 | background: var(--background); 13 | color: var(--textPrimary); 14 | } 15 | 16 | #loading { 17 | background: var(--background); 18 | } 19 | #loading .spinner div { 20 | background: var(--icon); 21 | } 22 | 23 | #login { 24 | background: var(--background); 25 | } 26 | 27 | header { 28 | background: var(--surfacePrimary); 29 | } 30 | 31 | #search #input { 32 | background: var(--surfaceSecondary); 33 | } 34 | #search.active #input, 35 | #search.active .boxes { 36 | background: var(--surfacePrimary); 37 | } 38 | #search.active input { 39 | color: var(--textPrimary); 40 | } 41 | #search.active #result { 42 | background: var(--background); 43 | color: var(--textPrimary); 44 | } 45 | #search.active .boxes h3 { 46 | color: var(--textPrimary); 47 | } 48 | 49 | .action { 50 | color: var(--textPrimary) !important; 51 | } 52 | .action i { 53 | color: var(--icon) !important; 54 | } 55 | .action .counter { 56 | border-color: var(--surfacePrimary); 57 | } 58 | 59 | nav > div { 60 | border-color: var(--divider); 61 | } 62 | 63 | #breadcrumbs { 64 | border-color: var(--divider); 65 | color: var(--textPrimary) !important; 66 | } 67 | #breadcrumbs span { 68 | color: var(--textPrimary) !important; 69 | } 70 | 71 | #listing .item { 72 | background: var(--surfacePrimary); 73 | color: var(--textPrimary); 74 | border-color: var(--divider) !important; 75 | } 76 | #listing .item i { 77 | color: var(--icon); 78 | } 79 | #listing .item .modified { 80 | color: var(--textSecondary); 81 | } 82 | #listing h2, 83 | #listing.list .header span { 84 | color: var(--textPrimary) !important; 85 | } 86 | #listing.list .header span { 87 | color: var(--textPrimary); 88 | } 89 | #listing.list .header i { 90 | color: var(--icon); 91 | } 92 | #listing.list .item.header { 93 | background: var(--background); 94 | } 95 | 96 | .card { 97 | background: var(--surfacePrimary); 98 | color: var(--textPrimary); 99 | } 100 | .button--flat:hover { 101 | background: var(--surfaceSecondary); 102 | } 103 | 104 | .card h3, 105 | .dashboard #nav, 106 | .dashboard p label { 107 | color: var(--textPrimary); 108 | } 109 | .input { 110 | background: var(--surfaceSecondary); 111 | color: var(--textPrimary); 112 | } 113 | 114 | .dashboard #nav li, 115 | .collapsible { 116 | border-color: var(--divider); 117 | } 118 | .collapsible > label * { 119 | color: var(--textPrimary); 120 | } 121 | 122 | .shell { 123 | background: var(--surfacePrimary); 124 | color: var(--textPrimary); 125 | } 126 | 127 | @media (max-width: 736px) { 128 | #file-selection { 129 | background: var(--surfaceSecondary) !important; 130 | } 131 | #file-selection span { 132 | color: var(--textPrimary) !important; 133 | } 134 | nav { 135 | background: var(--surfaceSecondary) !important; 136 | } 137 | #dropdown { 138 | background: var(--surfaceSecondary) !important; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /frontend/src/api/commands.js: -------------------------------------------------------------------------------- 1 | import { removePrefix } from './utils' 2 | import { baseURL } from '@/utils/constants' 3 | import store from '@/store' 4 | 5 | const ssl = (window.location.protocol === 'https:') 6 | const protocol = (ssl ? 'wss:' : 'ws:') 7 | 8 | export default function command(url, command, onmessage, onclose) { 9 | url = removePrefix(url) 10 | url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}` 11 | 12 | let conn = new window.WebSocket(url) 13 | conn.onopen = () => conn.send(command) 14 | conn.onmessage = onmessage 15 | conn.onclose = onclose 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/api/index.js: -------------------------------------------------------------------------------- 1 | import * as files from './files' 2 | import * as share from './share' 3 | import * as users from './users' 4 | import * as settings from './settings' 5 | import search from './search' 6 | import commands from './commands' 7 | 8 | export { 9 | files, 10 | share, 11 | users, 12 | settings, 13 | commands, 14 | search 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/api/search.js: -------------------------------------------------------------------------------- 1 | import { fetchJSON, removePrefix } from './utils' 2 | 3 | export default async function search (url, query) { 4 | url = removePrefix(url) 5 | query = encodeURIComponent(query) 6 | 7 | return fetchJSON(`/api/search${url}?query=${query}`, {}) 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/api/settings.js: -------------------------------------------------------------------------------- 1 | import { fetchURL, fetchJSON } from './utils' 2 | 3 | export function get () { 4 | return fetchJSON(`/api/settings`, {}) 5 | } 6 | 7 | export async function update (settings) { 8 | const res = await fetchURL(`/api/settings`, { 9 | method: 'PUT', 10 | body: JSON.stringify(settings) 11 | }) 12 | 13 | if (res.status !== 200) { 14 | throw new Error(res.status) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/api/share.js: -------------------------------------------------------------------------------- 1 | import { fetchURL, fetchJSON, removePrefix } from './utils' 2 | 3 | export async function getHash(hash) { 4 | return fetchJSON(`/api/public/share/${hash}`) 5 | } 6 | 7 | export async function get(url) { 8 | url = removePrefix(url) 9 | return fetchJSON(`/api/share${url}`) 10 | } 11 | 12 | export async function remove(hash) { 13 | const res = await fetchURL(`/api/share/${hash}`, { 14 | method: 'DELETE' 15 | }) 16 | 17 | if (res.status !== 200) { 18 | throw new Error(res.status) 19 | } 20 | } 21 | 22 | export async function create(url, expires = '', unit = 'hours') { 23 | url = removePrefix(url) 24 | url = `/api/share${url}` 25 | if (expires !== '') { 26 | url += `?expires=${expires}&unit=${unit}` 27 | } 28 | 29 | return fetchJSON(url, { 30 | method: 'POST' 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/api/users.js: -------------------------------------------------------------------------------- 1 | import { fetchURL, fetchJSON } from './utils' 2 | 3 | export async function getAll () { 4 | return fetchJSON(`/api/users`, {}) 5 | } 6 | 7 | export async function get (id) { 8 | return fetchJSON(`/api/users/${id}`, {}) 9 | } 10 | 11 | export async function create (user) { 12 | const res = await fetchURL(`/api/users`, { 13 | method: 'POST', 14 | body: JSON.stringify({ 15 | what: 'user', 16 | which: [], 17 | data: user 18 | }) 19 | }) 20 | 21 | if (res.status === 201) { 22 | return res.headers.get('Location') 23 | } else { 24 | throw new Error(res.status) 25 | } 26 | 27 | } 28 | 29 | export async function update (user, which = ['all']) { 30 | const res = await fetchURL(`/api/users/${user.id}`, { 31 | method: 'PUT', 32 | body: JSON.stringify({ 33 | what: 'user', 34 | which: which, 35 | data: user 36 | }) 37 | }) 38 | 39 | if (res.status !== 200) { 40 | throw new Error(res.status) 41 | } 42 | } 43 | 44 | export async function remove (id) { 45 | const res = await fetchURL(`/api/users/${id}`, { 46 | method: 'DELETE' 47 | }) 48 | 49 | if (res.status !== 200) { 50 | throw new Error(res.status) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/api/utils.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import { renew } from '@/utils/auth' 3 | import { baseURL } from '@/utils/constants' 4 | 5 | export async function fetchURL (url, opts) { 6 | opts = opts || {} 7 | opts.headers = opts.headers || {} 8 | 9 | let { headers, ...rest } = opts 10 | 11 | const res = await fetch(`${baseURL}${url}`, { 12 | headers: { 13 | 'X-Auth': store.state.jwt, 14 | ...headers 15 | }, 16 | ...rest 17 | }) 18 | 19 | if (res.headers.get('X-Renew-Token') === 'true') { 20 | await renew(store.state.jwt) 21 | } 22 | 23 | return res 24 | } 25 | 26 | export async function fetchJSON (url, opts) { 27 | const res = await fetchURL(url, opts) 28 | 29 | if (res.status === 200) { 30 | return res.json() 31 | } else { 32 | throw new Error(res.status) 33 | } 34 | } 35 | 36 | export function removePrefix (url) { 37 | if (url.startsWith('/files')) { 38 | url = url.slice(6) 39 | } 40 | 41 | if (url === '') url = '/' 42 | if (url[0] !== '/') url = '/' + url 43 | return url 44 | } 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/medium-cyrillic-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/medium-cyrillic-ext.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/medium-cyrillic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/medium-cyrillic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/medium-greek-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/medium-greek-ext.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/medium-greek.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/medium-greek.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/medium-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/medium-latin-ext.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/medium-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/medium-latin.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/medium-vietnamese.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/medium-vietnamese.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/normal-cyrillic-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/normal-cyrillic-ext.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/normal-cyrillic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/normal-cyrillic.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/normal-greek-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/normal-greek-ext.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/normal-greek.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/normal-greek.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/normal-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/normal-latin-ext.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/normal-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/normal-latin.woff2 -------------------------------------------------------------------------------- /frontend/src/assets/fonts/roboto/normal-vietnamese.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-techmoe/filebrowser/05953b2b6130ae6d1b70c2e7cdf87a4aaef36e98/frontend/src/assets/fonts/roboto/normal-vietnamese.woff2 -------------------------------------------------------------------------------- /frontend/src/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 83 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Copy.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Delete.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Download.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Info.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Move.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Rename.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Share.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Shell.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/SwitchView.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 41 | -------------------------------------------------------------------------------- /frontend/src/components/buttons/Upload.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /frontend/src/components/files/Editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 78 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/Copy.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 68 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/Delete.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 67 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/Download.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/Help.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/Move.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 68 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/NewDir.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 71 | 72 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/NewFile.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 71 | 72 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/Prompts.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 77 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/Rename.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 90 | -------------------------------------------------------------------------------- /frontend/src/components/prompts/Replace.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/settings/Commands.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /frontend/src/components/settings/Languages.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 47 | -------------------------------------------------------------------------------- /frontend/src/components/settings/Permissions.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 40 | -------------------------------------------------------------------------------- /frontend/src/components/settings/Rules.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /frontend/src/components/settings/Themes.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/components/settings/UserForm.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 66 | -------------------------------------------------------------------------------- /frontend/src/css/_buttons.css: -------------------------------------------------------------------------------- 1 | .button { 2 | outline: 0; 3 | border: 0; 4 | padding: .5em 1em; 5 | border-radius: .1em; 6 | cursor: pointer; 7 | background: var(--blue); 8 | color: white; 9 | border: 1px solid rgba(0, 0, 0, 0.05); 10 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.05); 11 | transition: .1s ease all; 12 | } 13 | 14 | .button:hover { 15 | background-color: var(--dark-blue); 16 | } 17 | 18 | .button--block { 19 | margin: 0 0 0.5em; 20 | display: block; 21 | width: 100%; 22 | } 23 | 24 | .button--red { 25 | background: var(--red); 26 | } 27 | 28 | .button--red:hover { 29 | background: var(--dark-red); 30 | } 31 | 32 | .button--flat { 33 | color: var(--dark-blue); 34 | background: transparent; 35 | box-shadow: 0 0 0; 36 | border: 0; 37 | text-transform: uppercase; 38 | } 39 | 40 | .button--flat:hover { 41 | background: var(--moon-grey); 42 | } 43 | 44 | .button--flat.button--red { 45 | color: var(--dark-red); 46 | } 47 | 48 | .button--flat.button--grey { 49 | color: #6f6f6f; 50 | } 51 | 52 | .button[disabled] { 53 | opacity: .5; 54 | cursor: not-allowed; 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/css/_inputs.css: -------------------------------------------------------------------------------- 1 | .input { 2 | border-radius: .1em; 3 | padding: .5em 1em; 4 | background: white; 5 | border: 1px solid rgba(0, 0, 0, 0.1); 6 | transition: .2s ease all; 7 | color: #333; 8 | margin: 0; 9 | } 10 | 11 | .input:hover, 12 | .input:focus { 13 | border-color: rgba(0, 0, 0, 0.2); 14 | } 15 | 16 | .input--block { 17 | margin-bottom: .5em; 18 | display: block; 19 | width: 100%; 20 | } 21 | 22 | .input--textarea { 23 | line-height: 1.15; 24 | font-family: monospace; 25 | min-height: 10em; 26 | resize: vertical; 27 | } 28 | 29 | .input--red { 30 | background: #fcd0cd; 31 | } 32 | 33 | .input--green { 34 | background: #c9f2da; 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/css/_share.css: -------------------------------------------------------------------------------- 1 | .share__box { 2 | text-align: center; 3 | box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; 4 | background: #fff; 5 | display: block; 6 | border-radius: 0.2em; 7 | width: 90%; 8 | max-width: 25em; 9 | margin: 6em auto; 10 | } 11 | 12 | .share__box__download { 13 | width: 100%; 14 | padding: 1em; 15 | cursor: pointer; 16 | background: #ffffff; 17 | color: rgba(0, 0, 0, 0.5); 18 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 19 | } 20 | 21 | .share__box__info { 22 | padding: 2em 3em; 23 | } 24 | 25 | .share__box__title { 26 | margin-top: .2em; 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/css/_shell.css: -------------------------------------------------------------------------------- 1 | .shell { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | height: 25em; 6 | max-height: calc(100% - 4em); 7 | background: white; 8 | color: #212121; 9 | z-index: 9999; 10 | width: 100%; 11 | font-family: monospace; 12 | overflow: auto; 13 | font-size: 1rem; 14 | cursor: text; 15 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 16 | transition: .2s ease transform; 17 | } 18 | 19 | .shell__result { 20 | display: flex; 21 | padding: 0.5em; 22 | align-items: flex-start; 23 | border-top: 1px solid rgba(0, 0, 0, 0.05); 24 | } 25 | 26 | .shell--hidden { 27 | transform: translateY(105%); 28 | } 29 | 30 | .shell__result--hidden { 31 | opacity: 0; 32 | } 33 | 34 | .shell__text, 35 | .shell__prompt, 36 | .shell__prompt i { 37 | font-size: inherit; 38 | } 39 | 40 | .shell__prompt { 41 | width: 1.2rem; 42 | } 43 | 44 | .shell__prompt i { 45 | color: var(--blue); 46 | } 47 | 48 | .shell__text { 49 | margin: 0; 50 | font-family: inherit; 51 | white-space: pre-wrap; 52 | width: 100%; 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/css/_variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue: #2196f3; 3 | --dark-blue: #1E88E5; 4 | --red: #F44336; 5 | --dark-red: #D32F2F; 6 | --moon-grey: #f2f2f2; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto', sans-serif; 3 | padding-top: 4em; 4 | background-color: #fafafa; 5 | color: #333333; 6 | } 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | *, 13 | *:hover, 14 | *:active, 15 | *:focus { 16 | outline: 0 17 | } 18 | 19 | a { 20 | text-decoration: none; 21 | } 22 | 23 | img { 24 | max-width: 100%; 25 | } 26 | 27 | audio, 28 | video { 29 | width: 100%; 30 | } 31 | 32 | .mobile-only { 33 | display: none !important; 34 | } 35 | 36 | .container { 37 | width: 95%; 38 | max-width: 960px; 39 | margin: 1em auto 0; 40 | } 41 | 42 | i.spin { 43 | animation: 1s spin linear infinite; 44 | } 45 | 46 | #app { 47 | transition: .2s ease padding; 48 | } 49 | 50 | #app.multiple { 51 | padding-bottom: 4em; 52 | } 53 | 54 | nav { 55 | width: 16em; 56 | position: fixed; 57 | top: 4em; 58 | left: 0; 59 | } 60 | 61 | nav .action { 62 | width: 100%; 63 | display: block; 64 | border-radius: 0; 65 | font-size: 1.1em; 66 | padding: .5em; 67 | white-space: nowrap; 68 | overflow: hidden; 69 | text-overflow: ellipsis; 70 | } 71 | 72 | nav>div { 73 | border-top: 1px solid rgba(0, 0, 0, 0.05); 74 | } 75 | 76 | nav .action>* { 77 | vertical-align: middle; 78 | } 79 | 80 | main { 81 | min-height: 1em; 82 | margin: 0 1em 1em auto; 83 | width: calc(100% - 19em); 84 | } 85 | 86 | #breadcrumbs { 87 | height: 3em; 88 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 89 | } 90 | 91 | #breadcrumbs span, 92 | #breadcrumbs { 93 | display: flex; 94 | align-items: center; 95 | color: #6f6f6f; 96 | } 97 | 98 | #breadcrumbs a { 99 | color: inherit; 100 | transition: .1s ease-in; 101 | border-radius: .125em; 102 | } 103 | 104 | #breadcrumbs a:hover { 105 | background-color: rgba(0,0,0, 0.05); 106 | } 107 | 108 | #breadcrumbs span a { 109 | padding: .2em; 110 | } 111 | 112 | #progress { 113 | position: fixed; 114 | top: 0; 115 | left: 0; 116 | width: 100%; 117 | height: 3px; 118 | z-index: 9999999999; 119 | } 120 | 121 | #progress div { 122 | height: 100%; 123 | background-color: #40c4ff; 124 | width: 0; 125 | transition: .2s ease width; 126 | } 127 | 128 | .break-word { 129 | word-break: break-all; 130 | } -------------------------------------------------------------------------------- /frontend/src/css/login.css: -------------------------------------------------------------------------------- 1 | #login { 2 | background: #fff; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | #login img { 11 | width: 4em; 12 | height: 4em; 13 | margin: 0 auto; 14 | display: block; 15 | } 16 | 17 | #login h1 { 18 | text-align: center; 19 | font-size: 2.5em; 20 | margin: .4em 0 .67em; 21 | } 22 | 23 | #login form { 24 | position: fixed; 25 | top: 50%; 26 | left: 50%; 27 | transform: translate(-50%, -50%); 28 | max-width: 16em; 29 | width: 90%; 30 | } 31 | 32 | #login.recaptcha form { 33 | min-width: 304px; 34 | } 35 | 36 | #login #recaptcha { 37 | margin: .5em 0 0; 38 | } 39 | 40 | #login .wrong { 41 | background: var(--red); 42 | color: #fff; 43 | padding: .5em; 44 | text-align: center; 45 | animation: .2s opac forwards; 46 | } 47 | 48 | @keyframes opac { 49 | 0% { 50 | opacity: 0; 51 | } 52 | 100% { 53 | opacity: 1; 54 | } 55 | } 56 | 57 | #login p { 58 | cursor: pointer; 59 | text-align: right; 60 | color: var(--blue); 61 | text-transform: lowercase; 62 | font-weight: 500; 63 | font-size: 0.9rem; 64 | margin: .5rem 0; 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/css/mobile.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 1024px) { 2 | nav { 3 | width: 10em 4 | } 5 | } 6 | 7 | @media (max-width: 1024px) { 8 | main { 9 | width: calc(100% - 13em) 10 | } 11 | } 12 | 13 | @media (max-width: 736px) { 14 | body { 15 | padding-bottom: 5em; 16 | } 17 | #listing.list .item .size { 18 | display: none; 19 | } 20 | #listing.list .item .name { 21 | width: 60%; 22 | } 23 | #more { 24 | display: inherit 25 | } 26 | header .overlay { 27 | width: 100%; 28 | height: 100%; 29 | background-color: rgba(0, 0, 0, 0.1); 30 | } 31 | #dropdown { 32 | position: fixed; 33 | top: 1em; 34 | right: 1em; 35 | display: block; 36 | background-color: #fff; 37 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 38 | transform: scale(0); 39 | transition: .1s ease-in-out transform; 40 | transform-origin: top right; 41 | z-index: 99999; 42 | } 43 | #dropdown > div { 44 | display: block; 45 | } 46 | #dropdown.active { 47 | transform: scale(1); 48 | } 49 | #dropdown .action { 50 | display: flex; 51 | align-items: center; 52 | border-radius: 0; 53 | width: 100%; 54 | } 55 | #dropdown .action span:not(.counter) { 56 | display: inline-block; 57 | padding: .4em; 58 | } 59 | #dropdown .counter { 60 | left: 2.25em; 61 | } 62 | #file-selection { 63 | position: fixed; 64 | bottom: 1em; 65 | left: 50%; 66 | transform: translateX(-50%); 67 | display: flex; 68 | align-items: center; 69 | background: #fff; 70 | box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px; 71 | width: 95%; 72 | max-width: 20em; 73 | } 74 | #file-selection .action { 75 | border-radius: 50%; 76 | width: auto; 77 | } 78 | #file-selection > span { 79 | display: inline-block; 80 | margin-left: 1em; 81 | color: #6f6f6f; 82 | margin-right: auto; 83 | } 84 | nav { 85 | top: 0; 86 | z-index: 99999; 87 | background: #fff; 88 | height: 100%; 89 | width: 16em; 90 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); 91 | transition: .1s ease left; 92 | left: -17em; 93 | } 94 | nav.active { 95 | left: 0; 96 | } 97 | header .search-button, 98 | header>div:first-child>.action { 99 | display: inherit; 100 | } 101 | header img { 102 | display: none; 103 | } 104 | #listing { 105 | margin-bottom: 5em; 106 | } 107 | main { 108 | margin: 0 1em; 109 | width: calc(100% - 2em); 110 | } 111 | #search { 112 | display: none; 113 | } 114 | #search.active { 115 | display: block; 116 | } 117 | } 118 | 119 | @media (max-width: 450px) { 120 | #listing.list .item .modified { 121 | display: none; 122 | } 123 | #listing.list .item .name { 124 | width: 100%; 125 | } 126 | } -------------------------------------------------------------------------------- /frontend/src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | import ar from './ar.json' 5 | import de from './de.json' 6 | import en from './en.json' 7 | import es from './es.json' 8 | import fr from './fr.json' 9 | import is from './is.json' 10 | import it from './it.json' 11 | import ja from './ja.json' 12 | import ko from './ko.json' 13 | import nlBE from './nl-be.json' 14 | import pl from './pl.json' 15 | import pt from './pt.json' 16 | import ptBR from './pt-br.json' 17 | import ro from './ro.json' 18 | import ru from './ru.json' 19 | import svSE from './sv-se.json' 20 | import zhCN from './zh-cn.json' 21 | import zhTW from './zh-tw.json' 22 | 23 | Vue.use(VueI18n) 24 | 25 | export function detectLocale () { 26 | let locale = (navigator.language || navigator.browserLangugae).toLowerCase() 27 | switch (true) { 28 | case /^ar.*/i.test(locale): 29 | locale = 'ar' 30 | break 31 | case /^es.*/i.test(locale): 32 | locale = 'es' 33 | break 34 | case /^en.*/i.test(locale): 35 | locale = 'en' 36 | break 37 | case /^it.*/i.test(locale): 38 | locale = 'it' 39 | break 40 | case /^fr.*/i.test(locale): 41 | locale = 'fr' 42 | break 43 | case /^pt.*/i.test(locale): 44 | locale = 'pt' 45 | break 46 | case /^pt-BR.*/i.test(locale): 47 | locale = 'pt-br' 48 | break 49 | case /^ja.*/i.test(locale): 50 | locale = 'ja' 51 | break 52 | case /^zh-CN/i.test(locale): 53 | locale = 'zh-cn' 54 | break 55 | case /^zh-TW/i.test(locale): 56 | locale = 'zh-tw' 57 | break 58 | case /^zh.*/i.test(locale): 59 | locale = 'zh-cn' 60 | break 61 | case /^de.*/i.test(locale): 62 | locale = 'de' 63 | break 64 | case /^ru.*/i.test(locale): 65 | locale = 'ru' 66 | break 67 | case /^pl.*/i.test(locale): 68 | locale = 'pl' 69 | break 70 | case /^ko.*/i.test(locale): 71 | locale = 'ko' 72 | break 73 | default: 74 | locale = 'en' 75 | } 76 | 77 | return locale 78 | } 79 | 80 | const i18n = new VueI18n({ 81 | locale: detectLocale(), 82 | fallbackLocale: 'en', 83 | messages: { 84 | 'ar': ar, 85 | 'de': de, 86 | 'en': en, 87 | 'es': es, 88 | 'fr': fr, 89 | 'is': is, 90 | 'it': it, 91 | 'ja': ja, 92 | 'ko': ko, 93 | 'nl-be': nlBE, 94 | 'pl': pl, 95 | 'pt-br': ptBR, 96 | 'pt': pt, 97 | 'ru': ru, 98 | 'ro': ro, 99 | 'sv-se': svSE, 100 | 'zh-cn': zhCN, 101 | 'zh-tw': zhTW 102 | } 103 | }) 104 | 105 | export default i18n 106 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { sync } from 'vuex-router-sync' 2 | import store from '@/store' 3 | import router from '@/router' 4 | import i18n from '@/i18n' 5 | import Vue from '@/utils/vue' 6 | import { recaptcha, loginPage } from '@/utils/constants' 7 | import { login, validateLogin } from '@/utils/auth' 8 | import App from '@/App' 9 | 10 | sync(store, router) 11 | 12 | async function start () { 13 | if (loginPage) { 14 | await validateLogin() 15 | } else { 16 | await login('', '', '') 17 | } 18 | 19 | if (recaptcha) { 20 | await new Promise (resolve => { 21 | const check = () => { 22 | if (typeof window.grecaptcha === 'undefined') { 23 | setTimeout(check, 100) 24 | } else { 25 | resolve() 26 | } 27 | } 28 | 29 | check() 30 | }) 31 | } 32 | 33 | new Vue({ 34 | el: '#app', 35 | store, 36 | router, 37 | i18n, 38 | template: '', 39 | components: { App } 40 | }) 41 | } 42 | 43 | start() 44 | -------------------------------------------------------------------------------- /frontend/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | isLogged: state => state.user !== null, 3 | isFiles: state => !state.loading && state.route.name === 'Files', 4 | isListing: (state, getters) => getters.isFiles && state.req.isDir, 5 | isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'), 6 | selectedCount: state => state.selected.length 7 | } 8 | 9 | export default getters 10 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import mutations from './mutations' 4 | import getters from './getters' 5 | 6 | Vue.use(Vuex) 7 | 8 | const state = { 9 | user: null, 10 | req: {}, 11 | oldReq: {}, 12 | clipboard: { 13 | key: '', 14 | items: [] 15 | }, 16 | jwt: '', 17 | progress: 0, 18 | loading: false, 19 | reload: false, 20 | selected: [], 21 | multiple: false, 22 | show: null, 23 | showShell: false, 24 | showMessage: null, 25 | showConfirm: null 26 | } 27 | 28 | export default new Vuex.Store({ 29 | strict: true, 30 | state, 31 | getters, 32 | mutations 33 | }) 34 | -------------------------------------------------------------------------------- /frontend/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as i18n from '@/i18n' 2 | import moment from 'moment' 3 | 4 | const mutations = { 5 | closeHovers: state => { 6 | state.show = null 7 | state.showMessage = null 8 | }, 9 | toggleShell: (state) => { 10 | state.showShell = !state.showShell 11 | }, 12 | showHover: (state, value) => { 13 | if (typeof value !== 'object') { 14 | state.show = value 15 | return 16 | } 17 | 18 | state.show = value.prompt 19 | state.showMessage = value.message 20 | state.showConfirm = value.confirm 21 | }, 22 | showError: (state, value) => { 23 | state.show = 'error' 24 | state.showMessage = value 25 | }, 26 | showSuccess: (state, value) => { 27 | state.show = 'success' 28 | state.showMessage = value 29 | }, 30 | setLoading: (state, value) => { state.loading = value }, 31 | setReload: (state, value) => { state.reload = value }, 32 | setUser: (state, value) => { 33 | if (value === null) { 34 | state.user = null 35 | return 36 | } 37 | 38 | let locale = value.locale 39 | 40 | if (locale === '') { 41 | locale = i18n.detectLocale() 42 | } 43 | 44 | moment.locale(locale) 45 | i18n.default.locale = locale 46 | state.user = value 47 | }, 48 | setJWT: (state, value) => (state.jwt = value), 49 | multiple: (state, value) => (state.multiple = value), 50 | addSelected: (state, value) => (state.selected.push(value)), 51 | addPlugin: (state, value) => { 52 | state.plugins.push(value) 53 | }, 54 | removeSelected: (state, value) => { 55 | let i = state.selected.indexOf(value) 56 | if (i === -1) return 57 | state.selected.splice(i, 1) 58 | }, 59 | resetSelected: (state) => { 60 | state.selected = [] 61 | }, 62 | updateUser: (state, value) => { 63 | if (typeof value !== 'object') return 64 | 65 | for (let field in value) { 66 | if (field === 'locale') { 67 | moment.locale(value[field]) 68 | i18n.default.locale = value[field] 69 | } 70 | 71 | state.user[field] = value[field] 72 | } 73 | }, 74 | updateRequest: (state, value) => { 75 | state.oldReq = state.req 76 | state.req = value 77 | }, 78 | updateClipboard: (state, value) => { 79 | state.clipboard.key = value.key 80 | state.clipboard.items = value.items 81 | }, 82 | resetClipboard: (state) => { 83 | state.clipboard.key = '' 84 | state.clipboard.items = [] 85 | }, 86 | setProgress: (state, value) => { 87 | state.progress = value 88 | } 89 | } 90 | 91 | export default mutations 92 | -------------------------------------------------------------------------------- /frontend/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | import router from '@/router' 3 | import { Base64 } from 'js-base64' 4 | import { baseURL } from '@/utils/constants' 5 | 6 | export function parseToken (token) { 7 | const parts = token.split('.') 8 | 9 | if (parts.length !== 3) { 10 | throw new Error('token malformed') 11 | } 12 | 13 | const data = JSON.parse(Base64.decode(parts[1])) 14 | 15 | if (Math.round(new Date().getTime() / 1000) > data.exp) { 16 | throw new Error('token expired') 17 | } 18 | 19 | localStorage.setItem('jwt', token) 20 | store.commit('setJWT', token) 21 | store.commit('setUser', data.user) 22 | } 23 | 24 | export async function validateLogin () { 25 | try { 26 | if (localStorage.getItem('jwt')) { 27 | await renew(localStorage.getItem('jwt')) 28 | } 29 | } catch (_) { 30 | console.warn('Invalid JWT token in storage') // eslint-disable-line 31 | } 32 | } 33 | 34 | export async function login (username, password, recaptcha) { 35 | const data = { username, password, recaptcha } 36 | 37 | const res = await fetch(`${baseURL}/api/login`, { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json' 41 | }, 42 | body: JSON.stringify(data) 43 | }) 44 | 45 | const body = await res.text() 46 | 47 | if (res.status === 200) { 48 | parseToken(body) 49 | } else { 50 | throw new Error(body) 51 | } 52 | } 53 | 54 | export async function renew (jwt) { 55 | const res = await fetch(`${baseURL}/api/renew`, { 56 | method: 'POST', 57 | headers: { 58 | 'X-Auth': jwt, 59 | } 60 | }) 61 | 62 | const body = await res.text() 63 | 64 | if (res.status === 200) { 65 | parseToken(body) 66 | } else { 67 | throw new Error(body) 68 | } 69 | } 70 | 71 | export async function signup (username, password) { 72 | const data = { username, password } 73 | 74 | const res = await fetch(`${baseURL}/api/signup`, { 75 | method: 'POST', 76 | headers: { 77 | 'Content-Type': 'application/json' 78 | }, 79 | body: JSON.stringify(data) 80 | }) 81 | 82 | if (res.status !== 200) { 83 | throw new Error(res.status) 84 | } 85 | } 86 | 87 | export function logout () { 88 | store.commit('setJWT', '') 89 | store.commit('setUser', null) 90 | localStorage.setItem('jwt', null) 91 | router.push({path: '/login'}) 92 | } 93 | -------------------------------------------------------------------------------- /frontend/src/utils/buttons.js: -------------------------------------------------------------------------------- 1 | function loading (button) { 2 | let el = document.querySelector(`#${button}-button > i`) 3 | 4 | if (el === undefined || el === null) { 5 | console.log('Error getting button ' + button) // eslint-disable-line 6 | return 7 | } 8 | 9 | el.dataset.icon = el.innerHTML 10 | el.style.opacity = 0 11 | 12 | setTimeout(() => { 13 | el.classList.add('spin') 14 | el.innerHTML = 'autorenew' 15 | el.style.opacity = 1 16 | }, 100) 17 | } 18 | 19 | function done (button) { 20 | let el = document.querySelector(`#${button}-button > i`) 21 | 22 | if (el === undefined || el === null) { 23 | console.log('Error getting button ' + button) // eslint-disable-line 24 | return 25 | } 26 | 27 | el.style.opacity = 0 28 | 29 | setTimeout(() => { 30 | el.classList.remove('spin') 31 | el.innerHTML = el.dataset.icon 32 | el.style.opacity = 1 33 | }, 100) 34 | } 35 | 36 | function success (button) { 37 | let el = document.querySelector(`#${button}-button > i`) 38 | 39 | if (el === undefined || el === null) { 40 | console.log('Error getting button ' + button) // eslint-disable-line 41 | return 42 | } 43 | 44 | el.style.opacity = 0 45 | 46 | setTimeout(() => { 47 | el.classList.remove('spin') 48 | el.innerHTML = 'done' 49 | el.style.opacity = 1 50 | 51 | setTimeout(() => { 52 | el.style.opacity = 0 53 | 54 | setTimeout(() => { 55 | el.innerHTML = el.dataset.icon 56 | el.style.opacity = 1 57 | }, 100) 58 | }, 500) 59 | }, 100) 60 | } 61 | 62 | export default { 63 | loading, 64 | done, 65 | success 66 | } 67 | -------------------------------------------------------------------------------- /frontend/src/utils/constants.js: -------------------------------------------------------------------------------- 1 | const name = window.FileBrowser.Name || 'File Browser' 2 | const disableExternal = window.FileBrowser.DisableExternal 3 | const baseURL = window.FileBrowser.BaseURL 4 | const staticURL = window.FileBrowser.StaticURL 5 | const recaptcha = window.FileBrowser.ReCaptcha 6 | const recaptchaKey = window.FileBrowser.ReCaptchaKey 7 | const signup = window.FileBrowser.Signup 8 | const version = window.FileBrowser.Version 9 | const logoURL = `${staticURL}/img/logo.svg` 10 | const noAuth = window.FileBrowser.NoAuth 11 | const authMethod = window.FileBrowser.AuthMethod 12 | const loginPage = window.FileBrowser.LoginPage 13 | const theme = window.FileBrowser.Theme 14 | 15 | export { 16 | name, 17 | disableExternal, 18 | baseURL, 19 | logoURL, 20 | recaptcha, 21 | recaptchaKey, 22 | signup, 23 | version, 24 | noAuth, 25 | authMethod, 26 | loginPage, 27 | theme 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/utils/cookie.js: -------------------------------------------------------------------------------- 1 | export default function (name) { 2 | let re = new RegExp('(?:(?:^|.*;\\s*)' + name + '\\s*\\=\\s*([^;]*).*$)|^.*$') 3 | return document.cookie.replace(re, '$1') 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/utils/css.js: -------------------------------------------------------------------------------- 1 | export default function getRule (rules) { 2 | for (let i = 0; i < rules.length; i++) { 3 | rules[i] = rules[i].toLowerCase() 4 | } 5 | 6 | let result = null 7 | let find = Array.prototype.find 8 | 9 | find.call(document.styleSheets, styleSheet => { 10 | result = find.call(styleSheet.cssRules, cssRule => { 11 | let found = false 12 | 13 | if (cssRule instanceof window.CSSStyleRule) { 14 | for (let i = 0; i < rules.length; i++) { 15 | if (cssRule.selectorText.toLowerCase() === rules[i]) { 16 | found = true 17 | } 18 | } 19 | } 20 | 21 | return found 22 | }) 23 | 24 | return result != null 25 | }) 26 | 27 | return result 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/utils/url.js: -------------------------------------------------------------------------------- 1 | function removeLastDir (url) { 2 | var arr = url.split('/') 3 | if (arr.pop() === '') { 4 | arr.pop() 5 | } 6 | 7 | return arr.join('/') 8 | } 9 | 10 | // this code borrow from mozilla 11 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples 12 | function encodeRFC5987ValueChars(str) { 13 | return encodeURIComponent(str). 14 | // Note that although RFC3986 reserves "!", RFC5987 does not, 15 | // so we do not need to escape it 16 | replace(/['()]/g, escape). // i.e., %27 %28 %29 17 | replace(/\*/g, '%2A'). 18 | // The following are not required for percent-encoding per RFC5987, 19 | // so we can allow for a little better readability over the wire: |`^ 20 | replace(/%(?:7C|60|5E)/g, unescape); 21 | } 22 | 23 | export default { 24 | encodeRFC5987ValueChars: encodeRFC5987ValueChars, 25 | removeLastDir: removeLastDir 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/utils/vue.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Noty from 'noty' 3 | import i18n from '@/i18n' 4 | import { disableExternal } from '@/utils/constants' 5 | 6 | Vue.config.productionTip = true 7 | 8 | const notyDefault = { 9 | type: 'info', 10 | layout: 'bottomRight', 11 | timeout: 1000, 12 | progressBar: true 13 | } 14 | 15 | Vue.prototype.$noty = (opts) => { 16 | new Noty(Object.assign({}, notyDefault, opts)).show() 17 | } 18 | 19 | Vue.prototype.$showSuccess = (message) => { 20 | new Noty(Object.assign({}, notyDefault, { 21 | text: message, 22 | type: 'success' 23 | })).show() 24 | } 25 | 26 | Vue.prototype.$showError = (error) => { 27 | let btns = [ 28 | Noty.button(i18n.t('buttons.close'), '', function () { 29 | n.close() 30 | }) 31 | ] 32 | 33 | if (!disableExternal) { 34 | btns.unshift(Noty.button(i18n.t('buttons.reportIssue'), '', function () { 35 | window.open('https://github.com/dev-techmoe/filebrowser/issues/new/choose') 36 | })) 37 | } 38 | 39 | let n = new Noty(Object.assign({}, notyDefault, { 40 | text: error.message || error, 41 | type: 'error', 42 | timeout: null, 43 | buttons: btns 44 | })) 45 | 46 | n.show() 47 | } 48 | 49 | Vue.directive('focus', { 50 | inserted: function (el) { 51 | el.focus() 52 | } 53 | }) 54 | 55 | export default Vue 56 | -------------------------------------------------------------------------------- /frontend/src/views/Layout.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 44 | -------------------------------------------------------------------------------- /frontend/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 96 | -------------------------------------------------------------------------------- /frontend/src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /frontend/src/views/Share.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 68 | -------------------------------------------------------------------------------- /frontend/src/views/errors/403.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/views/errors/404.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/views/errors/500.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/views/settings/Profile.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 104 | -------------------------------------------------------------------------------- /frontend/src/views/settings/Users.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 49 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runtimeCompiler: true, 3 | publicPath: '[{[ .StaticURL ]}]' 4 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dev-techmoe/filebrowser/v2 2 | 3 | require ( 4 | github.com/DataDog/zstd v1.4.5 // indirect 5 | github.com/GeertJohan/go.rice v1.0.0 6 | github.com/Sereal/Sereal v0.0.0-20200430150152-3c99d16fbeb1 // indirect 7 | github.com/asdine/storm v2.1.2+incompatible 8 | github.com/caddyserver/caddy v1.0.3 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 10 | github.com/dsnet/compress v0.0.1 // indirect 11 | github.com/frankban/quicktest v1.10.0 // indirect 12 | github.com/golang/snappy v0.0.1 // indirect 13 | github.com/gorilla/mux v1.7.3 14 | github.com/gorilla/websocket v1.4.1 15 | github.com/hacdias/fileutils v0.0.0-20181202104838-227b317161a1 16 | github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 17 | github.com/mholt/archiver v3.1.1+incompatible 18 | github.com/mitchellh/go-homedir v1.1.0 19 | github.com/nwaples/rardecode v1.1.0 // indirect 20 | github.com/pelletier/go-toml v1.6.0 21 | github.com/pierrec/lz4 v2.5.2+incompatible // indirect 22 | github.com/spf13/afero v1.2.2 23 | github.com/spf13/cobra v0.0.5 24 | github.com/spf13/pflag v1.0.5 25 | github.com/spf13/viper v1.6.1 26 | github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce 27 | github.com/ulikunitz/xz v0.5.7 // indirect 28 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 29 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 30 | go.etcd.io/bbolt v1.3.3 31 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 32 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 33 | gopkg.in/yaml.v2 v2.2.7 34 | ) 35 | 36 | go 1.13 37 | -------------------------------------------------------------------------------- /http/commands.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os/exec" 9 | "strings" 10 | "time" 11 | 12 | "github.com/dev-techmoe/filebrowser/v2/runner" 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | var upgrader = websocket.Upgrader{ 17 | ReadBufferSize: 1024, 18 | WriteBufferSize: 1024, 19 | } 20 | 21 | var ( 22 | cmdNotAllowed = []byte("Command not allowed.") 23 | ) 24 | 25 | func wsErr(ws *websocket.Conn, r *http.Request, status int, err error) { 26 | txt := http.StatusText(status) 27 | if err != nil || status >= 400 { 28 | log.Printf("%s: %v %s %v", r.URL.Path, status, r.RemoteAddr, err) 29 | } 30 | ws.WriteControl(websocket.CloseInternalServerErr, []byte(txt), time.Now().Add(10*time.Second)) 31 | } 32 | 33 | var commandsHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 34 | conn, err := upgrader.Upgrade(w, r, nil) 35 | if err != nil { 36 | return http.StatusInternalServerError, err 37 | } 38 | defer conn.Close() 39 | 40 | var raw string 41 | 42 | for { 43 | _, msg, err := conn.ReadMessage() 44 | if err != nil { 45 | wsErr(conn, r, http.StatusInternalServerError, err) 46 | return 0, nil 47 | } 48 | 49 | raw = strings.TrimSpace(string(msg)) 50 | if raw != "" { 51 | break 52 | } 53 | } 54 | 55 | if !d.user.CanExecute(strings.Split(raw, " ")[0]) { 56 | err := conn.WriteMessage(websocket.TextMessage, cmdNotAllowed) 57 | if err != nil { 58 | wsErr(conn, r, http.StatusInternalServerError, err) 59 | } 60 | 61 | return 0, nil 62 | } 63 | 64 | command, err := runner.ParseCommand(d.settings, raw) 65 | if err != nil { 66 | err := conn.WriteMessage(websocket.TextMessage, []byte(err.Error())) 67 | if err != nil { 68 | wsErr(conn, r, http.StatusInternalServerError, err) 69 | } 70 | 71 | return 0, nil 72 | } 73 | 74 | cmd := exec.Command(command[0], command[1:]...) 75 | cmd.Dir = d.user.FullPath(r.URL.Path) 76 | 77 | stdout, err := cmd.StdoutPipe() 78 | if err != nil { 79 | wsErr(conn, r, http.StatusInternalServerError, err) 80 | return 0, nil 81 | } 82 | 83 | stderr, err := cmd.StderrPipe() 84 | if err != nil { 85 | wsErr(conn, r, http.StatusInternalServerError, err) 86 | return 0, nil 87 | } 88 | 89 | if err := cmd.Start(); err != nil { 90 | wsErr(conn, r, http.StatusInternalServerError, err) 91 | return 0, nil 92 | } 93 | 94 | s := bufio.NewScanner(io.MultiReader(stdout, stderr)) 95 | for s.Scan() { 96 | conn.WriteMessage(websocket.TextMessage, s.Bytes()) 97 | } 98 | 99 | if err := cmd.Wait(); err != nil { 100 | wsErr(conn, r, http.StatusInternalServerError, err) 101 | } 102 | 103 | return 0, nil 104 | }) 105 | -------------------------------------------------------------------------------- /http/data.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/tomasen/realip" 9 | 10 | "github.com/dev-techmoe/filebrowser/v2/runner" 11 | "github.com/dev-techmoe/filebrowser/v2/settings" 12 | "github.com/dev-techmoe/filebrowser/v2/storage" 13 | "github.com/dev-techmoe/filebrowser/v2/users" 14 | ) 15 | 16 | type handleFunc func(w http.ResponseWriter, r *http.Request, d *data) (int, error) 17 | 18 | type data struct { 19 | *runner.Runner 20 | settings *settings.Settings 21 | server *settings.Server 22 | store *storage.Storage 23 | user *users.User 24 | raw interface{} 25 | } 26 | 27 | // Check implements rules.Checker. 28 | func (d *data) Check(path string) bool { 29 | for _, rule := range d.user.Rules { 30 | if rule.Matches(path) { 31 | return rule.Allow 32 | } 33 | } 34 | 35 | for _, rule := range d.settings.Rules { 36 | if rule.Matches(path) { 37 | return rule.Allow 38 | } 39 | } 40 | 41 | return true 42 | } 43 | 44 | func handle(fn handleFunc, prefix string, storage *storage.Storage, server *settings.Server) http.Handler { 45 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 | settings, err := storage.Settings.Get() 47 | if err != nil { 48 | log.Fatalln("ERROR: couldn't get settings") 49 | return 50 | } 51 | 52 | status, err := fn(w, r, &data{ 53 | Runner: &runner.Runner{Settings: settings}, 54 | store: storage, 55 | settings: settings, 56 | server: server, 57 | }) 58 | 59 | if status != 0 { 60 | txt := http.StatusText(status) 61 | http.Error(w, strconv.Itoa(status)+" "+txt, status) 62 | } 63 | 64 | if status >= 400 || err != nil { 65 | clientIP := realip.FromRequest(r) 66 | log.Printf("%s: %v %s %v", r.URL.Path, status, clientIP, err) 67 | } 68 | }) 69 | 70 | return http.StripPrefix(prefix, handler) 71 | } 72 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/settings" 7 | "github.com/dev-techmoe/filebrowser/v2/storage" 8 | "github.com/gorilla/mux" 9 | ) 10 | 11 | type modifyRequest struct { 12 | What string `json:"what"` // Answer to: what data type? 13 | Which []string `json:"which"` // Answer to: which fields? 14 | } 15 | 16 | func NewHandler(storage *storage.Storage, server *settings.Server) (http.Handler, error) { 17 | server.Clean() 18 | 19 | r := mux.NewRouter() 20 | index, static := getStaticHandlers(storage, server) 21 | 22 | // NOTE: This fixes the issue where it would redirect if people did not put a 23 | // trailing slash in the end. I hate this decision since this allows some awful 24 | // URLs https://www.gorillatoolkit.org/pkg/mux#Router.SkipClean 25 | r = r.SkipClean(true) 26 | 27 | monkey := func(fn handleFunc, prefix string) http.Handler { 28 | return handle(fn, prefix, storage, server) 29 | } 30 | 31 | r.PathPrefix("/static").Handler(static) 32 | r.NotFoundHandler = index 33 | 34 | api := r.PathPrefix("/api").Subrouter() 35 | 36 | api.Handle("/login", monkey(loginHandler, "")) 37 | api.Handle("/signup", monkey(signupHandler, "")) 38 | api.Handle("/renew", monkey(renewHandler, "")) 39 | 40 | users := api.PathPrefix("/users").Subrouter() 41 | users.Handle("", monkey(usersGetHandler, "")).Methods("GET") 42 | users.Handle("", monkey(userPostHandler, "")).Methods("POST") 43 | users.Handle("/{id:[0-9]+}", monkey(userPutHandler, "")).Methods("PUT") 44 | users.Handle("/{id:[0-9]+}", monkey(userGetHandler, "")).Methods("GET") 45 | users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE") 46 | 47 | api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET") 48 | api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler, "/api/resources")).Methods("DELETE") 49 | api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST") 50 | api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT") 51 | api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH") 52 | 53 | api.PathPrefix("/share").Handler(monkey(shareGetsHandler, "/api/share")).Methods("GET") 54 | api.PathPrefix("/share").Handler(monkey(sharePostHandler, "/api/share")).Methods("POST") 55 | api.PathPrefix("/share").Handler(monkey(shareDeleteHandler, "/api/share")).Methods("DELETE") 56 | 57 | api.Handle("/settings", monkey(settingsGetHandler, "")).Methods("GET") 58 | api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT") 59 | 60 | api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET") 61 | api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET") 62 | api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET") 63 | 64 | public := api.PathPrefix("/public").Subrouter() 65 | public.PathPrefix("/dl").Handler(monkey(publicDlHandler, "/api/public/dl/")).Methods("GET") 66 | public.PathPrefix("/share").Handler(monkey(publicShareHandler, "/api/public/share/")).Methods("GET") 67 | 68 | return stripPrefix(server.BaseURL, r), nil 69 | } 70 | -------------------------------------------------------------------------------- /http/public.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/files" 8 | ) 9 | 10 | var withHashFile = func(fn handleFunc) handleFunc { 11 | return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 12 | link, err := d.store.Share.GetByHash(r.URL.Path) 13 | if err != nil { 14 | link, err = d.store.Share.GetByHash(ifPathWithName(r)) 15 | if err != nil { 16 | return errToStatus(err), err 17 | } 18 | } 19 | 20 | user, err := d.store.Users.Get(d.server.Root, link.UserID) 21 | if err != nil { 22 | return errToStatus(err), err 23 | } 24 | 25 | d.user = user 26 | 27 | file, err := files.NewFileInfo(files.FileOptions{ 28 | Fs: d.user.Fs, 29 | Path: link.Path, 30 | Modify: d.user.Perm.Modify, 31 | Expand: false, 32 | Checker: d, 33 | }) 34 | if err != nil { 35 | return errToStatus(err), err 36 | } 37 | 38 | d.raw = file 39 | return fn(w, r, d) 40 | } 41 | } 42 | 43 | // ref to https://github.com/dev-techmoe/filebrowser/pull/727 44 | // `/api/public/dl/MEEuZK-v/file-name.txt` for old browsers to save file with correct name 45 | func ifPathWithName(r *http.Request) string { 46 | pathElements := strings.Split(r.URL.Path, "/") 47 | // prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name` 48 | // len(pathElements) will be 1, and golang will panic `runtime error: index out of range` 49 | if len(pathElements) < 2 { 50 | return r.URL.Path 51 | } 52 | id := pathElements[len(pathElements)-2] 53 | return id 54 | } 55 | 56 | var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 57 | return renderJSON(w, r, d.raw) 58 | }) 59 | 60 | var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 61 | file := d.raw.(*files.FileInfo) 62 | if !file.IsDir { 63 | return rawFileHandler(w, r, file) 64 | } 65 | 66 | return rawDirHandler(w, r, d, file) 67 | }) 68 | -------------------------------------------------------------------------------- /http/search.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/search" 8 | ) 9 | 10 | var searchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 11 | response := []map[string]interface{}{} 12 | query := r.URL.Query().Get("query") 13 | 14 | err := search.Search(d.user.Fs, r.URL.Path, query, d, func(path string, f os.FileInfo) error { 15 | response = append(response, map[string]interface{}{ 16 | "dir": f.IsDir(), 17 | "path": path, 18 | }) 19 | 20 | return nil 21 | }) 22 | 23 | if err != nil { 24 | return http.StatusInternalServerError, err 25 | } 26 | 27 | return renderJSON(w, r, response) 28 | }) 29 | -------------------------------------------------------------------------------- /http/settings.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/rules" 8 | "github.com/dev-techmoe/filebrowser/v2/settings" 9 | ) 10 | 11 | type settingsData struct { 12 | Signup bool `json:"signup"` 13 | CreateUserDir bool `json:"createUserDir"` 14 | Defaults settings.UserDefaults `json:"defaults"` 15 | Rules []rules.Rule `json:"rules"` 16 | Branding settings.Branding `json:"branding"` 17 | Shell []string `json:"shell"` 18 | Commands map[string][]string `json:"commands"` 19 | } 20 | 21 | var settingsGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 22 | data := &settingsData{ 23 | Signup: d.settings.Signup, 24 | CreateUserDir: d.settings.CreateUserDir, 25 | Defaults: d.settings.Defaults, 26 | Rules: d.settings.Rules, 27 | Branding: d.settings.Branding, 28 | Shell: d.settings.Shell, 29 | Commands: d.settings.Commands, 30 | } 31 | 32 | return renderJSON(w, r, data) 33 | }) 34 | 35 | var settingsPutHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 36 | req := &settingsData{} 37 | err := json.NewDecoder(r.Body).Decode(req) 38 | if err != nil { 39 | return http.StatusBadRequest, err 40 | } 41 | 42 | d.settings.Signup = req.Signup 43 | d.settings.CreateUserDir = req.CreateUserDir 44 | d.settings.Defaults = req.Defaults 45 | d.settings.Rules = req.Rules 46 | d.settings.Branding = req.Branding 47 | d.settings.Shell = req.Shell 48 | d.settings.Commands = req.Commands 49 | 50 | err = d.store.Settings.Save(d.settings) 51 | return errToStatus(err), err 52 | }) 53 | -------------------------------------------------------------------------------- /http/share.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/dev-techmoe/filebrowser/v2/errors" 12 | "github.com/dev-techmoe/filebrowser/v2/share" 13 | ) 14 | 15 | func withPermShare(fn handleFunc) handleFunc { 16 | return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 17 | if !d.user.Perm.Share { 18 | return http.StatusForbidden, nil 19 | } 20 | 21 | return fn(w, r, d) 22 | }) 23 | } 24 | 25 | var shareGetsHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 26 | s, err := d.store.Share.Gets(r.URL.Path, d.user.ID) 27 | if err == errors.ErrNotExist { 28 | return renderJSON(w, r, []*share.Link{}) 29 | } 30 | 31 | if err != nil { 32 | return http.StatusInternalServerError, err 33 | } 34 | 35 | return renderJSON(w, r, s) 36 | }) 37 | 38 | var shareDeleteHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 39 | hash := strings.TrimSuffix(r.URL.Path, "/") 40 | hash = strings.TrimPrefix(hash, "/") 41 | 42 | if hash == "" { 43 | return http.StatusBadRequest, nil 44 | } 45 | 46 | err := d.store.Share.Delete(hash) 47 | return errToStatus(err), err 48 | }) 49 | 50 | var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { 51 | var s *share.Link 52 | rawExpire := r.URL.Query().Get("expires") 53 | unit := r.URL.Query().Get("unit") 54 | 55 | if rawExpire == "" { 56 | var err error 57 | s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID) 58 | if err == nil { 59 | w.Write([]byte(d.server.BaseURL + "/share/" + s.Hash)) 60 | return 0, nil 61 | } 62 | } 63 | 64 | bytes := make([]byte, 6) 65 | _, err := rand.Read(bytes) 66 | if err != nil { 67 | return http.StatusInternalServerError, err 68 | } 69 | 70 | str := base64.URLEncoding.EncodeToString(bytes) 71 | 72 | var expire int64 = 0 73 | 74 | if rawExpire != "" { 75 | num, err := strconv.Atoi(rawExpire) 76 | if err != nil { 77 | return http.StatusInternalServerError, err 78 | } 79 | 80 | var add time.Duration 81 | switch unit { 82 | case "seconds": 83 | add = time.Second * time.Duration(num) 84 | case "minutes": 85 | add = time.Minute * time.Duration(num) 86 | case "days": 87 | add = time.Hour * 24 * time.Duration(num) 88 | default: 89 | add = time.Hour * time.Duration(num) 90 | } 91 | 92 | expire = time.Now().Add(add).Unix() 93 | } 94 | 95 | s = &share.Link{ 96 | Path: r.URL.Path, 97 | Hash: str, 98 | Expire: expire, 99 | UserID: d.user.ID, 100 | } 101 | 102 | if err := d.store.Share.Save(s); err != nil { 103 | return http.StatusInternalServerError, err 104 | } 105 | 106 | return renderJSON(w, r, s) 107 | }) 108 | -------------------------------------------------------------------------------- /http/utils.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "strings" 9 | 10 | "github.com/dev-techmoe/filebrowser/v2/errors" 11 | ) 12 | 13 | func renderJSON(w http.ResponseWriter, r *http.Request, data interface{}) (int, error) { 14 | marsh, err := json.Marshal(data) 15 | 16 | if err != nil { 17 | return http.StatusInternalServerError, err 18 | } 19 | 20 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 21 | if _, err := w.Write(marsh); err != nil { 22 | return http.StatusInternalServerError, err 23 | } 24 | 25 | return 0, nil 26 | } 27 | 28 | func errToStatus(err error) int { 29 | switch { 30 | case err == nil: 31 | return http.StatusOK 32 | case os.IsPermission(err): 33 | return http.StatusForbidden 34 | case os.IsNotExist(err), err == errors.ErrNotExist: 35 | return http.StatusNotFound 36 | case os.IsExist(err), err == errors.ErrExist: 37 | return http.StatusConflict 38 | default: 39 | return http.StatusInternalServerError 40 | } 41 | } 42 | 43 | // This is an addaptation if http.StripPrefix in which we don't 44 | // return 404 if the page doesn't have the needed prefix. 45 | func stripPrefix(prefix string, h http.Handler) http.Handler { 46 | if prefix == "" { 47 | return h 48 | } 49 | 50 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 51 | p := strings.TrimPrefix(r.URL.Path, prefix) 52 | r2 := new(http.Request) 53 | *r2 = *r 54 | r2.URL = new(url.URL) 55 | *r2.URL = *r.URL 56 | r2.URL.Path = p 57 | h.ServeHTTP(w, r2) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/cmd" 7 | ) 8 | 9 | func main() { 10 | runtime.GOMAXPROCS(runtime.NumCPU()) 11 | cmd.Execute() 12 | } 13 | -------------------------------------------------------------------------------- /rules/rules.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | // Checker is a Rules checker. 9 | type Checker interface { 10 | Check(path string) bool 11 | } 12 | 13 | // Rule is a allow/disallow rule. 14 | type Rule struct { 15 | Regex bool `json:"regex"` 16 | Allow bool `json:"allow"` 17 | Path string `json:"path"` 18 | Regexp *Regexp `json:"regexp"` 19 | } 20 | 21 | // Matches matches a path against a rule. 22 | func (r *Rule) Matches(path string) bool { 23 | if r.Regex { 24 | return r.Regexp.MatchString(path) 25 | } 26 | 27 | return strings.HasPrefix(path, r.Path) 28 | } 29 | 30 | // Regexp is a wrapper to the native regexp type where we 31 | // save the raw expression. 32 | type Regexp struct { 33 | Raw string `json:"raw"` 34 | regexp *regexp.Regexp 35 | } 36 | 37 | // MatchString checks if a string matches the regexp. 38 | func (r *Regexp) MatchString(s string) bool { 39 | if r.regexp == nil { 40 | r.regexp = regexp.MustCompile(r.Raw) 41 | } 42 | 43 | return r.regexp.MatchString(s) 44 | } 45 | -------------------------------------------------------------------------------- /runner/parser.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "github.com/caddyserver/caddy" 7 | "github.com/dev-techmoe/filebrowser/v2/settings" 8 | ) 9 | 10 | // ParseCommand parses the command taking in account if the current 11 | // instance uses a shell to run the commands or just calls the binary 12 | // directyly. 13 | func ParseCommand(s *settings.Settings, raw string) ([]string, error) { 14 | command := []string{} 15 | 16 | if len(s.Shell) == 0 { 17 | cmd, args, err := caddy.SplitCommandAndArgs(raw) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | _, err = exec.LookPath(cmd) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | command = append(command, cmd) 28 | command = append(command, args...) 29 | } else { 30 | command = append(s.Shell, raw) 31 | } 32 | 33 | return command, nil 34 | } 35 | -------------------------------------------------------------------------------- /runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/dev-techmoe/filebrowser/v2/settings" 11 | "github.com/dev-techmoe/filebrowser/v2/users" 12 | ) 13 | 14 | // Runner is a commands runner. 15 | type Runner struct { 16 | *settings.Settings 17 | } 18 | 19 | // RunHook runs the hooks for the before and after event. 20 | func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error { 21 | path = user.FullPath(path) 22 | dst = user.FullPath(dst) 23 | 24 | if val, ok := r.Commands["before_"+evt]; ok { 25 | for _, command := range val { 26 | err := r.exec(command, "before_"+evt, path, dst, user) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | } 32 | 33 | err := fn() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if val, ok := r.Commands["after_"+evt]; ok { 39 | for _, command := range val { 40 | err := r.exec(command, "after_"+evt, path, dst, user) 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error { 51 | blocking := true 52 | 53 | if strings.HasSuffix(raw, "&") { 54 | blocking = false 55 | raw = strings.TrimSpace(strings.TrimSuffix(raw, "&")) 56 | } 57 | 58 | command, err := ParseCommand(r.Settings, raw) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | cmd := exec.Command(command[0], command[1:]...) 64 | cmd.Env = append(os.Environ(), fmt.Sprintf("FILE=%s", path)) 65 | cmd.Env = append(cmd.Env, fmt.Sprintf("SCOPE=%s", user.Scope)) 66 | cmd.Env = append(cmd.Env, fmt.Sprintf("TRIGGER=%s", evt)) 67 | cmd.Env = append(cmd.Env, fmt.Sprintf("USERNAME=%s", user.Username)) 68 | cmd.Env = append(cmd.Env, fmt.Sprintf("DESTINATION=%s", dst)) 69 | 70 | cmd.Stdin = os.Stdin 71 | cmd.Stdout = os.Stdout 72 | cmd.Stderr = os.Stderr 73 | 74 | if !blocking { 75 | log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " ")) 76 | return cmd.Start() 77 | } 78 | 79 | log.Printf("[INFO] Blocking Command: \"%s\"", strings.Join(command, " ")) 80 | return cmd.Run() 81 | } 82 | -------------------------------------------------------------------------------- /search/conditions.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "mime" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | typeRegexp = regexp.MustCompile(`type:(\w+)`) 12 | ) 13 | 14 | type condition func(path string) bool 15 | 16 | func extensionCondition(extension string) condition { 17 | return func(path string) bool { 18 | return filepath.Ext(path) == "."+extension 19 | } 20 | } 21 | 22 | func imageCondition(path string) bool { 23 | extension := filepath.Ext(path) 24 | mimetype := mime.TypeByExtension(extension) 25 | 26 | return strings.HasPrefix(mimetype, "image") 27 | } 28 | 29 | func audioCondition(path string) bool { 30 | extension := filepath.Ext(path) 31 | mimetype := mime.TypeByExtension(extension) 32 | 33 | return strings.HasPrefix(mimetype, "audio") 34 | } 35 | 36 | func videoCondition(path string) bool { 37 | extension := filepath.Ext(path) 38 | mimetype := mime.TypeByExtension(extension) 39 | 40 | return strings.HasPrefix(mimetype, "video") 41 | } 42 | 43 | func parseSearch(value string) *searchOptions { 44 | opts := &searchOptions{ 45 | CaseSensitive: strings.Contains(value, "case:sensitive"), 46 | Conditions: []condition{}, 47 | Terms: []string{}, 48 | } 49 | 50 | // removes the options from the value 51 | value = strings.Replace(value, "case:insensitive", "", -1) 52 | value = strings.Replace(value, "case:sensitive", "", -1) 53 | value = strings.TrimSpace(value) 54 | 55 | types := typeRegexp.FindAllStringSubmatch(value, -1) 56 | for _, t := range types { 57 | if len(t) == 1 { 58 | continue 59 | } 60 | 61 | switch t[1] { 62 | case "image": 63 | opts.Conditions = append(opts.Conditions, imageCondition) 64 | case "audio", "music": 65 | opts.Conditions = append(opts.Conditions, audioCondition) 66 | case "video": 67 | opts.Conditions = append(opts.Conditions, videoCondition) 68 | default: 69 | opts.Conditions = append(opts.Conditions, extensionCondition(t[1])) 70 | } 71 | } 72 | 73 | if len(types) > 0 { 74 | // Remove the fields from the search value. 75 | value = typeRegexp.ReplaceAllString(value, "") 76 | } 77 | 78 | // If it's canse insensitive, put everything in lowercase. 79 | if !opts.CaseSensitive { 80 | value = strings.ToLower(value) 81 | } 82 | 83 | // Remove the spaces from the search value. 84 | value = strings.TrimSpace(value) 85 | 86 | if value == "" { 87 | return opts 88 | } 89 | 90 | // if the value starts with " and finishes what that character, we will 91 | // only search for that term 92 | if value[0] == '"' && value[len(value)-1] == '"' { 93 | unique := strings.TrimPrefix(value, "\"") 94 | unique = strings.TrimSuffix(unique, "\"") 95 | 96 | opts.Terms = []string{unique} 97 | return opts 98 | } 99 | 100 | opts.Terms = strings.Split(value, " ") 101 | return opts 102 | } 103 | -------------------------------------------------------------------------------- /search/search.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/rules" 8 | "github.com/spf13/afero" 9 | ) 10 | 11 | type searchOptions struct { 12 | CaseSensitive bool 13 | Conditions []condition 14 | Terms []string 15 | } 16 | 17 | // Search searches for a query in a fs. 18 | func Search(fs afero.Fs, scope, query string, checker rules.Checker, found func(path string, f os.FileInfo) error) error { 19 | search := parseSearch(query) 20 | 21 | scope = strings.Replace(scope, "\\", "/", -1) 22 | scope = strings.TrimPrefix(scope, "/") 23 | scope = strings.TrimSuffix(scope, "/") 24 | scope = "/" + scope + "/" 25 | 26 | return afero.Walk(fs, scope, func(originalPath string, f os.FileInfo, err error) error { 27 | originalPath = strings.Replace(originalPath, "\\", "/", -1) 28 | originalPath = strings.TrimPrefix(originalPath, "/") 29 | originalPath = "/" + originalPath 30 | path := originalPath 31 | 32 | if path == scope { 33 | return nil 34 | } 35 | 36 | if !checker.Check(path) { 37 | return nil 38 | } 39 | 40 | if !search.CaseSensitive { 41 | path = strings.ToLower(path) 42 | } 43 | 44 | if len(search.Conditions) > 0 { 45 | match := false 46 | 47 | for _, t := range search.Conditions { 48 | if t(path) { 49 | match = true 50 | break 51 | } 52 | } 53 | 54 | if !match { 55 | return nil 56 | } 57 | } 58 | 59 | if len(search.Terms) > 0 { 60 | for _, term := range search.Terms { 61 | if strings.Contains(path, term) { 62 | return found(strings.TrimPrefix(originalPath, scope), f) 63 | } 64 | } 65 | } 66 | 67 | return nil 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /settings/branding.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | // Branding contains the branding settings of the app. 4 | type Branding struct { 5 | Name string `json:"name"` 6 | DisableExternal bool `json:"disableExternal"` 7 | Files string `json:"files"` 8 | Theme string `json:"theme"` 9 | } 10 | -------------------------------------------------------------------------------- /settings/defaults.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/dev-techmoe/filebrowser/v2/files" 5 | "github.com/dev-techmoe/filebrowser/v2/users" 6 | ) 7 | 8 | // UserDefaults is a type that holds the default values 9 | // for some fields on User. 10 | type UserDefaults struct { 11 | Scope string `json:"scope"` 12 | Locale string `json:"locale"` 13 | ViewMode users.ViewMode `json:"viewMode"` 14 | Sorting files.Sorting `json:"sorting"` 15 | Perm users.Permissions `json:"perm"` 16 | Commands []string `json:"commands"` 17 | } 18 | 19 | // Apply applies the default options to a user. 20 | func (d *UserDefaults) Apply(u *users.User) { 21 | u.Scope = d.Scope 22 | u.Locale = d.Locale 23 | u.ViewMode = d.ViewMode 24 | u.Perm = d.Perm 25 | u.Sorting = d.Sorting 26 | u.Commands = d.Commands 27 | } 28 | -------------------------------------------------------------------------------- /settings/dir.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/spf13/afero" 11 | ) 12 | 13 | var ( 14 | invalidFilenameChars = regexp.MustCompile(`[^0-9A-Za-z@_\-.]`) 15 | 16 | dashes = regexp.MustCompile(`[\-]+`) 17 | ) 18 | 19 | // MakeUserDir makes the user directory according to settings. 20 | func (settings *Settings) MakeUserDir(username, userScope, serverRoot string) (string, error) { 21 | var err error 22 | userScope = strings.TrimSpace(userScope) 23 | if userScope == "" || userScope == "./" { 24 | userScope = "." 25 | } 26 | 27 | if !settings.CreateUserDir { 28 | return userScope, nil 29 | } 30 | 31 | fs := afero.NewBasePathFs(afero.NewOsFs(), serverRoot) 32 | 33 | // Use the default auto create logic only if specific scope is not the default scope 34 | if userScope != settings.Defaults.Scope { 35 | // Try create the dir, for example: settings.Defaults.Scope == "." and userScope == "./foo" 36 | if userScope != "." { 37 | err = fs.MkdirAll(userScope, os.ModePerm) 38 | if err != nil { 39 | log.Printf("create user: failed to mkdir user home dir: [%s]", userScope) 40 | } 41 | } 42 | return userScope, err 43 | } 44 | 45 | // Clean username first 46 | username = cleanUsername(username) 47 | if username == "" || username == "-" || username == "." { 48 | log.Printf("create user: invalid user for home dir creation: [%s]", username) 49 | return "", errors.New("invalid user for home dir creation") 50 | } 51 | 52 | // Create default user dir 53 | userHomeBase := settings.Defaults.Scope + string(os.PathSeparator) + "users" 54 | userHome := userHomeBase + string(os.PathSeparator) + username 55 | err = fs.MkdirAll(userHome, os.ModePerm) 56 | if err != nil { 57 | log.Printf("create user: failed to mkdir user home dir: [%s]", userHome) 58 | } else { 59 | log.Printf("create user: mkdir user home dir: [%s] successfully.", userHome) 60 | } 61 | return userHome, err 62 | } 63 | 64 | func cleanUsername(s string) string { 65 | // Remove any trailing space to avoid ending on - 66 | s = strings.Trim(s, " ") 67 | s = strings.Replace(s, "..", "", -1) 68 | 69 | // Replace all characters which not in the list `0-9A-Za-z@_\-.` with a dash 70 | s = invalidFilenameChars.ReplaceAllString(s, "-") 71 | 72 | // Remove any multiple dashes caused by replacements above 73 | s = dashes.ReplaceAllString(s, "-") 74 | return s 75 | } 76 | -------------------------------------------------------------------------------- /settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "crypto/rand" 5 | "strings" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/rules" 8 | ) 9 | 10 | // AuthMethod describes an authentication method. 11 | type AuthMethod string 12 | 13 | // Settings contain the main settings of the application. 14 | type Settings struct { 15 | Key []byte `json:"key"` 16 | Signup bool `json:"signup"` 17 | CreateUserDir bool `json:"createUserDir"` 18 | Defaults UserDefaults `json:"defaults"` 19 | AuthMethod AuthMethod `json:"authMethod"` 20 | Branding Branding `json:"branding"` 21 | Commands map[string][]string `json:"commands"` 22 | Shell []string `json:"shell"` 23 | Rules []rules.Rule `json:"rules"` 24 | } 25 | 26 | // GetRules implements rules.Provider. 27 | func (s *Settings) GetRules() []rules.Rule { 28 | return s.Rules 29 | } 30 | 31 | // Server specific settings. 32 | type Server struct { 33 | Root string `json:"root"` 34 | BaseURL string `json:"baseURL"` 35 | Socket string `json:"socket"` 36 | TLSKey string `json:"tlsKey"` 37 | TLSCert string `json:"tlsCert"` 38 | Port string `json:"port"` 39 | Address string `json:"address"` 40 | Log string `json:"log"` 41 | } 42 | 43 | // Clean cleans any variables that might need cleaning. 44 | func (s *Server) Clean() { 45 | s.BaseURL = strings.TrimSuffix(s.BaseURL, "/") 46 | } 47 | 48 | // GenerateKey generates a key of 256 bits. 49 | func GenerateKey() ([]byte, error) { 50 | b := make([]byte, 64) 51 | _, err := rand.Read(b) 52 | // Note that err == nil only if we read len(b) bytes. 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return b, nil 58 | } 59 | -------------------------------------------------------------------------------- /settings/storage.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/dev-techmoe/filebrowser/v2/errors" 5 | "github.com/dev-techmoe/filebrowser/v2/rules" 6 | "github.com/dev-techmoe/filebrowser/v2/users" 7 | ) 8 | 9 | // StorageBackend is a settings storage backend. 10 | type StorageBackend interface { 11 | Get() (*Settings, error) 12 | Save(*Settings) error 13 | GetServer() (*Server, error) 14 | SaveServer(*Server) error 15 | } 16 | 17 | // Storage is a settings storage. 18 | type Storage struct { 19 | back StorageBackend 20 | } 21 | 22 | // NewStorage creates a settings storage from a backend. 23 | func NewStorage(back StorageBackend) *Storage { 24 | return &Storage{back: back} 25 | } 26 | 27 | // Get returns the settings for the current instance. 28 | func (s *Storage) Get() (*Settings, error) { 29 | return s.back.Get() 30 | } 31 | 32 | var defaultEvents = []string{ 33 | "save", 34 | "copy", 35 | "rename", 36 | "upload", 37 | "delete", 38 | } 39 | 40 | // Save saves the settings for the current instance. 41 | func (s *Storage) Save(set *Settings) error { 42 | if len(set.Key) == 0 { 43 | return errors.ErrEmptyKey 44 | } 45 | 46 | if set.Defaults.Locale == "" { 47 | set.Defaults.Locale = "en" 48 | } 49 | 50 | if set.Defaults.Commands == nil { 51 | set.Defaults.Commands = []string{} 52 | } 53 | 54 | if set.Defaults.ViewMode == "" { 55 | set.Defaults.ViewMode = users.MosaicViewMode 56 | } 57 | 58 | if set.Rules == nil { 59 | set.Rules = []rules.Rule{} 60 | } 61 | 62 | if set.Shell == nil { 63 | set.Shell = []string{} 64 | } 65 | 66 | if set.Commands == nil { 67 | set.Commands = map[string][]string{} 68 | } 69 | 70 | for _, event := range defaultEvents { 71 | if _, ok := set.Commands["before_"+event]; !ok { 72 | set.Commands["before_"+event] = []string{} 73 | } 74 | 75 | if _, ok := set.Commands["after_"+event]; !ok { 76 | set.Commands["after_"+event] = []string{} 77 | } 78 | } 79 | 80 | err := s.back.Save(set) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // GetServer wraps StorageBackend.GetServer. 89 | func (s *Storage) GetServer() (*Server, error) { 90 | return s.back.GetServer() 91 | } 92 | 93 | // SaveServer wraps StorageBackend.SaveServer and adds some verification. 94 | func (s *Storage) SaveServer(ser *Server) error { 95 | ser.Clean() 96 | return s.back.SaveServer(ser) 97 | } 98 | -------------------------------------------------------------------------------- /share/share.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | // Link is the information needed to build a shareable link. 4 | type Link struct { 5 | Hash string `json:"hash" storm:"id,index"` 6 | Path string `json:"path" storm:"index"` 7 | UserID uint `json:"userID"` 8 | Expire int64 `json:"expire"` 9 | } 10 | -------------------------------------------------------------------------------- /share/storage.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dev-techmoe/filebrowser/v2/errors" 7 | ) 8 | 9 | // StorageBackend is the interface to implement for a share storage. 10 | type StorageBackend interface { 11 | GetByHash(hash string) (*Link, error) 12 | GetPermanent(path string, id uint) (*Link, error) 13 | Gets(path string, id uint) ([]*Link, error) 14 | Save(s *Link) error 15 | Delete(hash string) error 16 | } 17 | 18 | // Storage is a storage. 19 | type Storage struct { 20 | back StorageBackend 21 | } 22 | 23 | // NewStorage creates a share links storage from a backend. 24 | func NewStorage(back StorageBackend) *Storage { 25 | return &Storage{back: back} 26 | } 27 | 28 | // GetByHash wraps a StorageBackend.GetByHash. 29 | func (s *Storage) GetByHash(hash string) (*Link, error) { 30 | link, err := s.back.GetByHash(hash) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if link.Expire != 0 && link.Expire <= time.Now().Unix() { 36 | s.Delete(link.Hash) 37 | return nil, errors.ErrNotExist 38 | } 39 | 40 | return link, nil 41 | } 42 | 43 | // GetPermanent wraps a StorageBackend.GetPermanent 44 | func (s *Storage) GetPermanent(path string, id uint) (*Link, error) { 45 | return s.back.GetPermanent(path, id) 46 | } 47 | 48 | // Gets wraps a StorageBackend.Gets 49 | func (s *Storage) Gets(path string, id uint) ([]*Link, error) { 50 | links, err := s.back.Gets(path, id) 51 | 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | for i, link := range links { 57 | if link.Expire != 0 && link.Expire <= time.Now().Unix() { 58 | s.Delete(link.Hash) 59 | links = append(links[:i], links[i+1:]...) 60 | } 61 | } 62 | 63 | return links, nil 64 | } 65 | 66 | // Save wraps a StorageBackend.Save 67 | func (s *Storage) Save(l *Link) error { 68 | return s.back.Save(l) 69 | } 70 | 71 | // Delete wraps a StorageBackend.Delete 72 | func (s *Storage) Delete(hash string) error { 73 | return s.back.Delete(hash) 74 | } 75 | -------------------------------------------------------------------------------- /storage/bolt/auth.go: -------------------------------------------------------------------------------- 1 | package bolt 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | "github.com/dev-techmoe/filebrowser/v2/auth" 6 | "github.com/dev-techmoe/filebrowser/v2/errors" 7 | "github.com/dev-techmoe/filebrowser/v2/settings" 8 | ) 9 | 10 | type authBackend struct { 11 | db *storm.DB 12 | } 13 | 14 | func (s authBackend) Get(t settings.AuthMethod) (auth.Auther, error) { 15 | var auther auth.Auther 16 | 17 | switch t { 18 | case auth.MethodJSONAuth: 19 | auther = &auth.JSONAuth{} 20 | case auth.MethodProxyAuth: 21 | auther = &auth.ProxyAuth{} 22 | case auth.MethodNoAuth: 23 | auther = &auth.NoAuth{} 24 | default: 25 | return nil, errors.ErrInvalidAuthMethod 26 | } 27 | 28 | return auther, get(s.db, "auther", auther) 29 | } 30 | 31 | func (s authBackend) Save(a auth.Auther) error { 32 | return save(s.db, "auther", a) 33 | } 34 | -------------------------------------------------------------------------------- /storage/bolt/bolt.go: -------------------------------------------------------------------------------- 1 | package bolt 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | "github.com/dev-techmoe/filebrowser/v2/auth" 6 | "github.com/dev-techmoe/filebrowser/v2/settings" 7 | "github.com/dev-techmoe/filebrowser/v2/share" 8 | "github.com/dev-techmoe/filebrowser/v2/storage" 9 | "github.com/dev-techmoe/filebrowser/v2/users" 10 | ) 11 | 12 | // NewStorage creates a storage.Storage based on Bolt DB. 13 | func NewStorage(db *storm.DB) (*storage.Storage, error) { 14 | users := users.NewStorage(usersBackend{db: db}) 15 | share := share.NewStorage(shareBackend{db: db}) 16 | settings := settings.NewStorage(settingsBackend{db: db}) 17 | auth := auth.NewStorage(authBackend{db: db}, users) 18 | 19 | err := save(db, "version", 2) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return &storage.Storage{ 25 | Auth: auth, 26 | Users: users, 27 | Share: share, 28 | Settings: settings, 29 | }, nil 30 | } 31 | -------------------------------------------------------------------------------- /storage/bolt/config.go: -------------------------------------------------------------------------------- 1 | package bolt 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | "github.com/dev-techmoe/filebrowser/v2/settings" 6 | ) 7 | 8 | type settingsBackend struct { 9 | db *storm.DB 10 | } 11 | 12 | func (s settingsBackend) Get() (*settings.Settings, error) { 13 | settings := &settings.Settings{} 14 | return settings, get(s.db, "settings", settings) 15 | } 16 | 17 | func (s settingsBackend) Save(settings *settings.Settings) error { 18 | return save(s.db, "settings", settings) 19 | } 20 | 21 | func (s settingsBackend) GetServer() (*settings.Server, error) { 22 | server := &settings.Server{} 23 | return server, get(s.db, "server", server) 24 | } 25 | 26 | func (s settingsBackend) SaveServer(server *settings.Server) error { 27 | return save(s.db, "server", server) 28 | } 29 | -------------------------------------------------------------------------------- /storage/bolt/importer/importer.go: -------------------------------------------------------------------------------- 1 | package importer 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | "github.com/dev-techmoe/filebrowser/v2/storage/bolt" 6 | ) 7 | 8 | // Import imports an old configuration to a newer database. 9 | func Import(oldDB, oldConf, newDB string) error { 10 | old, err := storm.Open(oldDB) 11 | if err != nil { 12 | return err 13 | } 14 | defer old.Close() 15 | 16 | new, err := storm.Open(newDB) 17 | if err != nil { 18 | return err 19 | } 20 | defer new.Close() 21 | 22 | sto, err := bolt.NewStorage(new) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | err = importUsers(old, sto) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | err = importConf(old, oldConf, sto) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /storage/bolt/share.go: -------------------------------------------------------------------------------- 1 | package bolt 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | "github.com/asdine/storm/q" 6 | "github.com/dev-techmoe/filebrowser/v2/errors" 7 | "github.com/dev-techmoe/filebrowser/v2/share" 8 | ) 9 | 10 | type shareBackend struct { 11 | db *storm.DB 12 | } 13 | 14 | func (s shareBackend) GetByHash(hash string) (*share.Link, error) { 15 | var v share.Link 16 | err := s.db.One("Hash", hash, &v) 17 | if err == storm.ErrNotFound { 18 | return nil, errors.ErrNotExist 19 | } 20 | 21 | return &v, err 22 | } 23 | 24 | func (s shareBackend) GetPermanent(path string, id uint) (*share.Link, error) { 25 | var v share.Link 26 | err := s.db.Select(q.Eq("Path", path), q.Eq("Expire", 0), q.Eq("UserID", id)).First(&v) 27 | if err == storm.ErrNotFound { 28 | return nil, errors.ErrNotExist 29 | } 30 | 31 | return &v, err 32 | } 33 | 34 | func (s shareBackend) Gets(path string, id uint) ([]*share.Link, error) { 35 | var v []*share.Link 36 | err := s.db.Select(q.Eq("Path", path), q.Eq("UserID", id)).Find(&v) 37 | if err == storm.ErrNotFound { 38 | return v, errors.ErrNotExist 39 | } 40 | 41 | return v, err 42 | } 43 | 44 | func (s shareBackend) Save(l *share.Link) error { 45 | return s.db.Save(l) 46 | } 47 | 48 | func (s shareBackend) Delete(hash string) error { 49 | return s.db.DeleteStruct(&share.Link{Hash: hash}) 50 | } 51 | -------------------------------------------------------------------------------- /storage/bolt/users.go: -------------------------------------------------------------------------------- 1 | package bolt 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/asdine/storm" 7 | "github.com/dev-techmoe/filebrowser/v2/errors" 8 | "github.com/dev-techmoe/filebrowser/v2/users" 9 | ) 10 | 11 | type usersBackend struct { 12 | db *storm.DB 13 | } 14 | 15 | func (st usersBackend) GetBy(i interface{}) (user *users.User, err error) { 16 | user = &users.User{} 17 | 18 | var arg string 19 | switch i.(type) { 20 | case uint: 21 | arg = "ID" 22 | case string: 23 | arg = "Username" 24 | default: 25 | return nil, errors.ErrInvalidDataType 26 | } 27 | 28 | err = st.db.One(arg, i, user) 29 | 30 | if err != nil { 31 | if err == storm.ErrNotFound { 32 | return nil, errors.ErrNotExist 33 | } 34 | return nil, err 35 | } 36 | 37 | return 38 | } 39 | 40 | func (st usersBackend) Gets() ([]*users.User, error) { 41 | users := []*users.User{} 42 | err := st.db.All(&users) 43 | if err == storm.ErrNotFound { 44 | return nil, errors.ErrNotExist 45 | } 46 | 47 | if err != nil { 48 | return users, err 49 | } 50 | 51 | return users, err 52 | } 53 | 54 | func (st usersBackend) Update(user *users.User, fields ...string) error { 55 | if len(fields) == 0 { 56 | return st.Save(user) 57 | } 58 | 59 | for _, field := range fields { 60 | val := reflect.ValueOf(user).Elem().FieldByName(field).Interface() 61 | if err := st.db.UpdateField(user, field, val); err != nil { 62 | return err 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (st usersBackend) Save(user *users.User) error { 70 | err := st.db.Save(user) 71 | if err == storm.ErrAlreadyExists { 72 | return errors.ErrExist 73 | } 74 | return err 75 | } 76 | 77 | func (st usersBackend) DeleteByID(id uint) error { 78 | return st.db.DeleteStruct(&users.User{ID: id}) 79 | } 80 | 81 | func (st usersBackend) DeleteByUsername(username string) error { 82 | user, err := st.GetBy(username) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | return st.db.DeleteStruct(user) 88 | } 89 | -------------------------------------------------------------------------------- /storage/bolt/utils.go: -------------------------------------------------------------------------------- 1 | package bolt 2 | 3 | import ( 4 | "github.com/asdine/storm" 5 | "github.com/dev-techmoe/filebrowser/v2/errors" 6 | ) 7 | 8 | func get(db *storm.DB, name string, to interface{}) error { 9 | err := db.Get("config", name, to) 10 | if err == storm.ErrNotFound { 11 | return errors.ErrNotExist 12 | } 13 | 14 | return err 15 | } 16 | 17 | func save(db *storm.DB, name string, from interface{}) error { 18 | return db.Set("config", name, from) 19 | } 20 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/dev-techmoe/filebrowser/v2/auth" 5 | "github.com/dev-techmoe/filebrowser/v2/settings" 6 | "github.com/dev-techmoe/filebrowser/v2/share" 7 | "github.com/dev-techmoe/filebrowser/v2/users" 8 | ) 9 | 10 | // Storage is a storage powered by a Backend whih makes the neccessary 11 | // verifications when fetching and saving data to ensure consistency. 12 | type Storage struct { 13 | Users *users.Storage 14 | Share *share.Storage 15 | Auth *auth.Storage 16 | Settings *settings.Storage 17 | } 18 | -------------------------------------------------------------------------------- /users/password.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "golang.org/x/crypto/bcrypt" 5 | ) 6 | 7 | // HashPwd hashes a password. 8 | func HashPwd(password string) (string, error) { 9 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 10 | return string(bytes), err 11 | } 12 | 13 | // CheckPwd checks if a password is correct. 14 | func CheckPwd(password, hash string) bool { 15 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 16 | return err == nil 17 | } 18 | -------------------------------------------------------------------------------- /users/permissions.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | // Permissions describe a user's permissions. 4 | type Permissions struct { 5 | Admin bool `json:"admin"` 6 | Execute bool `json:"execute"` 7 | Create bool `json:"create"` 8 | Rename bool `json:"rename"` 9 | Modify bool `json:"modify"` 10 | Delete bool `json:"delete"` 11 | Share bool `json:"share"` 12 | Download bool `json:"download"` 13 | } 14 | -------------------------------------------------------------------------------- /users/storage.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/errors" 8 | ) 9 | 10 | // StorageBackend is the interface to implement for a users storage. 11 | type StorageBackend interface { 12 | GetBy(interface{}) (*User, error) 13 | Gets() ([]*User, error) 14 | Save(u *User) error 15 | Update(u *User, fields ...string) error 16 | DeleteByID(uint) error 17 | DeleteByUsername(string) error 18 | } 19 | 20 | // Storage is a users storage. 21 | type Storage struct { 22 | back StorageBackend 23 | updated map[uint]int64 24 | mux sync.RWMutex 25 | } 26 | 27 | // NewStorage creates a users storage from a backend. 28 | func NewStorage(back StorageBackend) *Storage { 29 | return &Storage{ 30 | back: back, 31 | updated: map[uint]int64{}, 32 | } 33 | } 34 | 35 | // Get allows you to get a user by its name or username. The provided 36 | // id must be a string for username lookup or a uint for id lookup. If id 37 | // is neither, a ErrInvalidDataType will be returned. 38 | func (s *Storage) Get(baseScope string, id interface{}) (user *User, err error) { 39 | user, err = s.back.GetBy(id) 40 | if err != nil { 41 | return 42 | } 43 | user.Clean(baseScope) 44 | return 45 | } 46 | 47 | // Gets gets a list of all users. 48 | func (s *Storage) Gets(baseScope string) ([]*User, error) { 49 | users, err := s.back.Gets() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | for _, user := range users { 55 | user.Clean(baseScope) 56 | } 57 | 58 | return users, err 59 | } 60 | 61 | // Update updates a user in the database. 62 | func (s *Storage) Update(user *User, fields ...string) error { 63 | err := user.Clean("", fields...) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | err = s.back.Update(user, fields...) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | s.mux.Lock() 74 | s.updated[user.ID] = time.Now().Unix() 75 | s.mux.Unlock() 76 | return nil 77 | } 78 | 79 | // Save saves the user in a storage. 80 | func (s *Storage) Save(user *User) error { 81 | if err := user.Clean(""); err != nil { 82 | return err 83 | } 84 | 85 | return s.back.Save(user) 86 | } 87 | 88 | // Delete allows you to delete a user by its name or username. The provided 89 | // id must be a string for username lookup or a uint for id lookup. If id 90 | // is neither, a ErrInvalidDataType will be returned. 91 | func (s *Storage) Delete(id interface{}) (err error) { 92 | switch id := id.(type) { 93 | case string: 94 | err = s.back.DeleteByUsername(id) 95 | case uint: 96 | err = s.back.DeleteByID(id) 97 | default: 98 | err = errors.ErrInvalidDataType 99 | } 100 | 101 | return 102 | } 103 | 104 | // LastUpdate gets the timestamp for the last update of an user. 105 | func (s *Storage) LastUpdate(id uint) int64 { 106 | s.mux.RLock() 107 | defer s.mux.RUnlock() 108 | if val, ok := s.updated[id]; ok { 109 | return val 110 | } 111 | return 0 112 | } 113 | -------------------------------------------------------------------------------- /users/users.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "path/filepath" 5 | "regexp" 6 | 7 | "github.com/dev-techmoe/filebrowser/v2/errors" 8 | 9 | "github.com/dev-techmoe/filebrowser/v2/files" 10 | "github.com/dev-techmoe/filebrowser/v2/rules" 11 | "github.com/spf13/afero" 12 | ) 13 | 14 | // ViewMode describes a view mode. 15 | type ViewMode string 16 | 17 | const ( 18 | ListViewMode ViewMode = "list" 19 | MosaicViewMode ViewMode = "mosaic" 20 | ) 21 | 22 | // User describes a user. 23 | type User struct { 24 | ID uint `storm:"id,increment" json:"id"` 25 | Username string `storm:"unique" json:"username"` 26 | Password string `json:"password"` 27 | Scope string `json:"scope"` 28 | Locale string `json:"locale"` 29 | LockPassword bool `json:"lockPassword"` 30 | ViewMode ViewMode `json:"viewMode"` 31 | Perm Permissions `json:"perm"` 32 | Commands []string `json:"commands"` 33 | Sorting files.Sorting `json:"sorting"` 34 | Fs afero.Fs `json:"-" yaml:"-"` 35 | Rules []rules.Rule `json:"rules"` 36 | } 37 | 38 | // GetRules implements rules.Provider. 39 | func (u *User) GetRules() []rules.Rule { 40 | return u.Rules 41 | } 42 | 43 | var checkableFields = []string{ 44 | "Username", 45 | "Password", 46 | "Scope", 47 | "ViewMode", 48 | "Commands", 49 | "Sorting", 50 | "Rules", 51 | } 52 | 53 | // Clean cleans up a user and verifies if all its fields 54 | // are alright to be saved. 55 | func (u *User) Clean(baseScope string, fields ...string) error { 56 | if len(fields) == 0 { 57 | fields = checkableFields 58 | } 59 | 60 | for _, field := range fields { 61 | switch field { 62 | case "Username": 63 | if u.Username == "" { 64 | return errors.ErrEmptyUsername 65 | } 66 | case "Password": 67 | if u.Password == "" { 68 | return errors.ErrEmptyPassword 69 | } 70 | case "ViewMode": 71 | if u.ViewMode == "" { 72 | u.ViewMode = ListViewMode 73 | } 74 | case "Commands": 75 | if u.Commands == nil { 76 | u.Commands = []string{} 77 | } 78 | case "Sorting": 79 | if u.Sorting.By == "" { 80 | u.Sorting.By = "name" 81 | } 82 | case "Rules": 83 | if u.Rules == nil { 84 | u.Rules = []rules.Rule{} 85 | } 86 | } 87 | } 88 | 89 | if u.Fs == nil { 90 | scope := u.Scope 91 | 92 | if !filepath.IsAbs(scope) { 93 | scope = filepath.Join(baseScope, scope) 94 | } 95 | 96 | u.Fs = afero.NewBasePathFs(afero.NewOsFs(), scope) 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // FullPath gets the full path for a user's relative path. 103 | func (u *User) FullPath(path string) string { 104 | return afero.FullBaseFsPath(u.Fs.(*afero.BasePathFs), path) 105 | } 106 | 107 | // CanExecute checks if an user can execute a specific command. 108 | func (u *User) CanExecute(command string) bool { 109 | if !u.Perm.Execute { 110 | return false 111 | } 112 | 113 | for _, cmd := range u.Commands { 114 | if regexp.MustCompile(cmd).MatchString(command) { 115 | return true 116 | } 117 | } 118 | 119 | return false 120 | } 121 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | // Version is the current File Browser version. 5 | Version = "(untracked)" 6 | // CommitSHA is the commmit sha. 7 | CommitSHA = "(unknown)" 8 | ) 9 | -------------------------------------------------------------------------------- /wizard.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | untracked="(untracked)" 6 | REPO=$(cd $(dirname $0); pwd) 7 | COMMIT_SHA=$(git rev-parse --short HEAD) 8 | ASSETS="false" 9 | BINARY="false" 10 | RELEASE="" 11 | 12 | debugInfo () { 13 | echo "Repo: $REPO" 14 | echo "Build assets: $ASSETS" 15 | echo "Build binary: $BINARY" 16 | echo "Release: $RELEASE" 17 | } 18 | 19 | buildAssets () { 20 | cd $REPO 21 | rm -rf frontend/dist 22 | rm -f http/rice-box.go 23 | 24 | cd $REPO/frontend 25 | 26 | if [ "$CI" = "true" ]; then 27 | npm ci 28 | else 29 | npm install 30 | fi 31 | 32 | npm run lint 33 | npm run build 34 | } 35 | 36 | buildBinary () { 37 | if ! [ -x "$(command -v rice)" ]; then 38 | go install github.com/GeertJohan/go.rice/rice 39 | fi 40 | 41 | cd $REPO/http 42 | rm -rf rice-box.go 43 | rice embed-go 44 | 45 | cd $REPO 46 | go build -a -o filebrowser -ldflags "-s -w -X github.com/dev-techmoe/filebrowser/v2/version.CommitSHA=$COMMIT_SHA" 47 | } 48 | 49 | release () { 50 | cd $REPO 51 | 52 | echo "👀 Checking semver format" 53 | 54 | if [ $# -ne 1 ]; then 55 | echo "❌ This release script requires a single argument corresponding to the semver to be released. See semver.org" 56 | exit 1 57 | fi 58 | 59 | GREP="grep" 60 | if [ -x "$(command -v ggrep)" ]; then 61 | GREP="ggrep" 62 | fi 63 | 64 | semver=$(echo "$1" | $GREP -P '^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)') 65 | 66 | if [ $? -ne 0 ]; then 67 | echo "❌ Not valid semver format. See semver.org" 68 | exit 1 69 | fi 70 | 71 | echo "🧼 Tidying up go modules" 72 | go mod tidy 73 | 74 | echo "🐑 Creating a new commit for the new release" 75 | git commit --allow-empty -am "chore: version $semver" 76 | git tag "$1" 77 | git push 78 | git push --tags origin 79 | 80 | echo "📦 Done! $semver released." 81 | } 82 | 83 | usage() { 84 | echo "Usage: $0 [-a] [-c] [-b] [-r ]" 1>&2; 85 | exit 1; 86 | } 87 | 88 | DEBUG="false" 89 | 90 | while getopts "bacr:d" o; do 91 | case "${o}" in 92 | b) 93 | ASSETS="true" 94 | BINARY="true" 95 | ;; 96 | a) 97 | ASSETS="true" 98 | ;; 99 | c) 100 | BINARY="true" 101 | ;; 102 | r) 103 | RELEASE=${OPTARG} 104 | ;; 105 | d) 106 | DEBUG="true" 107 | ;; 108 | *) 109 | usage 110 | ;; 111 | esac 112 | done 113 | shift $((OPTIND-1)) 114 | 115 | if [ "$DEBUG" = "true" ]; then 116 | debugInfo 117 | fi 118 | 119 | if [ "$ASSETS" = "true" ]; then 120 | buildAssets 121 | fi 122 | 123 | if [ "$BINARY" = "true" ]; then 124 | buildBinary 125 | fi 126 | 127 | if [ "$RELEASE" != "" ]; then 128 | release $RELEASE 129 | fi 130 | --------------------------------------------------------------------------------