Compare commits

..

101 Commits

Author SHA1 Message Date
Oleg Lobanov
0d179eca4d chore(release): 2.14.0 2021-03-21 13:19:53 +01:00
Oleg Lobanov
dacd511d24 chore: run npm update 2021-03-21 13:05:22 +01:00
Oleg Lobanov
c44b37c50c chore: add prettier frontent linter 2021-03-21 12:51:58 +01:00
Oleg Lobanov
a721dc1f31 feat: add health check handler 2021-03-21 12:30:48 +01:00
Oleg Lobanov
d2e6d23741 Merge pull request #1339 from ramiresviana/fixes-7
Some fixes
2021-03-19 17:32:41 +01:00
Ramires Viana
5f4a0317ab fix: hide dotfile error on share 2021-03-18 18:24:24 +00:00
Ramires Viana
22f4be8f54 fix: qr code url on share 2021-03-18 13:10:10 +00:00
Ramires Viana
eeadc532fe fix: text file detection on editor 2021-03-17 18:06:56 +00:00
Ramires Viana
93a35ad251 fix: prefix handling on http router 2021-03-17 17:54:25 +00:00
Oleg Lobanov
99787287bb Merge pull request #1331 from ramiresviana/tweaks-2
Some development tweaks
2021-03-15 16:56:20 +01:00
Ramires Viana
bdd523190e chore: frontend DirFS for development 2021-03-15 14:06:21 +00:00
Ramires Viana
4c1dd5c097 chore: automatic output name on build 2021-03-15 14:00:23 +00:00
Oleg Lobanov
e1f658633d chore(release): 2.13.0 2021-03-14 20:02:02 +01:00
Oleg Lobanov
9c79105c02 chore: prevent deleting .gitignore from dist folder 2021-03-14 19:59:55 +01:00
Jürgen Hötzel
6d5ceae8b4 fix: wait for async command exit (#1326)
This prevents the accumulation of zombie processes when using
async (&) event commands. Also log async command failures.
2021-03-14 19:32:14 +01:00
Oleg Lobanov
381f09087a Merge pull request #1321 from ramiresviana/fixes-6 2021-03-14 14:19:50 +01:00
Ramires Viana
426b38bb33 fix: root path name on archive 2021-03-12 15:52:52 +00:00
Ramires Viana
488d98045e fix: download current dir on file listing 2021-03-12 15:28:49 +00:00
Ramires Viana
7955e0720b fix: encoded file path on share 2021-03-12 15:15:56 +00:00
Ramires Viana
e017a19985 fix: full file path on share 2021-03-12 12:14:58 +00:00
Ramires Viana
f8df76f526 fix: header dropdown icon color on previewer 2021-03-11 16:01:54 +00:00
Ramires Viana
11ebaec5f0 fix: modified time on info prompt 2021-03-11 15:20:37 +00:00
Ramires Viana
326b35a7ac fix: item dragging on file listing 2021-03-11 12:09:12 +00:00
Ramires Viana
5bf15548d0 fix: check rules on http resource handlers 2021-03-10 17:38:11 +00:00
Ramires Viana
6a734c0139 fix: stuck icon on header button 2021-03-10 15:32:10 +00:00
Ramires Viana
81b6f4d6f6 fix: update image cache when replacing 2021-03-10 15:14:01 +00:00
Ramires Viana
0b92d94570 chore: split POST method on resource http handler 2021-03-10 13:32:11 +00:00
Oleg Lobanov
fc5506179a refactor: migrate from rice to embed.FS 2021-03-09 19:09:32 +01:00
Oleg Lobanov
0fe34ad224 Merge pull request #1307 from ramiresviana/tweaks-1
Frontend code quality changes
2021-03-09 18:26:46 +01:00
Ramires Viana
54f35701a2 fix: archive contains parent path on Windows 2021-03-09 15:54:54 +00:00
FrzMtrsprt
a809404ce1 chore: update zh-cn.json (#1311) 2021-03-08 10:24:16 +01:00
Po Chen
fb32e44b47 chore: update zh-cn.json (#1309) 2021-03-07 15:41:05 +01:00
Oleg Lobanov
e9c0369062 chore(release): 2.12.1 2021-03-07 15:25:34 +01:00
Oleg Lobanov
7358b3fe31 fix: add missing default config into the docker image 2021-03-07 15:23:12 +01:00
Ramires Viana
2a1f759e9e chore: remove prompts events 2021-03-04 14:40:18 +00:00
Oleg Lobanov
2fccb8c367 chore(release): 2.12.0 2021-03-04 13:12:19 +01:00
Oleg Lobanov
e039d95192 chore: fix major docker tag name 2021-03-04 13:12:03 +01:00
Oleg Lobanov
0f96031d6f Merge pull request #1305 from filebrowser/github_actions 2021-03-04 11:39:30 +01:00
Oleg Lobanov
6214fc84fa chore: add github action badge 2021-03-04 11:35:52 +01:00
Oleg Lobanov
47578e02e3 ci: migrate to github actions 2021-03-04 11:35:52 +01:00
Oleg Lobanov
35a4379b67 chore: go mod tidy 2021-03-04 00:57:35 +01:00
Oleg Lobanov
23f84642e6 build: use make for building the project (#1304) 2021-03-04 00:10:08 +01:00
Ramires Viana
edb9e85efd chore: share view logic responsability 2021-03-03 17:46:37 +00:00
Oleg Lobanov
2d2c598fa6 feat: add homebrew tap 2021-03-03 16:25:03 +01:00
Oleg Lobanov
cf4836dc75 feat: build multi-arch docker images 2021-03-03 13:46:22 +01:00
Ramires Viana
d8306559fd chore: breadcrumbs component 2021-03-03 12:25:59 +00:00
WeidiDeng
e8c9d1c539 feat: added tiff files preview support (#1222) 2021-03-02 12:14:32 +01:00
Alvaro Aleman
d8f415f8ab feat: allow to password protect shares (#1252)
This changes allows to password protect shares. It works by:
* Allowing to optionally pass a password when creating a share
* If set, the password + salt that is configured via a new flag will be
  hashed via bcrypt and the hash stored together with the rest of the
  share
* Additionally, a random 96 byte long token gets generated and stored
  as part of the share
* When the backend retrieves an unauthenticated request for a share that
  has authentication configured, it will return a http 401
* The frontend detects this and will show a login prompt
* The actual download links are protected via an url arg that contains
  the previously generated token. This allows us to avoid buffering the
  download in the browser and allows pasting the link without breaking
  it
2021-03-02 12:00:18 +01:00
Ramires Viana
7b6579ac8a chore: files view dynamic component 2021-03-01 16:12:17 +00:00
Ramires Viana
057307181e chore: removed header buttons components 2021-03-01 13:41:35 +00:00
Ramires Viana
4fb832c042 feat: increased header button counter size 2021-03-01 12:57:39 +00:00
Ramires Viana
e503cb69f2 chore: files pages logic responsability 2021-02-26 15:10:21 +00:00
Ramires Viana
95811e99bc chore: header bar component 2021-02-25 18:37:07 +00:00
Ramires Viana
62fff5ca60 feat: larger previewer content 2021-02-22 16:01:13 +00:00
Ramires Viana
5b28aa0848 feat: improved settings navbar 2021-02-19 16:01:43 +00:00
Ramires Viana
db5aad8eb6 feat: dual pane settings view 2021-02-19 13:15:46 +00:00
Alvaro Aleman
977ec33918 chore: close preview on esc key press (#1288) 2021-02-16 17:15:04 +01:00
Ramires Viana
1819377897 feat: improved sharing prompt 2021-02-16 15:39:11 +00:00
niubility000
f1b7bd59f6 fix: double click to zoom pics in phone's browser (#1274)
fixed this : https://github.com/filebrowser/filebrowser/issues/1266
2021-02-08 09:54:40 +01:00
WeidiDeng
f3afd5cb79 fix: environmental variables not expanded in command (#1241) 2021-01-19 17:02:46 +01:00
Oleg Lobanov
e6a5bf116e chore: remove empty string from locales (closes #1245) 2021-01-19 16:56:06 +01:00
Oleg Lobanov
21b5a76fa7 chore: refactor search function 2021-01-12 19:14:23 +01:00
WeidiDeng
b6263eb607 chore: search by type with empty file name (#1228) 2021-01-12 18:08:23 +01:00
WeidiDeng
c8257e848e chore: move single click to user profile page (#1236)
Single clicks applies to FileList component.
2021-01-12 00:15:26 +01:00
叫我彭一凡
05bb7c8553 fix: fetch resource api once when sorting (closes #1172) (#1202) 2021-01-12 00:00:40 +01:00
WeidiDeng
b600b11415 feat: share management delete confirm (#1212) 2021-01-11 23:54:16 +01:00
Oleg Lobanov
019ce80fc5 fix: don't allow to remove root user 2021-01-11 22:33:36 +01:00
Adam Dobrawy
8cea2f75b3 chore: fix url to documentation in templates (#1231) 2021-01-07 11:48:53 +01:00
WeidiDeng
6914063853 feat: allow disabling file detections by reading header (#1175) 2021-01-07 11:30:17 +01:00
Oleg Lobanov
43e0d4a856 chore: fix translation files indent 2020-12-31 12:31:26 +01:00
Oleg Lobanov
066d8e8d6c chore: add ru translation 2020-12-31 12:26:49 +01:00
Oleg Lobanov
948e05c083 chore(release): 2.11.0 2020-12-28 17:37:19 +01:00
WeidiDeng
fb5b28d9cb feat: download shared subdirectory (#1184)
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
2020-12-28 17:35:29 +01:00
WeidiDeng
677bce376b feat: add sharing management (#1178) (closes #1000) 2020-12-24 19:02:28 +01:00
Alexis Lefebvre
8faa96f5e6 chore: fix typo costumize -> costumize (#1194) 2020-12-24 18:24:15 +01:00
WeidiDeng
f62806f6c9 fix: check user input to prevent permission elevation (#1196) (closes #1195) 2020-12-24 18:22:48 +01:00
Oleg Lobanov
58835b7e53 fix: move files between different volumes (closes #1177) 2020-12-24 17:50:27 +01:00
WeidiDeng
7a5298a755 fix: delete extra remove prefix (#1186)
Fix file actions within /files dir
2020-12-16 16:08:56 +01:00
WeidiDeng
bc4a6462ce chore: use command key to select multiple files on mac (#1183) 2020-12-12 14:09:50 +01:00
WeidiDeng
ac3673e111 fix: recaptcha race condition (#1176) 2020-12-08 11:26:29 +01:00
Oleg Lobanov
c746c1931d chore(release): 2.10.0 2020-11-24 12:00:10 +01:00
Oleg Lobanov
586d198d47 fix: fix hanging when reading a named pipe file (closes #1155) 2020-11-24 11:37:31 +01:00
Matt Doyle
9515ceeb42 feat: automatically jump to the next photo when deleting while previewing (#1143) 2020-11-23 19:08:14 +01:00
Julien Loir
e8b4e9af46 feat: add single click mode (#1139) 2020-11-23 19:06:37 +01:00
Tiger Nie
10e399b3c3 feat: add hide dotfiles param (#1148) 2020-11-20 11:51:28 +01:00
Oleg Lobanov
dcbc3286e2 Merge pull request #1133 from ramiresviana/fixes-5
Some fixes and shared view improvements
2020-11-04 17:58:18 +01:00
xufanglu
b185f9b56e chore: fix typo in config_init.go (#1131) 2020-11-04 17:47:36 +01:00
Ramires Viana
7096b3dab9 fix: empty folder in archive 2020-11-04 15:56:27 +00:00
Ramires Viana
36cacdf598 feat: shared item information 2020-11-04 15:56:27 +00:00
Ramires Viana
4e48ffc14d fix: previewer title overflow 2020-11-04 15:56:27 +00:00
Ramires Viana
e119bc55ea feat: shared folder file listing 2020-11-04 15:56:05 +00:00
Ramires Viana
1ce3068a99 fix: resource rename action invalid path 2020-11-03 12:30:56 +00:00
Liubomyr Piadyk
d562d1a60d chore: fix readme typo (#1128)
Commander runner -> Command runner
At other places it's referenced as Command runner.
2020-10-29 17:29:29 +01:00
Oleg Lobanov
9f858398ab chore(release): 2.9.0 2020-10-21 16:52:29 +02:00
Aiden McClelland
0ac80e8387 feat: support WKWebview custom protocol (#1113) 2020-10-21 16:41:09 +02:00
Hissy
0dca0b92d1 chore: update zh-cn and zh-tw (#1121) 2020-10-21 16:39:41 +02:00
Oleg Lobanov
c9b36ba32e Merge pull request #1124 from ramiresviana/fixes-4 2020-10-21 16:37:39 +02:00
Ramires Viana
f2c4e78381 fix: allow start from Windows explorer 2020-10-19 13:41:40 +00:00
Ramires Viana
05bff54b71 fix: preview case sensitive file extension 2020-10-19 13:25:09 +00:00
Ramires Viana
2bd163d92a fix: search missing path slash 2020-10-19 13:14:36 +00:00
Ramires Viana
5e27ba5c8c fix: file upload missing path slash 2020-10-19 13:11:26 +00:00
186 changed files with 27875 additions and 11916 deletions

View File

@@ -1,92 +0,0 @@
version: 2
jobs:
lint:
docker:
- image: golangci/golangci-lint:v1.27.0
steps:
- checkout
- run: golangci-lint run -v
build-node:
docker:
- image: circleci/node
steps:
- checkout
- run:
name: "Build"
command: ./wizard.sh -a
- run:
name: "Cleanup"
command: rm -rf frontend/node_modules
- persist_to_workspace:
root: .
paths:
- '*'
test:
docker:
- image: circleci/golang:1.15.2
steps:
- checkout
- run:
name: "Test"
command: go test ./...
build-go:
docker:
- image: circleci/golang:1.15.2
steps:
- attach_workspace:
at: '~/project'
- run:
name: "Compile"
command: GOOS=linux GOARCH=amd64 ./wizard.sh -c
- run:
name: "Cleanup"
command: |
rm -rf frontend/build
git checkout -- go.sum # TODO: why is it being changed?
- persist_to_workspace:
root: .
paths:
- '*'
release:
docker:
- image: circleci/golang:1.15.2
steps:
- attach_workspace:
at: '~/project'
- setup_remote_docker
- run: echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin
- run: curl -sL https://git.io/goreleaser | bash
- run: docker logout
workflows:
version: 2
build-workflow:
jobs:
- lint:
filters:
tags:
only: /.*/
- test:
filters:
tags:
only: /.*/
- build-node:
filters:
tags:
only: /.*/
- build-go:
filters:
tags:
only: /.*/
requires:
- build-node
- lint
- test
- release:
context: deploy
requires:
- build-go
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/

View File

@@ -5,4 +5,4 @@
"log": "stdout",
"database": "/database.db",
"root": "/srv"
}
}

View File

@@ -1,3 +1,3 @@
testdata/
.github/
**.git
*
!.docker.json
!filebrowser

5
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,5 @@
# These owners will be the default owners for everything in the repo.
# Unless a later match takes precedence, @o1egl will be requested for
# review when someone opens a pull request.
* @o1egl

66
.github/workflows/main.yaml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: main
on:
push:
branches:
- 'master'
tags:
- 'v*'
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.16
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm i -g commitlint
- run: make lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.16
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: make test
release:
runs-on: ubuntu-latest
needs: [lint, test]
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-go@v2
with:
go-version: 1.16
- uses: actions/setup-node@v2
with:
node-version: '14'
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build fronetend
run: make build-frontend
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}

5
.gitignore vendored
View File

@@ -5,11 +5,10 @@ _old
rice-box.go
.idea/
filebrowser
dist/
filebrowser.exe
.DS_Store
node_modules
/frontend/dist
# local env files
.env.local
@@ -28,3 +27,5 @@ yarn-error.log*
*.njsproj
*.sln
*.sw*
bin/
build/

View File

@@ -63,7 +63,6 @@ linters:
- goconst
- gocritic
- gocyclo
- gofmt
- goimports
- golint
- gomnd

View File

@@ -3,10 +3,6 @@ project_name: filebrowser
env:
- GO111MODULE=on
before:
hooks:
- go mod download
build:
env:
- CGO_ENABLED=0
@@ -19,10 +15,6 @@ build:
- linux
- windows
- freebsd
- netbsd
- openbsd
- dragonfly
- solaris
goarch:
- amd64
- 386
@@ -35,14 +27,8 @@ build:
ignore:
- goos: darwin
goarch: 386
- goos: openbsd
goarch: arm
- goos: freebsd
goarch: arm
- goos: netbsd
goarch: arm
- goos: solaris
goarch: arm
archives:
-
@@ -55,53 +41,106 @@ archives:
dockers:
-
dockerfile: Dockerfile
binaries:
- filebrowser
use_buildx: true
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/amd64"
goos: linux
goarch: amd64
goarm: ''
image_templates:
- "filebrowser/filebrowser:latest"
- "filebrowser/filebrowser:{{ .Tag }}"
- "filebrowser/filebrowser:v{{ .Major }}"
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
extra_files:
- .docker.json
-
dockerfile: Dockerfile
binaries:
- filebrowser
use_buildx: true
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm64"
goos: linux
goarch: arm64
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
extra_files:
- .docker.json
-
dockerfile: Dockerfile
use_buildx: true
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm/v6"
goos: linux
goarch: arm
goarm: '5'
goarm: '6'
image_templates:
- "filebrowser/filebrowser:pi"
- "filebrowser/filebrowser:{{ .Tag }}-pi"
- "filebrowser/filebrowser:v{{ .Major }}-pi"
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
extra_files:
- .docker.json
-
dockerfile: Dockerfile.alpine
binaries:
- filebrowser
dockerfile: Dockerfile
use_buildx: true
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.name={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/arm/v7"
goos: linux
goarch: amd64
goarm: ''
goarch: arm
goarm: '7'
image_templates:
- "filebrowser/filebrowser:alpine"
- "filebrowser/filebrowser:{{ .Tag }}-alpine"
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
extra_files:
- .docker.json
-
dockerfile: Dockerfile.debian
binaries:
- filebrowser
goos: linux
goarch: amd64
goarm: ''
docker_manifests:
- name_template: "filebrowser/filebrowser:latest"
image_templates:
- "filebrowser/filebrowser:debian"
- "filebrowser/filebrowser:{{ .Tag }}-debian"
- "filebrowser/filebrowser:v{{ .Major }}-debian"
extra_files:
- .docker.json
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
- name_template: "filebrowser/filebrowser:{{ .Tag }}"
image_templates:
- "filebrowser/filebrowser:{{ .Tag }}-amd64"
- "filebrowser/filebrowser:{{ .Tag }}-arm64"
- "filebrowser/filebrowser:{{ .Tag }}-armv6"
- "filebrowser/filebrowser:{{ .Tag }}-armv7"
- name_template: "filebrowser/filebrowser:v{{ .Major }}"
image_templates:
- "filebrowser/filebrowser:v{{ .Major }}-amd64"
- "filebrowser/filebrowser:v{{ .Major }}-arm64"
- "filebrowser/filebrowser:v{{ .Major }}-armv6"
- "filebrowser/filebrowser:v{{ .Major }}-armv7"
brews:
- name: filebrowser
tap:
owner: filebrowser
name: homebrew-tap
folder: Formula
homepage: https://filebrowser.org
commit_author:
name: FileBrowser Robot
email: robot@filebrowser.org
description: File Browser is a create-your-own-cloud-kind of software where you can install it on a server, direct it to a path and then access your files through a nice web interface
license: "MIT"

14
.versionrc Normal file
View File

@@ -0,0 +1,14 @@
{
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance improvements" },
{ "type": "revert", "section": "Reverts" },
{ "type": "refactor", "section": "Refactorings" },
{ "type": "build", "section": "Build" },
{ "type": "ci", "hidden": true },
{ "type": "test", "hidden": true },
{ "type": "chore", "hidden": true },
{ "type": "docs", "hidden": true }
]
}

View File

@@ -2,6 +2,135 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.14.0](https://github.com/filebrowser/filebrowser/compare/v2.13.0...v2.14.0) (2021-03-21)
### Features
* add health check handler ([a721dc1](https://github.com/filebrowser/filebrowser/commit/a721dc1f314732e60d331a1a7da97d06e0e8b613))
### Bug Fixes
* hide dotfile error on share ([5f4a031](https://github.com/filebrowser/filebrowser/commit/5f4a0317ab5685fe4a558df74e604c12e04a1c10))
* prefix handling on http router ([93a35ad](https://github.com/filebrowser/filebrowser/commit/93a35ad2516accdcb9735db509550979d01de2c3))
* qr code url on share ([22f4be8](https://github.com/filebrowser/filebrowser/commit/22f4be8f54162b7cf494177705ffb8b09117bd01))
* text file detection on editor ([eeadc53](https://github.com/filebrowser/filebrowser/commit/eeadc532fe6057969b3c1a4726f236851b154cfa))
## [2.13.0](https://github.com/filebrowser/filebrowser/compare/v2.12.1...v2.13.0) (2021-03-14)
### Features
* dual pane settings view ([db5aad8](https://github.com/filebrowser/filebrowser/commit/db5aad8eb679cfe1b1ace5142cf342951217f0f7))
* improved settings navbar ([5b28aa0](https://github.com/filebrowser/filebrowser/commit/5b28aa0848710b9d3ee02a2aa912856395f48bd2))
* improved sharing prompt ([1819377](https://github.com/filebrowser/filebrowser/commit/18193778971e27d18b5a35df8c2d0e2953b48111))
* increased header button counter size ([4fb832c](https://github.com/filebrowser/filebrowser/commit/4fb832c0422107e16f22b7aa928224f36de4978f))
* larger previewer content ([62fff5c](https://github.com/filebrowser/filebrowser/commit/62fff5ca60da1f887c1f95fa4808d3753596dab2))
### Bug Fixes
* archive contains parent path on Windows ([54f3570](https://github.com/filebrowser/filebrowser/commit/54f35701a2bd5cb7ec0628ca9789047072c073db))
* check rules on http resource handlers ([5bf1554](https://github.com/filebrowser/filebrowser/commit/5bf15548d0ad147acfad5000277531be2671f7ce))
* download current dir on file listing ([488d980](https://github.com/filebrowser/filebrowser/commit/488d98045e7476ed11e53c13d9498a9db3165bbc))
* encoded file path on share ([7955e07](https://github.com/filebrowser/filebrowser/commit/7955e0720baef3710106c7e69bbbf078d5489220))
* full file path on share ([e017a19](https://github.com/filebrowser/filebrowser/commit/e017a199850e19dd51b960ba59402c215fd8f1af))
* header dropdown icon color on previewer ([f8df76f](https://github.com/filebrowser/filebrowser/commit/f8df76f52684f10722ce123fec2c90e321ddf103))
* item dragging on file listing ([326b35a](https://github.com/filebrowser/filebrowser/commit/326b35a7ac7871afcdf892ca150349665b7f6379))
* modified time on info prompt ([11ebaec](https://github.com/filebrowser/filebrowser/commit/11ebaec5f0671ec02ebe55d4a73a514bce3a6713))
* root path name on archive ([426b38b](https://github.com/filebrowser/filebrowser/commit/426b38bb3362d2d477d0d8aa27d880664d537431))
* stuck icon on header button ([6a734c0](https://github.com/filebrowser/filebrowser/commit/6a734c01391b437c2842f5d97fb63f29a0017510))
* update image cache when replacing ([81b6f4d](https://github.com/filebrowser/filebrowser/commit/81b6f4d6f6a01886583016f61f4f1951a59f244d))
* wait for async command exit ([#1326](https://github.com/filebrowser/filebrowser/issues/1326)) ([6d5ceae](https://github.com/filebrowser/filebrowser/commit/6d5ceae8b454edd749b3b65c88aacc0a31ce9215))
### Refactorings
* migrate from rice to embed.FS ([fc55061](https://github.com/filebrowser/filebrowser/commit/fc5506179a64e9e2f57f7b6d6cce4b95f5ebc235))
### [2.12.1](https://github.com/filebrowser/filebrowser/compare/v2.12.0...v2.12.1) (2021-03-07)
### Bug Fixes
* add missing default config into the docker image ([7358b3f](https://github.com/filebrowser/filebrowser/commit/7358b3fe3178c20007b4b5ef5c03705badd538c4))
## [2.12.0](https://github.com/filebrowser/filebrowser/compare/v2.11.0...v2.12.0) (2021-03-04)
### Features
* add homebrew tap ([2d2c598](https://github.com/filebrowser/filebrowser/commit/2d2c598fa6bd1ecaf39c542182890c8dd9b1cad0))
* added tiff files preview support ([#1222](https://github.com/filebrowser/filebrowser/issues/1222)) ([e8c9d1c](https://github.com/filebrowser/filebrowser/commit/e8c9d1c53989b4b52f6fba2a8ac41ae612c03a7c))
* allow disabling file detections by reading header ([#1175](https://github.com/filebrowser/filebrowser/issues/1175)) ([6914063](https://github.com/filebrowser/filebrowser/commit/6914063853a8a3f3cecfa4b21f223820c2a0b7df))
* allow to password protect shares ([#1252](https://github.com/filebrowser/filebrowser/issues/1252)) ([d8f415f](https://github.com/filebrowser/filebrowser/commit/d8f415f8abd0c4301803bd968c54429dd3fe4b59))
* build multi-arch docker images ([cf4836d](https://github.com/filebrowser/filebrowser/commit/cf4836dc757ef79ad615179bb7a6c7bbd3b09c2c))
* share management delete confirm ([#1212](https://github.com/filebrowser/filebrowser/issues/1212)) ([b600b11](https://github.com/filebrowser/filebrowser/commit/b600b11415fd1fb90ff2f5136be95a9c737ae1cb))
### Bug Fixes
* don't allow to remove root user ([019ce80](https://github.com/filebrowser/filebrowser/commit/019ce80fc529a0437984fdc3d1ab6916f34dd594))
* double click to zoom pics in phone's browser ([#1274](https://github.com/filebrowser/filebrowser/issues/1274)) ([f1b7bd5](https://github.com/filebrowser/filebrowser/commit/f1b7bd59f67e719b7bfd203b0d7ec016fd21ab49))
* environmental variables not expanded in command ([#1241](https://github.com/filebrowser/filebrowser/issues/1241)) ([f3afd5c](https://github.com/filebrowser/filebrowser/commit/f3afd5cb79d6ad8b9cc8d54cb8fc2344b7c07d3d))
* fetch resource api once when sorting (closes [#1172](https://github.com/filebrowser/filebrowser/issues/1172)) ([#1202](https://github.com/filebrowser/filebrowser/issues/1202)) ([05bb7c8](https://github.com/filebrowser/filebrowser/commit/05bb7c85531349f3e9d1d8a523bb1243587b2ebc))
### Build
* use make for building the project ([#1304](https://github.com/filebrowser/filebrowser/issues/1304)) ([23f8464](https://github.com/filebrowser/filebrowser/commit/23f84642e6c1e07f89f98d2c1bb6fc9da36cc71c))
## [2.11.0](https://github.com/filebrowser/filebrowser/compare/v2.10.0...v2.11.0) (2020-12-28)
### Features
* add sharing management ([#1178](https://github.com/filebrowser/filebrowser/issues/1178)) (closes [#1000](https://github.com/filebrowser/filebrowser/issues/1000)) ([677bce3](https://github.com/filebrowser/filebrowser/commit/677bce376b024d9ff38f34e74243034fe5a1ec3c))
* download shared subdirectory ([#1184](https://github.com/filebrowser/filebrowser/issues/1184)) ([fb5b28d](https://github.com/filebrowser/filebrowser/commit/fb5b28d9cbdee10d38fcd719b9fd832121be58ef))
### Bug Fixes
* check user input to prevent permission elevation ([#1196](https://github.com/filebrowser/filebrowser/issues/1196)) (closes [#1195](https://github.com/filebrowser/filebrowser/issues/1195)) ([f62806f](https://github.com/filebrowser/filebrowser/commit/f62806f6c9e9c7f392d1b747d65b8fe40b313e89))
* delete extra remove prefix ([#1186](https://github.com/filebrowser/filebrowser/issues/1186)) ([7a5298a](https://github.com/filebrowser/filebrowser/commit/7a5298a7556f7dcc52f59b8ea76d040d3ddc3d12))
* move files between different volumes (closes [#1177](https://github.com/filebrowser/filebrowser/issues/1177)) ([58835b7](https://github.com/filebrowser/filebrowser/commit/58835b7e535cc96e1c8a5d85821c1545743ca757))
* recaptcha race condition ([#1176](https://github.com/filebrowser/filebrowser/issues/1176)) ([ac3673e](https://github.com/filebrowser/filebrowser/commit/ac3673e111afac6616af9650ca07028b6c27e6cd))
## [2.10.0](https://github.com/filebrowser/filebrowser/compare/v2.9.0...v2.10.0) (2020-11-24)
### Features
* add hide dotfiles param ([#1148](https://github.com/filebrowser/filebrowser/issues/1148)) ([10e399b](https://github.com/filebrowser/filebrowser/commit/10e399b3c3dbdcfb4465a9d4138e1da6bae0873d))
* add single click mode ([#1139](https://github.com/filebrowser/filebrowser/issues/1139)) ([e8b4e9a](https://github.com/filebrowser/filebrowser/commit/e8b4e9af46d6e99dbeb965dd9727d9ed017d52a2))
* automatically jump to the next photo when deleting while previewing ([#1143](https://github.com/filebrowser/filebrowser/issues/1143)) ([9515cee](https://github.com/filebrowser/filebrowser/commit/9515ceeb42e5ef5267400220a2082dec775e843d))
* shared folder file listing ([e119bc5](https://github.com/filebrowser/filebrowser/commit/e119bc55ea82cefcbcc0571650107dfd5d73f570))
* shared item information ([36cacdf](https://github.com/filebrowser/filebrowser/commit/36cacdf598e4e09f064c8ace0ca7a6c24b23028e))
### Bug Fixes
* empty folder in archive ([7096b3d](https://github.com/filebrowser/filebrowser/commit/7096b3dab92441981c9964e4a6175af0a255d2be))
* fix hanging when reading a named pipe file (closes [#1155](https://github.com/filebrowser/filebrowser/issues/1155)) ([586d198](https://github.com/filebrowser/filebrowser/commit/586d198d47b525eeccc6fe587573a3ad83adb4f6))
* previewer title overflow ([4e48ffc](https://github.com/filebrowser/filebrowser/commit/4e48ffc14d09dabeea12dc495144277db62b5b7d))
* resource rename action invalid path ([1ce3068](https://github.com/filebrowser/filebrowser/commit/1ce3068a99c80c153fd41359255d173bce6e79e8))
## [2.9.0](https://github.com/filebrowser/filebrowser/compare/v2.8.0...v2.9.0) (2020-10-21)
### Features
* support WKWebview custom protocol ([#1113](https://github.com/filebrowser/filebrowser/issues/1113)) ([0ac80e8](https://github.com/filebrowser/filebrowser/commit/0ac80e8387a69924284259bde448af2813d84ed1))
### Bug Fixes
* allow start from Windows explorer ([f2c4e78](https://github.com/filebrowser/filebrowser/commit/f2c4e78381610879eda5316d38a999c89df6c14a))
* file upload missing path slash ([5e27ba5](https://github.com/filebrowser/filebrowser/commit/5e27ba5c8c1be603c6ae7fec8de48e3532dea1f7))
* preview case sensitive file extension ([05bff54](https://github.com/filebrowser/filebrowser/commit/05bff54b71543fd232f1089c40504d0cbfd106be))
* search missing path slash ([2bd163d](https://github.com/filebrowser/filebrowser/commit/2bd163d92a856d65c8d4615e37898470c1edf2f4))
## [2.8.0](https://github.com/filebrowser/filebrowser/compare/v2.7.0...v2.8.0) (2020-10-05)

View File

@@ -1,10 +1,10 @@
FROM alpine:latest as alpine
RUN apk --update add ca-certificates
RUN apk --update add mailcap
FROM alpine:latest
RUN apk --update add ca-certificates \
mailcap \
curl
FROM scratch
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=alpine /etc/mime.types /etc/mime.types
HEALTHCHECK --start-period=2s --interval=5s --timeout=3s \
CMD curl -f http://localhost/health || exit 1
VOLUME /srv
EXPOSE 80
@@ -12,4 +12,4 @@ EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]
ENTRYPOINT [ "/filebrowser" ]

View File

@@ -1,11 +0,0 @@
FROM alpine:latest as alpine
RUN apk --update add ca-certificates
RUN apk --update add mailcap
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

View File

@@ -1,9 +0,0 @@
FROM debian:buster
VOLUME /srv
EXPOSE 80
COPY .docker.json /.filebrowser.json
COPY filebrowser /filebrowser
ENTRYPOINT [ "/filebrowser" ]

94
Makefile Normal file
View File

@@ -0,0 +1,94 @@
SHELL := /bin/bash
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BIN = $(BASE_PATH)/bin
PATH := $(BIN):$(PATH)
export PATH
# printing
V = 0
Q = $(if $(filter 1,$V),,@)
M = $(shell printf "\033[34;1m▶\033[0m")
GO = GOGC=off go
# go module
MODULE = $(shell env GO111MODULE=on $(GO) list -m)
DATE ?= $(shell date +%FT%T%z)
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
LDFLAGS += -X "$(MODULE)/varsion.Version=$(VERSION)" -X "$(MODULE)/varsion.CommitSHA=$(VERSION_HASH)"
# tools
$(BIN):
@mkdir -p $@
$(BIN)/%: | $(BIN) ; $(info $(M) installing $(PACKAGE))
$Q env GOBIN=$(BIN) $(GO) install $(PACKAGE)
GOLANGCI_LINT = $(BIN)/golangci-lint
$(BIN)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/cmd/golangci-lint@v1.37.1
GOIMPORTS = $(BIN)/goimports
$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@v0.1.0
## build: Build
.PHONY: build
build: | build-frontend build-backend ; $(info $(M) building)
## build-frontend: Build frontend
.PHONY: build-frontend
build-frontend: | ; $(info $(M) building frontend)
$Q cd frontend && npm ci && npm run build
## build-backend: Build backend
.PHONY: build-backend
build-backend: | ; $(info $(M) building backend)
$Q $(GO) build -ldflags '$(LDFLAGS)' -o .
## test: Run all tests
.PHONY: test
test: | test-frontend test-backend ; $(info $(M) running tests)
## test-frontend: Run frontend tests
.PHONY: test-frontend
test-frontend: | ; $(info $(M) running frontend tests)
## test-backend: Run backend tests
.PHONY: test-backend
test-backend: | ; $(info $(M) running backend tests)
$Q $(GO) test -v ./...
## lint: Lint
.PHONY: lint
lint: lint-frontend lint-backend lint-commits | ; $(info $(M) running all linters)
## lint-frontend: Lint frontend
.PHONY: lint-frontend
lint-frontend: | ; $(info $(M) running frontend linters)
$Q cd frontend && npm ci && npm run lint
## lint-backend: Lint backend
.PHONY: lint-backend
lint-backend: | $(GOLANGCI_LINT) ; $(info $(M) running backend linters)
$Q $(GOLANGCI_LINT) run
## lint-commits: Lint commits
.PHONY: lint-commits
lint-commits: | ; $(info $(M) running commitlint)
$Q ./scripts/commitlint.sh
## bump-version: Bump app version
.PHONY: bump-version
bump-version: | ; $(info $(M) creating a new release)
$Q ./scripts/bump_version.sh
## help: Show this help
.PHONY: help
help:
@sed -n 's/^## //p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' | sort

View File

@@ -4,7 +4,7 @@
![Preview](https://user-images.githubusercontent.com/5447088/50716739-ebd26700-107a-11e9-9817-14230c53efd2.gif)
[![Travis](https://img.shields.io/travis/com/filebrowser/filebrowser.svg?style=flat-square)](https://travis-ci.com/filebrowser/filebrowser)
[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/main.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser?style=flat-square)](https://goreportcard.com/report/github.com/filebrowser/filebrowser)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/filebrowser/filebrowser)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg?style=flat-square)](https://github.com/filebrowser/filebrowser/releases/latest)
@@ -24,7 +24,7 @@ For installation instructions please refer to our docs at [https://filebrowser.o
[Authentication Method](https://filebrowser.org/configuration/authentication-method) - You can change the way the user authenticates with the filebrowser server
[Commander Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
[Command Runner](https://filebrowser.org/configuration/command-runner) - The command runner is a feature that enables you to execute any shell command you want before or after a certain event.
[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want.

View File

@@ -9,7 +9,7 @@ import (
// Auther is the authentication interface.
type Auther interface {
// Auth is called to authenticate a request.
Auth(r *http.Request, s *users.Storage, root string) (*users.User, error)
Auth(r *http.Request, s users.Store, root string) (*users.User, error)
// LoginPage indicates if this auther needs a login page.
LoginPage() bool
}

View File

@@ -26,7 +26,7 @@ type JSONAuth struct {
}
// Auth authenticates the user via a json in content body.
func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
func (a JSONAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
var cred jsonCred
if r.Body == nil {
@@ -40,7 +40,7 @@ func (a JSONAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users
// If ReCaptcha is enabled, check the code.
if a.ReCaptcha != nil && len(a.ReCaptcha.Secret) > 0 {
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:shadow
ok, err := a.ReCaptcha.Ok(cred.ReCaptcha) //nolint:govet
if err != nil {
return nil, err

View File

@@ -14,7 +14,7 @@ const MethodNoAuth settings.AuthMethod = "noauth"
type NoAuth struct{}
// Auth uses authenticates user 1.
func (a NoAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
func (a NoAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
return sto.Get(root, uint(1))
}

View File

@@ -18,7 +18,7 @@ type ProxyAuth struct {
}
// Auth authenticates the user via an HTTP header.
func (a ProxyAuth) Auth(r *http.Request, sto *users.Storage, root string) (*users.User, error) {
func (a ProxyAuth) Auth(r *http.Request, sto users.Store, root string) (*users.User, error) {
username := r.Header.Get(a.Header)
user, err := sto.Get(root, username)
if err == errors.ErrNotExist {

View File

@@ -14,7 +14,7 @@ var cmdsAddCmd = &cobra.Command{
Use: "add <event> <command>",
Short: "Add a command to run on a specific event",
Long: `Add a command to run on a specific event.`,
Args: cobra.MinimumNArgs(2), //nolint:mnd
Args: cobra.MinimumNArgs(2), //nolint:gomnd
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)

View File

@@ -23,7 +23,7 @@ You can also specify an optional parameter (index_end) so
you can remove all commands from 'index' to 'index_end',
including 'index_end'.`,
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:mnd
if err := cobra.RangeArgs(2, 3)(cmd, args); err != nil { //nolint:gomnd
return err
}
@@ -43,7 +43,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[1])
checkErr(err)
f := i
if len(args) == 3 { //nolint:mnd
if len(args) == 3 { //nolint:gomnd
f, err = strconv.Atoi(args[2])
checkErr(err)
}

View File

@@ -145,6 +145,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)

View File

@@ -61,7 +61,7 @@ override the options.`,
fmt.Printf(`
Congratulations! You've set up your database to use with File Browser.
Now add your first user via 'filebrowser users new' and then you just
Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
printSettings(ser, s, auther)

View File

@@ -3,6 +3,7 @@ package cmd
import (
"crypto/tls"
"errors"
"io/fs"
"io/ioutil"
"log"
"net"
@@ -22,6 +23,7 @@ import (
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
"github.com/filebrowser/filebrowser/v2/settings"
@@ -35,6 +37,7 @@ var (
func init() {
cobra.OnInitialize(initConfig)
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
@@ -65,6 +68,7 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", false, "disables Command Runner feature")
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
}
var rootCmd = &cobra.Command{
@@ -150,12 +154,15 @@ user created with the credentials from options "username" and "password".`,
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
checkErr(err)
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
checkErr(err)
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
listener, err = tls.Listen("tcp", adr, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
checkErr(err)
default:
listener, err = net.Listen("tcp", adr) //nolint:shadow
listener, err = net.Listen("tcp", adr)
checkErr(err)
}
@@ -163,7 +170,12 @@ user created with the credentials from options "username" and "password".`,
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc)
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server)
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
if err != nil {
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
checkErr(err)
defer listener.Close()
@@ -242,6 +254,9 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
_, disableTypeDetectionByHeader := getParamB(flags, "disable-type-detection-by-header")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
_, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec
@@ -301,8 +316,9 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
Signup: false,
CreateUserDir: false,
Defaults: settings.UserDefaults{
Scope: ".",
Locale: "en",
Scope: ".",
Locale: "en",
SingleClick: false,
Perm: users.Permissions{
Admin: false,
Execute: true,

View File

@@ -44,7 +44,7 @@ including 'index_end'.`,
i, err := strconv.Atoi(args[0])
checkErr(err)
f := i
if len(args) == 2 { //nolint:mnd
if len(args) == 2 { //nolint:gomnd
f, err = strconv.Atoi(args[1])
checkErr(err)
}

View File

@@ -27,15 +27,16 @@ var usersCmd = &cobra.Command{
func printUsers(usrs []*users.User) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
fmt.Fprintln(w, "ID\tUsername\tScope\tLocale\tV. Mode\tS.Click\tAdmin\tExecute\tCreate\tRename\tModify\tDelete\tShare\tDownload\tPwd Lock")
for _, u := range usrs {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t%t\t\n",
u.ID,
u.Username,
u.Scope,
u.Locale,
u.ViewMode,
u.SingleClick,
u.Perm.Admin,
u.Perm.Execute,
u.Perm.Create,
@@ -75,6 +76,7 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.String("scope", ".", "scope for users")
flags.String("locale", "en", "locale for users")
flags.String("viewMode", string(users.ListViewMode), "view mode for users")
flags.Bool("singleClick", false, "use single clicks only")
}
func getViewMode(flags *pflag.FlagSet) users.ViewMode {
@@ -95,6 +97,8 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
defaults.Locale = mustGetString(flags, flag.Name)
case "viewMode":
defaults.ViewMode = getViewMode(flags)
case "singleClick":
defaults.SingleClick = mustGetBool(flags, flag.Name)
case "perm.admin":
defaults.Perm.Admin = mustGetBool(flags, flag.Name)
case "perm.execute":

View File

@@ -15,7 +15,7 @@ var usersAddCmd = &cobra.Command{
Use: "add <username> <password>",
Short: "Create a new user",
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2), //nolint:mnd
Args: cobra.ExactArgs(2), //nolint:gomnd
Run: python(func(cmd *cobra.Command, args []string, d pythonData) {
s, err := d.store.Settings.Get()
checkErr(err)

View File

@@ -67,7 +67,7 @@ list or set it to 0.`,
// with the new username. If there is, print an error and cancel the
// operation
if user.Username != onDB.Username {
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:shadow
if conflictuous, err := d.store.Users.Get("", user.Username); err == nil { //nolint:govet
checkErr(usernameConflictError(user.Username, conflictuous.ID, user.ID))
}
}

View File

@@ -41,17 +41,19 @@ options you want to change.`,
checkErr(err)
defaults := settings.UserDefaults{
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
Scope: user.Scope,
Locale: user.Locale,
ViewMode: user.ViewMode,
SingleClick: user.SingleClick,
Perm: user.Perm,
Sorting: user.Sorting,
Commands: user.Commands,
}
getUserDefaults(flags, &defaults, false)
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
user.SingleClick = defaults.SingleClick
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting

View File

@@ -72,7 +72,7 @@ func dbExists(path string) (bool, error) {
d := filepath.Dir(path)
_, err = os.Stat(d)
if os.IsNotExist(err) {
if err := os.MkdirAll(d, 0700); err != nil { //nolint:shadow
if err := os.MkdirAll(d, 0700); err != nil { //nolint:govet
return false, err
}
return false, nil

34
commitlint.config.js Normal file
View File

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

View File

@@ -17,4 +17,5 @@ var (
ErrPermissionDenied = errors.New("permission denied")
ErrInvalidRequestParams = errors.New("invalid request params")
ErrSourceIsParent = errors.New("source is parent")
ErrRootUserDeletion = errors.New("user with id 1 can't be deleted")
)

View File

@@ -38,15 +38,18 @@ type FileInfo struct {
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
}
// FileOptions are the options when getting a file info.
type FileOptions struct {
Fs afero.Fs
Path string
Modify bool
Expand bool
Checker rules.Checker
Fs afero.Fs
Path string
Modify bool
Expand bool
ReadHeader bool
Token string
Checker rules.Checker
}
// NewFileInfo creates a File object from a path and a given user. This File
@@ -71,17 +74,18 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
IsDir: info.IsDir(),
Size: info.Size(),
Extension: filepath.Ext(info.Name()),
Token: opts.Token,
}
if opts.Expand {
if file.IsDir {
if err := file.readListing(opts.Checker); err != nil { //nolint:shadow
if err := file.readListing(opts.Checker, opts.ReadHeader); err != nil { //nolint:govet
return nil, err
}
return file, nil
}
err = file.detectType(opts.Modify, true)
err = file.detectType(opts.Modify, true, true)
if err != nil {
return nil, err
}
@@ -134,11 +138,63 @@ func (i *FileInfo) Checksum(algo string) error {
//nolint:goconst
//TODO: use constants
func (i *FileInfo) detectType(modify, saveContent bool) error {
func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
if IsNamedPipe(i.Mode) {
i.Type = "blob"
return nil
}
// failing to detect the type should not return error.
// imagine the situation where a file in a dir with thousands
// of files couldn't be opened: we'd have immediately
// a 500 even though it doesn't matter. So we just log it.
mimetype := mime.TypeByExtension(i.Extension)
var buffer []byte
if readHeader {
buffer = i.readFirstBytes()
if mimetype == "" {
mimetype = http.DetectContentType(buffer)
}
}
switch {
case strings.HasPrefix(mimetype, "video"):
i.Type = "video"
i.detectSubtitles()
return nil
case strings.HasPrefix(mimetype, "audio"):
i.Type = "audio"
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
return nil
case (strings.HasPrefix(mimetype, "text") || (len(buffer) > 0 && !isBinary(buffer))) && i.Size <= 10*1024*1024: // 10 MB
i.Type = "text"
if !modify {
i.Type = "textImmutable"
}
if saveContent {
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(i.Path)
if err != nil {
return err
}
i.Content = string(content)
}
return nil
default:
i.Type = "blob"
}
return nil
}
func (i *FileInfo) readFirstBytes() []byte {
reader, err := i.Fs.Open(i.Path)
if err != nil {
log.Print(err)
@@ -155,44 +211,7 @@ func (i *FileInfo) detectType(modify, saveContent bool) error {
return nil
}
mimetype := mime.TypeByExtension(i.Extension)
if mimetype == "" {
mimetype = http.DetectContentType(buffer[:n])
}
switch {
case strings.HasPrefix(mimetype, "video"):
i.Type = "video"
i.detectSubtitles()
return nil
case strings.HasPrefix(mimetype, "audio"):
i.Type = "audio"
return nil
case strings.HasPrefix(mimetype, "image"):
i.Type = "image"
return nil
case isBinary(buffer[:n], n) || i.Size > 10*1024*1024: // 10 MB
i.Type = "blob"
return nil
default:
i.Type = "text"
if !modify {
i.Type = "textImmutable"
}
if saveContent {
afs := &afero.Afero{Fs: i.Fs}
content, err := afs.ReadFile(i.Path)
if err != nil {
return err
}
i.Content = string(content)
}
}
return nil
return buffer[:n]
}
func (i *FileInfo) detectSubtitles() {
@@ -211,7 +230,7 @@ func (i *FileInfo) detectSubtitles() {
}
}
func (i *FileInfo) readListing(checker rules.Checker) error {
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)
if err != nil {
@@ -232,9 +251,9 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
continue
}
if strings.HasPrefix(f.Mode().String(), "L") {
if IsSymlink(f.Mode()) {
// It's a symbolic link. We try to follow it. If it doesn't work,
// we stay with the link information instead if the target's.
// we stay with the link information instead of the target's.
info, err := i.Fs.Stat(fPath)
if err == nil {
f = info
@@ -257,7 +276,7 @@ func (i *FileInfo) readListing(checker rules.Checker) error {
} else {
listing.NumFiles++
err := file.detectType(true, false)
err := file.detectType(true, false, readHeader)
if err != nil {
return err
}

View File

@@ -1,10 +1,11 @@
package files
import (
"os"
"unicode/utf8"
)
func isBinary(content []byte, _ int) bool {
func isBinary(content []byte) bool {
maybeStr := string(content)
runeCnt := utf8.RuneCount(content)
runeIndex := 0
@@ -48,3 +49,11 @@ func isBinary(content []byte, _ int) bool {
}
return false
}
func IsNamedPipe(mode os.FileMode) bool {
return mode&os.ModeNamedPipe != 0
}
func IsSymlink(mode os.FileMode) bool {
return mode&os.ModeSymlink != 0
}

View File

@@ -9,6 +9,25 @@ import (
"github.com/spf13/afero"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(fs afero.Fs, src, dst string) error {
if fs.Rename(src, dst) == nil {
return nil
}
// fallback
err := CopyFile(fs, src, dst)
if err != nil {
_ = fs.Remove(dst)
return err
}
if err := fs.Remove(src); err != nil {
return err
}
return nil
}
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(fs afero.Fs, source, dest string) error {
@@ -39,14 +58,14 @@ func CopyFile(fs afero.Fs, source, dest string) error {
return err
}
// Copy the mode if the user can't
// open the file.
// Copy the mode
info, err := fs.Stat(source)
if err != nil {
err = fs.Chmod(dest, info.Mode())
if err != nil {
return err
}
return err
}
err = fs.Chmod(dest, info.Mode())
if err != nil {
return err
}
return nil

12
frontend/assets.go Normal file
View File

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

14
frontend/assets_dev.go Normal file
View File

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

View File

@@ -1,5 +1,3 @@
module.exports = {
presets: [
'@vue/app'
]
}
presets: ["@vue/app"],
};

4
frontend/dist/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

25292
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,15 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"watch": "vue-cli-service build --watch",
"lint": "vue-cli-service lint --fix"
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
"lint": "npx vue-cli-service lint --no-fix",
"fix": "npx vue-cli-service lint",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean"
},
"dependencies": {
"ace-builds": "^1.4.7",
"clipboard": "^2.0.4",
"core-js": "^3.9.1",
"js-base64": "^2.5.1",
"lodash.clonedeep": "^4.5.0",
"lodash.throttle": "^4.1.1",
@@ -19,6 +21,7 @@
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta",
"qrcode.vue": "^1.7.0",
"utif": "^3.1.0",
"vue": "^2.6.10",
"vue-i18n": "^8.15.3",
"vue-lazyload": "^1.3.3",
@@ -28,11 +31,14 @@
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.2",
"@vue/cli-plugin-eslint": "^4.1.1",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "^4.1.2",
"babel-eslint": "^10.0.3",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.1.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^6.2.2",
"prettier": "^2.2.1",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
@@ -42,7 +48,8 @@
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
"eslint:recommended",
"@vue/prettier"
],
"rules": {},
"parserOptions": {

View File

@@ -69,13 +69,16 @@ nav > div {
border-color: var(--divider);
}
#breadcrumbs {
.breadcrumbs {
border-color: var(--divider);
color: var(--textPrimary) !important;
}
#breadcrumbs span {
.breadcrumbs span {
color: var(--textPrimary) !important;
}
.breadcrumbs a:hover {
background-color: rgba(255, 255, 255, .1);
}
#listing .item {
background: var(--surfacePrimary);
@@ -114,13 +117,20 @@ nav > div {
background: var(--surfaceSecondary);
}
.dashboard #nav ul li {
color: var(--textSecondary);
}
.dashboard #nav ul li:hover {
background: var(--surfaceSecondary);
}
.card h3,
.dashboard #nav,
.dashboard p label {
color: var(--textPrimary);
}
.card#share ul li input,
.card#share ul li select,
.card#share input,
.card#share select,
.input {
background: var(--surfaceSecondary);
color: var(--textPrimary);
@@ -138,7 +148,7 @@ nav > div {
background: #147A41;
}
.dashboard #nav li,
.dashboard #nav .wrapper,
.collapsible {
border-color: var(--divider);
}
@@ -191,10 +201,11 @@ table th {
}
}
.share__box, .share__box__download {
background: var(--surfaceSecondary) !important;
.share__box {
background: var(--surfacePrimary) !important;
color: var(--textPrimary);
}
.share__box__download {
border-bottom-color: var(--divider);
.share__box__element {
border-top-color: var(--divider);
}

View File

@@ -6,18 +6,18 @@
<script>
export default {
name: 'app',
mounted () {
const loading = document.getElementById('loading')
loading.classList.add('done')
name: "app",
mounted() {
const loading = document.getElementById("loading");
loading.classList.add("done");
setTimeout(function () {
loading.parentNode.removeChild(loading)
}, 200)
}
}
loading.parentNode.removeChild(loading);
}, 200);
},
};
</script>
<style>
@import './css/styles.css';
@import "./css/styles.css";
</style>

View File

@@ -1,16 +1,16 @@
import { removePrefix } from './utils'
import { baseURL } from '@/utils/constants'
import store from '@/store'
import { removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
const ssl = (window.location.protocol === 'https:')
const protocol = (ssl ? 'wss:' : 'ws:')
const ssl = window.location.protocol === "https:";
const protocol = ssl ? "wss:" : "ws:";
export default function command(url, command, onmessage, onclose) {
url = removePrefix(url)
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`
url = removePrefix(url);
url = `${protocol}//${window.location.host}${baseURL}/api/command${url}?auth=${store.state.jwt}`;
let conn = new window.WebSocket(url)
conn.onopen = () => conn.send(command)
conn.onmessage = onmessage
conn.onclose = onclose
let conn = new window.WebSocket(url);
conn.onopen = () => conn.send(command);
conn.onmessage = onmessage;
conn.onclose = onclose;
}

View File

@@ -1,139 +1,156 @@
import { fetchURL, removePrefix } from './utils'
import { baseURL } from '@/utils/constants'
import store from '@/store'
import { fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
export async function fetch (url) {
url = removePrefix(url)
export async function fetch(url) {
url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, {})
const res = await fetchURL(`/api/resources${url}`, {});
if (res.status === 200) {
let data = await res.json()
data.url = `/files${url}`
let data = await res.json();
data.url = `/files${url}`;
if (data.isDir) {
if (!data.url.endsWith('/')) data.url += '/'
if (!data.url.endsWith("/")) data.url += "/";
data.items = data.items.map((item, index) => {
item.index = index
item.url = `${data.url}${encodeURIComponent(item.name)}`
item.index = index;
item.url = `${data.url}${encodeURIComponent(item.name)}`;
if (item.isDir) {
item.url += '/'
item.url += "/";
}
return item
})
return item;
});
}
return data
return data;
} else {
throw new Error(res.status)
throw new Error(res.status);
}
}
async function resourceAction (url, method, content) {
url = removePrefix(url)
async function resourceAction(url, method, content) {
url = removePrefix(url);
let opts = { method }
let opts = { method };
if (content) {
opts.body = content
opts.body = content;
}
const res = await fetchURL(`/api/resources${url}`, opts)
const res = await fetchURL(`/api/resources${url}`, opts);
if (res.status !== 200) {
throw new Error(await res.text())
throw new Error(await res.text());
} else {
return res
return res;
}
}
export async function remove (url) {
return resourceAction(url, 'DELETE')
export async function remove(url) {
return resourceAction(url, "DELETE");
}
export async function put (url, content = '') {
return resourceAction(url, 'PUT', content)
export async function put(url, content = "") {
return resourceAction(url, "PUT", content);
}
export function download (format, ...files) {
let url = `${baseURL}/api/raw`
export function download(format, ...files) {
let url = `${baseURL}/api/raw`;
if (files.length === 1) {
url += removePrefix(files[0]) + '?'
url += removePrefix(files[0]) + "?";
} else {
let arg = ''
let arg = "";
for (let file of files) {
arg += removePrefix(file) + ','
arg += removePrefix(file) + ",";
}
arg = arg.substring(0, arg.length - 1)
arg = encodeURIComponent(arg)
url += `/?files=${arg}&`
arg = arg.substring(0, arg.length - 1);
arg = encodeURIComponent(arg);
url += `/?files=${arg}&`;
}
if (format !== null) {
url += `algo=${format}&`
if (format) {
url += `algo=${format}&`;
}
url += `auth=${store.state.jwt}`
window.open(url)
if (store.state.jwt) {
url += `auth=${store.state.jwt}&`;
}
window.open(url);
}
export async function post (url, content = '', overwrite = false, onupload) {
url = removePrefix(url)
export async function post(url, content = "", overwrite = false, onupload) {
url = removePrefix(url);
let bufferContent;
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer();
}
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest()
request.open('POST', `${baseURL}/api/resources${url}?override=${overwrite}`, true)
request.setRequestHeader('X-Auth', store.state.jwt)
let request = new XMLHttpRequest();
request.open(
"POST",
`${baseURL}/api/resources${url}?override=${overwrite}`,
true
);
request.setRequestHeader("X-Auth", store.state.jwt);
if (typeof onupload === 'function') {
request.upload.onprogress = onupload
if (typeof onupload === "function") {
request.upload.onprogress = onupload;
}
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status)
reject(request.status);
} else {
reject(request.responseText)
reject(request.responseText);
}
}
};
request.onerror = (error) => {
reject(error)
}
reject(error);
};
request.send(content)
})
request.send(bufferContent || content);
});
}
function moveCopy (items, copy = false, overwrite = false, rename = false) {
let promises = []
function moveCopy(items, copy = false, overwrite = false, rename = false) {
let promises = [];
for (let item of items) {
const from = removePrefix(item.from)
const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}`
promises.push(resourceAction(url, 'PATCH'))
const from = item.from;
const to = encodeURIComponent(removePrefix(item.to));
const url = `${from}?action=${
copy ? "copy" : "rename"
}&destination=${to}&override=${overwrite}&rename=${rename}`;
promises.push(resourceAction(url, "PATCH"));
}
return Promise.all(promises)
return Promise.all(promises);
}
export function move (items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename)
export function move(items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename);
}
export function copy (items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename)
export function copy(items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename);
}
export async function checksum (url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, 'GET')
return (await data.json()).checksums[algo]
export async function checksum(url, algo) {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
return (await data.json()).checksums[algo];
}

View File

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

61
frontend/src/api/pub.js Normal file
View File

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

View File

@@ -1,26 +1,31 @@
import { fetchURL, removePrefix } from './utils'
import url from '../utils/url'
import { fetchURL, removePrefix } from "./utils";
import url from "../utils/url";
export default async function search (base, query) {
base = removePrefix(base)
query = encodeURIComponent(query)
export default async function search(base, query) {
base = removePrefix(base);
query = encodeURIComponent(query);
if (!base.endsWith('/')) {
base += '/'
if (!base.endsWith("/")) {
base += "/";
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {})
let res = await fetchURL(`/api/search${base}?query=${query}`, {});
if (res.status === 200) {
let data = await res.json()
let data = await res.json();
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path)
return item
})
item.url = `/files${base}` + url.encodePath(item.path);
return data
if (item.dir) {
item.url += "/";
}
return item;
});
return data;
} else {
throw Error(res.status)
throw Error(res.status);
}
}
}

View File

@@ -1,16 +1,16 @@
import { fetchURL, fetchJSON } from './utils'
import { fetchURL, fetchJSON } from "./utils";
export function get () {
return fetchJSON(`/api/settings`, {})
export function get() {
return fetchJSON(`/api/settings`, {});
}
export async function update (settings) {
export async function update(settings) {
const res = await fetchURL(`/api/settings`, {
method: 'PUT',
body: JSON.stringify(settings)
})
method: "PUT",
body: JSON.stringify(settings),
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}

View File

@@ -1,32 +1,36 @@
import { fetchURL, fetchJSON, removePrefix } from './utils'
import { fetchURL, fetchJSON, removePrefix } from "./utils";
export async function getHash(hash) {
return fetchJSON(`/api/public/share/${hash}`)
export async function list() {
return fetchJSON("/api/shares");
}
export async function get(url) {
url = removePrefix(url)
return fetchJSON(`/api/share${url}`)
url = removePrefix(url);
return fetchJSON(`/api/share${url}`);
}
export async function remove(hash) {
const res = await fetchURL(`/api/share/${hash}`, {
method: 'DELETE'
})
method: "DELETE",
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}
export async function create(url, expires = '', unit = 'hours') {
url = removePrefix(url)
url = `/api/share${url}`
if (expires !== '') {
url += `?expires=${expires}&unit=${unit}`
export async function create(url, password = "", expires = "", unit = "hours") {
url = removePrefix(url);
url = `/api/share${url}`;
if (expires !== "") {
url += `?expires=${expires}&unit=${unit}`;
}
let body = "{}";
if (password != "" || expires !== "" || unit !== "hours") {
body = JSON.stringify({ password: password, expires: expires, unit: unit });
}
return fetchJSON(url, {
method: 'POST'
})
method: "POST",
body: body,
});
}

View File

@@ -1,52 +1,51 @@
import { fetchURL, fetchJSON } from './utils'
import { fetchURL, fetchJSON } from "./utils";
export async function getAll () {
return fetchJSON(`/api/users`, {})
export async function getAll() {
return fetchJSON(`/api/users`, {});
}
export async function get (id) {
return fetchJSON(`/api/users/${id}`, {})
export async function get(id) {
return fetchJSON(`/api/users/${id}`, {});
}
export async function create (user) {
export async function create(user) {
const res = await fetchURL(`/api/users`, {
method: 'POST',
method: "POST",
body: JSON.stringify({
what: 'user',
what: "user",
which: [],
data: user
})
})
data: user,
}),
});
if (res.status === 201) {
return res.headers.get('Location')
return res.headers.get("Location");
} else {
throw new Error(res.status)
throw new Error(res.status);
}
}
export async function update (user, which = ['all']) {
export async function update(user, which = ["all"]) {
const res = await fetchURL(`/api/users/${user.id}`, {
method: 'PUT',
method: "PUT",
body: JSON.stringify({
what: 'user',
what: "user",
which: which,
data: user
})
})
data: user,
}),
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}
export async function remove (id) {
export async function remove(id) {
const res = await fetchURL(`/api/users/${id}`, {
method: 'DELETE'
})
method: "DELETE",
});
if (res.status !== 200) {
throw new Error(res.status)
throw new Error(res.status);
}
}

View File

@@ -1,45 +1,42 @@
import store from '@/store'
import { renew } from '@/utils/auth'
import { baseURL } from '@/utils/constants'
import store from "@/store";
import { renew } from "@/utils/auth";
import { baseURL } from "@/utils/constants";
export async function fetchURL (url, opts) {
opts = opts || {}
opts.headers = opts.headers || {}
export async function fetchURL(url, opts) {
opts = opts || {};
opts.headers = opts.headers || {};
let { headers, ...rest } = opts
let { headers, ...rest } = opts;
const res = await fetch(`${baseURL}${url}`, {
headers: {
'X-Auth': store.state.jwt,
...headers
"X-Auth": store.state.jwt,
...headers,
},
...rest
})
...rest,
});
if (res.headers.get('X-Renew-Token') === 'true') {
await renew(store.state.jwt)
if (res.headers.get("X-Renew-Token") === "true") {
await renew(store.state.jwt);
}
return res
return res;
}
export async function fetchJSON (url, opts) {
const res = await fetchURL(url, opts)
export async function fetchJSON(url, opts) {
const res = await fetchURL(url, opts);
if (res.status === 200) {
return res.json()
return res.json();
} else {
throw new Error(res.status)
throw new Error(res.status);
}
}
export function removePrefix (url) {
if (url.startsWith('/files')) {
url = url.slice(6)
}
export function removePrefix(url) {
url = url.split("/").splice(2).join("/");
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
return url
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
return url;
}

View File

@@ -0,0 +1,75 @@
<template>
<div class="breadcrumbs">
<component
:is="element"
:to="base || ''"
:aria-label="$t('files.home')"
:title="$t('files.home')"
>
<i class="material-icons">home</i>
</component>
<span v-for="(link, index) in items" :key="index">
<span class="chevron"
><i class="material-icons">keyboard_arrow_right</i></span
>
<component :is="element" :to="link.url">{{ link.name }}</component>
</span>
</div>
</template>
<script>
export default {
name: "breadcrumbs",
props: ["base", "noLink"],
computed: {
items() {
const relativePath = this.$route.path.replace(this.base, "");
let parts = relativePath.split("/");
if (parts[0] === "") {
parts.shift();
}
if (parts[parts.length - 1] === "") {
parts.pop();
}
let breadcrumbs = [];
for (let i = 0; i < parts.length; i++) {
if (i === 0) {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: this.base + "/" + parts[i] + "/",
});
} else {
breadcrumbs.push({
name: decodeURIComponent(parts[i]),
url: breadcrumbs[i - 1].url + parts[i] + "/",
});
}
}
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
}
breadcrumbs[0].name = "...";
}
return breadcrumbs;
},
element() {
if (this.noLink !== undefined) {
return "span";
}
return "router-link";
},
},
};
</script>
<style></style>

View File

@@ -1,184 +0,0 @@
<template>
<header v-if="!isEditor && !isPreview">
<div>
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
<i class="material-icons">menu</i>
</button>
<img :src="logoURL" alt="File Browser">
<search v-if="isLogged"></search>
</div>
<div>
<template v-if="isLogged">
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
<i class="material-icons">search</i>
</button>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
</button>
<!-- Menu that shows on listing AND mobile when there are files selected -->
<div id="file-selection" v-if="isMobile && isListing">
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
<share-button v-show="showShareButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showCopyButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
</div>
<!-- This buttons are shown on a dropdown on mobile phones -->
<div id="dropdown" :class="{ active: showMore }">
<div v-if="!isListing || !isMobile">
<share-button v-show="showShareButton"></share-button>
<rename-button v-show="showRenameButton"></rename-button>
<copy-button v-show="showCopyButton"></copy-button>
<move-button v-show="showMoveButton"></move-button>
<delete-button v-show="showDeleteButton"></delete-button>
</div>
<shell-button v-if="isExecEnabled && user.perm.execute" />
<switch-button v-show="isListing"></switch-button>
<download-button v-show="showDownloadButton"></download-button>
<upload-button v-show="showUpload"></upload-button>
<info-button v-show="isFiles"></info-button>
<button v-show="isListing" @click="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" >
<i class="material-icons">check_circle</i>
<span>{{ $t('buttons.select') }}</span>
</button>
</div>
</template>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
</div>
</header>
</template>
<script>
import Search from './Search'
import InfoButton from './buttons/Info'
import DeleteButton from './buttons/Delete'
import RenameButton from './buttons/Rename'
import UploadButton from './buttons/Upload'
import DownloadButton from './buttons/Download'
import SwitchButton from './buttons/SwitchView'
import MoveButton from './buttons/Move'
import CopyButton from './buttons/Copy'
import ShareButton from './buttons/Share'
import ShellButton from './buttons/Shell'
import {mapGetters, mapState} from 'vuex'
import { logoURL, enableExec } from '@/utils/constants'
import * as api from '@/api'
import buttons from '@/utils/buttons'
export default {
name: 'header-layout',
components: {
Search,
InfoButton,
DeleteButton,
ShareButton,
RenameButton,
DownloadButton,
CopyButton,
UploadButton,
SwitchButton,
MoveButton,
ShellButton
},
data: function () {
return {
width: window.innerWidth,
pluginData: {
api,
buttons,
'store': this.$store,
'router': this.$router
}
}
},
created () {
window.addEventListener('resize', () => {
this.width = window.innerWidth
})
},
computed: {
...mapGetters([
'selectedCount',
'isFiles',
'isEditor',
'isPreview',
'isListing',
'isLogged'
]),
...mapState([
'req',
'user',
'loading',
'reload',
'multiple'
]),
logoURL: () => logoURL,
isExecEnabled: () => enableExec,
isMobile () {
return this.width <= 736
},
showUpload () {
return this.isListing && this.user.perm.create
},
showDownloadButton () {
return this.isFiles && this.user.perm.download
},
showDeleteButton () {
return this.isFiles && (this.isListing
? (this.selectedCount !== 0 && this.user.perm.delete)
: this.user.perm.delete)
},
showRenameButton () {
return this.isFiles && (this.isListing
? (this.selectedCount === 1 && this.user.perm.rename)
: this.user.perm.rename)
},
showShareButton () {
return this.isFiles && (this.isListing
? (this.selectedCount === 1 && this.user.perm.share)
: this.user.perm.share)
},
showMoveButton () {
return this.isFiles && (this.isListing
? (this.selectedCount > 0 && this.user.perm.rename)
: this.user.perm.rename)
},
showCopyButton () {
return this.isFiles && (this.isListing
? (this.selectedCount > 0 && this.user.perm.create)
: this.user.perm.create)
},
showMore () {
return this.isFiles && this.$store.state.show === 'more'
},
showOverlay () {
return this.showMore
}
},
methods: {
openSidebar () {
this.$store.commit('showHover', 'sidebar')
},
openMore () {
this.$store.commit('showHover', 'more')
},
openSearch () {
this.$store.commit('showHover', 'search')
},
toggleMultipleSelection () {
this.$store.commit('multiple', !this.multiple)
this.resetPrompts()
},
resetPrompts () {
this.$store.commit('closeHovers')
}
}
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div id="search" @click="open" v-bind:class="{ active , ongoing }">
<div id="search" @click="open" v-bind:class="{ active, ongoing }">
<div id="input">
<button
v-if="active"
@@ -20,7 +20,7 @@
v-model.trim="value"
:aria-label="$t('search.search')"
:placeholder="$t('search.search')"
>
/>
</div>
<div id="result" ref="result">
@@ -30,25 +30,25 @@
<template v-if="value.length === 0">
<div class="boxes">
<h3>{{ $t('search.types') }}</h3>
<h3>{{ $t("search.types") }}</h3>
<div>
<div
tabindex="0"
v-for="(v,k) in boxes"
v-for="(v, k) in boxes"
:key="k"
role="button"
@click="init('type:'+k)"
:aria-label="$t('search.'+v.label)"
@click="init('type:' + k)"
:aria-label="$t('search.' + v.label)"
>
<i class="material-icons">{{v.icon}}</i>
<p>{{ $t('search.'+v.label) }}</p>
<i class="material-icons">{{ v.icon }}</i>
<p>{{ $t("search." + v.label) }}</p>
</div>
</div>
</div>
</template>
</template>
<ul v-show="results.length > 0">
<li v-for="(s,k) in filteredResults" :key="k">
<li v-for="(s, k) in filteredResults" :key="k">
<router-link @click.native="close" :to="s.url">
<i v-if="s.dir" class="material-icons">folder</i>
<i v-else class="material-icons">insert_drive_file</i>
@@ -65,20 +65,20 @@
</template>
<script>
import { mapState, mapGetters, mapMutations } from "vuex"
import url from "@/utils/url"
import { search } from "@/api"
import { mapState, mapGetters, mapMutations } from "vuex";
import url from "@/utils/url";
import { search } from "@/api";
var boxes = {
image: { label: "images", icon: "insert_photo" },
audio: { label: "music", icon: "volume_up" },
video: { label: "video", icon: "movie" },
pdf: { label: "pdf", icon: "picture_as_pdf" }
}
pdf: { label: "pdf", icon: "picture_as_pdf" },
};
export default {
name: "search",
data: function() {
data: function () {
return {
value: "",
active: false,
@@ -86,111 +86,116 @@ export default {
results: [],
reload: false,
resultsCount: 50,
scrollable: null
}
scrollable: null,
};
},
watch: {
show (val, old) {
this.active = val === "search"
show(val, old) {
this.active = val === "search";
if (old === "search" && !this.active) {
if (this.reload) {
this.setReload(true)
this.setReload(true);
}
document.body.style.overflow = "auto"
this.reset()
this.value = ''
this.active = false
this.$refs.input.blur()
document.body.style.overflow = "auto";
this.reset();
this.value = "";
this.active = false;
this.$refs.input.blur();
} else if (this.active) {
this.reload = false
this.$refs.input.focus()
document.body.style.overflow = "hidden"
this.reload = false;
this.$refs.input.focus();
document.body.style.overflow = "hidden";
}
},
value () {
value() {
if (this.results.length) {
this.reset()
this.reset();
}
}
},
},
computed: {
...mapState(["user", "show"]),
...mapGetters(["isListing"]),
boxes() {
return boxes
return boxes;
},
isEmpty() {
return this.results.length === 0
return this.results.length === 0;
},
text() {
if (this.ongoing) {
return ""
return "";
}
return this.value === '' ? this.$t("search.typeToSearch") : this.$t("search.pressToSearch")
return this.value === ""
? this.$t("search.typeToSearch")
: this.$t("search.pressToSearch");
},
filteredResults() {
return this.results.slice(0, this.resultsCount);
},
filteredResults () {
return this.results.slice(0, this.resultsCount)
}
},
mounted() {
this.$refs.result.addEventListener('scroll', event => {
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
this.resultsCount += 50
this.$refs.result.addEventListener("scroll", (event) => {
if (
event.target.offsetHeight + event.target.scrollTop >=
event.target.scrollHeight - 100
) {
this.resultsCount += 50;
}
})
});
},
methods: {
...mapMutations(["showHover", "closeHovers", "setReload"]),
open() {
this.showHover("search")
this.showHover("search");
},
close(event) {
event.stopPropagation()
event.preventDefault()
this.closeHovers()
event.stopPropagation();
event.preventDefault();
this.closeHovers();
},
keyup(event) {
if (event.keyCode === 27) {
this.close(event)
return
this.close(event);
return;
}
this.results.length = 0
this.results.length = 0;
},
init (string) {
this.value = `${string} `
this.$refs.input.focus()
init(string) {
this.value = `${string} `;
this.$refs.input.focus();
},
reset () {
this.ongoing = false
this.resultsCount = 50
this.results = []
reset() {
this.ongoing = false;
this.resultsCount = 50;
this.results = [];
},
async submit(event) {
event.preventDefault()
event.preventDefault();
if (this.value === '') {
return
if (this.value === "") {
return;
}
let path = this.$route.path
let path = this.$route.path;
if (!this.isListing) {
path = url.removeLastDir(path) + "/"
path = url.removeLastDir(path) + "/";
}
this.ongoing = true
this.ongoing = true;
try {
this.results = await search(path, this.value)
this.results = await search(path, this.value);
} catch (error) {
this.$showError(error)
this.$showError(error);
}
this.ongoing = false
}
}
}
this.ongoing = false;
},
},
};
</script>

View File

@@ -1,12 +1,21 @@
<template>
<div @click="focus" class="shell" ref="scrollable" :class="{ ['shell--hidden']: !showShell}">
<div v-for="(c, index) in content" :key="index" class="shell__result" >
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<div
@click="focus"
class="shell"
ref="scrollable"
:class="{ ['shell--hidden']: !showShell }"
>
<div v-for="(c, index) in content" :key="index" class="shell__result">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre class="shell__text">{{ c.text }}</pre>
</div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }" >
<div class="shell__prompt"><i class="material-icons">chevron_right</i></div>
<div class="shell__result" :class="{ 'shell__result--hidden': !canInput }">
<div class="shell__prompt">
<i class="material-icons">chevron_right</i>
</div>
<pre
tabindex="0"
ref="input"
@@ -14,102 +23,103 @@
contenteditable="true"
@keydown.prevent.38="historyUp"
@keydown.prevent.40="historyDown"
@keypress.prevent.enter="submit" />
@keypress.prevent.enter="submit"
/>
</div>
</div>
</template>
<script>
import { mapMutations, mapState, mapGetters } from 'vuex'
import { commands } from '@/api'
import { mapMutations, mapState, mapGetters } from "vuex";
import { commands } from "@/api";
export default {
name: 'shell',
name: "shell",
computed: {
...mapState([ 'user', 'showShell' ]),
...mapGetters([ 'isFiles', 'isLogged' ]),
...mapState(["user", "showShell"]),
...mapGetters(["isFiles", "isLogged"]),
path: function () {
if (this.isFiles) {
return this.$route.path
return this.$route.path;
}
return ''
}
return "";
},
},
data: () => ({
content: [],
history: [],
historyPos: 0,
canInput: true
canInput: true,
}),
methods: {
...mapMutations([ 'toggleShell' ]),
...mapMutations(["toggleShell"]),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},
focus: function () {
this.$refs.input.focus()
this.$refs.input.focus();
},
historyUp () {
historyUp() {
if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos]
this.focus()
this.$refs.input.innerText = this.history[--this.historyPos];
this.focus();
}
},
historyDown () {
historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos]
this.focus()
this.$refs.input.innerText = this.history[++this.historyPos];
this.focus();
} else {
this.historyPos = this.history.length
this.$refs.input.innerText = ''
this.historyPos = this.history.length;
this.$refs.input.innerText = "";
}
},
submit: function (event) {
const cmd = event.target.innerText.trim()
const cmd = event.target.innerText.trim();
if (cmd === '') {
return
if (cmd === "") {
return;
}
if (cmd === 'clear') {
this.content = []
event.target.innerHTML = ''
return
if (cmd === "clear") {
this.content = [];
event.target.innerHTML = "";
return;
}
if (cmd === 'exit') {
event.target.innerHTML = ''
this.toggleShell()
return
if (cmd === "exit") {
event.target.innerHTML = "";
this.toggleShell();
return;
}
this.canInput = false
event.target.innerHTML = ''
this.canInput = false;
event.target.innerHTML = "";
let results = {
text: `${cmd}\n\n`
}
this.history.push(cmd)
this.historyPos = this.history.length
this.content.push(results)
text: `${cmd}\n\n`,
};
this.history.push(cmd);
this.historyPos = this.history.length;
this.content.push(results);
commands(
this.path,
cmd,
event => {
results.text += `${event.data}\n`
this.scroll()
(event) => {
results.text += `${event.data}\n`;
this.scroll();
},
() => {
results.text = results.text.trimEnd()
this.canInput = true
this.$refs.input.focus()
this.scroll()
results.text = results.text.trimEnd();
this.canInput = true;
this.$refs.input.focus();
this.scroll();
}
)
}
}
}
);
},
},
};
</script>

View File

@@ -1,82 +1,134 @@
<template>
<nav :class="{active}">
<nav :class="{ active }">
<template v-if="isLogged">
<router-link class="action" to="/files/" :aria-label="$t('sidebar.myFiles')" :title="$t('sidebar.myFiles')">
<router-link
class="action"
to="/files/"
:aria-label="$t('sidebar.myFiles')"
:title="$t('sidebar.myFiles')"
>
<i class="material-icons">folder</i>
<span>{{ $t('sidebar.myFiles') }}</span>
<span>{{ $t("sidebar.myFiles") }}</span>
</router-link>
<div v-if="user.perm.create">
<button @click="$store.commit('showHover', 'newDir')" class="action" :aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')">
<button
@click="$store.commit('showHover', 'newDir')"
class="action"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t('sidebar.newFolder') }}</span>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
<button @click="$store.commit('showHover', 'newFile')" class="action" :aria-label="$t('sidebar.newFile')" :title="$t('sidebar.newFile')">
<button
@click="$store.commit('showHover', 'newFile')"
class="action"
:aria-label="$t('sidebar.newFile')"
:title="$t('sidebar.newFile')"
>
<i class="material-icons">note_add</i>
<span>{{ $t('sidebar.newFile') }}</span>
<span>{{ $t("sidebar.newFile") }}</span>
</button>
</div>
<div>
<router-link class="action" to="/settings" :aria-label="$t('sidebar.settings')" :title="$t('sidebar.settings')">
<router-link
class="action"
to="/settings"
:aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')"
>
<i class="material-icons">settings_applications</i>
<span>{{ $t('sidebar.settings') }}</span>
<span>{{ $t("sidebar.settings") }}</span>
</router-link>
<button v-if="authMethod == 'json'" @click="logout" class="action" id="logout" :aria-label="$t('sidebar.logout')" :title="$t('sidebar.logout')">
<button
v-if="authMethod == 'json'"
@click="logout"
class="action"
id="logout"
:aria-label="$t('sidebar.logout')"
:title="$t('sidebar.logout')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.logout') }}</span>
<span>{{ $t("sidebar.logout") }}</span>
</button>
</div>
</template>
<template v-else>
<router-link class="action" to="/login" :aria-label="$t('sidebar.login')" :title="$t('sidebar.login')">
<router-link
class="action"
to="/login"
:aria-label="$t('sidebar.login')"
:title="$t('sidebar.login')"
>
<i class="material-icons">exit_to_app</i>
<span>{{ $t('sidebar.login') }}</span>
<span>{{ $t("sidebar.login") }}</span>
</router-link>
<router-link v-if="signup" class="action" to="/login" :aria-label="$t('sidebar.signup')" :title="$t('sidebar.signup')">
<router-link
v-if="signup"
class="action"
to="/login"
:aria-label="$t('sidebar.signup')"
:title="$t('sidebar.signup')"
>
<i class="material-icons">person_add</i>
<span>{{ $t('sidebar.signup') }}</span>
<span>{{ $t("sidebar.signup") }}</span>
</router-link>
</template>
<p class="credits">
<span>
<span v-if="disableExternal">File Browser</span>
<a v-else rel="noopener noreferrer" target="_blank" href="https://github.com/filebrowser/filebrowser">File Browser</a>
<a
v-else
rel="noopener noreferrer"
target="_blank"
href="https://github.com/filebrowser/filebrowser"
>File Browser</a
>
<span> {{ version }}</span>
</span>
<span><a @click="help">{{ $t('sidebar.help') }}</a></span>
<span
><a @click="help">{{ $t("sidebar.help") }}</a></span
>
</p>
</nav>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import * as auth from '@/utils/auth'
import { version, signup, disableExternal, noAuth, authMethod } from '@/utils/constants'
import { mapState, mapGetters } from "vuex";
import * as auth from "@/utils/auth";
import {
version,
signup,
disableExternal,
noAuth,
authMethod,
} from "@/utils/constants";
export default {
name: 'sidebar',
name: "sidebar",
computed: {
...mapState([ 'user' ]),
...mapGetters([ 'isLogged' ]),
active () {
return this.$store.state.show === 'sidebar'
...mapState(["user"]),
...mapGetters(["isLogged"]),
active() {
return this.$store.state.show === "sidebar";
},
signup: () => signup,
version: () => version,
disableExternal: () => disableExternal,
noAuth: () => noAuth,
authMethod: () => authMethod
authMethod: () => authMethod,
},
methods: {
help () {
this.$store.commit('showHover', 'help')
help() {
this.$store.commit("showHover", "help");
},
logout: auth.logout
}
}
logout: auth.logout,
},
};
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.copy')" :title="$t('buttons.copy')" class="action" id="copy-button">
<i class="material-icons">content_copy</i>
<span>{{ $t('buttons.copyFile') }}</span>
</button>
</template>
<script>
export default {
name: 'copy-button',
methods: {
show: function () {
this.$store.commit('showHover', 'copy')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.delete')" :title="$t('buttons.delete')" class="action" id="delete-button">
<i class="material-icons">delete</i>
<span>{{ $t('buttons.delete') }}</span>
</button>
</template>
<script>
export default {
name: 'delete-button',
methods: {
show: function () {
this.$store.commit('showHover', 'delete')
}
}
}
</script>

View File

@@ -1,35 +0,0 @@
<template>
<button @click="download" :aria-label="$t('buttons.download')" :title="$t('buttons.download')" id="download-button" class="action">
<i class="material-icons">file_download</i>
<span>{{ $t('buttons.download') }}</span>
<span v-if="selectedCount > 0" class="counter">{{ selectedCount }}</span>
</button>
</template>
<script>
import {mapGetters, mapState} from 'vuex'
import { files as api } from '@/api'
export default {
name: 'download-button',
computed: {
...mapState(['req', 'selected']),
...mapGetters(['isListing', 'selectedCount'])
},
methods: {
download: function () {
if (!this.isListing) {
api.download(null, this.$route.path)
return
}
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.req.items[this.selected[0]].url)
return
}
this.$store.commit('showHover', 'download')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="show">
<i class="material-icons">info</i>
<span>{{ $t('buttons.info') }}</span>
</button>
</template>
<script>
export default {
name: 'info-button',
methods: {
show: function () {
this.$store.commit('showHover', 'info')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.move')" :title="$t('buttons.move')" class="action" id="move-button">
<i class="material-icons">forward</i>
<span>{{ $t('buttons.moveFile') }}</span>
</button>
</template>
<script>
export default {
name: 'move-button',
methods: {
show: function () {
this.$store.commit('showHover', 'move')
}
}
}
</script>

View File

@@ -1,22 +0,0 @@
<template>
<button :title="$t('buttons.info')" :aria-label="$t('buttons.info')" class="action" @click="$emit('change-size')">
<i class="material-icons">{{ this.icon }}</i>
<span>{{ $t('buttons.info') }}</span>
</button>
</template>
<script>
export default {
name: 'preview-size-button',
props: [ 'size' ],
computed: {
icon () {
if (this.size) {
return 'photo_size_select_large'
}
return 'hd'
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.rename')" :title="$t('buttons.rename')" class="action" id="rename-button">
<i class="material-icons">mode_edit</i>
<span>{{ $t('buttons.rename') }}</span>
</button>
</template>
<script>
export default {
name: 'rename-button',
methods: {
show: function () {
this.$store.commit('showHover', 'rename')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.share')" :title="$t('buttons.share')" class="action">
<i class="material-icons">share</i>
<span>{{ $t('buttons.share') }}</span>
</button>
</template>
<script>
export default {
name: 'share-button',
methods: {
show () {
this.$store.commit('showHover', 'share')
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
<template>
<button @click="show" :aria-label="$t('buttons.shell')" :title="$t('buttons.shell')" class="action">
<i class="material-icons">code</i>
<span>{{ $t('buttons.shell') }}</span>
</button>
</template>
<script>
export default {
name: 'shell-button',
methods: {
show: function () {
this.$store.commit('toggleShell')
}
}
}
</script>

View File

@@ -1,40 +0,0 @@
<template>
<button @click="change" :aria-label="$t('buttons.switchView')" :title="$t('buttons.switchView')" class="action" id="switch-view-button">
<i class="material-icons">{{ icon }}</i>
<span>{{ $t('buttons.switchView') }}</span>
</button>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import { users as api } from '@/api'
export default {
name: 'switch-button',
computed: {
...mapState(['user']),
icon: function () {
if (this.user.viewMode === 'mosaic') return 'view_list'
return 'view_module'
}
},
methods: {
...mapMutations([ 'updateUser', 'closeHovers' ]),
change: async function () {
this.closeHovers()
const data = {
id: this.user.id,
viewMode: (this.icon === 'view_list') ? 'list' : 'mosaic'
}
try {
await api.update(data, ['viewMode'])
this.updateUser(data)
} catch (e) {
this.$showError(e)
}
}
}
}
</script>

View File

@@ -1,21 +0,0 @@
<template>
<button @click="upload" :aria-label="$t('buttons.upload')" :title="$t('buttons.upload')" class="action" id="upload-button">
<i class="material-icons">file_upload</i>
<span>{{ $t('buttons.upload') }}</span>
</button>
</template>
<script>
export default {
name: 'upload-button',
methods: {
upload: function () {
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
this.$store.commit('showHover', 'upload')
} else {
document.getElementById('upload-input').click();
}
}
}
}
</script>

View File

@@ -1,132 +0,0 @@
<template>
<div id="editor-container">
<div class="bar">
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
<i class="material-icons">close</i>
</button>
<div class="title">
<span>{{ req.name }}</span>
</div>
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" id="save-button" class="action">
<i class="material-icons">save</i>
</button>
</div>
<div id="breadcrumbs">
<span><i class="material-icons">home</i></span>
<span v-for="(link, index) in breadcrumbs" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<span>{{ link.name }}</span>
</span>
</div>
<form id="editor"></form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
import url from '@/utils/url'
import ace from 'ace-builds/src-min-noconflict/ace.js'
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
import 'ace-builds/webpack-resolver'
import { theme } from '@/utils/constants'
export default {
name: 'editor',
data: function () {
return {}
},
computed: {
...mapState(['req', 'user']),
breadcrumbs () {
let parts = this.$route.path.split('/')
if (parts[0] === '') {
parts.shift()
}
if (parts[parts.length - 1] === '') {
parts.pop()
}
let breadcrumbs = []
for (let i = 0; i < parts.length; i++) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
}
breadcrumbs.shift()
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift()
}
breadcrumbs[0].name = '...'
}
return breadcrumbs
}
},
created () {
window.addEventListener('keydown', this.keyEvent)
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
this.editor.destroy();
},
mounted: function () {
const fileContent = this.req.content || '';
this.editor = ace.edit('editor', {
value: fileContent,
showPrintMargin: false,
readOnly: this.req.type === 'textImmutable',
theme: 'ace/theme/chrome',
mode: modelist.getModeForPath(this.req.name).mode,
wrap: true
})
if (theme == 'dark') {
this.editor.setTheme("ace/theme/twilight");
}
},
methods: {
back () {
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
keyEvent (event) {
if (!event.ctrlKey && !event.metaKey) {
return
}
if (String.fromCharCode(event.which).toLowerCase() !== 's') {
return
}
event.preventDefault()
this.save()
},
async save () {
const button = 'save'
buttons.loading('save')
try {
await api.put(this.$route.path, this.editor.getValue())
buttons.success(button)
} catch (e) {
buttons.done(button)
this.$showError(e)
}
}
}
}
</script>

View File

@@ -10,39 +10,45 @@
@mouseup="mouseUp"
@wheel="wheelMove"
>
<img :src="src" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad">
<img
src=""
class="image-ex-img image-ex-img-center"
ref="imgex"
@load="onLoad"
/>
</div>
</template>
<script>
import throttle from 'lodash.throttle'
import throttle from "lodash.throttle";
import UTIF from "utif";
export default {
props: {
src: String,
moveDisabledTime: {
type: Number,
default: () => 200
default: () => 200,
},
maxScale: {
type: Number,
default: () => 4
default: () => 4,
},
minScale: {
type: Number,
default: () => 0.25
default: () => 0.25,
},
classList: {
type: Array,
default: () => []
default: () => [],
},
zoomStep: {
type: Number,
default: () => 0.25
default: () => 0.25,
},
autofill: {
type: Boolean,
default: () => false
}
default: () => false,
},
},
data() {
return {
@@ -56,177 +62,208 @@ export default {
imageLoaded: false,
position: {
center: { x: 0, y: 0 },
relative: { x: 0, y: 0 }
}
}
relative: { x: 0, y: 0 },
},
};
},
mounted() {
let container = this.$refs.container
this.classList.forEach(className => container.classList.add(className))
if (!this.decodeUTIF()) {
this.$refs.imgex.src = this.src;
}
let container = this.$refs.container;
this.classList.forEach((className) => container.classList.add(className));
// set width and height if they are zero
if (getComputedStyle(container).width === "0px") {
container.style.width = "100%"
container.style.width = "100%";
}
if (getComputedStyle(container).height === "0px") {
container.style.height = "100%"
container.style.height = "100%";
}
window.addEventListener('resize', this.onResize)
window.addEventListener("resize", this.onResize);
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize)
document.removeEventListener('mouseup', this.onMouseUp)
beforeDestroy() {
window.removeEventListener("resize", this.onResize);
document.removeEventListener("mouseup", this.onMouseUp);
},
watch: {
src: function () {
this.scale = 1
this.setZoom()
this.setCenter()
}
this.scale = 1;
this.setZoom();
this.setCenter();
},
},
methods: {
// Modified from UTIF.replaceIMG
decodeUTIF() {
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
let suff = document.location.pathname.split(".").pop().toLowerCase();
if (sufs.indexOf(suff) == -1) return false;
let xhr = new XMLHttpRequest();
UTIF._xhrs.push(xhr);
UTIF._imgs.push(this.$refs.imgex);
xhr.open("GET", this.src);
xhr.responseType = "arraybuffer";
xhr.onload = UTIF._imgLoaded;
xhr.send();
return true;
},
onLoad() {
let img = this.$refs.imgex
let img = this.$refs.imgex;
this.imageLoaded = true
this.imageLoaded = true;
if (img === undefined) {
return
return;
}
img.classList.remove('image-ex-img-center')
this.setCenter()
img.classList.add('image-ex-img-ready')
img.classList.remove("image-ex-img-center");
this.setCenter();
img.classList.add("image-ex-img-ready");
document.addEventListener('mouseup', this.onMouseUp)
document.addEventListener("mouseup", this.onMouseUp);
},
onMouseUp() {
this.inDrag = false
this.inDrag = false;
},
onResize: throttle(function() {
onResize: throttle(function () {
if (this.imageLoaded) {
this.setCenter()
this.doMove(this.position.relative.x, this.position.relative.y)
this.setCenter();
this.doMove(this.position.relative.x, this.position.relative.y);
}
}, 100),
setCenter() {
let container = this.$refs.container
let img = this.$refs.imgex
let container = this.$refs.container;
let img = this.$refs.imgex;
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2)
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2)
this.position.center.x = Math.floor(
(container.clientWidth - img.clientWidth) / 2
);
this.position.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2
);
img.style.left = this.position.center.x + 'px'
img.style.top = this.position.center.y + 'px'
img.style.left = this.position.center.x + "px";
img.style.top = this.position.center.y + "px";
},
mousedownStart(event) {
this.lastX = null
this.lastY = null
this.inDrag = true
event.preventDefault()
this.lastX = null;
this.lastY = null;
this.inDrag = true;
event.preventDefault();
},
mouseMove(event) {
if (!this.inDrag) return
this.doMove(event.movementX, event.movementY)
event.preventDefault()
if (!this.inDrag) return;
this.doMove(event.movementX, event.movementY);
event.preventDefault();
},
mouseUp(event) {
this.inDrag = false
event.preventDefault()
this.inDrag = false;
event.preventDefault();
},
touchStart(event) {
this.lastX = null
this.lastY = null
this.lastTouchDistance = null
event.preventDefault()
this.lastX = null;
this.lastY = null;
this.lastTouchDistance = null;
if (event.targetTouches.length < 2) {
setTimeout(() => {
this.touches = 0;
}, 300);
this.touches++;
if (this.touches > 1) {
this.zoomAuto(event);
}
}
event.preventDefault();
},
zoomAuto(event) {
switch (this.scale) {
case 1:
this.scale = 2
break
this.scale = 2;
break;
case 2:
this.scale = 4
break
this.scale = 4;
break;
default:
case 4:
this.scale = 1
break
this.scale = 1;
this.setCenter();
break;
}
this.setZoom()
event.preventDefault()
this.setZoom();
event.preventDefault();
},
touchMove(event) {
event.preventDefault()
event.preventDefault();
if (this.lastX === null) {
this.lastX = event.targetTouches[0].pageX
this.lastY = event.targetTouches[0].pageY
return
this.lastX = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY;
return;
}
let step = this.$refs.imgex.width / 5
let step = this.$refs.imgex.width / 5;
if (event.targetTouches.length === 2) {
this.moveDisabled = true
clearTimeout(this.disabledTimer)
this.moveDisabled = true;
clearTimeout(this.disabledTimer);
this.disabledTimer = setTimeout(
() => (this.moveDisabled = false),
this.moveDisabledTime
)
);
let p1 = event.targetTouches[0]
let p2 = event.targetTouches[1]
let p1 = event.targetTouches[0];
let p2 = event.targetTouches[1];
let touchDistance = Math.sqrt(
Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)
)
);
if (!this.lastTouchDistance) {
this.lastTouchDistance = touchDistance
return
this.lastTouchDistance = touchDistance;
return;
}
this.scale += (touchDistance - this.lastTouchDistance) / step
this.lastTouchDistance = touchDistance
this.setZoom()
this.scale += (touchDistance - this.lastTouchDistance) / step;
this.lastTouchDistance = touchDistance;
this.setZoom();
} else if (event.targetTouches.length === 1) {
if (this.moveDisabled) return
let x = event.targetTouches[0].pageX - this.lastX
let y = event.targetTouches[0].pageY - this.lastY
if (Math.abs(x) >= step && Math.abs(y) >= step) return
this.lastX = event.targetTouches[0].pageX
this.lastY = event.targetTouches[0].pageY
this.doMove(x, y)
if (this.moveDisabled) return;
let x = event.targetTouches[0].pageX - this.lastX;
let y = event.targetTouches[0].pageY - this.lastY;
if (Math.abs(x) >= step && Math.abs(y) >= step) return;
this.lastX = event.targetTouches[0].pageX;
this.lastY = event.targetTouches[0].pageY;
this.doMove(x, y);
}
},
doMove(x, y) {
let style = this.$refs.imgex.style
let posX = this.pxStringToNumber(style.left) + x
let posY = this.pxStringToNumber(style.top) + y
let style = this.$refs.imgex.style;
let posX = this.pxStringToNumber(style.left) + x;
let posY = this.pxStringToNumber(style.top) + y;
style.left = posX + 'px'
style.top = posY + 'px'
style.left = posX + "px";
style.top = posY + "px";
this.position.relative.x = Math.abs(this.position.center.x - posX)
this.position.relative.y = Math.abs(this.position.center.y - posY)
this.position.relative.x = Math.abs(this.position.center.x - posX);
this.position.relative.y = Math.abs(this.position.center.y - posY);
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1
this.position.relative.x = this.position.relative.x * -1;
}
if (posY < this.position.center.y) {
this.position.relative.y = this.position.relative.y * -1
this.position.relative.y = this.position.relative.y * -1;
}
},
wheelMove(event) {
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
this.setZoom()
this.scale += (event.wheelDeltaY / 100) * this.zoomStep;
this.setZoom();
},
setZoom() {
this.scale = this.scale < this.minScale ? this.minScale : this.scale
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale
this.$refs.imgex.style.transform = `scale(${this.scale})`
this.scale = this.scale < this.minScale ? this.minScale : this.scale;
this.scale = this.scale > this.maxScale ? this.maxScale : this.scale;
this.$refs.imgex.style.transform = `scale(${this.scale})`;
},
pxStringToNumber(style) {
return +style.replace("px", "")
}
}
}
return +style.replace("px", "");
},
},
};
</script>
<style>
.image-ex-container {

View File

@@ -1,464 +0,0 @@
<template>
<div v-if="(req.numDirs + req.numFiles) == 0">
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
</h2>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
</div>
<div v-else id="listing"
:class="user.viewMode">
<div>
<div class="item header">
<div></div>
<div>
<p :class="{ active: nameSorted }" class="name"
role="button"
tabindex="0"
@click="sort('name')"
:title="$t('files.sortByName')"
:aria-label="$t('files.sortByName')">
<span>{{ $t('files.name') }}</span>
<i class="material-icons">{{ nameIcon }}</i>
</p>
<p :class="{ active: sizeSorted }" class="size"
role="button"
tabindex="0"
@click="sort('size')"
:title="$t('files.sortBySize')"
:aria-label="$t('files.sortBySize')">
<span>{{ $t('files.size') }}</span>
<i class="material-icons">{{ sizeIcon }}</i>
</p>
<p :class="{ active: modifiedSorted }" class="modified"
role="button"
tabindex="0"
@click="sort('modified')"
:title="$t('files.sortByLastModified')"
:aria-label="$t('files.sortByLastModified')">
<span>{{ $t('files.lastModified') }}</span>
<i class="material-icons">{{ modifiedIcon }}</i>
</p>
</div>
</div>
</div>
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
<div v-if="req.numDirs > 0">
<item v-for="(item) in dirs"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.isDir"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size">
</item>
</div>
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
<div v-if="req.numFiles > 0">
<item v-for="(item) in files"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.isDir"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size">
</item>
</div>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
<i class="material-icons">clear</i>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import Item from './ListingItem'
import css from '@/utils/css'
import { users, files as api } from '@/api'
import * as upload from '@/utils/upload'
export default {
name: 'listing',
components: { Item },
data: function () {
return {
showLimit: 50,
dragCounter: 0
}
},
computed: {
...mapState(['req', 'selected', 'user', 'show']),
nameSorted () {
return (this.req.sorting.by === 'name')
},
sizeSorted () {
return (this.req.sorting.by === 'size')
},
modifiedSorted () {
return (this.req.sorting.by === 'modified')
},
ascOrdered () {
return this.req.sorting.asc
},
items () {
const dirs = []
const files = []
this.req.items.forEach((item) => {
if (item.isDir) {
dirs.push(item)
} else {
files.push(item)
}
})
return { dirs, files }
},
dirs () {
return this.items.dirs.slice(0, this.showLimit)
},
files () {
let showLimit = this.showLimit - this.items.dirs.length
if (showLimit < 0) showLimit = 0
return this.items.files.slice(0, showLimit)
},
nameIcon () {
if (this.nameSorted && !this.ascOrdered) {
return 'arrow_upward'
}
return 'arrow_downward'
},
sizeIcon () {
if (this.sizeSorted && this.ascOrdered) {
return 'arrow_downward'
}
return 'arrow_upward'
},
modifiedIcon () {
if (this.modifiedSorted && this.ascOrdered) {
return 'arrow_downward'
}
return 'arrow_upward'
}
},
mounted: function () {
// Check the columns size for the first time.
this.resizeEvent()
// Add the needed event listeners to the window and document.
window.addEventListener('keydown', this.keyEvent)
window.addEventListener('resize', this.resizeEvent)
window.addEventListener('scroll', this.scrollEvent)
document.addEventListener('dragover', this.preventDefault)
document.addEventListener('dragenter', this.dragEnter)
document.addEventListener('dragleave', this.dragLeave)
document.addEventListener('drop', this.drop)
},
beforeDestroy () {
// Remove event listeners before destroying this page.
window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('resize', this.resizeEvent)
window.removeEventListener('scroll', this.scrollEvent)
document.removeEventListener('dragover', this.preventDefault)
document.removeEventListener('dragenter', this.dragEnter)
document.removeEventListener('dragleave', this.dragLeave)
document.removeEventListener('drop', this.drop)
},
methods: {
...mapMutations([ 'updateUser', 'addSelected' ]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
},
keyEvent (event) {
if (this.show !== null) {
return
}
if (!event.ctrlKey && !event.metaKey) {
return
}
let key = String.fromCharCode(event.which).toLowerCase()
switch (key) {
case 'f':
event.preventDefault()
this.$store.commit('showHover', 'search')
break
case 'c':
case 'x':
this.copyCut(event, key)
break
case 'v':
this.paste(event)
break
case 'a':
event.preventDefault()
for (let file of this.items.files) {
if (this.$store.state.selected.indexOf(file.index) === -1) {
this.addSelected(file.index)
}
}
for (let dir of this.items.dirs) {
if (this.$store.state.selected.indexOf(dir.index) === -1) {
this.addSelected(dir.index)
}
}
break
}
},
preventDefault (event) {
// Wrapper around prevent default.
event.preventDefault()
},
copyCut (event, key) {
if (event.target.tagName.toLowerCase() === 'input') {
return
}
let items = []
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
name: encodeURIComponent(this.req.items[i].name)
})
}
if (items.length == 0) {
return
}
this.$store.commit('updateClipboard', {
key: key,
items: items,
path: this.$route.path
})
},
paste (event) {
if (event.target.tagName.toLowerCase() === 'input') {
return
}
let items = []
for (let item of this.$store.state.clipboard.items) {
const from = item.from.endsWith('/') ? item.from.slice(0, -1) : item.from
const to = this.$route.path + item.name
items.push({ from, to, name: item.name })
}
if (items.length === 0) {
return
}
let action = (overwrite, rename) => {
api.copy(items, overwrite, rename).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
if (this.$store.state.clipboard.key === 'x') {
action = (overwrite, rename) => {
api.move(items, overwrite, rename).then(() => {
this.$store.commit('resetClipboard')
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
}
if (this.$store.state.clipboard.path == this.$route.path) {
action(false, true)
return
}
let conflict = upload.checkConflict(items, this.req.items)
let overwrite = false
let rename = false
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
return
}
action(overwrite, rename)
},
resizeEvent () {
// Update the columns size based on the window width.
let columns = Math.floor(document.querySelector('main').offsetWidth / 300)
let items = css(['#listing.mosaic .item', '.mosaic#listing .item'])
if (columns === 0) columns = 1
items.style.width = `calc(${100 / columns}% - 1em)`
},
scrollEvent () {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
this.showLimit += 50
}
},
dragEnter () {
this.dragCounter++
// When the user starts dragging an item, put every
// file on the listing with 50% opacity.
let items = document.getElementsByClassName('item')
Array.from(items).forEach(file => {
file.style.opacity = 0.5
})
},
dragLeave () {
this.dragCounter--
if (this.dragCounter == 0) {
this.resetOpacity()
}
},
drop: async function (event) {
event.preventDefault()
this.dragCounter = 0
this.resetOpacity()
let dt = event.dataTransfer
let el = event.target
if (dt.files.length <= 0) return
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
el = el.parentElement
}
}
let base = ''
if (el !== null && el.classList.contains('item') && el.dataset.dir === 'true') {
base = el.querySelector('.name').innerHTML + '/'
}
let files = await upload.scanFiles(dt)
let path = this.$route.path + base
let items = this.req.items
if (base !== '') {
try {
items = (await api.fetch(path)).items
} catch (error) {
this.$showError(error)
}
}
let conflict = upload.checkConflict(files, items)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
upload.handleFiles(files, path, true)
}
})
return
}
upload.handleFiles(files, path)
},
uploadInput (event) {
this.$store.commit('closeHovers')
let files = event.currentTarget.files
let folder_upload = files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== ''
if (folder_upload) {
for (let i = 0; i < files.length; i++) {
let file = files[i]
files[i].fullPath = file.webkitRelativePath
}
}
let path = this.$route.path
let conflict = upload.checkConflict(files, this.req.items)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
upload.handleFiles(files, path, true)
}
})
return
}
upload.handleFiles(files, path)
},
resetOpacity () {
let items = document.getElementsByClassName('item')
Array.from(items).forEach(file => {
file.style.opacity = 1
})
},
async sort (by) {
let asc = false
if (by === 'name') {
if (this.nameIcon === 'arrow_upward') {
asc = true
}
} else if (by === 'size') {
if (this.sizeIcon === 'arrow_upward') {
asc = true
}
} else if (by === 'modified') {
if (this.modifiedIcon === 'arrow_upward') {
asc = true
}
}
try {
await users.update({ id: this.user.id, sorting: { by, asc } }, ['sorting'])
} catch (e) {
this.$showError(e)
}
this.$store.commit('setReload', true)
}
}
}
</script>

View File

@@ -1,19 +1,24 @@
<template>
<div class="item"
role="button"
tabindex="0"
:draggable="isDraggable"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="click"
@dblclick="open"
@touchstart="touchstart"
:data-dir="isDir"
:aria-label="name"
:aria-selected="isSelected">
<div
class="item"
role="button"
tabindex="0"
:draggable="isDraggable"
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="itemClick"
@dblclick="dblclick"
@touchstart="touchstart"
:data-dir="isDir"
:aria-label="name"
:aria-selected="isSelected"
>
<div>
<img v-if="type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
<img
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled"
v-lazy="thumbnailUrl"
/>
<i v-else class="material-icons">{{ icon }}</i>
</div>
@@ -31,189 +36,221 @@
</template>
<script>
import { baseURL, enableThumbs } from '@/utils/constants'
import { mapMutations, mapGetters, mapState } from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import { files as api } from '@/api'
import * as upload from '@/utils/upload'
import { baseURL, enableThumbs } from "@/utils/constants";
import { mapMutations, mapGetters, mapState } from "vuex";
import filesize from "filesize";
import moment from "moment";
import { files as api } from "@/api";
import * as upload from "@/utils/upload";
export default {
name: 'item',
name: "item",
data: function () {
return {
touches: 0
}
touches: 0,
};
},
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
props: [
"name",
"isDir",
"url",
"type",
"size",
"modified",
"index",
"readOnly",
],
computed: {
...mapState(['selected', 'req', 'user', 'jwt']),
...mapGetters(['selectedCount']),
isSelected () {
return (this.selected.indexOf(this.index) !== -1)
...mapState(["user", "selected", "req", "jwt"]),
...mapGetters(["selectedCount"]),
singleClick() {
return this.readOnly == undefined && this.user.singleClick;
},
icon () {
if (this.isDir) return 'folder'
if (this.type === 'image') return 'insert_photo'
if (this.type === 'audio') return 'volume_up'
if (this.type === 'video') return 'movie'
return 'insert_drive_file'
isSelected() {
return this.selected.indexOf(this.index) !== -1;
},
isDraggable () {
return this.user.perm.rename
icon() {
if (this.isDir) return "folder";
if (this.type === "image") return "insert_photo";
if (this.type === "audio") return "volume_up";
if (this.type === "video") return "movie";
return "insert_drive_file";
},
canDrop () {
if (!this.isDir) return false
isDraggable() {
return this.readOnly == undefined && this.user.perm.rename;
},
canDrop() {
if (!this.isDir || this.readOnly !== undefined) return false;
for (let i of this.selected) {
if (this.req.items[i].url === this.url) {
return false
return false;
}
}
return true
return true;
},
thumbnailUrl () {
const path = this.url.replace(/^\/files\//, '')
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
thumbnailUrl() {
const path = this.url.replace(/^\/files\//, "");
// reload the image when the file is replaced
const key = Date.parse(this.modified);
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true&k=${key}`;
},
isThumbsEnabled() {
return enableThumbs;
},
isThumbsEnabled () {
return enableThumbs
}
},
methods: {
...mapMutations(['addSelected', 'removeSelected', 'resetSelected']),
...mapMutations(["addSelected", "removeSelected", "resetSelected"]),
humanSize: function () {
return filesize(this.size)
return filesize(this.size);
},
humanTime: function () {
return moment(this.modified).fromNow()
return moment(this.modified).fromNow();
},
dragStart: function () {
if (this.selectedCount === 0) {
this.addSelected(this.index)
return
this.addSelected(this.index);
return;
}
if (!this.isSelected) {
this.resetSelected()
this.addSelected(this.index)
this.resetSelected();
this.addSelected(this.index);
}
},
dragOver: function (event) {
if (!this.canDrop) return
if (!this.canDrop) return;
event.preventDefault()
let el = event.target
event.preventDefault();
let el = event.target;
for (let i = 0; i < 5; i++) {
if (!el.classList.contains('item')) {
el = el.parentElement
if (!el.classList.contains("item")) {
el = el.parentElement;
}
}
el.style.opacity = 1
el.style.opacity = 1;
},
drop: async function (event) {
if (!this.canDrop) return
event.preventDefault()
if (!this.canDrop) return;
event.preventDefault();
if (this.selectedCount === 0) return
if (this.selectedCount === 0) return;
let el = event.target
let el = event.target;
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
el = el.parentElement
if (el !== null && !el.classList.contains("item")) {
el = el.parentElement;
}
}
let items = []
let items = [];
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
to: this.url + this.req.items[i].name,
name: this.req.items[i].name
})
}
name: this.req.items[i].name,
});
}
let base = el.querySelector('.name').innerHTML + '/'
let path = this.$route.path + base
let baseItems = (await api.fetch(path)).items
let base = el.querySelector(".name").innerHTML + "/";
let path = this.$route.path + base;
let baseItems = (await api.fetch(path)).items;
let action = (overwrite, rename) => {
api.move(items, overwrite, rename).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
}
api
.move(items, overwrite, rename)
.then(() => {
this.$store.commit("setReload", true);
})
.catch(this.$showError);
};
let conflict = upload.checkConflict(items, baseItems)
let conflict = upload.checkConflict(items, baseItems);
let overwrite = false
let rename = false
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return
return;
}
action(overwrite, rename)
action(overwrite, rename);
},
itemClick: function (event) {
if (this.singleClick && !this.$store.state.multiple) this.open();
else this.click(event);
},
click: function (event) {
if (this.selectedCount !== 0) event.preventDefault()
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault();
if (this.$store.state.selected.indexOf(this.index) !== -1) {
this.removeSelected(this.index)
return
this.removeSelected(this.index);
return;
}
if (event.shiftKey && this.selected.length > 0) {
let fi = 0
let la = 0
let fi = 0;
let la = 0;
if (this.index > this.selected[0]) {
fi = this.selected[0] + 1
la = this.index
fi = this.selected[0] + 1;
la = this.index;
} else {
fi = this.index
la = this.selected[0] - 1
fi = this.index;
la = this.selected[0] - 1;
}
for (; fi <= la; fi++) {
if (this.$store.state.selected.indexOf(fi) == -1) {
this.addSelected(fi)
this.addSelected(fi);
}
}
return
return;
}
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
if (
!this.singleClick &&
!event.ctrlKey &&
!event.metaKey &&
!this.$store.state.multiple
)
this.resetSelected();
this.addSelected(this.index);
},
touchstart () {
dblclick: function () {
if (!this.singleClick) this.open();
},
touchstart() {
setTimeout(() => {
this.touches = 0
}, 300)
this.touches = 0;
}, 300);
this.touches++
this.touches++;
if (this.touches > 1) {
this.open()
this.open();
}
},
open: function () {
this.$router.push({path: this.url})
}
}
}
</script>
this.$router.push({ path: this.url });
},
},
};
</script>

View File

@@ -1,226 +0,0 @@
<template>
<div id="previewer">
<div class="bar">
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
<i class="material-icons">close</i>
</button>
<div class="title">
<span>{{ this.name }}</span>
</div>
<preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
</button>
<div id="dropdown" :class="{ active : showMore }">
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
<info-button :disabled="loading"></info-button>
</div>
</div>
<div class="loading" v-if="loading">
<div class="spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
<i class="material-icons">chevron_left</i>
</button>
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<i class="material-icons">chevron_right</i>
</button>
<template v-if="!loading">
<div class="preview">
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
<track
kind="captions"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="'Subtitle ' + index" :default="index === 0">
Sorry, your browser doesn't support embedded videos,
but don't worry, you can <a :href="download">download it</a>
and watch it with your favorite video player!
</video>
<object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>
<a v-else-if="req.type == 'blob'" :href="download">
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
</a>
</div>
</template>
<div v-show="showMore" @click="resetPrompts" class="overlay"></div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import { baseURL, resizePreview } from '@/utils/constants'
import { files as api } from '@/api'
import PreviewSizeButton from '@/components/buttons/PreviewSize'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'
import DownloadButton from '@/components/buttons/Download'
import ExtendedImage from './ExtendedImage'
const mediaTypes = [
"image",
"video",
"audio",
"blob"
]
export default {
name: 'preview',
components: {
PreviewSizeButton,
InfoButton,
DeleteButton,
RenameButton,
DownloadButton,
ExtendedImage
},
data: function () {
return {
previousLink: '',
nextLink: '',
listing: null,
name: '',
subtitles: [],
fullSize: false
}
},
computed: {
...mapState(['req', 'user', 'oldReq', 'jwt', 'loading', 'show']),
hasPrevious () {
return (this.previousLink !== '')
},
hasNext () {
return (this.nextLink !== '')
},
download () {
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
},
previewUrl () {
if (this.req.type === 'image' && !this.fullSize) {
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
}
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
},
raw () {
return `${this.previewUrl}&inline=true`
},
showMore () {
return this.$store.state.show === 'more'
},
isResizeEnabled () {
return resizePreview
}
},
watch: {
$route: function () {
this.updatePreview()
}
},
async mounted () {
window.addEventListener('keyup', this.key)
this.$store.commit('setPreviewMode', true)
this.listing = this.oldReq.items
this.updatePreview()
},
beforeDestroy () {
window.removeEventListener('keyup', this.key)
this.$store.commit('setPreviewMode', false)
},
methods: {
back () {
this.$store.commit('setPreviewMode', false)
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
prev () {
this.$router.push({ path: this.previousLink })
},
next () {
this.$router.push({ path: this.nextLink })
},
key (event) {
event.preventDefault()
if (this.show !== null) {
return
}
if (event.which === 13 || event.which === 39) { // right arrow
if (this.hasNext) this.next()
} else if (event.which === 37) { // left arrow
if (this.hasPrevious) this.prev()
}
},
async updatePreview () {
if (this.req.subtitles) {
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
}
let dirs = this.$route.fullPath.split("/")
this.name = decodeURIComponent(dirs[dirs.length - 1])
if (!this.listing) {
try {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.listing = res.items
} catch (e) {
this.$showError(e)
}
}
this.previousLink = ''
this.nextLink = ''
for (let i = 0; i < this.listing.length; i++) {
if (this.listing[i].name !== this.name) {
continue
}
for (let j = i - 1; j >= 0; j--) {
if (mediaTypes.includes(this.listing[j].type)) {
this.previousLink = this.listing[j].url
break
}
}
for (let j = i + 1; j < this.listing.length; j++) {
if (mediaTypes.includes(this.listing[j].type)) {
this.nextLink = this.listing[j].url
break
}
}
return
}
},
openMore () {
this.$store.commit('showHover', 'more')
},
resetPrompts () {
this.$store.commit('closeHovers')
},
toggleSize () {
this.fullSize = !this.fullSize
}
}
}
</script>

View File

@@ -0,0 +1,25 @@
<template>
<button @click="action" :aria-label="label" :title="label" class="action">
<i class="material-icons">{{ icon }}</i>
<span>{{ label }}</span>
<span v-if="counter > 0" class="counter">{{ counter }}</span>
</button>
</template>
<script>
export default {
name: "action",
props: ["icon", "label", "counter", "show"],
methods: {
action: function () {
if (this.show) {
this.$store.commit("showHover", this.show);
}
this.$emit("action");
},
},
};
</script>
<style></style>

View File

@@ -0,0 +1,58 @@
<template>
<header>
<img v-if="showLogo !== undefined" :src="logoURL" />
<action
v-if="showMenu !== undefined"
class="menu-button"
icon="menu"
:label="$t('buttons.toggleSidebar')"
@action="openSidebar()"
/>
<slot />
<div id="dropdown" :class="{ active: this.$store.state.show === 'more' }">
<slot name="actions" />
</div>
<action
v-if="this.$slots.actions"
id="more"
icon="more_vert"
:label="$t('buttons.more')"
@action="$store.commit('showHover', 'more')"
/>
<div
class="overlay"
v-show="this.$store.state.show == 'more'"
@click="$store.commit('closeHovers')"
/>
</header>
</template>
<script>
import { logoURL } from "@/utils/constants";
import Action from "@/components/header/Action";
export default {
name: "header-bar",
props: ["showLogo", "showMenu"],
components: {
Action,
},
data: function () {
return {
logoURL,
};
},
methods: {
openSidebar() {
this.$store.commit("showHover", "sidebar");
},
},
};
</script>
<style></style>

View File

@@ -1,108 +1,119 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.copy') }}</h2>
<h2>{{ $t("prompts.copy") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.copyMessage') }}</p>
<file-list @update:selected="val => dest = val"></file-list>
<p>{{ $t("prompts.copyMessage") }}</p>
<file-list @update:selected="(val) => (dest = val)"></file-list>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
:title="$t('buttons.copy')"
>
{{ $t("buttons.copy") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
import * as upload from '@/utils/upload'
import { mapState } from "vuex";
import FileList from "./FileList";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
export default {
name: 'copy',
name: "copy",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null
}
dest: null,
};
},
computed: mapState(['req', 'selected']),
computed: mapState(["req", "selected"]),
methods: {
copy: async function (event) {
event.preventDefault()
let items = []
event.preventDefault();
let items = [];
// Create a new promise for each file.
for (let item of this.selected) {
items.push({
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
})
name: this.req.items[item].name,
});
}
let action = async (overwrite, rename) => {
buttons.loading('copy')
buttons.loading("copy");
await api.copy(items, overwrite, rename).then(() => {
buttons.success('copy')
await api
.copy(items, overwrite, rename)
.then(() => {
buttons.success("copy");
if (this.$route.path === this.dest) {
this.$store.commit('setReload', true)
if (this.$route.path === this.dest) {
this.$store.commit("setReload", true);
return
}
return;
}
this.$router.push({ path: this.dest })
}).catch((e) => {
buttons.done('copy')
this.$showError(e)
})
}
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("copy");
this.$showError(e);
});
};
if (this.$route.path === this.dest) {
this.$store.commit('closeHovers')
action(false, true)
this.$store.commit("closeHovers");
action(false, true);
return
return;
}
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
let dstItems = (await api.fetch(this.dest)).items;
let conflict = upload.checkConflict(items, dstItems);
let overwrite = false
let rename = false
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return
return;
}
action(overwrite, rename)
}
}
}
action(overwrite, rename);
},
},
};
</script>

View File

@@ -1,66 +1,80 @@
<template>
<div class="card floating">
<div class="card-content">
<p v-if="req.kind !== 'listing'">{{ $t('prompts.deleteMessageSingle') }}</p>
<p v-else>{{ $t('prompts.deleteMessageMultiple', { count: selectedCount}) }}</p>
<p v-if="req.kind !== 'listing'">
{{ $t("prompts.deleteMessageSingle") }}
</p>
<p v-else>
{{ $t("prompts.deleteMessageMultiple", { count: selectedCount }) }}
</p>
</div>
<div class="card-action">
<button @click="$store.commit('closeHovers')"
<button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
:title="$t('buttons.delete')"
>
{{ $t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script>
import {mapGetters, mapMutations, mapState} from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import buttons from '@/utils/buttons'
import { mapGetters, mapMutations, mapState } from "vuex";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
export default {
name: 'delete',
name: "delete",
computed: {
...mapGetters(['isListing', 'selectedCount']),
...mapState(['req', 'selected'])
...mapGetters(["isListing", "selectedCount"]),
...mapState(["req", "selected", "showConfirm"]),
},
methods: {
...mapMutations(['closeHovers']),
...mapMutations(["closeHovers"]),
submit: async function () {
this.closeHovers()
buttons.loading('delete')
buttons.loading("delete");
try {
if (!this.isListing) {
await api.remove(this.$route.path)
buttons.success('delete')
this.$router.push({ path: url.removeLastDir(this.$route.path) + '/' })
return
await api.remove(this.$route.path);
buttons.success("delete");
this.showConfirm();
this.closeHovers();
return;
}
this.closeHovers();
if (this.selectedCount === 0) {
return
return;
}
let promises = []
let promises = [];
for (let index of this.selected) {
promises.push(api.remove(this.req.items[index].url))
promises.push(api.remove(this.req.items[index].url));
}
await Promise.all(promises)
buttons.success('delete')
this.$store.commit('setReload', true)
await Promise.all(promises);
buttons.success("delete");
this.$store.commit("setReload", true);
} catch (e) {
buttons.done('delete')
this.$showError(e)
if (this.isListing) this.$store.commit('setReload', true)
buttons.done("delete");
this.$showError(e);
if (this.isListing) this.$store.commit("setReload", true);
}
}
}
}
},
},
};
</script>

View File

@@ -1,49 +1,43 @@
<template>
<div class="card floating" id="download">
<div class="card-title">
<h2>{{ $t('prompts.download') }}</h2>
<h2>{{ $t("prompts.download") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.downloadMessage') }}</p>
<p>{{ $t("prompts.downloadMessage") }}</p>
<button class="button button--block" @click="download('zip')" v-focus>zip</button>
<button class="button button--block" @click="download('tar')" v-focus>tar</button>
<button class="button button--block" @click="download('targz')" v-focus>tar.gz</button>
<button class="button button--block" @click="download('tarbz2')" v-focus>tar.bz2</button>
<button class="button button--block" @click="download('tarxz')" v-focus>tar.xz</button>
<button class="button button--block" @click="download('tarlz4')" v-focus>tar.lz4</button>
<button class="button button--block" @click="download('tarsz')" v-focus>tar.sz</button>
<button
v-for="(ext, format) in formats"
:key="format"
class="button button--block"
@click="showConfirm(format)"
v-focus
>
{{ ext }}
</button>
</div>
</div>
</template>
<script>
import {mapGetters, mapState} from 'vuex'
import { files as api } from '@/api'
import { mapState } from "vuex";
export default {
name: 'download',
computed: {
...mapState(['selected', 'req']),
...mapGetters(['selectedCount'])
name: "download",
data: function () {
return {
formats: {
zip: "zip",
tar: "tar",
targz: "tar.gz",
tarbz2: "tar.bz2",
tarxz: "tar.xz",
tarlz4: "tar.lz4",
tarsz: "tar.sz",
},
};
},
methods: {
download: function (format) {
if (this.selectedCount === 0) {
api.download(format, this.$route.path)
} else {
let files = []
for (let i of this.selected) {
files.push(this.req.items[i].url)
}
api.download(format, ...files)
}
this.$store.commit('closeHovers')
}
}
}
computed: mapState(["showConfirm"]),
};
</script>

View File

@@ -1,128 +1,138 @@
<template>
<div>
<ul class="file-list">
<li @click="select"
<li
@click="itemClick"
@touchstart="touchstart"
@dblclick="next"
role="button"
tabindex="0"
:aria-label="item.name"
:aria-selected="selected == item.url"
:key="item.name" v-for="item in items"
:data-url="item.url">{{ item.name }}</li>
:key="item.name"
v-for="item in items"
:data-url="item.url"
>
{{ item.name }}
</li>
</ul>
<p>{{ $t('prompts.currentlyNavigating') }} <code>{{ nav }}</code>.</p>
<p>
{{ $t("prompts.currentlyNavigating") }} <code>{{ nav }}</code
>.
</p>
</div>
</template>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import { files } from '@/api'
import { mapState } from "vuex";
import url from "@/utils/url";
import { files } from "@/api";
export default {
name: 'file-list',
name: "file-list",
data: function () {
return {
items: [],
touches: {
id: '',
count: 0
id: "",
count: 0,
},
selected: null,
current: window.location.pathname
}
current: window.location.pathname,
};
},
computed: {
...mapState([ 'req' ]),
nav () {
return decodeURIComponent(this.current)
}
...mapState(["req", "user"]),
nav() {
return decodeURIComponent(this.current);
},
},
mounted () {
this.fillOptions(this.req)
mounted() {
this.fillOptions(this.req);
},
methods: {
fillOptions (req) {
fillOptions(req) {
// Sets the current path and resets
// the current items.
this.current = req.url
this.items = []
this.current = req.url;
this.items = [];
this.$emit('update:selected', this.current)
this.$emit("update:selected", this.current);
// If the path isn't the root path,
// show a button to navigate to the previous
// directory.
if (req.url !== '/files/') {
if (req.url !== "/files/") {
this.items.push({
name: '..',
url: url.removeLastDir(req.url) + '/'
})
name: "..",
url: url.removeLastDir(req.url) + "/",
});
}
// If this folder is empty, finish here.
if (req.items === null) return
if (req.items === null) return;
// Otherwise we add every directory to the
// move options.
for (let item of req.items) {
if (!item.isDir) continue
if (!item.isDir) continue;
this.items.push({
name: item.name,
url: item.url
})
url: item.url,
});
}
},
next: function (event) {
// Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
let uri = event.currentTarget.dataset.url
let uri = event.currentTarget.dataset.url;
files.fetch(uri)
.then(this.fillOptions)
.catch(this.$showError)
files.fetch(uri).then(this.fillOptions).catch(this.$showError);
},
touchstart (event) {
let url = event.currentTarget.dataset.url
touchstart(event) {
let url = event.currentTarget.dataset.url;
// In 300 milliseconds, we shall reset the count.
setTimeout(() => {
this.touches.count = 0
}, 300)
this.touches.count = 0;
}, 300);
// If the element the user is touching
// is different from the last one he touched,
// reset the count.
if (this.touches.id !== url) {
this.touches.id = url
this.touches.count = 1
return
this.touches.id = url;
this.touches.count = 1;
return;
}
this.touches.count++
this.touches.count++;
// If there is more than one touch already,
// open the next screen.
if (this.touches.count > 1) {
this.next(event)
this.next(event);
}
},
itemClick: function (event) {
if (this.user.singleClick) this.next(event);
else this.select(event);
},
select: function (event) {
// If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) {
this.selected = null
this.$emit('update:selected', this.current)
return
this.selected = null;
this.$emit("update:selected", this.current);
return;
}
// Otherwise select the element.
this.selected = event.currentTarget.dataset.url
this.$emit('update:selected', this.selected)
}
}
}
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
},
},
};
</script>

View File

@@ -1,34 +1,37 @@
<template>
<div class="card floating help">
<div class="card-title">
<h2>{{ $t('help.help') }}</h2>
<h2>{{ $t("help.help") }}</h2>
</div>
<div class="card-content">
<ul>
<li><strong>F1</strong> - {{ $t('help.f1') }}</li>
<li><strong>F2</strong> - {{ $t('help.f2') }}</li>
<li><strong>DEL</strong> - {{ $t('help.del') }}</li>
<li><strong>ESC</strong> - {{ $t('help.esc') }}</li>
<li><strong>CTRL + S</strong> - {{ $t('help.ctrl.s') }}</li>
<li><strong>CTRL + F</strong> - {{ $t('help.ctrl.f') }}</li>
<li><strong>CTRL + Click</strong> - {{ $t('help.ctrl.click') }}</li>
<li><strong>Click</strong> - {{ $t('help.click') }}</li>
<li><strong>Double click</strong> - {{ $t('help.doubleClick') }}</li>
<li><strong>F1</strong> - {{ $t("help.f1") }}</li>
<li><strong>F2</strong> - {{ $t("help.f2") }}</li>
<li><strong>DEL</strong> - {{ $t("help.del") }}</li>
<li><strong>ESC</strong> - {{ $t("help.esc") }}</li>
<li><strong>CTRL + S</strong> - {{ $t("help.ctrl.s") }}</li>
<li><strong>CTRL + F</strong> - {{ $t("help.ctrl.f") }}</li>
<li><strong>CTRL + Click</strong> - {{ $t("help.ctrl.click") }}</li>
<li><strong>Click</strong> - {{ $t("help.click") }}</li>
<li><strong>Double click</strong> - {{ $t("help.doubleClick") }}</li>
</ul>
</div>
<div class="card-action">
<button type="submit"
<button
type="submit"
@click="$store.commit('closeHovers')"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
:title="$t('buttons.ok')"
>
{{ $t("buttons.ok") }}
</button>
</div>
</div>
</template>
<script>
export default { name: 'help' }
export default { name: "help" };
</script>

View File

@@ -1,99 +1,149 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.fileInfo') }}</h2>
<h2>{{ $t("prompts.fileInfo") }}</h2>
</div>
<div class="card-content">
<p v-if="selected.length > 1">{{ $t('prompts.filesSelected', { count: selected.length }) }}</p>
<p v-if="selected.length > 1">
{{ $t("prompts.filesSelected", { count: selected.length }) }}
</p>
<p class="break-word" v-if="selected.length < 2"><strong>{{ $t('prompts.displayName') }}</strong> {{ name }}</p>
<p v-if="!dir || selected.length > 1"><strong>{{ $t('prompts.size') }}:</strong> <span id="content_length"></span> {{ humanSize }}</p>
<p v-if="selected.length < 2"><strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}</p>
<p class="break-word" v-if="selected.length < 2">
<strong>{{ $t("prompts.displayName") }}</strong> {{ name }}
</p>
<p v-if="!dir || selected.length > 1">
<strong>{{ $t("prompts.size") }}:</strong>
<span id="content_length"></span> {{ humanSize }}
</p>
<p v-if="selected.length < 2">
<strong>{{ $t("prompts.lastModified") }}:</strong> {{ humanTime }}
</p>
<template v-if="dir && selected.length === 0">
<p><strong>{{ $t('prompts.numberFiles') }}:</strong> {{ req.numFiles }}</p>
<p><strong>{{ $t('prompts.numberDirs') }}:</strong> {{ req.numDirs }}</p>
<p>
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
</p>
<p>
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
</p>
</template>
<template v-if="!dir">
<p><strong>MD5: </strong><code><a @click="checksum($event, 'md5')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA1: </strong><code><a @click="checksum($event, 'sha1')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA256: </strong><code><a @click="checksum($event, 'sha256')">{{ $t('prompts.show') }}</a></code></p>
<p><strong>SHA512: </strong><code><a @click="checksum($event, 'sha512')">{{ $t('prompts.show') }}</a></code></p>
<p>
<strong>MD5: </strong
><code
><a @click="checksum($event, 'md5')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA1: </strong
><code
><a @click="checksum($event, 'sha1')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA256: </strong
><code
><a @click="checksum($event, 'sha256')">{{
$t("prompts.show")
}}</a></code
>
</p>
<p>
<strong>SHA512: </strong
><code
><a @click="checksum($event, 'sha512')">{{
$t("prompts.show")
}}</a></code
>
</p>
</template>
</div>
<div class="card-action">
<button type="submit"
<button
type="submit"
@click="$store.commit('closeHovers')"
class="button button--flat"
:aria-label="$t('buttons.ok')"
:title="$t('buttons.ok')">{{ $t('buttons.ok') }}</button>
:title="$t('buttons.ok')"
>
{{ $t("buttons.ok") }}
</button>
</div>
</div>
</template>
<script>
import {mapState, mapGetters} from 'vuex'
import filesize from 'filesize'
import moment from 'moment'
import { files as api } from '@/api'
import { mapState, mapGetters } from "vuex";
import filesize from "filesize";
import moment from "moment";
import { files as api } from "@/api";
export default {
name: 'info',
name: "info",
computed: {
...mapState(['req', 'selected']),
...mapGetters(['selectedCount', 'isListing']),
...mapState(["req", "selected"]),
...mapGetters(["selectedCount", "isListing"]),
humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size)
return filesize(this.req.size);
}
let sum = 0
let sum = 0;
for (let selected of this.selected) {
sum += this.req.items[selected].size
sum += this.req.items[selected].size;
}
return filesize(sum)
return filesize(sum);
},
humanTime: function () {
if (this.selectedCount === 0) {
return moment(this.req.modified).fromNow()
return moment(this.req.modified).fromNow();
}
return moment(this.req.items[this.selected[0]]).fromNow()
return moment(this.req.items[this.selected[0]].modified).fromNow();
},
name: function () {
return this.selectedCount === 0 ? this.req.name : this.req.items[this.selected[0]].name
return this.selectedCount === 0
? this.req.name
: this.req.items[this.selected[0]].name;
},
dir: function () {
return this.selectedCount > 1 || (this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
}
return (
this.selectedCount > 1 ||
(this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
);
},
},
methods: {
checksum: async function (event, algo) {
event.preventDefault()
event.preventDefault();
let link
let link;
if (this.selectedCount) {
link = this.req.items[this.selected[0]].url
link = this.req.items[this.selected[0]].url;
} else {
link = this.$route.path
link = this.$route.path;
}
try {
const hash = await api.checksum(link, algo)
const hash = await api.checksum(link, algo);
// eslint-disable-next-line
event.target.innerHTML = hash
} catch (e) {
this.$showError(e)
this.$showError(e);
}
}
}
}
},
},
};
</script>

View File

@@ -1,93 +1,104 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.move') }}</h2>
<h2>{{ $t("prompts.move") }}</h2>
</div>
<div class="card-content">
<file-list @update:selected="val => dest = val"></file-list>
<file-list @update:selected="(val) => (dest = val)"></file-list>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="move"
:disabled="$route.path === dest"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')">{{ $t('buttons.move') }}</button>
:title="$t('buttons.move')"
>
{{ $t("buttons.move") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import FileList from './FileList'
import { files as api } from '@/api'
import buttons from '@/utils/buttons'
import * as upload from '@/utils/upload'
import { mapState } from "vuex";
import FileList from "./FileList";
import { files as api } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
export default {
name: 'move',
name: "move",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null
}
dest: null,
};
},
computed: mapState(['req', 'selected']),
computed: mapState(["req", "selected"]),
methods: {
move: async function (event) {
event.preventDefault()
let items = []
event.preventDefault();
let items = [];
for (let item of this.selected) {
items.push({
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
})
name: this.req.items[item].name,
});
}
let action = async (overwrite, rename) => {
buttons.loading('move')
buttons.loading("move");
await api.move(items, overwrite, rename).then(() => {
buttons.success('move')
this.$router.push({ path: this.dest })
}).catch((e) => {
buttons.done('move')
this.$showError(e)
})
}
await api
.move(items, overwrite, rename)
.then(() => {
buttons.success("move");
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("move");
this.$showError(e);
});
};
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
let dstItems = (await api.fetch(this.dest)).items;
let conflict = upload.checkConflict(items, dstItems);
let overwrite = false
let rename = false
let overwrite = false;
let rename = false;
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace-rename',
this.$store.commit("showHover", {
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == 'overwrite'
rename = option == 'rename'
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault()
this.$store.commit('closeHovers')
action(overwrite, rename)
}
})
event.preventDefault();
this.$store.commit("closeHovers");
action(overwrite, rename);
},
});
return
return;
}
action(overwrite, rename)
}
}
}
action(overwrite, rename);
},
},
};
</script>

View File

@@ -1,12 +1,18 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newDir') }}</h2>
<h2>{{ $t("prompts.newDir") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newDirMessage') }}</p>
<input class="input input--block" type="text" @keyup.enter="submit" v-model.trim="name" v-focus>
<p>{{ $t("prompts.newDirMessage") }}</p>
<input
class="input input--block"
type="text"
@keyup.enter="submit"
v-model.trim="name"
v-focus
/>
</div>
<div class="card-action">
@@ -15,57 +21,60 @@
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>{{ $t('buttons.cancel') }}</button>
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
@click="submit"
>{{ $t('buttons.create') }}</button>
>
{{ $t("buttons.create") }}
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import { mapGetters } from "vuex";
import { files as api } from "@/api";
import url from "@/utils/url";
export default {
name: 'new-dir',
data: function() {
name: "new-dir",
data: function () {
return {
name: ''
name: "",
};
},
computed: {
...mapGetters([ 'isFiles', 'isListing' ])
...mapGetters(["isFiles", "isListing"]),
},
methods: {
submit: async function(event) {
event.preventDefault()
if (this.new === '') return
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + '/' : '/'
let uri = this.isFiles ? this.$route.path + "/" : "/";
if (!this.isListing) {
uri = url.removeLastDir(uri) + '/'
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(this.name) + '/'
uri = uri.replace('//', '/')
uri += encodeURIComponent(this.name) + "/";
uri = uri.replace("//", "/");
try {
await api.post(uri)
this.$router.push({ path: uri })
await api.post(uri);
this.$router.push({ path: uri });
} catch (e) {
this.$showError(e)
this.$showError(e);
}
this.$store.commit('closeHovers')
}
}
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -1,12 +1,18 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.newFile') }}</h2>
<h2>{{ $t("prompts.newFile") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.newFileMessage') }}</p>
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name">
<p>{{ $t("prompts.newFileMessage") }}</p>
<input
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
/>
</div>
<div class="card-action">
@@ -15,57 +21,60 @@
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>{{ $t('buttons.cancel') }}</button>
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"
>{{ $t('buttons.create') }}</button>
>
{{ $t("buttons.create") }}
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import { mapGetters } from "vuex";
import { files as api } from "@/api";
import url from "@/utils/url";
export default {
name: 'new-file',
data: function() {
name: "new-file",
data: function () {
return {
name: ''
name: "",
};
},
computed: {
...mapGetters([ 'isFiles', 'isListing' ])
...mapGetters(["isFiles", "isListing"]),
},
methods: {
submit: async function(event) {
event.preventDefault()
if (this.new === '') return
submit: async function (event) {
event.preventDefault();
if (this.new === "") return;
// Build the path of the new directory.
let uri = this.isFiles ? this.$route.path + '/' : '/'
let uri = this.isFiles ? this.$route.path + "/" : "/";
if (!this.isListing) {
uri = url.removeLastDir(uri) + '/'
uri = url.removeLastDir(uri) + "/";
}
uri += encodeURIComponent(this.name)
uri = uri.replace('//', '/')
uri += encodeURIComponent(this.name);
uri = uri.replace("//", "/");
try {
await api.post(uri)
this.$router.push({ path: uri })
await api.post(uri);
this.$router.push({ path: uri });
} catch (e) {
this.$showError(e)
this.$showError(e);
}
this.$store.commit('closeHovers')
}
}
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -6,24 +6,25 @@
</template>
<script>
import Help from './Help'
import Info from './Info'
import Delete from './Delete'
import Rename from './Rename'
import Download from './Download'
import Move from './Move'
import Copy from './Copy'
import NewFile from './NewFile'
import NewDir from './NewDir'
import Replace from './Replace'
import ReplaceRename from './ReplaceRename'
import Share from './Share'
import Upload from './Upload'
import { mapState } from 'vuex'
import buttons from '@/utils/buttons'
import Help from "./Help";
import Info from "./Info";
import Delete from "./Delete";
import Rename from "./Rename";
import Download from "./Download";
import Move from "./Move";
import Copy from "./Copy";
import NewFile from "./NewFile";
import NewDir from "./NewDir";
import Replace from "./Replace";
import ReplaceRename from "./ReplaceRename";
import Share from "./Share";
import Upload from "./Upload";
import ShareDelete from "./ShareDelete";
import { mapState } from "vuex";
import buttons from "@/utils/buttons";
export default {
name: 'prompts',
name: "prompts",
components: {
Info,
Delete,
@@ -37,73 +38,76 @@ export default {
Help,
Replace,
ReplaceRename,
Upload
Upload,
ShareDelete,
},
data: function () {
return {
pluginData: {
buttons,
'store': this.$store,
'router': this.$router
}
}
store: this.$store,
router: this.$router,
},
};
},
created () {
window.addEventListener('keydown', (event) => {
if (this.show == null)
return
created() {
window.addEventListener("keydown", (event) => {
if (this.show == null) return;
let prompt = this.$refs.currentComponent;
// Enter
if (event.keyCode == 13) {
switch (this.show) {
case 'delete':
prompt.submit()
case "delete":
prompt.submit();
break;
case 'copy':
prompt.copy(event)
case "copy":
prompt.copy(event);
break;
case 'move':
prompt.move(event)
case "move":
prompt.move(event);
break;
case 'replace':
prompt.showConfirm(event)
case "replace":
prompt.showConfirm(event);
break;
}
}
})
});
},
computed: {
...mapState(['show', 'plugins']),
...mapState(["show", "plugins"]),
currentComponent: function () {
const matched = [
'info',
'help',
'delete',
'rename',
'move',
'copy',
'newFile',
'newDir',
'download',
'replace',
'replace-rename',
'share',
'upload'
].indexOf(this.show) >= 0;
const matched =
[
"info",
"help",
"delete",
"rename",
"move",
"copy",
"newFile",
"newDir",
"download",
"replace",
"replace-rename",
"share",
"upload",
"share-delete",
].indexOf(this.show) >= 0;
return matched && this.show || null;
return (matched && this.show) || null;
},
showOverlay: function () {
return (this.show !== null && this.show !== 'search' && this.show !== 'more')
}
return (
this.show !== null && this.show !== "search" && this.show !== "more"
);
},
},
methods: {
resetPrompts () {
this.$store.commit('closeHovers')
}
}
}
resetPrompts() {
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -1,89 +1,107 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.rename') }}</h2>
<h2>{{ $t("prompts.rename") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.renameMessage') }} <code>{{ oldName() }}</code>:</p>
<input class="input input--block" v-focus type="text" @keyup.enter="submit" v-model.trim="name">
<p>
{{ $t("prompts.renameMessage") }} <code>{{ oldName() }}</code
>:
</p>
<input
class="input input--block"
v-focus
type="text"
@keyup.enter="submit"
v-model.trim="name"
/>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button @click="submit"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat"
type="submit"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
:title="$t('buttons.rename')"
>
{{ $t("buttons.rename") }}
</button>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import url from '@/utils/url'
import { files as api } from '@/api'
import { mapState, mapGetters } from "vuex";
import url from "@/utils/url";
import { files as api } from "@/api";
export default {
name: 'rename',
name: "rename",
data: function () {
return {
name: ''
}
name: "",
};
},
created () {
this.name = this.oldName()
created() {
this.name = this.oldName();
},
computed: {
...mapState(['req', 'selected', 'selectedCount']),
...mapGetters(['isListing'])
...mapState(["req", "selected", "selectedCount"]),
...mapGetters(["isListing"]),
},
methods: {
cancel: function () {
this.$store.commit('closeHovers')
this.$store.commit("closeHovers");
},
oldName: function () {
if (!this.isListing) {
return this.req.name
return this.req.name;
}
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return
return;
}
return this.req.items[this.selected[0]].name
return this.req.items[this.selected[0]].name;
},
submit: async function () {
let oldLink = ''
let newLink = ''
let oldLink = "";
let newLink = "";
if (!this.isListing) {
oldLink = this.req.url
oldLink = this.req.url;
} else {
oldLink = this.req.items[this.selected[0]].url
oldLink = this.req.items[this.selected[0]].url;
}
newLink = url.removeLastDir(oldLink) + '/' + encodeURIComponent(this.name)
newLink =
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
try {
await api.move([{ from: oldLink, to: newLink }])
await api.move([{ from: oldLink, to: newLink }]);
if (!this.isListing) {
this.$router.push({ path: newLink })
return
this.$router.push({ path: newLink });
return;
}
this.$store.commit('setReload', true)
this.$store.commit("setReload", true);
} catch (e) {
this.$showError(e)
this.$showError(e);
}
this.$store.commit('closeHovers')
}
}
}
this.$store.commit("closeHovers");
},
},
};
</script>

View File

@@ -1,31 +1,39 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2>
<h2>{{ $t("prompts.replace") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p>
<p>{{ $t("prompts.replaceMessage") }}</p>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat button--red"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--red"
@click="showConfirm"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
:title="$t('buttons.replace')"
>
{{ $t("buttons.replace") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { mapState } from "vuex";
export default {
name: 'replace',
computed: mapState(['showConfirm'])
}
name: "replace",
computed: mapState(["showConfirm"]),
};
</script>

View File

@@ -1,35 +1,47 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2>
<h2>{{ $t("prompts.replace") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p>
<p>{{ $t("prompts.replaceMessage") }}</p>
</div>
<div class="card-action">
<button class="button button--flat button--grey"
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat button--blue"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="(event) => showConfirm(event, 'rename')"
:aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')">{{ $t('buttons.rename') }}</button>
<button class="button button--flat button--red"
:title="$t('buttons.rename')"
>
{{ $t("buttons.rename") }}
</button>
<button
class="button button--flat button--red"
@click="(event) => showConfirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
:title="$t('buttons.replace')"
>
{{ $t("buttons.replace") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { mapState } from "vuex";
export default {
name: 'replace-rename',
computed: mapState(['showConfirm'])
}
name: "replace-rename",
computed: mapState(["showConfirm"]),
};
</script>

View File

@@ -1,167 +1,235 @@
<template>
<div class="card floating" id="share">
<div class="card floating share__promt__card" id="share">
<div class="card-title">
<h2>{{ $t('buttons.share') }}</h2>
<h2>{{ $t("buttons.share") }}</h2>
</div>
<div class="card-content">
<ul>
<li v-if="!hasPermanent">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
</li>
<template v-if="listing">
<div class="card-content">
<table>
<tr>
<th>#</th>
<th>{{ $t("settings.shareDuration") }}</th>
<th></th>
<th></th>
</tr>
<li v-for="link in links" :key="link.hash">
<a :href="buildLink(link.hash)" target="_blank">
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
<template v-else>{{ $t('permanent') }}</template>
</a>
<tr v-for="link in links" :key="link.hash">
<td>{{ link.hash }}</td>
<td>
<template v-if="link.expire !== 0">{{
humanTime(link.expire)
}}</template>
<template v-else>{{ $t("permanent") }}</template>
</td>
<td class="small">
<button
class="action copy-clipboard"
:data-clipboard-text="buildLink(link.hash)"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"
>
<i class="material-icons">content_paste</i>
</button>
</td>
<td class="small">
<button
class="action"
@click="deleteLink($event, link)"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"
>
<i class="material-icons">delete</i>
</button>
</td>
</tr>
</table>
</div>
<button class="action"
@click="deleteLink($event, link)"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"><i class="material-icons">delete</i></button>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')"
>
{{ $t("buttons.close") }}
</button>
<button
class="button button--flat button--blue"
@click="() => switchListing()"
:aria-label="$t('buttons.new')"
:title="$t('buttons.new')"
>
{{ $t("buttons.new") }}
</button>
</div>
</template>
<button class="action copy-clipboard"
:data-clipboard-text="buildLink(link.hash)"
:aria-label="$t('buttons.copyToClipboard')"
:title="$t('buttons.copyToClipboard')"><i class="material-icons">content_paste</i></button>
</li>
<li>
<input v-focus
<template v-else>
<div class="card-content">
<p>{{ $t("settings.shareDuration") }}</p>
<div class="input-group input">
<input
v-focus
type="number"
max="2147483647"
min="0"
min="1"
@keyup.enter="submit"
v-model.trim="time">
<select v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t('time.seconds') }}</option>
<option value="minutes">{{ $t('time.minutes') }}</option>
<option value="hours">{{ $t('time.hours') }}</option>
<option value="days">{{ $t('time.days') }}</option>
v-model.trim="time"
/>
<select class="right" v-model="unit" :aria-label="$t('time.unit')">
<option value="seconds">{{ $t("time.seconds") }}</option>
<option value="minutes">{{ $t("time.minutes") }}</option>
<option value="hours">{{ $t("time.hours") }}</option>
<option value="days">{{ $t("time.days") }}</option>
</select>
<button class="action"
@click="submit"
:aria-label="$t('buttons.create')"
:title="$t('buttons.create')"><i class="material-icons">add</i></button>
</li>
</ul>
</div>
</div>
<p>{{ $t("prompts.optionalPassword") }}</p>
<input
class="input input--block"
type="password"
v-model.trim="password"
/>
</div>
<div class="card-action">
<button class="button button--flat"
@click="$store.commit('closeHovers')"
:aria-label="$t('buttons.close')"
:title="$t('buttons.close')">{{ $t('buttons.close') }}</button>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="() => switchListing()"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="submit"
:aria-label="$t('buttons.share')"
:title="$t('buttons.share')"
>
{{ $t("buttons.share") }}
</button>
</div>
</template>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import { share as api } from '@/api'
import { baseURL } from '@/utils/constants'
import moment from 'moment'
import Clipboard from 'clipboard'
import { mapState, mapGetters } from "vuex";
import { share as api } from "@/api";
import { baseURL } from "@/utils/constants";
import moment from "moment";
import Clipboard from "clipboard";
export default {
name: 'share',
name: "share",
data: function () {
return {
time: '',
unit: 'hours',
hasPermanent: false,
time: "",
unit: "hours",
links: [],
clip: null
}
clip: null,
password: "",
listing: true,
};
},
computed: {
...mapState([ 'req', 'selected', 'selectedCount' ]),
...mapGetters([ 'isListing' ]),
url () {
...mapState(["req", "selected", "selectedCount"]),
...mapGetters(["isListing"]),
url() {
if (!this.isListing) {
return this.$route.path
return this.$route.path;
}
if (this.selectedCount === 0 || this.selectedCount > 1) {
// This shouldn't happen.
return
return;
}
return this.req.items[this.selected[0]].url
}
return this.req.items[this.selected[0]].url;
},
},
async beforeMount () {
async beforeMount() {
try {
const links = await api.get(this.url)
this.links = links
this.sort()
const links = await api.get(this.url);
this.links = links;
this.sort();
for (let link of this.links) {
if (link.expire === 0) {
this.hasPermanent = true
break
}
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
mounted () {
this.clip = new Clipboard('.copy-clipboard')
this.clip.on('success', () => {
this.$showSuccess(this.$t('success.linkCopied'))
})
mounted() {
this.clip = new Clipboard(".copy-clipboard");
this.clip.on("success", () => {
this.$showSuccess(this.$t("success.linkCopied"));
});
},
beforeDestroy () {
this.clip.destroy()
beforeDestroy() {
this.clip.destroy();
},
methods: {
submit: async function () {
if (!this.time) return
let isPermanent = !this.time || this.time == 0;
try {
const res = await api.create(this.url, this.time, this.unit)
this.links.push(res)
this.sort()
let res = null;
if (isPermanent) {
res = await api.create(this.url, this.password);
} else {
res = await api.create(this.url, this.password, this.time, this.unit);
}
this.links.push(res);
this.sort();
this.time = "";
this.unit = "hours";
this.password = "";
this.listing = true;
} catch (e) {
this.$showError(e)
}
},
getPermalink: async function () {
try {
const res = await api.create(this.url)
this.links.push(res)
this.sort()
this.hasPermanent = true
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
deleteLink: async function (event, link) {
event.preventDefault()
try {
await api.remove(link.hash)
if (link.expire === 0) this.hasPermanent = false
this.links = this.links.filter(item => item.hash !== link.hash)
event.preventDefault();
try {
await api.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e)
this.$showError(e);
}
},
humanTime (time) {
return moment(time * 1000).fromNow()
humanTime(time) {
return moment(time * 1000).fromNow();
},
buildLink (hash) {
return `${window.location.origin}${baseURL}/share/${hash}`
buildLink(hash) {
return `${window.location.origin}${baseURL}/share/${hash}`;
},
sort () {
sort() {
this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1
if (b.expire === 0) return 1
return new Date(a.expire) - new Date(b.expire)
})
}
}
}
</script>
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire);
});
},
switchListing() {
if (this.links.length == 0 && !this.listing) {
this.$store.commit("closeHovers");
}
this.listing = !this.listing;
},
},
};
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div class="card floating">
<div class="card-content">
<p>{{ $t("prompts.deleteMessageShare", { path: "" }) }}</p>
</div>
<div class="card-action">
<button
@click="$store.commit('closeHovers')"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
@click="submit"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')"
>
{{ $t("buttons.delete") }}
</button>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "share-delete",
computed: {
...mapState(["showConfirm"]),
},
methods: {
submit: function () {
this.showConfirm();
},
},
};
</script>

View File

@@ -1,11 +1,11 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.upload') }}</h2>
<h2>{{ $t("prompts.upload") }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.uploadMessage') }}</p>
<p>{{ $t("prompts.uploadMessage") }}</p>
</div>
<div class="card-action full">
@@ -22,18 +22,17 @@
</template>
<script>
export default {
name: 'upload',
name: "upload",
methods: {
uploadFile: function () {
document.getElementById('upload-input').value = ''
document.getElementById('upload-input').click()
document.getElementById("upload-input").value = "";
document.getElementById("upload-input").click();
},
uploadFolder: function () {
document.getElementById('upload-folder-input').value = ''
document.getElementById('upload-folder-input').click()
}
}
}
document.getElementById("upload-folder-input").value = "";
document.getElementById("upload-folder-input").click();
},
},
};
</script>

View File

@@ -1,28 +1,30 @@
<template>
<div>
<h3>{{ $t('settings.userCommands') }}</h3>
<p class="small">{{ $t('settings.userCommandsHelp') }} <i>git svn hg</i>.</p>
<input class="input input--block" type="text" v-model.trim="raw">
<h3>{{ $t("settings.userCommands") }}</h3>
<p class="small">
{{ $t("settings.userCommandsHelp") }} <i>git svn hg</i>.
</p>
<input class="input input--block" type="text" v-model.trim="raw" />
</div>
</template>
<script>
export default {
name: 'permissions',
props: ['commands'],
name: "permissions",
props: ["commands"],
computed: {
raw: {
get () {
return this.commands.join(' ')
get() {
return this.commands.join(" ");
},
set (value) {
if (value !== '') {
this.$emit('update:commands', value.split(' '))
set(value) {
if (value !== "") {
this.$emit("update:commands", value.split(" "));
} else {
this.$emit('update:commands', [])
this.$emit("update:commands", []);
}
}
}
}
}
},
},
},
};
</script>

View File

@@ -1,46 +1,50 @@
<template>
<select v-on:change="change" :value="locale">
<option v-for="(language, value) in locales" :key="value" :value="value">{{ $t('languages.' + language) }}</option>
<option v-for="(language, value) in locales" :key="value" :value="value">
{{ $t("languages." + language) }}
</option>
</select>
</template>
<script>
export default {
name: 'languages',
props: [ 'locale' ],
name: "languages",
props: ["locale"],
data() {
let dataObj = {
locales: {
ar: 'ar',
de: 'de',
en: 'en',
es: 'es',
fr: 'fr',
is: 'is',
it: 'it',
ja: 'ja',
ko: 'ko',
'nl-be': 'nlBE',
pl: 'pl',
'pt-br': 'ptBR',
pt: 'pt',
ro: 'ro',
ru: 'ru',
'sv-se': 'svSE',
'zh-cn': 'zhCN',
'zh-tw': 'zhTW'
}
ar: "ar",
de: "de",
en: "en",
es: "es",
fr: "fr",
is: "is",
it: "it",
ja: "ja",
ko: "ko",
"nl-be": "nlBE",
pl: "pl",
"pt-br": "ptBR",
pt: "pt",
ro: "ro",
ru: "ru",
"sv-se": "svSE",
"zh-cn": "zhCN",
"zh-tw": "zhTW",
},
};
Object.defineProperty(dataObj, "locales", { configurable: false, writable: false });
Object.defineProperty(dataObj, "locales", {
configurable: false,
writable: false,
});
return dataObj;
},
methods: {
change (event) {
this.$emit('update:locale', event.target.value)
}
}
}
change(event) {
this.$emit("update:locale", event.target.value);
},
},
};
</script>

View File

@@ -1,41 +1,65 @@
<template>
<div>
<h3>{{ $t('settings.permissions') }}</h3>
<p class="small">{{ $t('settings.permissionsHelp') }}</p>
<h3>{{ $t("settings.permissions") }}</h3>
<p class="small">{{ $t("settings.permissionsHelp") }}</p>
<p><input type="checkbox" v-model="admin"> {{ $t('settings.administrator') }}</p>
<p>
<input type="checkbox" v-model="admin" />
{{ $t("settings.administrator") }}
</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.create"> {{ $t('settings.perm.create') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.delete"> {{ $t('settings.perm.delete') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.download"> {{ $t('settings.perm.download') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.modify"> {{ $t('settings.perm.modify') }}</p>
<p v-if="isExecEnabled"><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.rename"> {{ $t('settings.perm.rename') }}</p>
<p><input type="checkbox" :disabled="admin" v-model="perm.share"> {{ $t('settings.perm.share') }}</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.create" />
{{ $t("settings.perm.create") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.delete" />
{{ $t("settings.perm.delete") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.download" />
{{ $t("settings.perm.download") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.modify" />
{{ $t("settings.perm.modify") }}
</p>
<p v-if="isExecEnabled">
<input type="checkbox" :disabled="admin" v-model="perm.execute" />
{{ $t("settings.perm.execute") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.rename" />
{{ $t("settings.perm.rename") }}
</p>
<p>
<input type="checkbox" :disabled="admin" v-model="perm.share" />
{{ $t("settings.perm.share") }}
</p>
</div>
</template>
<script>
import { enableExec } from '@/utils/constants'
import { enableExec } from "@/utils/constants";
export default {
name: 'permissions',
props: ['perm'],
name: "permissions",
props: ["perm"],
computed: {
admin: {
get () {
return this.perm.admin
get() {
return this.perm.admin;
},
set (value) {
set(value) {
if (value) {
for (const key in this.perm) {
this.perm[key] = true
this.perm[key] = true;
}
}
this.perm.admin = value
}
this.perm.admin = value;
},
},
isExecEnabled: () => enableExec
}
}
isExecEnabled: () => enableExec,
},
};
</script>

View File

@@ -1,57 +1,63 @@
<template>
<form class="rules small">
<div v-for="(rule, index) in rules" :key="index">
<input type="checkbox" v-model="rule.regex"><label>Regex</label>
<input type="checkbox" v-model="rule.allow"><label>Allow</label>
<input type="checkbox" v-model="rule.regex" /><label>Regex</label>
<input type="checkbox" v-model="rule.allow" /><label>Allow</label>
<input
@keypress.enter.prevent
type="text"
v-if="rule.regex"
v-model="rule.regexp.raw"
:placeholder="$t('settings.insertRegex')" />
:placeholder="$t('settings.insertRegex')"
/>
<input
@keypress.enter.prevent
type="text"
v-else
v-model="rule.path"
:placeholder="$t('settings.insertPath')" />
:placeholder="$t('settings.insertPath')"
/>
<button class="button button--red" @click="remove($event, index)">-</button>
<button class="button button--red" @click="remove($event, index)">
-
</button>
</div>
<div>
<button class="button" @click="create" default="false">{{ $t('buttons.new') }}</button>
<button class="button" @click="create" default="false">
{{ $t("buttons.new") }}
</button>
</div>
</form>
</template>
<script>
export default {
name: 'rules-textarea',
props: ['rules'],
name: "rules-textarea",
props: ["rules"],
methods: {
remove (event, index) {
event.preventDefault()
let rules = [ ...this.rules ]
rules.splice(index, 1)
this.$emit('update:rules', [ ...rules ])
remove(event, index) {
event.preventDefault();
let rules = [...this.rules];
rules.splice(index, 1);
this.$emit("update:rules", [...rules]);
},
create (event) {
event.preventDefault()
create(event) {
event.preventDefault();
this.$emit('update:rules', [
this.$emit("update:rules", [
...this.rules,
{
allow: true,
path: '',
path: "",
regex: false,
regexp: {
raw: ''
}
}
])
}
}
}
raw: "",
},
},
]);
},
},
};
</script>

View File

@@ -1,18 +1,18 @@
<template>
<select v-on:change="change" :value="theme">
<option value="">{{ $t('settings.themes.light') }}</option>
<option value="dark">{{ $t('settings.themes.dark') }}</option>
<option value="">{{ $t("settings.themes.light") }}</option>
<option value="dark">{{ $t("settings.themes.dark") }}</option>
</select>
</template>
<script>
export default {
name: 'themes',
props: [ 'theme' ],
name: "themes",
props: ["theme"],
methods: {
change (event) {
this.$emit('update:theme', event.target.value)
}
}
}
</script>
change(event) {
this.$emit("update:theme", event.target.value);
},
},
};
</script>

View File

@@ -1,67 +1,92 @@
<template>
<div>
<p v-if="!isDefault">
<label for="username">{{ $t('settings.username') }}</label>
<input class="input input--block" type="text" v-model="user.username" id="username">
<label for="username">{{ $t("settings.username") }}</label>
<input
class="input input--block"
type="text"
v-model="user.username"
id="username"
/>
</p>
<p v-if="!isDefault">
<label for="password">{{ $t('settings.password') }}</label>
<input class="input input--block" type="password" :placeholder="passwordPlaceholder" v-model="user.password" id="password">
<label for="password">{{ $t("settings.password") }}</label>
<input
class="input input--block"
type="password"
:placeholder="passwordPlaceholder"
v-model="user.password"
id="password"
/>
</p>
<p>
<label for="scope">{{ $t('settings.scope') }}</label>
<input class="input input--block" type="text" v-model="user.scope" id="scope">
<label for="scope">{{ $t("settings.scope") }}</label>
<input
class="input input--block"
type="text"
v-model="user.scope"
id="scope"
/>
</p>
<p>
<label for="locale">{{ $t('settings.language') }}</label>
<languages class="input input--block" id="locale" :locale.sync="user.locale"></languages>
<label for="locale">{{ $t("settings.language") }}</label>
<languages
class="input input--block"
id="locale"
:locale.sync="user.locale"
></languages>
</p>
<p v-if="!isDefault">
<input type="checkbox" :disabled="user.perm.admin" v-model="user.lockPassword"> {{ $t('settings.lockPassword') }}
<input
type="checkbox"
:disabled="user.perm.admin"
v-model="user.lockPassword"
/>
{{ $t("settings.lockPassword") }}
</p>
<permissions :perm.sync="user.perm" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" />
<div v-if="!isDefault">
<h3>{{ $t('settings.rules') }}</h3>
<p class="small">{{ $t('settings.rulesHelp') }}</p>
<h3>{{ $t("settings.rules") }}</h3>
<p class="small">{{ $t("settings.rulesHelp") }}</p>
<rules :rules.sync="user.rules" />
</div>
</div>
</template>
<script>
import Languages from './Languages'
import Rules from './Rules'
import Permissions from './Permissions'
import Commands from './Commands'
import { enableExec } from '@/utils/constants'
import Languages from "./Languages";
import Rules from "./Rules";
import Permissions from "./Permissions";
import Commands from "./Commands";
import { enableExec } from "@/utils/constants";
export default {
name: 'user',
name: "user",
components: {
Permissions,
Languages,
Rules,
Commands
Commands,
},
props: [ 'user', 'isNew', 'isDefault' ],
props: ["user", "isNew", "isDefault"],
computed: {
passwordPlaceholder () {
return this.isNew ? '' : this.$t('settings.avoidChanges')
passwordPlaceholder() {
return this.isNew ? "" : this.$t("settings.avoidChanges");
},
isExecEnabled: () => enableExec
isExecEnabled: () => enableExec,
},
watch: {
'user.perm.admin': function () {
if (!this.user.perm.admin) return
this.user.lockPassword = false
}
}
}
"user.perm.admin": function () {
if (!this.user.perm.admin) return;
this.user.lockPassword = false;
},
},
};
</script>

View File

@@ -1,29 +1,73 @@
.share__box {
text-align: center;
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
display: block;
border-radius: 0.2em;
width: 90%;
max-width: 25em;
margin: 6em auto;
.share {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
}
.share__box__download {
width: 100%;
@media (max-width: 736px) {
.share {
display: block;
}
}
.share__box {
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
background: #fff;
border-radius: 0.2em;
margin: 5px;
overflow: hidden;
}
.share__box__header {
padding: 1em;
cursor: pointer;
background: #ffffff;
color: rgba(0, 0, 0, 0.5);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
text-align: center;
}
.share__box__icon i {
font-size: 10em;
color: #40c4ff;
}
.share__box__center {
text-align: center;
}
.share__box__info {
padding: 2em 3em;
flex: 1 1 auto;
}
.share__box__title {
margin-top: .2em;
overflow: hidden;
text-overflow: ellipsis;
.share__box__element {
padding: 1em;
border-top: 1px solid rgba(0, 0, 0, 0.1);
word-break: break-all;
}
.share__box__items {
text-align: left;
flex: 10 0 25em;
}
.share__box__items #listing.list .item {
cursor: pointer;
border-left: 0;
border-right: 0;
border-bottom: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.share__box__items #listing.list .item .name {
width: 50%;
}
.share__box__items #listing.list .item .modified {
width: 25%;
}
.share__wrong__password {
background: var(--red);
color: #fff;
padding: .5em;
text-align: center;
animation: .2s opac forwards;
}

View File

@@ -83,29 +83,29 @@ main {
width: calc(100% - 19em);
}
#breadcrumbs {
.breadcrumbs {
height: 3em;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
#breadcrumbs span,
#breadcrumbs {
.breadcrumbs span,
.breadcrumbs {
display: flex;
align-items: center;
color: #6f6f6f;
}
#breadcrumbs a {
.breadcrumbs a {
color: inherit;
transition: .1s ease-in;
border-radius: .125em;
}
#breadcrumbs a:hover {
.breadcrumbs a:hover {
background-color: rgba(0,0,0, 0.05);
}
#breadcrumbs span a {
.breadcrumbs span a {
padding: .2em;
}

View File

@@ -1,8 +1,29 @@
.dashboard {
max-width: 600px;
margin: 1em 0;
}
.dashboard .row {
display: flex;
margin: 0 -.5em;
flex-wrap: wrap;
}
.dashboard .row .column {
display: flex;
padding: 0 .5em;
width: 50%;
}
.dashboard .row .column .card {
flex-grow: 1;
}
@media(max-width: 1200px) {
.dashboard .row .column {
width: 100%;
}
}
a {
color: inherit
}
@@ -28,25 +49,56 @@ p code {
}
.dashboard #nav {
display: flex;
padding-bottom: 1em;
overflow: auto;
}
.dashboard #nav .wrapper {
display: flex;
flex-grow: 1;
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
}
.dashboard #nav ul {
list-style: none;
display: flex;
color: rgb(84, 110, 122);
font-weight: 500;
margin: 0 0 1em;
padding: 0;
margin: 0 0 -2px 0;
font-size: .8em;
text-align: center;
justify-content: space-between;
padding: 0;
justify-content: left;
}
.dashboard #nav li {
.dashboard #nav ul li {
position: relative;
padding: 1.5em 2em;
white-space: nowrap;
border-bottom: 2px solid transparent;
transition: .1s ease-in-out all;
}
.dashboard #nav ul li:hover {
background: var(--moon-grey);
}
.dashboard #nav ul li.active {
border-color: var(--blue);
color: var(--blue);
}
.dashboard #nav ul li.active::before {
width: 100%;
padding: 0 0 1em;
border-bottom: 2px solid rgba(0, 0, 0, 0.05);
}
.dashboard #nav li.active {
border-color: var(--blue)
height: 100%;
position: absolute;
top: 0;
left: 0;
content: "";
background: var(--blue);
opacity: 0.08;
}
.dashboard #nav i {
@@ -92,7 +144,7 @@ table tr>*:last-child {
.card {
position: relative;
margin: .5rem 0 1rem 0;
margin: 0 0 1rem 0;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
@@ -151,6 +203,7 @@ table tr>*:last-child {
.card .card-content.full {
padding-bottom: 0;
overflow: auto;
}
.card h2 {
@@ -226,6 +279,18 @@ table tr>*:last-child {
opacity: 1;
}
.card#share .input-group {
display: flex;
}
.card#share .input-group * {
border: none;
}
.card#share .input-group input {
flex: 1;
}
.overlay {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;

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