Compare commits

...

157 Commits

Author SHA1 Message Date
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
Oleg Lobanov
5aaeb3b76d chore(release): 2.8.0 2020-10-05 09:53:09 +02:00
Daniel Pham
36fb9f562a fix: fix empty command name (#1106) 2020-10-05 09:52:27 +02:00
Xabi
ad99bf1801 fix: fix panic when accessing nonexistent .js file in static path (#1105) 2020-10-02 15:09:03 +02:00
Oleg Lobanov
4c2a094255 Merge pull request #1100 from ramiresviana/fixes-3 2020-10-01 16:53:35 +02:00
Keagan McClelland
97693cc611 feat: add disable exec flag (#1090) 2020-10-01 16:45:24 +02:00
Ramires Viana
c6d4fcd08f fix: empty commands setting 2020-09-29 14:05:03 +00:00
Ramires Viana
dd7b9ddd85 fix: preview key shortcut conflict 2020-09-29 14:04:55 +00:00
Ramires Viana
26d62e4117 fix: search results absolute url 2020-09-29 14:04:43 +00:00
Ramires Viana
babd7783af fix: file upload path encoding 2020-09-29 14:04:03 +00:00
Oleg Lobanov
1529e796df chore(release): 2.7.0 2020-09-11 19:21:08 +02:00
Oleg Lobanov
d4b904b92b chore: pass docker password via stdin 2020-09-11 18:57:14 +02:00
Oleg Lobanov
12d4177823 build: bump go version to 1.15.2 (#1081) 2020-09-11 18:07:01 +02:00
Oleg Lobanov
8142b32f38 feat: put selected files in the root of the archive (closes #1065) 2020-09-11 16:54:22 +02:00
Oleg Lobanov
c5abbb4e1c chore: fix lint errors 2020-09-11 16:02:16 +02:00
Oleg Lobanov
65ac73414f feat: add --socket-perm flag to control unix socket file permissions (closes #1060) 2020-09-11 15:59:06 +02:00
Oleg Lobanov
ede4213c8e chore: fix french translation (closes #1071) 2020-09-11 15:16:58 +02:00
Agneev Mukherjee
b60d291490 chore: fix URLs for assets (#1074) 2020-09-05 15:23:42 +02:00
Oleg Lobanov
b9ede79888 Merge pull request #1066 from ramiresviana/preview-mobile-dropdown 2020-08-25 16:33:57 +02:00
Ramires Viana
3d2cb838d1 feat: preview size button 2020-08-25 14:14:15 +00:00
Ramires Viana
778734419d feat: preview mobile dropdown 2020-08-18 12:47:23 +00:00
Oleg Lobanov
be8683f556 chore(release): 2.6.2 2020-08-05 11:55:16 +02:00
Davide Maggio
c3450f4614 chore: return text/plain header in auth response (#1051) 2020-08-05 10:48:03 +02:00
Oleg Lobanov
5881bc9ab0 chore: fix preview of files with non-latin names (closes #1056) 2020-08-05 10:40:03 +02:00
Oleg Lobanov
a2fb499a20 chore(release): 2.6.1 2020-07-28 13:40:19 +02:00
Oleg Lobanov
411a928fea chore: fix lint errors 2020-07-28 13:40:06 +02:00
Oleg Lobanov
f5d02cdde9 fix: delete cached previews when deleting file 2020-07-28 11:59:55 +02:00
Oleg Lobanov
c9340af8d0 fix: escape special characters in preview url (closes #1002) 2020-07-28 11:59:32 +02:00
Oleg Lobanov
a722bcc13f chore(release): 2.6.0 2020-07-27 19:52:48 +02:00
Oleg Lobanov
77fe3cfc60 ci: fix go version on release step 2020-07-27 19:51:09 +02:00
Oleg Lobanov
470f93cefc Merge pull request #1044 from filebrowser/fix_img_resize 2020-07-27 19:39:08 +02:00
Oleg Lobanov
92fde4dd12 build: set limit for vuejs build threads 2020-07-27 19:35:02 +02:00
Oleg Lobanov
95bc92955f feat: cache resized images 2020-07-27 19:26:45 +02:00
Oleg Lobanov
f2f914221c chore: bump go to 1.14.6 2020-07-27 19:26:45 +02:00
Oleg Lobanov
c2d8038c63 chore: add testing step to ci 2020-07-27 19:26:44 +02:00
Oleg Lobanov
cb8ac5ebf1 chore: add resize tests 2020-07-27 19:26:44 +02:00
Oleg Lobanov
aa78e3ab1f feat: add param to disable img resizing 2020-07-27 19:26:44 +02:00
Oleg Lobanov
bc00165094 feat: add lazy load of image thumbnails 2020-07-27 19:26:44 +02:00
Oleg Lobanov
94ef59602f feat: limit image resize workers 2020-07-27 19:26:44 +02:00
Oleg Lobanov
14e2f84ceb Merge pull request #1042 from ramiresviana/fixes-2 2020-07-23 15:03:10 +02:00
Ramires Viana
f228fa5540 fix: conflict handling on upload button 2020-07-23 12:02:09 +00:00
Ramires Viana
f2d2c1cbf8 fix: drop feedback 2020-07-23 12:02:09 +00:00
Ramires Viana
d9be370e24 fix: missing error message 2020-07-23 12:02:09 +00:00
Ramires Viana
727c63b98e fix: parent verification on copy 2020-07-23 12:02:02 +00:00
Ramires Viana
34dfb49b71 fix: path separator inconsistency on rename 2020-07-20 17:45:45 +00:00
Henrique Dias
0b0a704d44 chore: remove hacdias/fileutils dep (#1037) 2020-07-18 20:10:22 +02:00
Oleg Lobanov
2d99d0bf13 chore(release): 2.5.0 2020-07-17 18:12:00 +02:00
Oleg Lobanov
1790df2090 Merge pull request #1026 from ramiresviana/fixes 2020-07-17 17:41:17 +02:00
Ramires Viana
10570ade44 fix: reset clipboard after pasting cutted files 2020-07-17 14:11:23 +00:00
Ramires Viana
43526d9d1a feat: duplicate files in the same directory 2020-07-17 14:11:23 +00:00
Ramires Viana
2636f876ab feat: rename option on replace prompt 2020-07-17 14:11:15 +00:00
Ramires Viana
eed9da1471 feat: file copy, move and paste conflict checking 2020-07-17 12:37:52 +00:00
Ramires Viana
9a2ebbabe2 fix: blinking previewer 2020-07-17 12:37:52 +00:00
Ramires Viana
716396a726 feat: add previewer title and loading indicator 2020-07-17 12:32:21 +00:00
Ramires Viana
0727496601 fix: remove incomplete uploaded files 2020-07-14 00:21:15 +00:00
Ramires Viana
194030fcfc fix: prompt before closing window 2020-07-14 00:12:41 +00:00
Ramires Viana
b3b644527d fix: dark theme colors 2020-07-14 00:12:33 +00:00
Ramires Viana
7e5beeff46 fix: directory conflict checking 2020-07-13 14:20:56 +00:00
Oleg Lobanov
a47b69bcec Merge pull request #1021 from ramiresviana/upload-queue 2020-07-13 11:20:59 +02:00
Ramires Viana
6ec6a23861 feat: upload queue 2020-07-10 00:01:37 +00:00
Ramires Viana
c9cc0d3d5d refactor: upload vuex module 2020-07-10 00:01:37 +00:00
Ramires Viana
28d2b35718 refactor: upload utils 2020-07-10 00:01:37 +00:00
Ramires Viana
b4f131be50 refactor: uploading counters vuex state 2020-07-10 00:01:37 +00:00
Oleg Lobanov
d0b359561f chore(release): 2.4.0 2020-07-07 16:53:51 +02:00
Fabian Fritzsche
453636dfe2 fix: add preview bypass for .gif files (#1012) 2020-07-07 16:47:11 +02:00
Oleg Lobanov
b1605aa6d3 Merge pull request #1014 from ramiresviana/full-screen-editor 2020-07-06 17:06:12 +02:00
Oleg Lobanov
23503b80a4 Merge pull request #1015 from ramiresviana/prompt-key-shortcut-conflict 2020-07-06 17:03:05 +02:00
Ramires Viana
0d69fbd9a3 fix: prompt key shortcut conflict 2020-07-04 14:19:03 +00:00
Ramires Viana
0d665e528f feat: full screen editor 2020-07-04 03:11:51 +00:00
176 changed files with 7892 additions and 4208 deletions

View File

@@ -1,79 +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:
- '*'
build-go:
docker:
- image: circleci/golang:1.14.3
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.14.3
steps:
- attach_workspace:
at: '~/project'
- setup_remote_docker
- run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- run: curl -sL https://git.io/goreleaser | bash
- run: docker logout
workflows:
version: 2
build-workflow:
jobs:
- lint:
filters:
tags:
only: /.*/
- build-node:
filters:
tags:
only: /.*/
- build-go:
filters:
tags:
only: /.*/
requires:
- build-node
- lint
- 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,2 @@
testdata/
.github/
**.git
*
!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 }}

4
.gitignore vendored
View File

@@ -5,11 +5,9 @@ _old
rice-box.go
.idea/
filebrowser
dist/
.DS_Store
node_modules
/frontend/dist
# local env files
.env.local
@@ -28,3 +26,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,210 @@
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.13.0](https://github.com/filebrowser/filebrowser/compare/v2.12.1...v2.13.0) (2021-03-14)
### Features
* dual pane settings view ([db5aad8](https://github.com/filebrowser/filebrowser/commit/db5aad8eb679cfe1b1ace5142cf342951217f0f7))
* improved settings navbar ([5b28aa0](https://github.com/filebrowser/filebrowser/commit/5b28aa0848710b9d3ee02a2aa912856395f48bd2))
* improved sharing prompt ([1819377](https://github.com/filebrowser/filebrowser/commit/18193778971e27d18b5a35df8c2d0e2953b48111))
* increased header button counter size ([4fb832c](https://github.com/filebrowser/filebrowser/commit/4fb832c0422107e16f22b7aa928224f36de4978f))
* larger previewer content ([62fff5c](https://github.com/filebrowser/filebrowser/commit/62fff5ca60da1f887c1f95fa4808d3753596dab2))
### Bug Fixes
* archive contains parent path on Windows ([54f3570](https://github.com/filebrowser/filebrowser/commit/54f35701a2bd5cb7ec0628ca9789047072c073db))
* check rules on http resource handlers ([5bf1554](https://github.com/filebrowser/filebrowser/commit/5bf15548d0ad147acfad5000277531be2671f7ce))
* download current dir on file listing ([488d980](https://github.com/filebrowser/filebrowser/commit/488d98045e7476ed11e53c13d9498a9db3165bbc))
* encoded file path on share ([7955e07](https://github.com/filebrowser/filebrowser/commit/7955e0720baef3710106c7e69bbbf078d5489220))
* full file path on share ([e017a19](https://github.com/filebrowser/filebrowser/commit/e017a199850e19dd51b960ba59402c215fd8f1af))
* header dropdown icon color on previewer ([f8df76f](https://github.com/filebrowser/filebrowser/commit/f8df76f52684f10722ce123fec2c90e321ddf103))
* item dragging on file listing ([326b35a](https://github.com/filebrowser/filebrowser/commit/326b35a7ac7871afcdf892ca150349665b7f6379))
* modified time on info prompt ([11ebaec](https://github.com/filebrowser/filebrowser/commit/11ebaec5f0671ec02ebe55d4a73a514bce3a6713))
* root path name on archive ([426b38b](https://github.com/filebrowser/filebrowser/commit/426b38bb3362d2d477d0d8aa27d880664d537431))
* stuck icon on header button ([6a734c0](https://github.com/filebrowser/filebrowser/commit/6a734c01391b437c2842f5d97fb63f29a0017510))
* update image cache when replacing ([81b6f4d](https://github.com/filebrowser/filebrowser/commit/81b6f4d6f6a01886583016f61f4f1951a59f244d))
* wait for async command exit ([#1326](https://github.com/filebrowser/filebrowser/issues/1326)) ([6d5ceae](https://github.com/filebrowser/filebrowser/commit/6d5ceae8b454edd749b3b65c88aacc0a31ce9215))
### Refactorings
* migrate from rice to embed.FS ([fc55061](https://github.com/filebrowser/filebrowser/commit/fc5506179a64e9e2f57f7b6d6cce4b95f5ebc235))
### [2.12.1](https://github.com/filebrowser/filebrowser/compare/v2.12.0...v2.12.1) (2021-03-07)
### Bug Fixes
* add missing default config into the docker image ([7358b3f](https://github.com/filebrowser/filebrowser/commit/7358b3fe3178c20007b4b5ef5c03705badd538c4))
## [2.12.0](https://github.com/filebrowser/filebrowser/compare/v2.11.0...v2.12.0) (2021-03-04)
### Features
* add homebrew tap ([2d2c598](https://github.com/filebrowser/filebrowser/commit/2d2c598fa6bd1ecaf39c542182890c8dd9b1cad0))
* added tiff files preview support ([#1222](https://github.com/filebrowser/filebrowser/issues/1222)) ([e8c9d1c](https://github.com/filebrowser/filebrowser/commit/e8c9d1c53989b4b52f6fba2a8ac41ae612c03a7c))
* allow disabling file detections by reading header ([#1175](https://github.com/filebrowser/filebrowser/issues/1175)) ([6914063](https://github.com/filebrowser/filebrowser/commit/6914063853a8a3f3cecfa4b21f223820c2a0b7df))
* allow to password protect shares ([#1252](https://github.com/filebrowser/filebrowser/issues/1252)) ([d8f415f](https://github.com/filebrowser/filebrowser/commit/d8f415f8abd0c4301803bd968c54429dd3fe4b59))
* build multi-arch docker images ([cf4836d](https://github.com/filebrowser/filebrowser/commit/cf4836dc757ef79ad615179bb7a6c7bbd3b09c2c))
* share management delete confirm ([#1212](https://github.com/filebrowser/filebrowser/issues/1212)) ([b600b11](https://github.com/filebrowser/filebrowser/commit/b600b11415fd1fb90ff2f5136be95a9c737ae1cb))
### Bug Fixes
* don't allow to remove root user ([019ce80](https://github.com/filebrowser/filebrowser/commit/019ce80fc529a0437984fdc3d1ab6916f34dd594))
* double click to zoom pics in phone's browser ([#1274](https://github.com/filebrowser/filebrowser/issues/1274)) ([f1b7bd5](https://github.com/filebrowser/filebrowser/commit/f1b7bd59f67e719b7bfd203b0d7ec016fd21ab49))
* environmental variables not expanded in command ([#1241](https://github.com/filebrowser/filebrowser/issues/1241)) ([f3afd5c](https://github.com/filebrowser/filebrowser/commit/f3afd5cb79d6ad8b9cc8d54cb8fc2344b7c07d3d))
* fetch resource api once when sorting (closes [#1172](https://github.com/filebrowser/filebrowser/issues/1172)) ([#1202](https://github.com/filebrowser/filebrowser/issues/1202)) ([05bb7c8](https://github.com/filebrowser/filebrowser/commit/05bb7c85531349f3e9d1d8a523bb1243587b2ebc))
### Build
* use make for building the project ([#1304](https://github.com/filebrowser/filebrowser/issues/1304)) ([23f8464](https://github.com/filebrowser/filebrowser/commit/23f84642e6c1e07f89f98d2c1bb6fc9da36cc71c))
## [2.11.0](https://github.com/filebrowser/filebrowser/compare/v2.10.0...v2.11.0) (2020-12-28)
### Features
* add sharing management ([#1178](https://github.com/filebrowser/filebrowser/issues/1178)) (closes [#1000](https://github.com/filebrowser/filebrowser/issues/1000)) ([677bce3](https://github.com/filebrowser/filebrowser/commit/677bce376b024d9ff38f34e74243034fe5a1ec3c))
* download shared subdirectory ([#1184](https://github.com/filebrowser/filebrowser/issues/1184)) ([fb5b28d](https://github.com/filebrowser/filebrowser/commit/fb5b28d9cbdee10d38fcd719b9fd832121be58ef))
### Bug Fixes
* check user input to prevent permission elevation ([#1196](https://github.com/filebrowser/filebrowser/issues/1196)) (closes [#1195](https://github.com/filebrowser/filebrowser/issues/1195)) ([f62806f](https://github.com/filebrowser/filebrowser/commit/f62806f6c9e9c7f392d1b747d65b8fe40b313e89))
* delete extra remove prefix ([#1186](https://github.com/filebrowser/filebrowser/issues/1186)) ([7a5298a](https://github.com/filebrowser/filebrowser/commit/7a5298a7556f7dcc52f59b8ea76d040d3ddc3d12))
* move files between different volumes (closes [#1177](https://github.com/filebrowser/filebrowser/issues/1177)) ([58835b7](https://github.com/filebrowser/filebrowser/commit/58835b7e535cc96e1c8a5d85821c1545743ca757))
* recaptcha race condition ([#1176](https://github.com/filebrowser/filebrowser/issues/1176)) ([ac3673e](https://github.com/filebrowser/filebrowser/commit/ac3673e111afac6616af9650ca07028b6c27e6cd))
## [2.10.0](https://github.com/filebrowser/filebrowser/compare/v2.9.0...v2.10.0) (2020-11-24)
### Features
* add hide dotfiles param ([#1148](https://github.com/filebrowser/filebrowser/issues/1148)) ([10e399b](https://github.com/filebrowser/filebrowser/commit/10e399b3c3dbdcfb4465a9d4138e1da6bae0873d))
* add single click mode ([#1139](https://github.com/filebrowser/filebrowser/issues/1139)) ([e8b4e9a](https://github.com/filebrowser/filebrowser/commit/e8b4e9af46d6e99dbeb965dd9727d9ed017d52a2))
* automatically jump to the next photo when deleting while previewing ([#1143](https://github.com/filebrowser/filebrowser/issues/1143)) ([9515cee](https://github.com/filebrowser/filebrowser/commit/9515ceeb42e5ef5267400220a2082dec775e843d))
* shared folder file listing ([e119bc5](https://github.com/filebrowser/filebrowser/commit/e119bc55ea82cefcbcc0571650107dfd5d73f570))
* shared item information ([36cacdf](https://github.com/filebrowser/filebrowser/commit/36cacdf598e4e09f064c8ace0ca7a6c24b23028e))
### Bug Fixes
* empty folder in archive ([7096b3d](https://github.com/filebrowser/filebrowser/commit/7096b3dab92441981c9964e4a6175af0a255d2be))
* fix hanging when reading a named pipe file (closes [#1155](https://github.com/filebrowser/filebrowser/issues/1155)) ([586d198](https://github.com/filebrowser/filebrowser/commit/586d198d47b525eeccc6fe587573a3ad83adb4f6))
* previewer title overflow ([4e48ffc](https://github.com/filebrowser/filebrowser/commit/4e48ffc14d09dabeea12dc495144277db62b5b7d))
* resource rename action invalid path ([1ce3068](https://github.com/filebrowser/filebrowser/commit/1ce3068a99c80c153fd41359255d173bce6e79e8))
## [2.9.0](https://github.com/filebrowser/filebrowser/compare/v2.8.0...v2.9.0) (2020-10-21)
### Features
* support WKWebview custom protocol ([#1113](https://github.com/filebrowser/filebrowser/issues/1113)) ([0ac80e8](https://github.com/filebrowser/filebrowser/commit/0ac80e8387a69924284259bde448af2813d84ed1))
### Bug Fixes
* allow start from Windows explorer ([f2c4e78](https://github.com/filebrowser/filebrowser/commit/f2c4e78381610879eda5316d38a999c89df6c14a))
* file upload missing path slash ([5e27ba5](https://github.com/filebrowser/filebrowser/commit/5e27ba5c8c1be603c6ae7fec8de48e3532dea1f7))
* preview case sensitive file extension ([05bff54](https://github.com/filebrowser/filebrowser/commit/05bff54b71543fd232f1089c40504d0cbfd106be))
* search missing path slash ([2bd163d](https://github.com/filebrowser/filebrowser/commit/2bd163d92a856d65c8d4615e37898470c1edf2f4))
## [2.8.0](https://github.com/filebrowser/filebrowser/compare/v2.7.0...v2.8.0) (2020-10-05)
### Features
* add disable exec flag ([#1090](https://github.com/filebrowser/filebrowser/issues/1090)) ([97693cc](https://github.com/filebrowser/filebrowser/commit/97693cc6117ce1c956baede91de5dd48b904e175))
### Bug Fixes
* empty commands setting ([c6d4fcd](https://github.com/filebrowser/filebrowser/commit/c6d4fcd08f5f1531c2cef514dc86019e23e7289f))
* file upload path encoding ([babd778](https://github.com/filebrowser/filebrowser/commit/babd7783afe85b790e1c558375d7b5013b2d366f))
* fix empty command name ([#1106](https://github.com/filebrowser/filebrowser/issues/1106)) ([36fb9f5](https://github.com/filebrowser/filebrowser/commit/36fb9f562a2c005ca4390fdebde0b4690201dff9))
* fix panic when accessing nonexistent .js file in static path ([#1105](https://github.com/filebrowser/filebrowser/issues/1105)) ([ad99bf1](https://github.com/filebrowser/filebrowser/commit/ad99bf180197e0e6d82231a86457585de16366a8))
* preview key shortcut conflict ([dd7b9dd](https://github.com/filebrowser/filebrowser/commit/dd7b9ddd8546361060ef99e838a691b2fc6c495a))
* search results absolute url ([26d62e4](https://github.com/filebrowser/filebrowser/commit/26d62e411716a5eb9a5a703e47484cfb3fbf3bd0))
## [2.7.0](https://github.com/filebrowser/filebrowser/compare/v2.6.2...v2.7.0) (2020-09-11)
### Features
* add --socket-perm flag to control unix socket file permissions (closes [#1060](https://github.com/filebrowser/filebrowser/issues/1060)) ([65ac734](https://github.com/filebrowser/filebrowser/commit/65ac73414fadc4686c94803a93ff319e8f7ce9d1))
* preview mobile dropdown ([7787344](https://github.com/filebrowser/filebrowser/commit/778734419de314d4cb64d07109bbab73f8e2e42a))
* preview size button ([3d2cb83](https://github.com/filebrowser/filebrowser/commit/3d2cb838d111ee61047599f49e76de80c821f341))
* put selected files in the root of the archive (closes [#1065](https://github.com/filebrowser/filebrowser/issues/1065)) ([8142b32](https://github.com/filebrowser/filebrowser/commit/8142b32f3865eccd3331328e0d087f805d186ed5))
### [2.6.2](https://github.com/filebrowser/filebrowser/compare/v2.6.1...v2.6.2) (2020-08-05)
### [2.6.1](https://github.com/filebrowser/filebrowser/compare/v2.6.0...v2.6.1) (2020-07-28)
### Bug Fixes
* delete cached previews when deleting file ([f5d02cd](https://github.com/filebrowser/filebrowser/commit/f5d02cdde97923b963878abf5a300393b9feb348))
* escape special characters in preview url (closes [#1002](https://github.com/filebrowser/filebrowser/issues/1002)) ([c9340af](https://github.com/filebrowser/filebrowser/commit/c9340af8d045671ad3338c5d2d887c335ab92de4))
## [2.6.0](https://github.com/filebrowser/filebrowser/compare/v2.5.0...v2.6.0) (2020-07-27)
### Features
* add lazy load of image thumbnails ([bc00165](https://github.com/filebrowser/filebrowser/commit/bc001650944ae963b12b5b2538a68de7cd0d8f82))
* add param to disable img resizing ([aa78e3a](https://github.com/filebrowser/filebrowser/commit/aa78e3ab1fcae6f618e811ba4e315a7a209f9df2))
* cache resized images ([95bc929](https://github.com/filebrowser/filebrowser/commit/95bc92955f391ece22c40d9592f2a3e6e26907b9))
* limit image resize workers ([94ef596](https://github.com/filebrowser/filebrowser/commit/94ef59602fb50fc21b1164feda90a3b9aeb5e972))
### Bug Fixes
* conflict handling on upload button ([f228fa5](https://github.com/filebrowser/filebrowser/commit/f228fa55408824618e9f0879da67c86d22b0d324))
* drop feedback ([f2d2c1c](https://github.com/filebrowser/filebrowser/commit/f2d2c1cbf85fba3edffb7b079f121ed3f0bc1e02))
* missing error message ([d9be370](https://github.com/filebrowser/filebrowser/commit/d9be370e2474b8070fa58db920c9481270cc4a48))
* parent verification on copy ([727c63b](https://github.com/filebrowser/filebrowser/commit/727c63b98e2964d0960d25914c296570f6c79478))
* path separator inconsistency on rename ([34dfb49](https://github.com/filebrowser/filebrowser/commit/34dfb49b719c948e709a4639b4af2c5cb73b3887))
## [2.5.0](https://github.com/filebrowser/filebrowser/compare/v2.4.0...v2.5.0) (2020-07-17)
### Features
* add previewer title and loading indicator ([716396a](https://github.com/filebrowser/filebrowser/commit/716396a726329f0ba42fc34167dd07497c5bf47c))
* duplicate files in the same directory ([43526d9](https://github.com/filebrowser/filebrowser/commit/43526d9d1a8c837245e3f5059e0b4737583eeaeb))
* file copy, move and paste conflict checking ([eed9da1](https://github.com/filebrowser/filebrowser/commit/eed9da1471723ed3fbe6c00b1d6362b1c5fd8b04))
* rename option on replace prompt ([2636f87](https://github.com/filebrowser/filebrowser/commit/2636f876ab8f88eea6d9548de524ca2339eb0843))
* upload queue ([6ec6a23](https://github.com/filebrowser/filebrowser/commit/6ec6a2386173410f5cab9941dbf1bacb6b70ddd2))
### Bug Fixes
* blinking previewer ([9a2ebba](https://github.com/filebrowser/filebrowser/commit/9a2ebbabe2e9f0c292701d33f36f9b7a457b1164))
* dark theme colors ([b3b6445](https://github.com/filebrowser/filebrowser/commit/b3b644527d5673e16e61d404ff58a3c7bd6b6637))
* directory conflict checking ([7e5beef](https://github.com/filebrowser/filebrowser/commit/7e5beeff464e75ab185c430cd96e7cc67209ccc1))
* prompt before closing window ([194030f](https://github.com/filebrowser/filebrowser/commit/194030fcfcf54a2cf5e2f8ececcbb4754474d8f8))
* remove incomplete uploaded files ([0727496](https://github.com/filebrowser/filebrowser/commit/0727496601a9918c8131c56f62419bfac7ac589a))
* reset clipboard after pasting cutted files ([10570ad](https://github.com/filebrowser/filebrowser/commit/10570ade442b573ebe00af08369e28b1b0688df6))
## [2.4.0](https://github.com/filebrowser/filebrowser/compare/v2.3.0...v2.4.0) (2020-07-07)
### Features
* full screen editor ([0d665e5](https://github.com/filebrowser/filebrowser/commit/0d665e528f880ceda0976ceed66070ac34de7969))
### Bug Fixes
* add preview bypass for .gif files ([#1012](https://github.com/filebrowser/filebrowser/issues/1012)) ([453636d](https://github.com/filebrowser/filebrowser/commit/453636dfe2bbf177c74617862eb763485d4774bf))
* prompt key shortcut conflict ([0d69fbd](https://github.com/filebrowser/filebrowser/commit/0d69fbd9a342aa2695859021df0c423e3ae4a4fa))
## [2.3.0](https://github.com/filebrowser/filebrowser/compare/v2.2.0...v2.3.0) (2020-06-26)

View File

@@ -1,15 +1,11 @@
FROM alpine:latest as alpine
FROM alpine:latest
RUN apk --update add ca-certificates
RUN apk --update add mailcap
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
VOLUME /srv
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 filebrowser
## 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: | $(RICE) ; $(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

@@ -140,10 +140,12 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tLocale:\t%s\n", set.Defaults.Locale)
fmt.Fprintf(w, "\tView mode:\t%s\n", set.Defaults.ViewMode)
fmt.Fprintf(w, "\tSingle Click:\t%t\n", set.Defaults.SingleClick)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)

View File

@@ -2,7 +2,6 @@ package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
@@ -32,7 +31,7 @@ override the options.`,
s := &settings.Settings{
Key: generateKey(),
Signup: mustGetBool(flags, "signup"),
Shell: strings.Split(strings.TrimSpace(mustGetString(flags, "shell")), " "),
Shell: convertCmdStrToCmdArray(mustGetString(flags, "shell")),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
@@ -62,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

@@ -1,8 +1,6 @@
package cmd
import (
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
@@ -50,7 +48,7 @@ you want to change. Other options will remain unchanged.`,
case "auth.method":
hasAuth = true
case "shell":
set.Shell = strings.Split(strings.TrimSpace(mustGetString(flags, flag.Name)), " ")
set.Shell = convertCmdStrToCmdArray(mustGetString(flags, flag.Name))
case "branding.name":
set.Branding.Name = mustGetString(flags, flag.Name)
case "branding.disableExternal":

View File

@@ -3,6 +3,7 @@ package cmd
import (
"crypto/tls"
"errors"
"io/fs"
"io/ioutil"
"log"
"net"
@@ -14,13 +15,17 @@ import (
"syscall"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/users"
@@ -32,6 +37,7 @@ var (
func init() {
cobra.OnInitialize(initConfig)
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
@@ -55,7 +61,14 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
flags.Uint32("socket-perm", 0666, "unix socket file permissions")
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.Int("img-processors", 4, "image processors count")
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{
@@ -103,6 +116,24 @@ user created with the credentials from options "username" and "password".`,
quickSetup(cmd.Flags(), d)
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
checkErr(err)
if workersCount < 1 {
log.Fatal("Image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
checkErr(err)
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil { //nolint:govet
log.Fatalf("can't make directory %s: %s", cacheDir, err)
}
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server := getRunParams(cmd.Flags(), d.store)
setupLog(server.Log)
@@ -118,13 +149,20 @@ user created with the credentials from options "username" and "password".`,
case server.Socket != "":
listener, err = net.Listen("unix", server.Socket)
checkErr(err)
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:shadow
socketPerm, err := cmd.Flags().GetUint32("socket-perm") //nolint:govet
checkErr(err)
listener, err = tls.Listen("tcp", adr, &tls.Config{Certificates: []tls.Certificate{cer}}) //nolint:shadow
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
checkErr(err)
case server.TLSKey != "" && server.TLSCert != "":
cer, err := tls.LoadX509KeyPair(server.TLSCert, server.TLSKey) //nolint:govet
checkErr(err)
listener, err = tls.Listen("tcp", adr, &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cer}},
)
checkErr(err)
default:
listener, err = net.Listen("tcp", adr) //nolint:shadow
listener, err = net.Listen("tcp", adr)
checkErr(err)
}
@@ -132,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(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()
@@ -205,6 +248,18 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) *settings.Server {
server.Socket = ""
}
_, disableThumbnails := getParamB(flags, "disable-thumbnails")
server.EnableThumbnails = !disableThumbnails
_, disablePreviewResize := getParamB(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
_, disableTypeDetectionByHeader := getParamB(flags, "disable-type-detection-by-header")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
_, disableExec := getParamB(flags, "disable-exec")
server.EnableExec = !disableExec
return server
}
@@ -261,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

@@ -7,6 +7,7 @@ import (
"log"
"os"
"path/filepath"
"strings"
"github.com/asdine/storm"
"github.com/spf13/cobra"
@@ -71,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
@@ -178,3 +179,15 @@ func cleanUpMapValue(v interface{}) interface{} {
return v
}
}
// convertCmdStrToCmdArray checks if cmd string is blank (whitespace included)
// then returns empty string array, else returns the splitted word array of cmd.
// This is to ensure the result will never be []string{""}
func convertCmdStrToCmdArray(cmd string) []string {
var cmdArray []string
trimmedCmdStr := strings.TrimSpace(cmd)
if trimmedCmdStr != "" {
cmdArray = strings.Split(trimmedCmdStr, " ")
}
return cmdArray
}

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',
],
],
},
};

11
diskcache/cache.go Normal file
View File

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

110
diskcache/file_cache.go Normal file
View File

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

View File

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

24
diskcache/noop_cache.go Normal file
View File

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

View File

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

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,60 @@ 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.
var buffer []byte
mimetype := mime.TypeByExtension(i.Extension)
if mimetype == "" && readHeader {
buffer = i.readFirstBytes()
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 +208,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 +227,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 +248,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 +273,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

@@ -2,11 +2,32 @@ package fileutils
import (
"io"
"os"
"path"
"path/filepath"
"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 {
@@ -25,7 +46,7 @@ func CopyFile(fs afero.Fs, source, dest string) error {
}
// Create the destination file.
dst, err := fs.Create(dest)
dst, err := fs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return err
}
@@ -37,15 +58,71 @@ 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
}
// CommonPrefix returns common directory path of provided files
func CommonPrefix(sep byte, paths ...string) string {
// Handle special cases.
switch len(paths) {
case 0:
return ""
case 1:
return path.Clean(paths[0])
}
// Note, we treat string as []byte, not []rune as is often
// done in Go. (And sep as byte, not rune). This is because
// most/all supported OS' treat paths as string of non-zero
// bytes. A filename may be displayed as a sequence of Unicode
// runes (typically encoded as UTF-8) but paths are
// not required to be valid UTF-8 or in any normalized form
// (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different
// file names.
c := []byte(path.Clean(paths[0]))
// We add a trailing sep to handle the case where the
// common prefix directory is included in the path list
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
// path.Clean will have cleaned off trailing / separators with
// the exception of the root directory, "/" (in which case we
// make it "//", but this will get fixed up to "/" bellow).
c = append(c, sep)
// Ignore the first path since it's already in c
for _, v := range paths[1:] {
// Clean up each path before testing it
v = path.Clean(v) + string(sep)
// Find the first non-common byte and truncate c
if len(v) < len(c) {
c = c[:len(v)]
}
for i := 0; i < len(c); i++ {
if v[i] != c[i] {
c = c[:i]
break
}
}
}
// Remove trailing non-separator characters and the final separator
for i := len(c) - 1; i >= 0; i-- {
if c[i] == sep {
c = c[:i]
break
}
}
return string(c)
}

46
fileutils/file_test.go Normal file
View File

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

10
frontend/assets.go Normal file
View File

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

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

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

View File

@@ -9929,8 +9929,7 @@
"pako": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz",
"integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==",
"dev": true
"integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA=="
},
"parallel-transform": {
"version": "1.1.0",
@@ -12895,6 +12894,14 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true
},
"utif": {
"version": "3.1.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/utif/-/utif-3.1.0.tgz",
"integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
"requires": {
"pako": "^1.0.5"
}
},
"util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
@@ -13030,6 +13037,11 @@
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.3.tgz",
"integrity": "sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew=="
},
"vue-lazyload": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/vue-lazyload/-/vue-lazyload-1.3.3.tgz",
"integrity": "sha512-uHnq0FTEeNmqnbBC2aRKlmtd9LofMZ6Q3mWvgfLa+i9vhxU8fDK+nGs9c1iVT85axSua/AUnMttIq3xPaU9G3A=="
},
"vue-loader": {
"version": "15.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.8.3.tgz",

View File

@@ -4,8 +4,8 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"watch": "vue-cli-service build --watch",
"build": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --no-clean",
"watch": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitignore' -exec rm -r {} + && vue-cli-service build --watch --no-clean",
"lint": "vue-cli-service lint --fix"
},
"dependencies": {
@@ -19,8 +19,10 @@
"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",
"vue-router": "^3.1.3",
"vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0"

View File

@@ -13,24 +13,25 @@
<link rel="icon" type="image/png" sizes="32x32" href="[{[ .StaticURL ]}]/img/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="[{[ .StaticURL ]}]/img/icons/favicon-16x16.png">
<!-- Add to home screen for Android and modern mobile browsers -->
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
<meta name="theme-color" content="#2979ff">
<!-- Add to home screen for Safari on iOS -->
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png">
<!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/msapplication-icon-144x144.png">
<meta name="msapplication-TileImage" content="[{[ .StaticURL ]}]/img/icons/mstile-144x144.png">
<meta name="msapplication-TileColor" content="#2979ff">
<!-- Inject Some Variables and generate the manifest json -->
<script>
window.FileBrowser = JSON.parse(`[{[ .Json ]}]`);
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
var dynamicManifest = {
"name": window.FileBrowser.Name || 'File Browser',

View File

@@ -1,7 +1,7 @@
:root {
--background: #121212;
--surfacePrimary: #171819;
--surfaceSecondary: #212528;
--background: #141D24;
--surfacePrimary: #20292F;
--surfaceSecondary: #3A4147;
--divider: rgba(255, 255, 255, 0.12);
--icon: #ffffff;
--textPrimary: rgba(255, 255, 255, 0.87);
@@ -16,7 +16,7 @@ body {
#loading {
background: var(--background);
}
#loading .spinner div {
#loading .spinner div, #previewer .loading .spinner div {
background: var(--icon);
}
@@ -30,25 +30,34 @@ header {
#search #input {
background: var(--surfaceSecondary);
border-color: var(--surfacePrimary);
}
#search.active #input,
#search.active .boxes {
#search #input input::placeholder {
color: var(--textSecondary);
}
#search.active #input {
background: var(--surfacePrimary);
}
#search.active input {
color: var(--textPrimary);
}
#search.active #result {
#search #result {
background: var(--background);
color: var(--textPrimary);
}
#search.active .boxes h3 {
#search .boxes {
background: var(--surfaceSecondary);
}
#search .boxes h3 {
color: var(--textPrimary);
}
.action {
color: var(--textPrimary) !important;
}
.action:hover {
background-color: rgba(255, 255, 255, .1);
}
.action i {
color: var(--icon) !important;
}
@@ -60,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);
@@ -93,6 +105,10 @@ nav > div {
background: var(--background);
}
.message {
color: var(--textPrimary);
}
.card {
background: var(--surfacePrimary);
color: var(--textPrimary);
@@ -101,17 +117,38 @@ 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 input,
.card#share select,
.input {
background: var(--surfaceSecondary);
color: var(--textPrimary);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.input:hover,
.input:focus {
border-color: rgba(255, 255, 255, 0.15);
}
.input--red {
background: #73302D;
}
.dashboard #nav li,
.input--green {
background: #147A41;
}
.dashboard #nav .wrapper,
.collapsible {
border-color: var(--divider);
}
@@ -119,10 +156,35 @@ nav > div {
color: var(--textPrimary);
}
table th {
color: var(--textSecondary);
}
.file-list li:hover {
background: var(--surfaceSecondary);
}
.file-list li:before {
color: var(--textSecondary);
}
.file-list li[aria-selected=true]:before {
color: var(--icon);
}
.shell {
background: var(--surfacePrimary);
color: var(--textPrimary);
}
.shell__result {
border-top: 1px solid var(--divider);
}
#editor-container {
background: var(--background);
}
#editor-container .bar {
background: var(--surfacePrimary);
}
@media (max-width: 736px) {
#file-selection {
@@ -138,3 +200,12 @@ nav > div {
background: var(--surfaceSecondary) !important;
}
}
.share__box {
background: var(--surfacePrimary) !important;
color: var(--textPrimary);
}
.share__box__element {
border-top-color: var(--divider);
}

View File

@@ -43,7 +43,7 @@ async function resourceAction (url, method, content) {
const res = await fetchURL(`/api/resources${url}`, opts)
if (res.status !== 200) {
throw new Error(res.responseText)
throw new Error(await res.text())
} else {
return res
}
@@ -74,17 +74,25 @@ export function download (format, ...files) {
url += `/?files=${arg}&`
}
if (format !== null) {
if (format) {
url += `algo=${format}&`
}
url += `auth=${store.state.jwt}`
if (store.state.jwt){
url += `auth=${store.state.jwt}&`
}
window.open(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)
@@ -94,9 +102,6 @@ export async function post (url, content = '', overwrite = false, onupload) {
request.upload.onprogress = onupload
}
// Send a message to user before closing the tab during file upload
window.onbeforeunload = () => "Files are being uploaded."
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
@@ -111,30 +116,29 @@ export async function post (url, content = '', overwrite = false, onupload) {
reject(error)
}
request.send(content)
// Upload is done no more message before closing the tab
}).finally(() => { window.onbeforeunload = null })
request.send(bufferContent || content)
})
}
function moveCopy (items, copy = false) {
function moveCopy (items, copy = false, overwrite = false, rename = false) {
let promises = []
for (let item of items) {
const from = removePrefix(item.from)
const from = item.from
const to = encodeURIComponent(removePrefix(item.to))
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}`
const url = `${from}?action=${copy ? 'copy' : 'rename'}&destination=${to}&override=${overwrite}&rename=${rename}`
promises.push(resourceAction(url, 'PATCH'))
}
return Promise.all(promises)
}
export function move (items) {
return moveCopy(items)
export function move (items, overwrite = false, rename = false) {
return moveCopy(items, false, overwrite, rename)
}
export function copy (items) {
return moveCopy(items, true)
export function copy (items, overwrite = false, rename = false) {
return moveCopy(items, true, overwrite, rename)
}
export async function checksum (url, algo) {

View File

@@ -2,6 +2,7 @@ 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'
@@ -10,6 +11,7 @@ export {
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,8 +1,31 @@
import { fetchJSON, removePrefix } from './utils'
import { fetchURL, removePrefix } from './utils'
import url from '../utils/url'
export default async function search (url, query) {
url = removePrefix(url)
export default async function search (base, query) {
base = removePrefix(base)
query = encodeURIComponent(query)
return fetchJSON(`/api/search${url}?query=${query}`, {})
}
if (!base.endsWith('/')) {
base += '/'
}
let res = await fetchURL(`/api/search${base}?query=${query}`, {})
if (res.status === 200) {
let data = await res.json()
data = data.map((item) => {
item.url = `/files${base}` + url.encodePath(item.path)
if (item.dir) {
item.url += '/'
}
return item
})
return data
} else {
throw Error(res.status)
}
}

View File

@@ -1,7 +1,7 @@
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) {
@@ -19,14 +19,18 @@ export async function remove(hash) {
}
}
export async function create(url, expires = '', unit = 'hours') {
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

@@ -34,9 +34,7 @@ export async function fetchJSON (url, opts) {
}
export function removePrefix (url) {
if (url.startsWith('/files')) {
url = url.slice(6)
}
url = url.split('/').splice(2).join('/')
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url

View File

@@ -0,0 +1,67 @@
<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,189 +0,0 @@
<template>
<header>
<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 v-show="showSaveButton" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" class="action" id="save-button">
<i class="material-icons">save</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-show="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 } 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',
'isListing',
'isLogged'
]),
...mapState([
'req',
'user',
'loading',
'reload',
'multiple'
]),
logoURL: () => logoURL,
isMobile () {
return this.width <= 736
},
showUpload () {
return this.isListing && this.user.perm.create
},
showSaveButton () {
return this.isEditor && this.user.perm.modify
},
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

@@ -49,7 +49,7 @@
</template>
<ul v-show="results.length > 0">
<li v-for="(s,k) in filteredResults" :key="k">
<router-link @click.native="close" :to="'./' + s.path">
<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>
<span>./{{ s.path }}</span>
@@ -136,12 +136,6 @@ export default {
}
},
mounted() {
window.addEventListener("keydown", event => {
if (event.keyCode === 27) {
this.closeHovers()
}
})
this.$refs.result.addEventListener('scroll', event => {
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
this.resultsCount += 50
@@ -189,8 +183,12 @@ export default {
this.ongoing = true
try {
this.results = await search(path, this.value)
} catch (error) {
this.$showError(error)
}
this.results = await search(path, this.value)
this.ongoing = false
}
}

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,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

@@ -10,10 +10,13 @@
@mouseup="mouseUp"
@wheel="wheelMove"
>
<img :src="src" class="image-ex-img" ref="imgex" @load="setCenter">
<img src="" class="image-ex-img image-ex-img-center" ref="imgex" @load="onLoad">
</div>
</template>
<script>
import throttle from 'lodash.throttle'
import UTIF from 'utif'
export default {
props: {
src: String,
@@ -50,10 +53,18 @@ export default {
inDrag: false,
lastTouchDistance: 0,
moveDisabled: false,
disabledTimer: null
disabledTimer: null,
imageLoaded: false,
position: {
center: { x: 0, y: 0 },
relative: { x: 0, y: 0 }
}
}
},
mounted() {
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
@@ -63,24 +74,68 @@ export default {
if (getComputedStyle(container).height === "0px") {
container.style.height = "100%"
}
window.addEventListener('resize', this.onResize)
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize)
document.removeEventListener('mouseup', this.onMouseUp)
},
watch: {
src: function () {
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
this.imageLoaded = true
if (img === undefined) {
return
}
img.classList.remove('image-ex-img-center')
this.setCenter()
img.classList.add('image-ex-img-ready')
document.addEventListener('mouseup', this.onMouseUp)
},
onMouseUp() {
this.inDrag = false
},
onResize: throttle(function() {
if (this.imageLoaded) {
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 rate = Math.min(
container.clientWidth / img.clientWidth,
container.clientHeight / img.clientHeight
)
if (!this.autofill && rate > 1) {
rate = 1
}
// height will be auto set
img.width = Math.floor(img.clientWidth * rate)
img.style.top = `${Math.floor((container.clientHeight - img.clientHeight) / 2)}px`
img.style.left = `${Math.floor((container.clientWidth - img.clientWidth) / 2)}px`
document.addEventListener('mouseup', () => this.inDrag = false )
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'
},
mousedownStart(event) {
this.lastX = null
@@ -101,6 +156,15 @@ export default {
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) {
@@ -114,6 +178,7 @@ export default {
default:
case 4:
this.scale = 1
this.setCenter()
break
}
this.setZoom()
@@ -159,8 +224,22 @@ export default {
},
doMove(x, y) {
let style = this.$refs.imgex.style
style.left = `${this.pxStringToNumber(style.left) + x}px`
style.top = `${this.pxStringToNumber(style.top) + y}px`
let posX = this.pxStringToNumber(style.left) + x
let posY = this.pxStringToNumber(style.top) + y
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)
if (posX < this.position.center.x) {
this.position.relative.x = this.position.relative.x * -1
}
if (posY < this.position.center.y) {
this.position.relative.y = this.position.relative.y * -1
}
},
wheelMove(event) {
this.scale += (event.wheelDeltaY / 100) * this.zoomStep
@@ -185,9 +264,20 @@ export default {
}
.image-ex-img {
position: absolute;
}
.image-ex-img-center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
position: absolute;
transition: none;
}
.image-ex-img-ready {
left: 0;
top: 0;
position: absolute;
transition: transform 0.1s ease;
}
</style>

View File

@@ -1,578 +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"
@dragenter="dragEnter"
@dragend="dragEnd">
<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 throttle from 'lodash.throttle'
import Item from './ListingItem'
import css from '@/utils/css'
import { users, files as api } from '@/api'
import buttons from '@/utils/buttons'
import url from '@/utils/url'
export default {
name: 'listing',
components: { Item },
data: function () {
return {
show: 50,
uploading: {
id: 0,
count: 0,
size: 0,
progress: []
}
}
},
computed: {
...mapState(['req', 'selected', 'user']),
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.show)
},
files () {
let show = this.show - this.items.dirs.length
if (show < 0) show = 0
return this.items.files.slice(0, show)
},
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('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('drop', this.drop)
},
methods: {
...mapMutations([ 'updateUser', 'addSelected' ]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
},
keyEvent (event) {
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
})
},
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 })
}
if (items.length === 0) {
return
}
if (this.$store.state.clipboard.key === 'x') {
api.move(items).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
return
}
api.copy(items).then(() => {
this.$store.commit('setReload', true)
}).catch(this.$showError)
},
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.show += 50
}
},
dragEnter () {
// 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
})
},
dragEnd () {
this.resetOpacity()
},
drop: function (event) {
event.preventDefault()
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 + '/'
}
if (base === '') {
this.scanFiles(dt).then((result) => {
this.checkConflict(result, this.req.items, base)
})
} else {
this.scanFiles(dt).then((result) => {
api.fetch(this.$route.path + base)
.then(req => {
this.checkConflict(result, req.items, base)
})
.catch(this.$showError)
})
}
},
checkConflict (files, items, base) {
if (typeof items === 'undefined' || items === null) {
items = []
}
let folder_upload = false
if (files[0].fullPath !== undefined) {
folder_upload = true
}
let conflict = false
for (let i = 0; i < files.length; i++) {
let file = files[i]
let name = file.name
if (folder_upload) {
let dirs = file.fullPath.split("/")
if (dirs.length > 1) {
name = dirs[0]
}
}
let res = items.findIndex(function hasConflict (element) {
return (element.name === this)
}, name)
if (res >= 0) {
conflict = true
break
}
}
if (!conflict) {
this.handleFiles(files, base)
return
}
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
this.handleFiles(files, base, true)
}
})
},
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
}
}
this.checkConflict(files, this.req.items, '')
},
resetOpacity () {
let items = document.getElementsByClassName('item')
Array.from(items).forEach(file => {
file.style.opacity = 1
})
},
scanFiles(dt) {
return new Promise((resolve) => {
let reading = 0
const contents = []
if (dt.items !== undefined) {
for (let item of dt.items) {
if (item.kind === "file" && typeof item.webkitGetAsEntry === "function") {
const entry = item.webkitGetAsEntry()
readEntry(entry)
}
}
} else {
resolve(dt.files)
}
function readEntry(entry, directory = "") {
if (entry.isFile) {
reading++
entry.file(file => {
reading--
file.fullPath = `${directory}${file.name}`
contents.push(file)
if (reading === 0) {
resolve(contents)
}
})
} else if (entry.isDirectory) {
const dir = {
isDir: true,
path: `${directory}${entry.name}`
}
contents.push(dir)
readReaderContent(entry.createReader(), `${directory}${entry.name}`)
}
}
function readReaderContent(reader, directory) {
reading++
reader.readEntries(function (entries) {
reading--
if (entries.length > 0) {
for (const entry of entries) {
readEntry(entry, `${directory}/`)
}
readReaderContent(reader, `${directory}/`)
}
if (reading === 0) {
resolve(contents)
}
})
}
})
},
setProgress: throttle(function() {
if (this.uploading.count == 0) {
return
}
let sum = this.uploading.progress.reduce((acc, val) => acc + val)
this.$store.commit('setProgress', Math.ceil(sum / this.uploading.size * 100))
}, 100, {leading: false, trailing: true}),
handleFiles (files, base, overwrite = false) {
if (this.uploading.count == 0) {
buttons.loading('upload')
}
let promises = []
let onupload = (id) => (event) => {
this.uploading.progress[id] = event.loaded
this.setProgress()
}
for (let i = 0; i < files.length; i++) {
let file = files[i]
if (!file.isDir) {
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
let id = this.uploading.id
this.uploading.size += file.size
this.uploading.id++
this.uploading.count++
let promise = api.post(this.$route.path + base + filenameEncoded, file, overwrite, throttle(onupload(id), 100)).finally(() => {
this.uploading.count--
})
promises.push(promise)
} else {
let uri = this.$route.path + base
let folders = file.path.split("/")
for (let i = 0; i < folders.length; i++) {
let folder = folders[i]
let folderEncoded = encodeURIComponent(folder)
uri += folderEncoded + "/"
}
api.post(uri)
}
}
let finish = () => {
if (this.uploading.count > 0) {
return
}
buttons.success('upload')
this.$store.commit('setProgress', 0)
this.$store.commit('setReload', true)
this.uploading.id = 0
this.uploading.sizes = []
this.uploading.progress = []
}
Promise.all(promises)
.then(() => {
finish()
})
.catch(error => {
finish()
this.$showError(error)
})
return false
},
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

@@ -6,14 +6,14 @@
@dragstart="dragStart"
@dragover="dragOver"
@drop="drop"
@click="click"
@dblclick="open"
@click="itemClick"
@dblclick="dblclick"
@touchstart="touchstart"
:data-dir="isDir"
:aria-label="name"
:aria-selected="isSelected">
<div>
<img v-if="type==='image'" :src="thumbnailUrl">
<img v-if="readOnly == undefined && type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
<i v-else class="material-icons">{{ icon }}</i>
</div>
@@ -31,11 +31,12 @@
</template>
<script>
import { baseURL } from '@/utils/constants'
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',
@@ -44,10 +45,13 @@ export default {
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']),
...mapState(['user', 'selected', 'req', 'jwt']),
...mapGetters(['selectedCount']),
singleClick () {
return this.readOnly == undefined && this.user.singleClick
},
isSelected () {
return (this.selected.indexOf(this.index) !== -1)
},
@@ -59,10 +63,10 @@ export default {
return 'insert_drive_file'
},
isDraggable () {
return this.user.perm.rename
return this.readOnly == undefined && this.user.perm.rename
},
canDrop () {
if (!this.isDir) return false
if (!this.isDir || this.readOnly !== undefined) return false
for (let i of this.selected) {
if (this.req.items[i].url === this.url) {
@@ -74,7 +78,14 @@ export default {
},
thumbnailUrl () {
const path = this.url.replace(/^\/files\//, '')
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
// 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
}
},
methods: {
@@ -110,29 +121,68 @@ export default {
el.style.opacity = 1
},
drop: function (event) {
drop: async function (event) {
if (!this.canDrop) return
event.preventDefault()
if (this.selectedCount === 0) return
let el = event.target
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains('item')) {
el = el.parentElement
}
}
let items = []
for (let i of this.selected) {
items.push({
from: this.req.items[i].url,
to: this.url + this.req.items[i].name
to: this.url + this.req.items[i].name,
name: this.req.items[i].name
})
}
api.move(items)
.then(() => {
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)
}
let conflict = upload.checkConflict(items, baseItems)
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)
}
})
.catch(this.$showError)
return
}
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
@@ -159,9 +209,12 @@ export default {
return
}
if (!event.ctrlKey && !this.$store.state.multiple) this.resetSelected()
if (!this.singleClick && !event.ctrlKey && !event.metaKey && !this.$store.state.multiple) this.resetSelected()
this.addSelected(this.index)
},
dblclick: function () {
if (!this.singleClick) this.open()
},
touchstart () {
setTimeout(() => {
this.touches = 0
@@ -177,4 +230,4 @@ export default {
}
}
}
</script>
</script>

View File

@@ -1,166 +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>
<rename-button v-if="user.perm.rename"></rename-button>
<delete-button v-if="user.perm.delete"></delete-button>
<download-button v-if="user.perm.download"></download-button>
<info-button></info-button>
</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>
<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>
</div>
</template>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import { baseURL } from '@/utils/constants'
import { files as api } from '@/api'
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: {
InfoButton,
DeleteButton,
RenameButton,
DownloadButton,
ExtendedImage
},
data: function () {
return {
previousLink: '',
nextLink: '',
listing: null,
subtitles: []
}
},
computed: {
...mapState(['req', 'user', 'oldReq', 'jwt']),
hasPrevious () {
return (this.previousLink !== '')
},
hasNext () {
return (this.nextLink !== '')
},
download () {
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
},
previewUrl () {
if (this.req.type === 'image') {
return `${baseURL}/api/preview/big${this.req.path}?auth=${this.jwt}`
}
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
},
raw () {
return `${this.previewUrl}&inline=true`
}
},
async mounted () {
window.addEventListener('keyup', this.key)
if (this.req.subtitles) {
this.subtitles = this.req.subtitles.map(sub => `${baseURL}/api/raw${sub}?auth=${this.jwt}&inline=true`)
}
try {
if (this.oldReq.items) {
this.updateLinks(this.oldReq.items)
} else {
const path = url.removeLastDir(this.$route.path)
const res = await api.fetch(path)
this.updateLinks(res.items)
}
} catch (e) {
this.$showError(e)
}
},
beforeDestroy () {
window.removeEventListener('keyup', this.key)
},
methods: {
back () {
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 (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()
}
},
updateLinks (items) {
for (let i = 0; i < items.length; i++) {
if (items[i].name !== this.req.name) {
continue
}
for (let j = i - 1; j >= 0; j--) {
if (mediaTypes.includes(items[j].type)) {
this.previousLink = items[j].url
break
}
}
for (let j = i + 1; j < items.length; j++) {
if (mediaTypes.includes(items[j].type)) {
this.nextLink = items[j].url
break
}
}
return
}
}
}
}
</script>

View File

@@ -0,0 +1,32 @@
<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,47 @@
<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

@@ -16,7 +16,6 @@
:title="$t('buttons.cancel')">{{ $t('buttons.cancel') }}</button>
<button class="button button--flat"
@click="copy"
:disabled="$route.path === dest"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')">{{ $t('buttons.copy') }}</button>
</div>
@@ -28,6 +27,7 @@ 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',
@@ -42,25 +42,66 @@ export default {
methods: {
copy: async function (event) {
event.preventDefault()
buttons.loading('copy')
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)
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
})
}
try {
await api.copy(items)
buttons.success('copy')
this.$router.push({ path: this.dest })
} catch (e) {
buttons.done('copy')
this.$showError(e)
let action = async (overwrite, rename) => {
buttons.loading('copy')
await api.copy(items, overwrite, rename).then(() => {
buttons.success('copy')
if (this.$route.path === this.dest) {
this.$store.commit('setReload', true)
return
}
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)
return
}
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
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)
}
}
}

View File

@@ -20,29 +20,31 @@
<script>
import {mapGetters, mapMutations, mapState} from 'vuex'
import { files as api } from '@/api'
import url from '@/utils/url'
import buttons from '@/utils/buttons'
export default {
name: 'delete',
computed: {
...mapGetters(['isListing', 'selectedCount']),
...mapState(['req', 'selected'])
...mapState(['req', 'selected', 'showConfirm'])
},
methods: {
...mapMutations(['closeHovers']),
submit: async function () {
this.closeHovers()
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) + '/' })
this.showConfirm()
this.closeHovers()
return
}
this.closeHovers()
if (this.selectedCount === 0) {
return
}

View File

@@ -7,43 +7,29 @@
<div class="card-content">
<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'])
},
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)
data: function () {
return {
formats: {
zip: 'zip',
tar: 'tar',
targz: 'tar.gz',
tarbz2: 'tar.bz2',
tarxz: 'tar.xz',
tarlz4: 'tar.lz4',
tarsz: 'tar.sz'
}
this.$store.commit('closeHovers')
}
}
},
computed: mapState(['showConfirm'])
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<ul class="file-list">
<li @click="select"
<li @click="itemClick"
@touchstart="touchstart"
@dblclick="next"
role="button"
@@ -35,25 +35,13 @@ export default {
}
},
computed: {
...mapState([ 'req' ]),
...mapState([ 'req', 'user' ]),
nav () {
return decodeURIComponent(this.current)
}
},
mounted () {
// If we're showing this on a listing,
// we can use the current request object
// to fill the move options.
if (this.req.kind === 'listing') {
this.fillOptions(this.req)
return
}
// Otherwise, we must be on a preview or editor
// so we fetch the data from the previous directory.
files.fetch(url.removeLastDir(this.$route.path))
.then(this.fillOptions)
.catch(this.$showError)
this.fillOptions(this.req)
},
methods: {
fillOptions (req) {
@@ -123,6 +111,10 @@ export default {
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) {

View File

@@ -63,7 +63,7 @@ export default {
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

View File

@@ -27,6 +27,7 @@ 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',
@@ -41,26 +42,51 @@ export default {
methods: {
move: async function (event) {
event.preventDefault()
buttons.loading('move')
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)
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name
})
}
try {
api.move(items)
buttons.success('move')
this.$router.push({ path: this.dest })
} catch (e) {
buttons.done('move')
this.$showError(e)
let action = async (overwrite, rename) => {
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)
})
}
event.preventDefault()
let dstItems = (await api.fetch(this.dest)).items
let conflict = upload.checkConflict(items, dstItems)
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)
}
}
}

View File

@@ -16,8 +16,10 @@ 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'
@@ -35,7 +37,9 @@ export default {
NewDir,
Help,
Replace,
Upload
ReplaceRename,
Upload,
ShareDelete
},
data: function () {
return {
@@ -52,7 +56,7 @@ export default {
return
let prompt = this.$refs.currentComponent;
// Enter
if (event.keyCode == 13) {
switch (this.show) {
@@ -87,8 +91,10 @@ export default {
'newDir',
'download',
'replace',
'replace-rename',
'share',
'upload'
'upload',
'share-delete'
].indexOf(this.show) >= 0;
return matched && this.show || null;

View File

@@ -0,0 +1,35 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t('prompts.replace') }}</h2>
</div>
<div class="card-content">
<p>{{ $t('prompts.replaceMessage') }}</p>
</div>
<div class="card-action">
<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"
@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"
@click="(event) => showConfirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')">{{ $t('buttons.replace') }}</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'replace-rename',
computed: mapState(['showConfirm'])
}
</script>

View File

@@ -1,59 +1,85 @@
<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>
</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>
<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="1"
@keyup.enter="submit"
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>
</div>
<p>{{ $t('prompts.optionalPassword') }}</p>
<input class="input input--block" type="password" v-model.trim="password">
</div>
<li>
<input v-focus
type="number"
max="2147483647"
min="0"
@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>
</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 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>
@@ -70,9 +96,10 @@ export default {
return {
time: '',
unit: 'hours',
hasPermanent: false,
links: [],
clip: null
clip: null,
password: '',
listing: true
}
},
computed: {
@@ -97,11 +124,8 @@ export default {
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)
@@ -118,22 +142,25 @@ export default {
},
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)
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()
} 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
this.time = ''
this.unit = 'hours'
this.password = ''
this.listing = true
} catch (e) {
this.$showError(e)
}
@@ -142,8 +169,11 @@ export default {
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)
if (this.links.length == 0) {
this.listing = false
}
} catch (e) {
this.$showError(e)
}
@@ -160,6 +190,13 @@ export default {
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
}
}
}

View File

@@ -0,0 +1,33 @@
<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

@@ -27,9 +27,11 @@ export default {
name: 'upload',
methods: {
uploadFile: function () {
document.getElementById('upload-input').value = ''
document.getElementById('upload-input').click()
},
uploadFolder: function () {
document.getElementById('upload-folder-input').value = ''
document.getElementById('upload-folder-input').click()
}
}

View File

@@ -16,7 +16,11 @@ export default {
return this.commands.join(' ')
},
set (value) {
this.$emit('update:commands', value.split(' '))
if (value !== '') {
this.$emit('update:commands', value.split(' '))
} else {
this.$emit('update:commands', [])
}
}
}
}

View File

@@ -9,13 +9,14 @@
<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><input type="checkbox" :disabled="admin" v-model="perm.execute"> {{ $t('settings.perm.execute') }}</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'
export default {
name: 'permissions',
props: ['perm'],
@@ -33,7 +34,8 @@ export default {
this.perm.admin = value
}
}
},
isExecEnabled: () => enableExec
}
}
</script>

View File

@@ -25,7 +25,7 @@
</p>
<permissions :perm.sync="user.perm" />
<commands :commands.sync="user.commands" />
<commands v-if="isExecEnabled" :commands.sync="user.commands" />
<div v-if="!isDefault">
<h3>{{ $t('settings.rules') }}</h3>
@@ -40,6 +40,7 @@ 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',
@@ -53,7 +54,8 @@ export default {
computed: {
passwordPlaceholder () {
return this.isNew ? '' : this.$t('settings.avoidChanges')
}
},
isExecEnabled: () => enableExec
},
watch: {
'user.perm.admin': function () {

View File

@@ -25,8 +25,8 @@
background: var(--red);
}
.button--red:hover {
background: var(--dark-red);
.button--blue {
background: var(--blue);
}
.button--flat {

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;

View File

@@ -6,9 +6,25 @@ header {
position: fixed;
top: 0;
left: 0;
height: 4em;
width: 100%;
padding: 0;
display: flex;
padding: 0.5em 0.5em 0.5em 1em;
align-items: center;
}
header > * {
flex: 0 0 auto;
}
header title {
display: block;
flex: 1 1 auto;
padding: 0 1em;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.2em;
}
header .overlay {
@@ -30,17 +46,6 @@ header img {
height: 2.5em;
}
header>div:first-child>.action {
display: none;
}
header>div {
display: flex;
width: 100%;
padding: 0.5em 0.5em 0.5em 1em;
align-items: center;
}
header .action span {
display: none;
}
@@ -50,19 +55,8 @@ header>div div {
position: relative;
}
header>div:last-child div {
display: flex;
}
header>div:first-child {
height: 4em;
}
header>div:last-child {
justify-content: flex-end;
}
header .search-button {
header .search-button,
header .menu-button {
display: none;
}

View File

@@ -70,6 +70,7 @@
box-shadow: rgba(0, 0, 0, 0.06) 0px 1px 3px, rgba(0, 0, 0, 0.12) 0px 1px 2px;
width: 95%;
max-width: 20em;
z-index: 1;
}
#file-selection .action {
border-radius: 50%;
@@ -81,6 +82,9 @@
color: #6f6f6f;
margin-right: auto;
}
#file-selection .action span {
display: none;
}
nav {
top: 0;
z-index: 99999;
@@ -95,7 +99,7 @@
left: 0;
}
header .search-button,
header>div:first-child>.action {
header .menu-button {
display: inherit;
}
header img {

View File

@@ -96,10 +96,11 @@
color: #fff;
border-radius: 50%;
font-size: .75em;
width: 1.5em;
height: 1.5em;
width: 1.8em;
height: 1.8em;
text-align: center;
line-height: 1.25em;
line-height: 1.55em;
font-weight: bold;
border: 2px solid white;
}
@@ -117,35 +118,33 @@
overflow: hidden;
}
#previewer .bar {
width: 100%;
text-align: right;
display: flex;
padding: 0.5em;
height: 3.7em;
}
#previewer .action:first-of-type {
margin-right: auto;
}
#previewer .action i {
#previewer header {
background: none;
color: #fff;
}
#previewer .action:hover {
#previewer header > .action i {
color: #fff;
}
@media (min-width: 738px) {
#previewer header #dropdown .action i {
color: #fff;
}
}
#previewer header .action:hover {
background-color: rgba(255, 255, 255, 0.3)
}
#previewer .action span {
#previewer header .action span {
display: none;
}
#previewer .preview {
margin: 2em auto 4em;
max-width: 80%;
margin-top: 4em;
text-align: center;
height: calc(100vh - 9.7em);
height: calc(100vh - 4em);
}
#previewer .preview pre {
@@ -160,6 +159,10 @@
margin: 0;
}
#previewer .preview video {
height: 100%;
}
#previewer .pdf {
width: 100%;
height: 100%;
@@ -172,8 +175,25 @@
#previewer>button {
margin: 0;
position: fixed;
top: 50%;
top: calc(50% + 1.85em);
transform: translateY(-50%);
background-color: rgba(80, 80, 80, .5);
color: white;
border-radius: 50%;
cursor: pointer;
border: 0;
margin: 0;
padding: 0;
transition: 0.2s ease all;
}
#previewer>button.hidden {
opacity: 0;
visibility: hidden;
}
#previewer>button>i {
padding: 0.4em;
}
#previewer>button:first-of-type {
@@ -184,6 +204,40 @@
right: 0.5em;
}
/* EDITOR */
#editor-container {
background-color: #fafafa;
position: fixed;
margin-top: 4em;
top: 0;
left: 0;
width: 100%;
z-index: 9999;
overflow: hidden;
}
#previewer .loading {
height: 100%;
width: 100%;
}
#editor-container #editor {
height: calc(100vh - 8.4em);
}
#editor-container .breadcrumbs {
height: 2.3em;
padding: 0 1em;
}
#editor-container .breadcrumbs span {
font-size: 12px;
}
#editor-container .breadcrumbs i {
font-size: 16px;
}
/* * * * * * * * * * * * * * * *
* PROMPT *

View File

@@ -1,7 +1,5 @@
{
"permanent": "دائم",
"buttons": {
"shell": "Toggle shell",
"cancel": "إلغاء",
"close": "إغلاق",
"copy": "نسخ",
@@ -10,6 +8,7 @@
"create": "إنشاء",
"delete": "حذف",
"download": "تحميل",
"hideDotfiles": "",
"info": "معلومات",
"more": "المزيد",
"move": "نقل",
@@ -17,25 +16,28 @@
"new": "جديد",
"next": "التالي",
"ok": "موافق",
"replace": "استبدال",
"permalink": "الحصول على لنك دائم",
"previous": "السابق",
"publish": "نشر",
"rename": "إعادة تسمية",
"replace": "استبدال",
"reportIssue": "إبلاغ عن مشكلة",
"save": "حفظ",
"schedule": "جدولة",
"search": "بحث",
"select": "تحديد",
"share": "مشاركة",
"publish": "نشر",
"selectMultiple": "تحديد متعدد",
"schedule": "جدولة",
"share": "مشاركة",
"shell": "Toggle shell",
"switchView": "تغيير العرض",
"toggleSidebar": "تبديل الشريط الجانبي",
"update": "تحديث",
"upload": "رفع",
"permalink": "الحصول على لنك دائم"
"upload": "رفع"
},
"success": {
"linkCopied": "تم نسخ الملف"
"download": {
"downloadFile": "Download File",
"downloadFolder": "Download Folder",
"downloadSelected": ""
},
"errors": {
"forbidden": "You don't have permissions to access this.",
@@ -43,11 +45,11 @@
"notFound": "لا يمكن الوصول لهذا المحتوى."
},
"files": {
"folders": "المجلدات",
"files": "الملفات",
"body": "الصفحة",
"clear": "مسح",
"closePreview": "إغلاق العرض",
"files": "الملفات",
"folders": "المجلدات",
"home": "الصفحة الاولى",
"lastModified": "آخر تعديل",
"loading": "جاري التحميل...",
@@ -56,9 +58,9 @@
"multipleSelectionEnabled": "التحديد المتعدد مفعل",
"name": "الإسم",
"size": "الحجم",
"sortByLastModified": "الترتيب بآخر تعديل",
"sortByName": "الترتيب بالإسم",
"sortBySize": "الترتيب بالحجم",
"sortByLastModified": "الترتيب بآخر تعديل"
"sortBySize": "الترتيب بالحجم"
},
"help": {
"click": "حدد الملف أو المجلد",
@@ -74,18 +76,39 @@
"f2": "إعادة تسمية الملف",
"help": "مساعدة"
},
"languages": {
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ru": "Русский",
"svSE": "",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"password": "كلمة المرور",
"passwordConfirm": "Password Confirmation",
"submit": "تسجيل دخول",
"createAnAccount": "Create an account",
"loginInstead": "Already have an account",
"password": "كلمة المرور",
"passwordConfirm": "Password Confirmation",
"passwordsDontMatch": "Passwords don't match",
"usernameTaken": "Username already taken",
"signup": "Signup",
"submit": "تسجيل دخول",
"username": "إسم المستخدم",
"usernameTaken": "Username already taken",
"wrongCredentials": "بيانات دخول خاطئة"
},
"permanent": "دائم",
"prompts": {
"copy": "نسخ",
"copyMessage": "رجاء حدد المكان لنسخ ملفاتك فيه:",
@@ -102,43 +125,64 @@
"lastModified": "آخر تعديل",
"move": "نقل",
"moveMessage": "إختر مكان جديد للملفات أو المجلدات المراد نقلها:",
"newArchetype": "إنشاء منشور من المنشور الأصلي. الملف سيتم انشاءه في مجلد المحتويات.",
"newDir": "مجلد جديد",
"newDirMessage": "رجاء أدخل اسم المجلد الجديد.",
"newFile": "ملف جديد",
"newFileMessage": "رجاء ادخل اسم الملف الجديد.",
"numberDirs": "عدد المجلدات",
"numberFiles": "عدد الملفات",
"replace": "إستبدال",
"replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس الإسم. هل تريد إستبدال الملف الموجود؟\n",
"rename": "إعادة تسمية",
"renameMessage": "إدراج اسم جديد لـ",
"show": "عرض",
"size": "الحجم",
"replace": "إستبدال",
"replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس الإسم. هل تريد إستبدال الملف الموجود؟\n",
"schedule": "جدولة",
"scheduleMessage": "أختر الوقت والتاريخ لجدولة نشر هذا المقال.",
"newArchetype": "إنشاء منشور من المنشور الأصلي. الملف سيتم انشاءه في مجلد المحتويات."
"show": "عرض",
"size": "الحجم",
"upload": "",
"uploadMessage": ""
},
"search": {
"images": "الصور",
"music": "الموسيقى",
"pdf": "PDF",
"pressToSearch": "Press enter to search...",
"search": "البحث...",
"typeToSearch": "Type to search...",
"types": "الأنواع",
"video": "فيديوهات"
},
"settings": {
"instanceName": "Instance name",
"brandingDirectoryPath": "Branding directory path",
"documentation": "documentation",
"branding": "Branding",
"disableExternalLinks": "Disable external links (except documentation)",
"brandingHelp": "You can costumize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"admin": "Admin",
"administrator": "Administrator",
"allowCommands": "تنفيذ الأوامر",
"allowEdit": "تعديل، إعادة تسمية وحذف الملفات والمجلدات",
"allowNew": "إنشاء ملفات ومجلدات جديدة",
"allowPublish": "نشر مقالات وصفحات جديدة",
"allowSignup": "Allow users to signup",
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",
"brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"changePassword": "تغيير كلمة المرور",
"commandRunner": "Command runner",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "تم تحديث الأوامر",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "ستايل مخصص",
"defaultUserDescription": "This are the default settings for new users.",
"disableExternalLinks": "Disable external links (except documentation)",
"documentation": "documentation",
"examples": "أمثلة",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"globalSettings": "إعدادات عامة",
"hideDotfiles": "",
"insertPath": "Insert the path",
"insertRegex": "Insert regex expression",
"instanceName": "Instance name",
"language": "اللغة",
"lockPassword": "منع المستخدم من تغيير كلمة المرور",
"newPassword": "كلمة المرور الجديدة",
@@ -146,6 +190,16 @@
"newUser": "مستخدم جديد",
"password": "كلمة المرور",
"passwordUpdated": "تم تغيير كلمة المرور",
"path": "",
"perm": {
"create": "Create files and directories",
"delete": "Delete files and directories",
"download": "Download",
"execute": "Execute commands",
"modify": "Edit files",
"rename": "Rename or move files and directories",
"share": "Share files"
},
"permissions": "الصلاحيات",
"permissionsHelp": "يمكنك تعيين المستخدم كـ \"مدير\" أو تحديد الصلاحيات بشكل منفرد.\n إذا قمت بتحديد المستخدم كـ \"مدير\"، باقي الخيارات سيتم تحديدها تلقائياً.\n إدارة المستخدمين تبقى صلاحية فريدة للـ \"مدير\" فقط.\n",
"profileSettings": "إعدادات الحساب",
@@ -155,82 +209,46 @@
"rulesHelp": "يمكنك هنا تحديد مجموعة من شروط السماح والمنع لهذا المستخدم. الملفات الممنوعة لن تظهر ضمن القائمة لهذا المستخدم ولن يستطيع الوصول لها. هنا ندعم الـ regex والـ relative path لنطاق المستخدمين.\n",
"scope": "نطاق",
"settingsUpdated": "تم تعديل الإعدادات",
"shareDuration": "",
"shareManagement": "",
"singleClick": "",
"themes": {
"dark": "",
"light": "",
"title": ""
},
"user": "المستخدم",
"userCommands": "الأوامر",
"userCommandsHelp": "الأوامر المتاحة لهذا المستخدم مفصولة فيما بينها بمسافة. مثال:\n",
"userCreated": "تم إنشاء المستخدم",
"userDefaults": "User default settings",
"userDeleted": "تم حذف المستخدم",
"userManagement": "إدارة المستخدمين",
"username": "إسم المستخدم",
"users": "المستخدمين",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"allowSignup": "Allow users to signup",
"createUserDir": "Auto create user home dir while adding new user",
"insertRegex": "Insert regex expression",
"insertPath": "Insert the path",
"userUpdated": "تم تعديل المستخدم",
"userDefaults": "User default settings",
"defaultUserDescription": "This are the default settings for new users.",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"perm": {
"create": "Create files and directories",
"delete": "Delete files and directories",
"download": "Download",
"modify": "Edit files",
"execute": "Execute commands",
"rename": "Rename or move files and directories",
"share": "Share files"
}
"username": "إسم المستخدم",
"users": "المستخدمين"
},
"sidebar": {
"help": "مساعدة",
"hugoNew": "هيوجو جديد",
"login": "Login",
"signup": "Signup",
"logout": "تسجيل خروج",
"myFiles": "ملفاتي",
"newFile": "ملف جديد",
"newFolder": "مجلد جديد",
"preview": "معاينة",
"settings": "الإعدادات",
"siteSettings": "إعدادات الموقع",
"hugoNew": "هيوجو جديد",
"preview": "معاينة"
"signup": "Signup",
"siteSettings": "إعدادات الموقع"
},
"search": {
"images": "الصور",
"music": "الموسيقى",
"pdf": "PDF",
"types": "الأنواع",
"video": "فيديوهات",
"search": "البحث...",
"typeToSearch": "Type to search...",
"pressToSearch": "Press enter to search..."
},
"languages": {
"ar": "العربية",
"en": "English",
"it": "Italiano",
"fr": "Français",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ja": "日本語",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"es": "Español",
"de": "Deutsch",
"ru": "Русский",
"pl": "Polski",
"ko": "한국어"
"success": {
"linkCopied": "تم نسخ الملف"
},
"time": {
"unit": "وحدة الوقت",
"seconds": "ثواني",
"minutes": "دقائق",
"days": "أيام",
"hours": "ساعات",
"days": "أيام"
},
"download": {
"downloadFile": "Download File",
"downloadFolder": "Download Folder"
"minutes": "دقائق",
"seconds": "ثواني",
"unit": "وحدة الوقت"
}
}
}

View File

@@ -1,7 +1,5 @@
{
"permanent": "Permanent",
"buttons": {
"shell": "Kommandozeile ein/ausschalten",
"cancel": "Abbrechen",
"close": "Schließen",
"copy": "Kopieren",
@@ -10,6 +8,7 @@
"create": "Neu",
"delete": "Löschen",
"download": "Downloaden",
"hideDotfiles": "",
"info": "Info",
"more": "mehr",
"move": "Verschieben",
@@ -17,25 +16,28 @@
"new": "Neu",
"next": "nächste",
"ok": "OK",
"replace": "Ersetzen",
"permalink": "permanenten Verweis anzeigen",
"previous": "vorherige",
"publish": "Veröffentlichen",
"rename": "umbenennen",
"replace": "Ersetzen",
"reportIssue": "Fehler melden",
"save": "Speichern",
"schedule": "Planung",
"search": "Suchen",
"select": "Auswählen",
"share": "Teilen",
"publish": "Veröffentlichen",
"selectMultiple": "Mehrfachauswahl",
"schedule": "Planung",
"share": "Teilen",
"shell": "Kommandozeile ein/ausschalten",
"switchView": "Ansicht wechseln",
"toggleSidebar": "Seitenleiste anzeigen",
"update": "Update",
"upload": "Upload",
"permalink": "permanenten Verweis anzeigen"
"upload": "Upload"
},
"success": {
"linkCopied": "Verweis wurde kopiert!"
"download": {
"downloadFile": "Download Datei",
"downloadFolder": "Download Ordner",
"downloadSelected": ""
},
"errors": {
"forbidden": "Sie haben keine Berechtigung dies abzurufen.",
@@ -43,11 +45,11 @@
"notFound": "Dieser Ort kann nicht angezeigt werden."
},
"files": {
"folders": "Ordner",
"files": "Dateien",
"body": "Body",
"clear": "Clear",
"closePreview": "Vorschau schließen",
"files": "Dateien",
"folders": "Ordner",
"home": "Home",
"lastModified": "zuletzt verändert",
"loading": "Lade...",
@@ -56,9 +58,9 @@
"multipleSelectionEnabled": "Mehrfachauswahl ausgewählt",
"name": "Name",
"size": "Größe",
"sortByLastModified": "Nach Änderungsdatum sortieren",
"sortByName": "Nach Namen sortieren",
"sortBySize": "Nach Größe sortieren",
"sortByLastModified": "Nach Änderungsdatum sortieren"
"sortBySize": "Nach Größe sortieren"
},
"help": {
"click": "wähle Datei oder Ordner",
@@ -74,18 +76,39 @@
"f2": "Datei umbenennen",
"help": "Hilfe"
},
"languages": {
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ru": "Русский",
"svSE": "",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"password": "Passwort",
"passwordConfirm": "Passwort Bestätigung",
"submit": "Login",
"createAnAccount": "Account erstellen",
"loginInstead": "Account besteht bereits",
"password": "Passwort",
"passwordConfirm": "Passwort Bestätigung",
"passwordsDontMatch": "Passwörter stimmen nicht überein",
"usernameTaken": "Benutzername ist bereits vergeben",
"signup": "Registrieren",
"submit": "Login",
"username": "Benutzername",
"usernameTaken": "Benutzername ist bereits vergeben",
"wrongCredentials": "Falsche Zugangsdaten"
},
"permanent": "Permanent",
"prompts": {
"copy": "Kopieren",
"copyMessage": "Wählen Sie einen Zielort zum Kopieren:",
@@ -102,43 +125,64 @@
"lastModified": "Zuletzt geändert",
"move": "Verschieben",
"moveMessage": "Wählen sie einen neuen Platz für ihre Datei(en)/Ordner:",
"newArchetype": "Erstelle neuen Beitrag auf dem Archetyp. Ihre Datei wird im Inhalteordner erstellt.",
"newDir": "Neuer Ordner",
"newDirMessage": "Geben Sie den Namen des neuen Ordners an.",
"newFile": "Neue Datei",
"newFileMessage": "Geben Sie den Namen der neuen Datei an.",
"numberDirs": "Anzahl der Ordner",
"numberFiles": "Anzahl der Dateien",
"replace": "Ersetzen",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei ersetzt werden ?\n",
"rename": "Umbenennen",
"renameMessage": "Fügen Sie einen Namen ein für",
"show": "Anzeigen",
"size": "Größe",
"replace": "Ersetzen",
"replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei ersetzt werden ?\n",
"schedule": "Plan",
"scheduleMessage": "Wählen Sie ein Datum und eine Zeit für die Veröffentlichung dieses Beitrags.",
"newArchetype": "Erstelle neuen Beitrag auf dem Archetyp. Ihre Datei wird im Inhalteordner erstellt."
"show": "Anzeigen",
"size": "Größe",
"upload": "",
"uploadMessage": ""
},
"search": {
"images": "Bilder",
"music": "Musik",
"pdf": "PDF",
"pressToSearch": "Drücken sie Enter um zu suchen...",
"search": "Suche...",
"typeToSearch": "Tippe um zu suchen...",
"types": "Typen",
"video": "Video"
},
"settings": {
"instanceName": "Instanzname",
"brandingDirectoryPath": "Markenverzeichnispfad",
"documentation": "Dokumentation",
"branding": "Marke",
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
"brandingHelp": "Sie können das Erscheinungsbild ihres File Browser anpassen, in dem sie den Namen ändern, das Logo austauchsen oder eigene Stile definieren und sogar externe Links zu GitHub deaktivieren.\nUm mehr Informationen zum Anpassen an ihre Marke zu bekommen, gehen sie bitte zu {0}.",
"admin": "Admin",
"administrator": "Administrator",
"allowCommands": "Befehle ausführen",
"allowEdit": "Bearbeiten, Umbenennen und Löschen von Dateien oder Ordnern",
"allowNew": "Erstellen neuer Dateien und Ordner",
"allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten",
"allowSignup": "Erlaube Benutzern sich zu registrieren",
"avoidChanges": "(leer lassen um Änderungen zu vermeiden)",
"branding": "Marke",
"brandingDirectoryPath": "Markenverzeichnispfad",
"brandingHelp": "Sie können das Erscheinungsbild ihres File Browser anpassen, in dem sie den Namen ändern, das Logo austauchsen oder eigene Stile definieren und sogar externe Links zu GitHub deaktivieren.\nUm mehr Informationen zum Anpassen an ihre Marke zu bekommen, gehen sie bitte zu {0}.",
"changePassword": "Ändere das Passwort",
"commandRunner": "Befehlseingabe",
"commandRunnerHelp": "Hier könne sie Befehle eintragen die bei benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen, lesen sie bitte das {2}.",
"commandsUpdated": "Befehle aktualisiert!",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "Individuelles Stylesheet",
"defaultUserDescription": "Das sind die Standard Einstellunge für Benutzer",
"disableExternalLinks": "Externe Links deaktivieren (außer Dokumentation)",
"documentation": "Dokumentation",
"examples": "Beispiele",
"executeOnShell": "In shell ausführen",
"executeOnShellDescription": "Es ist voreingestellt das der File Brower Befehle ausführt in dem er die Befehlsdatein direkt auf ruft. Wenn sie wollen das sie auf einer Kommandozeile (wo Bash oder PowerShell) laufen, könne sie das hier definieren mit allen bennötigten Argumenten und Optionen. Wenn gesetzt, wird das Kommando das ausgeführt werden soll als Parameter angehängt. Das gilt für Benuzerkommandos sowie auch für Ereignisse.",
"globalRules": "Das ist ein globales Set von Regeln die erlauben oder nicht erlauben. Die sind für alle Benutzer zutreffend. Es können spezielle Regeln in den Einstellungen der Benutzer definiert werden die diese übersteuern.",
"globalSettings": "Globale Einstellungen",
"hideDotfiles": "",
"insertPath": "Pfad einfügen",
"insertRegex": "Regex Ausdruck einfügen",
"instanceName": "Instanzname",
"language": "Sprache",
"lockPassword": "Verhindere, dass der Benutzer sein Passwort ändert",
"newPassword": "Ihr neues Passwort.",
@@ -146,6 +190,16 @@
"newUser": "Neuer Benutzer",
"password": "Passwort",
"passwordUpdated": "Passwort aktualisiert!",
"path": "",
"perm": {
"create": "Dateien und Ordner erstellen",
"delete": "Dateien und Ordner löschen",
"download": "Download",
"execute": "Befehle ausführen",
"modify": "Dateien editieren",
"rename": "Umbenennen oder Verschieben von Dateien oder Ordnern",
"share": "Datei teilen"
},
"permissions": "Berechtigungen",
"permissionsHelp": "Sie können einem Benutzer Administratorrechte einräumen oder die Berechtigunen individuell festlegen. Wenn Sie \"Administrator\" auswählen, werden alle anderen Rechte automatisch vergeben. Die Nutzerverwaltung kann nur durch einen Administrator erfolgen.\n",
"profileSettings": "Profileinstellungen",
@@ -155,82 +209,46 @@
"rulesHelp": "Hier können Sie erlaubte und verbotene Aktionen für einen einzelnen Benutzer festlegen. Bockierte Dateien werden nicht im Listing angezeigt und sind nicht erreichbar für den Nutzer. Wir unterstützen reguläre Ausdrücke (Regex) und Pfade die relativ zum Benutzerordner sind. \n",
"scope": "Scope",
"settingsUpdated": "Einstellungen aktualisiert!",
"shareDuration": "",
"shareManagement": "",
"singleClick": "",
"themes": {
"dark": "",
"light": "",
"title": ""
},
"user": "Benutzer",
"userCommands": "Befehle",
"userCommandsHelp": "Eine Liste, mit einem Leerzeichen als Trennung, mit den für diesen Nutzer verfügbaren Befehlen. Example:\n",
"userCreated": "Benutzer angelegt!",
"userDefaults": "Benutzer Standard Einstellungen",
"userDeleted": "Benutzer gelöscht!",
"userManagement": "Benutzerverwaltung",
"username": "Nutzername",
"users": "Nutzer",
"globalRules": "Das ist ein globales Set von Regeln die erlauben oder nicht erlauben. Die sind für alle Benutzer zutreffend. Es können spezielle Regeln in den Einstellungen der Benutzer definiert werden die diese übersteuern.",
"allowSignup": "Erlaube Benutzern sich zu registrieren",
"createUserDir": "Auto create user home dir while adding new user",
"insertRegex": "Regex Ausdruck einfügen",
"insertPath": "Pfad einfügen",
"userUpdated": "Benutzer aktualisiert!",
"userDefaults": "Benutzer Standard Einstellungen",
"defaultUserDescription": "Das sind die Standard Einstellunge für Benutzer",
"executeOnShell": "In shell ausführen",
"executeOnShellDescription": "Es ist voreingestellt das der File Brower Befehle ausführt in dem er die Befehlsdatein direkt auf ruft. Wenn sie wollen das sie auf einer Kommandozeile (wo Bash oder PowerShell) laufen, könne sie das hier definieren mit allen bennötigten Argumenten und Optionen. Wenn gesetzt, wird das Kommando das ausgeführt werden soll als Parameter angehängt. Das gilt für Benuzerkommandos sowie auch für Ereignisse.",
"perm": {
"create": "Dateien und Ordner erstellen",
"delete": "Dateien und Ordner löschen",
"download": "Download",
"modify": "Dateien editieren",
"execute": "Befehle ausführen",
"rename": "Umbenennen oder Verschieben von Dateien oder Ordnern",
"share": "Datei teilen"
}
"username": "Nutzername",
"users": "Nutzer"
},
"sidebar": {
"help": "Hilfe",
"hugoNew": "Hugo Neu",
"login": "Anmelden",
"signup": "Registrieren",
"logout": "Logout",
"myFiles": "Meine Dateien",
"newFile": "Neue Datei",
"newFolder": "Neuer Ordner",
"preview": "Vorschau",
"settings": "Einstellungen",
"siteSettings": "Seiteneinstellungen",
"hugoNew": "Hugo Neu",
"preview": "Vorschau"
"signup": "Registrieren",
"siteSettings": "Seiteneinstellungen"
},
"search": {
"images": "Bilder",
"music": "Musik",
"pdf": "PDF",
"types": "Typen",
"video": "Video",
"search": "Suche...",
"typeToSearch": "Tippe um zu suchen...",
"pressToSearch": "Drücken sie Enter um zu suchen..."
},
"languages": {
"ar": "العربية",
"en": "English",
"it": "Italiano",
"fr": "Français",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ja": "日本語",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"es": "Español",
"de": "Deutsch",
"ru": "Русский",
"pl": "Polski",
"ko": "한국어"
"success": {
"linkCopied": "Verweis wurde kopiert!"
},
"time": {
"unit": "Zeiteinheit",
"seconds": "Sekunden",
"minutes": "Minuten",
"days": "Tage",
"hours": "Stunden",
"days": "Tage"
},
"download": {
"downloadFile": "Download Datei",
"downloadFolder": "Download Ordner"
"minutes": "Minuten",
"seconds": "Sekunden",
"unit": "Zeiteinheit"
}
}
}

View File

@@ -1,7 +1,5 @@
{
"permanent": "Permanent",
"buttons": {
"shell": "Toggle shell",
"cancel": "Cancel",
"close": "Close",
"copy": "Copy",
@@ -10,6 +8,7 @@
"create": "Create",
"delete": "Delete",
"download": "Download",
"hideDotfiles": "Hide dotfiles",
"info": "Info",
"more": "More",
"move": "Move",
@@ -17,25 +16,29 @@
"new": "New",
"next": "Next",
"ok": "OK",
"replace": "Replace",
"permalink": "Get Permanent Link",
"previous": "Previous",
"publish": "Publish",
"rename": "Rename",
"replace": "Replace",
"reportIssue": "Report Issue",
"save": "Save",
"schedule": "Schedule",
"search": "Search",
"select": "Select",
"share": "Share",
"publish": "Publish",
"selectMultiple": "Select multiple",
"schedule": "Schedule",
"share": "Share",
"shell": "Toggle shell",
"submit": "Submit",
"switchView": "Switch view",
"toggleSidebar": "Toggle sidebar",
"update": "Update",
"upload": "Upload",
"permalink": "Get Permanent Link"
"upload": "Upload"
},
"success": {
"linkCopied": "Link copied!"
"download": {
"downloadFile": "Download File",
"downloadFolder": "Download Folder",
"downloadSelected": "Download Selected"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
@@ -43,11 +46,11 @@
"notFound": "This location can't be reached."
},
"files": {
"folders": "Folders",
"files": "Files",
"body": "Body",
"clear": "Clear",
"closePreview": "Close preview",
"files": "Files",
"folders": "Folders",
"home": "Home",
"lastModified": "Last modified",
"loading": "Loading...",
@@ -56,9 +59,9 @@
"multipleSelectionEnabled": "Multiple selection enabled",
"name": "Name",
"size": "Size",
"sortByLastModified": "Sort by last modified",
"sortByName": "Sort by name",
"sortBySize": "Sort by size",
"sortByLastModified": "Sort by last modified"
"sortBySize": "Sort by size"
},
"help": {
"click": "select file or directory",
@@ -74,24 +77,46 @@
"f2": "rename file",
"help": "Help"
},
"languages": {
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"svSE": "Swedish (Sweden)",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"password": "Password",
"passwordConfirm": "Password Confirmation",
"submit": "Login",
"createAnAccount": "Create an account",
"loginInstead": "Already have an account",
"password": "Password",
"passwordConfirm": "Password Confirmation",
"passwordsDontMatch": "Passwords don't match",
"usernameTaken": "Username already taken",
"signup": "Signup",
"submit": "Login",
"username": "Username",
"usernameTaken": "Username already taken",
"wrongCredentials": "Wrong credentials"
},
"permanent": "Permanent",
"prompts": {
"copy": "Copy",
"copyMessage": "Choose the place to copy your files:",
"currentlyNavigating": "Currently navigating on:",
"deleteMessageMultiple": "Are you sure you want to delete {count} file(s)?",
"deleteMessageSingle": "Are you sure you want to delete this file/folder?",
"deleteMessageShare": "Are you sure you want to delete this share({path})?",
"deleteTitle": "Delete files",
"displayName": "Display Name:",
"download": "Download files",
@@ -102,50 +127,65 @@
"lastModified": "Last Modified",
"move": "Move",
"moveMessage": "Choose new house for your file(s)/folder(s):",
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
"newDir": "New directory",
"newDirMessage": "Write the name of the new directory.",
"newFile": "New file",
"newFileMessage": "Write the name of the new file.",
"numberDirs": "Number of directories",
"numberFiles": "Number of files",
"replace": "Replace",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to replace the existing one?\n",
"rename": "Rename",
"renameMessage": "Insert a new name for",
"show": "Show",
"size": "Size",
"replace": "Replace",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to replace the existing one?\n",
"schedule": "Schedule",
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
"show": "Show",
"size": "Size",
"upload": "Upload",
"uploadMessage": "Select an option to upload."
"uploadMessage": "Select an option to upload.",
"optionalPassword": "Optional password"
},
"search": {
"images": "Images",
"music": "Music",
"pdf": "PDF",
"pressToSearch": "Press enter to search...",
"search": "Search...",
"typeToSearch": "Type to search...",
"types": "Types",
"video": "Video"
},
"settings": {
"themes": {
"title": "Theme",
"light": "Light",
"dark": "Dark"
},
"instanceName": "Instance name",
"brandingDirectoryPath": "Branding directory path",
"documentation": "documentation",
"branding": "Branding",
"disableExternalLinks": "Disable external links (except documentation)",
"brandingHelp": "You can costumize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"admin": "Admin",
"administrator": "Administrator",
"allowCommands": "Execute commands",
"allowEdit": "Edit, rename and delete files or directories",
"allowNew": "Create new files and directories",
"allowPublish": "Publish new posts and pages",
"allowSignup": "Allow users to signup",
"avoidChanges": "(leave blank to avoid changes)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",
"brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"changePassword": "Change Password",
"commandRunner": "Command runner",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "Commands updated!",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "Custom Stylesheet",
"defaultUserDescription": "This are the default settings for new users.",
"disableExternalLinks": "Disable external links (except documentation)",
"documentation": "documentation",
"examples": "Examples",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"globalSettings": "Global Settings",
"hideDotfiles": "Hide dotfiles",
"insertPath": "Insert the path",
"insertRegex": "Insert regex expression",
"instanceName": "Instance name",
"language": "Language",
"lockPassword": "Prevent the user from changing the password",
"newPassword": "Your new password",
@@ -153,6 +193,16 @@
"newUser": "New User",
"password": "Password",
"passwordUpdated": "Password updated!",
"path": "Path",
"perm": {
"create": "Create files and directories",
"delete": "Delete files and directories",
"download": "Download",
"execute": "Execute commands",
"modify": "Edit files",
"rename": "Rename or move files and directories",
"share": "Share files"
},
"permissions": "Permissions",
"permissionsHelp": "You can set the user to be an administrator or choose the permissions individually. If you select \"Administrator\", all of the other options will be automatically checked. The management of users remains a privilege of an administrator.\n",
"profileSettings": "Profile Settings",
@@ -162,86 +212,46 @@
"rulesHelp": "Here you can define a set of allow and disallow rules for this specific user. The blocked files won't show up in the listings and they wont be accessible to the user. We support regex and paths relative to the users scope.\n",
"scope": "Scope",
"settingsUpdated": "Settings updated!",
"shareDuration": "Share Duration",
"shareManagement": "Share Management",
"singleClick": "Use single clicks to open files and directories",
"themes": {
"dark": "Dark",
"light": "Light",
"title": "Theme"
},
"user": "User",
"userCommands": "Commands",
"userCommandsHelp": "A space separated list with the available commands for this user. Example:\n",
"userCreated": "User created!",
"userDefaults": "User default settings",
"userDeleted": "User deleted!",
"userManagement": "User Management",
"username": "Username",
"users": "Users",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"allowSignup": "Allow users to signup",
"createUserDir": "Auto create user home dir while adding new user",
"insertRegex": "Insert regex expression",
"insertPath": "Insert the path",
"userUpdated": "User updated!",
"userDefaults": "User default settings",
"defaultUserDescription": "This are the default settings for new users.",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"perm": {
"create": "Create files and directories",
"delete": "Delete files and directories",
"download": "Download",
"modify": "Edit files",
"execute": "Execute commands",
"rename": "Rename or move files and directories",
"share": "Share files"
}
"username": "Username",
"users": "Users"
},
"sidebar": {
"help": "Help",
"hugoNew": "Hugo New",
"login": "Login",
"signup": "Signup",
"logout": "Logout",
"myFiles": "My files",
"newFile": "New file",
"newFolder": "New folder",
"preview": "Preview",
"settings": "Settings",
"siteSettings": "Site Settings",
"hugoNew": "Hugo New",
"preview": "Preview"
"signup": "Signup",
"siteSettings": "Site Settings"
},
"search": {
"images": "Images",
"music": "Music",
"pdf": "PDF",
"types": "Types",
"video": "Video",
"search": "Search...",
"typeToSearch": "Type to search...",
"pressToSearch": "Press enter to search..."
},
"languages": {
"ar": "العربية",
"en": "English",
"is": "Icelandic",
"it": "Italiano",
"fr": "Français",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ja": "日本語",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"es": "Español",
"de": "Deutsch",
"ru": "Русский",
"pl": "Polski",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"ro": "Romanian",
"svSE": "Swedish (Sweden)"
"success": {
"linkCopied": "Link copied!"
},
"time": {
"unit": "Time Unit",
"seconds": "Seconds",
"minutes": "Minutes",
"days": "Days",
"hours": "Hours",
"days": "Days"
},
"download": {
"downloadFile": "Download File",
"downloadFolder": "Download Folder"
"minutes": "Minutes",
"seconds": "Seconds",
"unit": "Time Unit"
}
}
}

View File

@@ -1,7 +1,5 @@
{
"permanent": "Permanente",
"buttons": {
"shell": "Presiona Enter para buscar...",
"cancel": "Cancelar",
"close": "Cerrar",
"copy": "Copiar",
@@ -10,6 +8,7 @@
"create": "Crear",
"delete": "Borrar",
"download": "Descargar",
"hideDotfiles": "",
"info": "Info",
"more": "Más",
"move": "Mover",
@@ -17,25 +16,28 @@
"new": "Nuevo",
"next": "Siguiente",
"ok": "OK",
"replace": "Reemplazar",
"permalink": "Link permanente",
"previous": "Anterior",
"publish": "Publicar",
"rename": "Renombrar",
"replace": "Reemplazar",
"reportIssue": "Reportar problema",
"save": "Guardar",
"schedule": "Programar",
"search": "Buscar",
"select": "Seleccionar",
"share": "Compartir",
"publish": "Publicar",
"selectMultiple": "Selección múltiple",
"schedule": "Programar",
"share": "Compartir",
"shell": "Presiona Enter para buscar...",
"switchView": "Cambiar vista",
"toggleSidebar": "Mostrar/Ocultar menú",
"update": "Actualizar",
"upload": "Subir",
"permalink": "Link permanente"
"upload": "Subir"
},
"success": {
"linkCopied": "¡Link copiado!"
"download": {
"downloadFile": "Descargar fichero",
"downloadFolder": "Descargar directorio",
"downloadSelected": ""
},
"errors": {
"forbidden": "No tienes los permisos necesarios para acceder.",
@@ -43,11 +45,11 @@
"notFound": "No se puede acceder a este lugar."
},
"files": {
"folders": "Carpetas",
"files": "Archivos",
"body": "Cuerpo",
"clear": "Limpiar",
"closePreview": "Cerrar vista previa",
"files": "Archivos",
"folders": "Carpetas",
"home": "Inicio",
"lastModified": "Última modificación",
"loading": "Cargando...",
@@ -56,9 +58,9 @@
"multipleSelectionEnabled": "Selección múltiple activada",
"name": "Nombre",
"size": "Tamaño",
"sortByLastModified": "Ordenar por última modificación",
"sortByName": "Ordenar por nombre",
"sortBySize": "Ordenar por tamaño",
"sortByLastModified": "Ordenar por última modificación"
"sortBySize": "Ordenar por tamaño"
},
"help": {
"click": "seleccionar archivo o carpeta",
@@ -74,18 +76,39 @@
"f2": "renombrar archivo",
"help": "Ayuda"
},
"languages": {
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ru": "Русский",
"svSE": "",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"password": "Contraseña",
"passwordConfirm": "Confirmación de contraseña",
"submit": "Iniciar sesión",
"createAnAccount": "Crear una cuenta",
"loginInstead": "Usuario ya existente",
"password": "Contraseña",
"passwordConfirm": "Confirmación de contraseña",
"passwordsDontMatch": "Las contraseñas no coinciden",
"usernameTaken": "Nombre usuario no disponible",
"signup": "Registrate",
"submit": "Iniciar sesión",
"username": "Usuario",
"usernameTaken": "Nombre usuario no disponible",
"wrongCredentials": "Usuario y/o contraseña incorrectos"
},
"permanent": "Permanente",
"prompts": {
"copy": "Copiar",
"copyMessage": "Elige el lugar donde quieres copiar tus archivos:",
@@ -102,43 +125,64 @@
"lastModified": "Última modificación",
"move": "Mover",
"moveMessage": "Elige una nueva casa para tus archivo(s)/carpeta(s):",
"newArchetype": "Crea un nuevo post basado en un arquetipo. Tu archivo será creado en la carpeta de contenido.",
"newDir": "Nueva carpeta",
"newDirMessage": "Escribe el nombre de la nueva carpeta.",
"newFile": "Nuevo archivo",
"newFileMessage": "Escribe el nombre del nuevo archivo.",
"numberDirs": "Número de carpetas",
"numberFiles": "Número de archivos",
"replace": "Reemplazar",
"replaceMessage": "Uno de los archivos ue intentas subir está creando conflicto por su nombre. ¿Quieres cambiar el nombre del ya existente?\n",
"rename": "Renombrar",
"renameMessage": "Escribe el nuevo nombre para",
"show": "Mostrar",
"size": "Tamaño",
"replace": "Reemplazar",
"replaceMessage": "Uno de los archivos ue intentas subir está creando conflicto por su nombre. ¿Quieres cambiar el nombre del ya existente?\n",
"schedule": "Programar",
"scheduleMessage": "Elige una hora y fecha para programar la publicación de este post.",
"newArchetype": "Crea un nuevo post basado en un arquetipo. Tu archivo será creado en la carpeta de contenido."
"show": "Mostrar",
"size": "Tamaño",
"upload": "",
"uploadMessage": ""
},
"search": {
"images": "Images",
"music": "Música",
"pdf": "PDF",
"pressToSearch": "Presiona enter para buscar...",
"search": "Buscar...",
"typeToSearch": "Escribe para realizar una busqueda...",
"types": "Tipos",
"video": "Vídeo"
},
"settings": {
"instanceName": "Nombre de la instancia",
"brandingDirectoryPath": "Ruta de la carpeta de personalizacion de marca",
"documentation": "documentación",
"branding": "Marca",
"disableExternalLinks": "Deshabilitar enlaces externos (excepto documentación)",
"brandingHelp": "Tú puedes personalizar como se ve tu instancia de FileBrowser cambiándole el nombre, reemplazando ellogo, agregar estilos personalizados e incluso deshabilitando loslinks externos que apuntan hacia GitHub. \nPara mayor información acerca de personalización de marca, por favor revisa el {0}.",
"admin": "Admin",
"administrator": "Administrador",
"allowCommands": "Ejecutar comandos",
"allowEdit": "Editar, renombrar y borrar archivos o carpetas",
"allowNew": "Crear nuevos archivos y carpetas",
"allowPublish": "Publicar nuevos posts y páginas",
"allowSignup": "Permitir registro de usuarios",
"avoidChanges": "(dejar en blanco para evitar cambios)",
"branding": "Marca",
"brandingDirectoryPath": "Ruta de la carpeta de personalizacion de marca",
"brandingHelp": "Tú puedes personalizar como se ve tu instancia de FileBrowser cambiándole el nombre, reemplazando ellogo, agregar estilos personalizados e incluso deshabilitando loslinks externos que apuntan hacia GitHub. \nPara mayor información acerca de personalización de marca, por favor revisa el {0}.",
"changePassword": "Cambiar contraseña",
"commandRunner": "Executor de comandos",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "¡Comandos actualizados!",
"createUserDir": "Crea automaticamente una carpeta de inicio cuando se agrega un usuario",
"customStylesheet": "Modificar hoja de estilos",
"defaultUserDescription": "Estas son las configuraciones por defecto para nuevos usuarios.",
"disableExternalLinks": "Deshabilitar enlaces externos (excepto documentación)",
"documentation": "documentación",
"examples": "Ejemplos",
"executeOnShell": "Ejecutar en la shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"globalSettings": "Ajustes globales",
"hideDotfiles": "",
"insertPath": "Introduce la ruta",
"insertRegex": "Introducir expresión regular",
"instanceName": "Nombre de la instancia",
"language": "Idioma",
"lockPassword": "Evitar que el usuario cambie la contraseña",
"newPassword": "Tu nueva contraseña",
@@ -146,6 +190,16 @@
"newUser": "Nuevo usuario",
"password": "Contraseña",
"passwordUpdated": "¡Contraseña actualizada!",
"path": "",
"perm": {
"create": "Crear ficheros y directorios",
"delete": "Eliminar ficheros y directorios",
"download": "Descargar",
"execute": "Executar comandos",
"modify": "Editar ficheros",
"rename": "Renombrar o mover ficheros y directorios",
"share": "Compartir ficheros"
},
"permissions": "Permisos",
"permissionsHelp": "Puedes nombrar al usuario como administrador o elegir los permisos individualmente. Si seleccionas \"Administrador\", todas las otras opciones serán activadas automáticamente. La administración de usuarios es un privilegio de administrador.\n",
"profileSettings": "Ajustes del perfil",
@@ -155,82 +209,46 @@
"rulesHelp": "Aquí puedes definir un conjunto de reglas de permisos para este usuario específico. Los archivos bloqueados no se mostrarán en las listas y no serán accesibles por el usuario. Puedes utilizar regex y rutas relativas a la raíz del usuario.\n",
"scope": "Raíz",
"settingsUpdated": "¡Ajustes actualizados!",
"shareDuration": "",
"shareManagement": "",
"singleClick": "",
"themes": {
"dark": "",
"light": "",
"title": ""
},
"user": "Usuario",
"userCommands": "Comandos",
"userCommandsHelp": "Una lista separada por espacios con los comandos permitidos para este usuario. Ejemplo:\n",
"userCreated": "¡Usuario creado!",
"userDefaults": "Configuración de usuario por defecto",
"userDeleted": "¡Usuario eliminado!",
"userManagement": "Administración de usuarios",
"username": "Usuario",
"users": "Usuarios",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"allowSignup": "Permitir registro de usuarios",
"createUserDir": "Crea automaticamente una carpeta de inicio cuando se agrega un usuario",
"insertRegex": "Introducir expresión regular",
"insertPath": "Introduce la ruta",
"userUpdated": "¡Usuario actualizado!",
"userDefaults": "Configuración de usuario por defecto",
"defaultUserDescription": "Estas son las configuraciones por defecto para nuevos usuarios.",
"executeOnShell": "Ejecutar en la shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"perm": {
"create": "Crear ficheros y directorios",
"delete": "Eliminar ficheros y directorios",
"download": "Descargar",
"modify": "Editar ficheros",
"execute": "Executar comandos",
"rename": "Renombrar o mover ficheros y directorios",
"share": "Compartir ficheros"
}
"username": "Usuario",
"users": "Usuarios"
},
"sidebar": {
"help": "Ayuda",
"hugoNew": "Nuevo Hugo",
"login": "Iniciar sesión",
"signup": "Registrate",
"logout": "Cerrar sesión",
"myFiles": "Mis archivos",
"newFile": "Nuevo archivo",
"newFolder": "Nueva carpeta",
"preview": "Vista previa",
"settings": "Ajustes",
"siteSettings": "Ajustes del sitio",
"hugoNew": "Nuevo Hugo",
"preview": "Vista previa"
"signup": "Registrate",
"siteSettings": "Ajustes del sitio"
},
"search": {
"images": "Images",
"music": "Música",
"pdf": "PDF",
"types": "Tipos",
"video": "Vídeo",
"search": "Buscar...",
"typeToSearch": "Escribe para realizar una busqueda...",
"pressToSearch": "Presiona enter para buscar..."
},
"languages": {
"ar": "العربية",
"en": "English",
"it": "Italiano",
"fr": "Français",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ja": "日本語",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"es": "Español",
"de": "Deutsch",
"ru": "Русский",
"pl": "Polski",
"ko": "한국어"
"success": {
"linkCopied": "¡Link copiado!"
},
"time": {
"unit": "Unidad",
"seconds": "Segundos",
"minutes": "Minutos",
"days": "Días",
"hours": "Horas",
"days": "Días"
},
"download": {
"downloadFile": "Descargar fichero",
"downloadFolder": "Descargar directorio"
"minutes": "Minutos",
"seconds": "Segundos",
"unit": "Unidad"
}
}
}

View File

@@ -1,7 +1,5 @@
{
"permanent": "Permanent",
"buttons": {
"shell": "Toggle shell",
"cancel": "Annuler",
"close": "Fermer",
"copy": "Copier",
@@ -10,6 +8,7 @@
"create": "Créer",
"delete": "Supprimer",
"download": "Télécharger",
"hideDotfiles": "",
"info": "Info",
"more": "Plus",
"move": "Déplacer",
@@ -17,25 +16,28 @@
"new": "Nouveau",
"next": "Suivant",
"ok": "OK",
"replace": "Remplacer",
"permalink": "Obtenir un lien permanent",
"previous": "Précédent",
"publish": "Publier",
"rename": "Renommer",
"replace": "Remplacer",
"reportIssue": "Rapport d'erreur",
"save": "Enregistrer",
"schedule": "Fixer la date",
"search": "Chercher",
"select": "Sélectionner",
"share": "Partager",
"publish": "Publier",
"selectMultiple": "Sélection multiple",
"schedule": "Fixer la date",
"share": "Partager",
"shell": "Toggle shell",
"switchView": "Changer le mode d'affichage",
"toggleSidebar": "Afficher/Masquer la barre latérale",
"update": "Mettre à jour",
"upload": "Importer",
"permalink": "Obtenir un lien permanent"
"upload": "Importer"
},
"success": {
"linkCopied": "Link copied!"
"download": {
"downloadFile": "Download File",
"downloadFolder": "Download Folder",
"downloadSelected": ""
},
"errors": {
"forbidden": "You don't have permissions to access this.",
@@ -43,22 +45,22 @@
"notFound": "Impossible d'accéder à cet emplacement."
},
"files": {
"folders": "Dossiers",
"files": "Fichiers",
"body": "Corps",
"clear": "Fermer",
"closePreview": "Fermer la prévisualisation",
"files": "Fichiers",
"folders": "Dossiers",
"home": "Accueil",
"lastModified": "Dernière modification",
"loading": "Chargement...",
"lonely": "Il semble qu'il n'y ai rien par ici...",
"lonely": "Il semble qu'il n'y ait rien par ici...",
"metadata": "Metadonnées",
"multipleSelectionEnabled": "Sélection multiple activée",
"name": "Nom",
"size": "Taille",
"sortByLastModified": "Trier par date de dernière modification",
"sortByName": "Trier par nom",
"sortBySize": "Trier par taille",
"sortByLastModified": "Trier par date de dernière modification"
"sortBySize": "Trier par taille"
},
"help": {
"click": "Sélectionner un élément",
@@ -74,18 +76,39 @@
"f2": "Renommer le fichier",
"help": "Aide"
},
"languages": {
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ru": "Русский",
"svSE": "",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"password": "Mot de passe",
"passwordConfirm": "Password Confirmation",
"submit": "Se connecter",
"createAnAccount": "Create an account",
"loginInstead": "Already have an account",
"password": "Mot de passe",
"passwordConfirm": "Password Confirmation",
"passwordsDontMatch": "Passwords don't match",
"usernameTaken": "Username already taken",
"signup": "Signup",
"submit": "Se connecter",
"username": "Utilisateur",
"usernameTaken": "Username already taken",
"wrongCredentials": "Identifiants incorrects !"
},
"permanent": "Permanent",
"prompts": {
"copy": "Copier",
"copyMessage": "Choisissez l'emplacement où copier la sélection :",
@@ -102,43 +125,64 @@
"lastModified": "Dernière modification",
"move": "Déplacer",
"moveMessage": "Choisissez l'emplacement où déplacer la sélection :",
"newArchetype": "Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu.",
"newDir": "Nouveau dossier",
"newDirMessage": "Nom du nouveau dossier :",
"newFile": "Nouveau fichier",
"newFileMessage": "Nom du nouveau fichier :",
"numberDirs": "Nombre de dossiers",
"numberFiles": "Nombre de fichiers",
"replace": "Remplacer",
"replaceMessage": "Un des fichiers que vous êtes en train d'importer a le même nom qu'un autre déjà présent. Voulez-vous remplacer le fichier actuel par le nouveau ?\n",
"rename": "Renommer",
"renameMessage": "Nouveau nom pour",
"show": "Montrer",
"size": "Taille",
"replace": "Remplacer",
"replaceMessage": "Un des fichiers que vous êtes en train d'importer a le même nom qu'un autre déjà présent. Voulez-vous remplacer le fichier actuel par le nouveau ?\n",
"schedule": "Fixer la date",
"scheduleMessage": "Choisissez une date pour planifier la publication de ce post",
"newArchetype": "Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu."
"show": "Montrer",
"size": "Taille",
"upload": "",
"uploadMessage": ""
},
"search": {
"images": "Images",
"music": "Musique",
"pdf": "PDF",
"pressToSearch": "Press enter to search...",
"search": "Recherche en cours...",
"typeToSearch": "Type to search...",
"types": "Types",
"video": "Video"
},
"settings": {
"instanceName": "Instance name",
"brandingDirectoryPath": "Branding directory path",
"documentation": "documentation",
"branding": "Branding",
"disableExternalLinks": "Disable external links (except documentation)",
"brandingHelp": "You can costumize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"admin": "Admin",
"administrator": "Administrateur",
"allowCommands": "Exécuter des commandes",
"allowEdit": "Editer, renommer et supprimer des fichiers ou des dossiers",
"allowNew": "Créer de nouveaux fichiers et dossiers",
"allowPublish": "Publier de nouveaux posts et pages",
"allowSignup": "Allow users to signup",
"avoidChanges": "(Laisser vide pour conserver l'actuel)",
"branding": "Branding",
"brandingDirectoryPath": "Branding directory path",
"brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.",
"changePassword": "Modifier le mot de passe",
"commandRunner": "Command runner",
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "Commandes mises à jour !",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "Feuille de style personnalisée",
"defaultUserDescription": "This are the default settings for new users.",
"disableExternalLinks": "Disable external links (except documentation)",
"documentation": "documentation",
"examples": "Exemples",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"globalSettings": "Paramètres généraux",
"hideDotfiles": "",
"insertPath": "Insert the path",
"insertRegex": "Insert regex expression",
"instanceName": "Instance name",
"language": "Langue",
"lockPassword": "Prevent the user from changing the password",
"newPassword": "Votre nouveau mot de passe",
@@ -146,6 +190,16 @@
"newUser": "Nouvel Utilisateur",
"password": "Mot de passe",
"passwordUpdated": "Mot de passe mis à jour !",
"path": "",
"perm": {
"create": "Create files and directories",
"delete": "Delete files and directories",
"download": "Download",
"execute": "Execute commands",
"modify": "Edit files",
"rename": "Rename or move files and directories",
"share": "Share files"
},
"permissions": "Permissions",
"permissionsHelp": "Vous pouvez définir l'utilisateur comme étant un administrateur ou encore choisir les permissions individuellement. Si vous sélectionnez \"Administrateur\", toutes les autres options seront automatiquement activées. La gestion des utilisateurs est un privilège que seul l'administrateur possède.\n",
"profileSettings": "Paramètres du profil",
@@ -155,82 +209,46 @@
"rulesHelp": "Vous pouvez définir ici un ensemble de règles pour cet utilisateur. Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur. Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur.\n",
"scope": "Portée du dossier utilisateur",
"settingsUpdated": "Les paramètres ont été mis à jour !",
"shareDuration": "",
"shareManagement": "",
"singleClick": "",
"themes": {
"dark": "",
"light": "",
"title": ""
},
"user": "Utilisateur",
"userCommands": "Commandes",
"userCommandsHelp": "Une liste séparée par des espaces des commandes permises pour l'utilisateur. Exemple :",
"userCreated": "Utilisateur créé !",
"userDefaults": "User default settings",
"userDeleted": "Utilisateur supprimé !",
"userManagement": "Gestion des utilisateurs",
"username": "Nom d'utilisateur",
"users": "Utilisateurs",
"globalRules": "This is a global set of allow and disallow rules. They apply to every user. You can define specific rules on each user's settings to override this ones.",
"allowSignup": "Allow users to signup",
"createUserDir": "Auto create user home dir while adding new user",
"insertRegex": "Insert regex expression",
"insertPath": "Insert the path",
"userUpdated": "Utilisateur mis à jour !",
"userDefaults": "User default settings",
"defaultUserDescription": "This are the default settings for new users.",
"executeOnShell": "Execute on shell",
"executeOnShellDescription": "By default, File Browser executes the commands by calling their binaries directly. If you want to run them on a shell instead (such as Bash or PowerShell), you can define it here with the required arguments and flags. If set, the command you execute will be appended as an argument. This apply to both user commands and event hooks.",
"perm": {
"create": "Create files and directories",
"delete": "Delete files and directories",
"download": "Download",
"modify": "Edit files",
"execute": "Execute commands",
"rename": "Rename or move files and directories",
"share": "Share files"
}
"username": "Nom d'utilisateur",
"users": "Utilisateurs"
},
"sidebar": {
"help": "Aide",
"hugoNew": "Nouveau Hugo",
"login": "Login",
"signup": "Signup",
"logout": "Se déconnecter",
"myFiles": "Mes fichiers",
"newFile": "Nouveau fichier",
"newFolder": "Nouveau dossier",
"preview": "Prévisualiser",
"settings": "Paramètres",
"siteSettings": "Paramètres du site",
"hugoNew": "Nouveau Hugo",
"preview": "Prévisualiser"
"signup": "Signup",
"siteSettings": "Paramètres du site"
},
"search": {
"images": "Images",
"music": "Musique",
"pdf": "PDF",
"types": "Types",
"video": "Video",
"search": "Recherche en cours...",
"typeToSearch": "Type to search...",
"pressToSearch": "Press enter to search..."
},
"languages": {
"ar": "العربية",
"en": "English",
"it": "Italiano",
"fr": "Français",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ja": "日本語",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"es": "Español",
"de": "Deutsch",
"ru": "Русский",
"pl": "Polski",
"ko": "한국어"
"success": {
"linkCopied": "Link copied!"
},
"time": {
"unit": "Unité de temps",
"seconds": "Secondes",
"minutes": "Minutes",
"days": "Jours",
"hours": "Heures",
"days": "Jours"
},
"download": {
"downloadFile": "Download File",
"downloadFolder": "Download Folder"
"minutes": "Minutes",
"seconds": "Secondes",
"unit": "Unité de temps"
}
}
}

View File

@@ -77,28 +77,39 @@ export function detectLocale () {
return locale
}
const removeEmpty = (obj) =>
Object.keys(obj)
.filter((k) => obj[k] !== null && obj[k] !== undefined && obj[k] !== '') // Remove undef. and null and empty.string.
.reduce(
(newObj, k) =>
typeof obj[k] === 'object'
? Object.assign(newObj, { [k]: removeEmpty(obj[k]) }) // Recurse.
: Object.assign(newObj, { [k]: obj[k] }), // Copy value.
{},
);
const i18n = new VueI18n({
locale: detectLocale(),
fallbackLocale: 'en',
messages: {
'ar': ar,
'de': de,
'ar': removeEmpty(ar),
'de': removeEmpty(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,
'ru': ru,
'ro': ro,
'sv-se': svSE,
'zh-cn': zhCN,
'zh-tw': zhTW
'es': removeEmpty(es),
'fr': removeEmpty(fr),
'is': removeEmpty(is),
'it': removeEmpty(it),
'ja': removeEmpty(ja),
'ko': removeEmpty(ko),
'nl-be': removeEmpty(nlBE),
'pl': removeEmpty(pl),
'pt-br': removeEmpty(ptBR),
'pt': removeEmpty(pt),
'ru': removeEmpty(ru),
'ro': removeEmpty(ro),
'sv-se': removeEmpty(svSE),
'zh-cn': removeEmpty(zhCN),
'zh-tw': removeEmpty(zhTW)
}
})

View File

@@ -1,7 +1,5 @@
{
"permanent": "Varanlegt",
"buttons": {
"shell": "Sýna skipanaglugga",
"cancel": "Hætta við",
"close": "Loka",
"copy": "Afrita",
@@ -10,6 +8,7 @@
"create": "Búa til",
"delete": "Eyða",
"download": "Sækja",
"hideDotfiles": "",
"info": "Upplýsingar",
"more": "Meira",
"move": "Færa",
@@ -17,25 +16,28 @@
"new": "Nýtt",
"next": "Næsta",
"ok": "OK",
"replace": "Skipta út",
"permalink": "Sækja fastan hlekk",
"previous": "Fyrri",
"publish": "Gefa út",
"rename": "Endurnefna",
"replace": "Skipta út",
"reportIssue": "Tilkynna vandamál",
"save": "Vista",
"schedule": "Áætlun",
"search": "Leita",
"select": "Velja",
"share": "Deila",
"publish": "Gefa út",
"selectMultiple": "Velja mörg",
"schedule": "Áætlun",
"share": "Deila",
"shell": "Sýna skipanaglugga",
"switchView": "Skipta um útlit",
"toggleSidebar": "Sýna hliðarstiku",
"update": "Vista",
"upload": "Hlaða upp",
"permalink": "Sækja fastan hlekk"
"upload": "Hlaða upp"
},
"success": {
"linkCopied": "Hlekkur afritaður!"
"download": {
"downloadFile": "Sækja skjal",
"downloadFolder": "Sækja möppu",
"downloadSelected": ""
},
"errors": {
"forbidden": "Þú hefur ekki aðgang að þessari síðu.",
@@ -43,11 +45,11 @@
"notFound": "Ekki er hægt að opna þessa síðu."
},
"files": {
"folders": "Möppur",
"files": "Skjöl",
"body": "Meginmál",
"clear": "Hreinsa",
"closePreview": "Loka forskoðun",
"files": "Skjöl",
"folders": "Möppur",
"home": "Heim",
"lastModified": "Seinast breytt",
"loading": "Hleð...",
@@ -56,9 +58,9 @@
"multipleSelectionEnabled": "Hægt að velja mörg skjöl/möppur",
"name": "Nafn",
"size": "Stærð",
"sortByLastModified": "Flokka eftir Seinast breytt",
"sortByName": "Flokka eftir nafni",
"sortBySize": "Flokka eftir stærð",
"sortByLastModified": "Flokka eftir Seinast breytt"
"sortBySize": "Flokka eftir stærð"
},
"help": {
"click": "velja skjal eða möppu",
@@ -74,18 +76,39 @@
"f2": "endurnefna skjal",
"help": "Hjálp"
},
"languages": {
"ar": "العربية",
"de": "Deutsch",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ru": "Русский",
"svSE": "",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
},
"login": {
"password": "Lykilorð",
"passwordConfirm": "Staðfesting lykilorðs",
"submit": "Innskráning",
"createAnAccount": "Búa til nýjan aðgang",
"loginInstead": "Þú ert þegar með aðgang",
"password": "Lykilorð",
"passwordConfirm": "Staðfesting lykilorðs",
"passwordsDontMatch": "Lykilorð eru mismunandi",
"usernameTaken": "Þetta norendanafn er þegar í notkun",
"signup": "Nýskráning",
"submit": "Innskráning",
"username": "Notendanafn",
"usernameTaken": "Þetta norendanafn er þegar í notkun",
"wrongCredentials": "Rangar notendaupplýsingar"
},
"permanent": "Varanlegt",
"prompts": {
"copy": "Afrita",
"copyMessage": "Veldu staðsetningu til að afrita gögn: ",
@@ -102,43 +125,64 @@
"lastModified": "Seinast breytt",
"move": "Færa",
"moveMessage": "Velja nýtt hús fyrir skjöl/möppur:",
"newArchetype": "Búðu til nýja færslu sem byggir á frumgerð. Skjalið verður búið til í content möppu. ",
"newDir": "Ný mappa",
"newDirMessage": "Hvað á mappan að heita?",
"newFile": "Nýtt skjal",
"newFileMessage": "Hvað á skjalið að heita?",
"numberDirs": "Fjöldi mappa",
"numberFiles": "Fjöldi skjala",
"replace": "Skipta út",
"replaceMessage": "Eitt af skjölunum sem þú ert að reyna að hlaða upp hefur sama nafn og annað skjal. Viltu skipta nýja skjalinu út fyrir það gamla?\n",
"rename": "Endurnefna",
"renameMessage": "Settu inn nýtt nafn fyrir",
"show": "Sýna",
"size": "Stærð",
"replace": "Skipta út",
"replaceMessage": "Eitt af skjölunum sem þú ert að reyna að hlaða upp hefur sama nafn og annað skjal. Viltu skipta nýja skjalinu út fyrir það gamla?\n",
"schedule": "Áætlun",
"scheduleMessage": "Veldu dagsetningu og tíma fyrir áætlaða útgáfu. ",
"newArchetype": "Búðu til nýja færslu sem byggir á frumgerð. Skjalið verður búið til í content möppu. "
"show": "Sýna",
"size": "Stærð",
"upload": "",
"uploadMessage": ""
},
"search": {
"images": "Myndir",
"music": "Tónlist",
"pdf": "PDF",
"pressToSearch": "Ýttu á Enter til að leita...",
"search": "Leita...",
"typeToSearch": "Skrifaðu til að leita...",
"types": "Skrárgerðir",
"video": "Myndbönd"
},
"settings": {
"instanceName": "Nafn tilviks",
"brandingDirectoryPath": "Mappa fyrir branding-skjöl",
"documentation": "leiðbeiningar",
"branding": "Útlit",
"disableExternalLinks": "Sýna ytri-hlekki (fyrir utan leiðbeiningar)",
"brandingHelp": "Þú getur breytt því hvernig File Browser lítur út með því að breyta nafninu, setja inn nýtt lógó, búa til þína eigin stíla og tekið út GitHub-hlekki. \nTil að lesa meira um custom-branding, kíktu á {0}.",
"admin": "Stjórnandi",
"administrator": "Stjórnandi",
"allowCommands": "Senda skipanir",
"allowEdit": "Breyta, endurnefna og eyða skjölum eða möppum",
"allowNew": "Búa til ný skjöl og möppur",
"allowPublish": "Gefa út nýjar færslur og síður",
"allowSignup": "Leyfa nýjum notendum að skrá sig",
"avoidChanges": "(engar breytingar ef ekkert er skrifað)",
"branding": "Útlit",
"brandingDirectoryPath": "Mappa fyrir branding-skjöl",
"brandingHelp": "Þú getur breytt því hvernig File Browser lítur út með því að breyta nafninu, setja inn nýtt lógó, búa til þína eigin stíla og tekið út GitHub-hlekki. \nTil að lesa meira um custom-branding, kíktu á {0}.",
"changePassword": "Breyta lykilorði",
"commandRunner": "Skipanagluggi",
"commandRunnerHelp": "Hér geturðu sett inn skipanir sem eru keyrðar eftir því sem þú tilgreinir. Skrifaðu eina skipun í hverja línu. Umhverfisbreyturnar {0} og {1} verða aðgengilegar ({0} miðast við {1}). Til að lesa meira og sjá lista yfir þær skipanir sem eru í boði, vinsamlegast lestu {2}. ",
"commandsUpdated": "Skipanastillingar vistaðar!",
"createUserDir": "Auto create user home dir while adding new user",
"customStylesheet": "Custom Stylesheet",
"defaultUserDescription": "Þetta eru sjálfgefnar stillingar fyrir nýja notendur.",
"disableExternalLinks": "Sýna ytri-hlekki (fyrir utan leiðbeiningar)",
"documentation": "leiðbeiningar",
"examples": "Dæmi",
"executeOnShell": "Keyra í skel",
"executeOnShellDescription": "Sjálfgefnar stillingar File Browser eru að keyra skipanir beint með því að sækja binaries. Ef þú villt keyra skipanir í skel (t.d. í Bash eða PowerShell), þá geturðu skilgreint það hér með nauðsynlegum arguments og flags. Ef þetta er stillt, þá verður skipuninni bætt fyrir aftan sem argument. Þetta gildir bæði um skipanir notenda og event hooks.",
"globalRules": "Þetta eru sjálfgegnar aðgangsreglur. Þær gilda um alla notendur. Þú getur tilgreint sérstakar reglur í stillingum fyrir hvern notenda til að ógilda þessar reglur. ",
"globalSettings": "Global stillingar",
"hideDotfiles": "",
"insertPath": "Settu inn slóð",
"insertRegex": "Setja inn reglulega segð",
"instanceName": "Nafn tilviks",
"language": "Tungumál",
"lockPassword": "Koma í veg fyrir að notandi breyti lykilorðinu",
"newPassword": "Nýja lykilorðið þitt",
@@ -146,6 +190,16 @@
"newUser": "Nýr notandi",
"password": "Lykilorð",
"passwordUpdated": "Lykilorð vistað!",
"path": "",
"perm": {
"create": "Búa til sköl og möppur",
"delete": "Eyða skjölum og möppum",
"download": "Sækja",
"execute": "Keyra skipanir",
"modify": "Breyta skjölum",
"rename": "Endurnefna eða færa skjöl og möppur",
"share": "Deila skjölum"
},
"permissions": "Heimildir",
"permissionsHelp": "Þú getur stillt notenda sem stjórnanda eða valið einstaklingsbundnar heimildir. Ef þú velur \"Stjórnandi\", þá verða allir aðrir valmöguleikar valdir sjálfrafa. Aðgangstýring notenda er á hendi stjórnenda. \n",
"profileSettings": "Stilla prófíl",
@@ -155,82 +209,46 @@
"rulesHelp": "Hér getur þú skilgreint hvaða reglur gilda um notandann. Skjölin sem hann hefur ekki aðgang að eru óaðgengileg og hann sér þau ekki. Stuðst er við reglulegar segðir og slóðir sem miðast við sýn notandans. ",
"scope": "Sýn notandans",
"settingsUpdated": "Stillingar vistaðar!",
"shareDuration": "",
"shareManagement": "",
"singleClick": "",
"themes": {
"dark": "",
"light": "",
"title": ""
},
"user": "Notandi",
"userCommands": "Skipanir",
"userCommandsHelp": "Listi þar sem gildum er skipt upp með bili og inniheldur tiltækar skipanir fyrir þennan notanda. Til dæmis:\n",
"userCreated": "Notandi stofnaður!",
"userDefaults": "Sjálfgefnar notendastillingar",
"userDeleted": "Notanda eytt!",
"userManagement": "Notendastýring",
"username": "Notendanafn",
"users": "Notendur",
"globalRules": "Þetta eru sjálfgegnar aðgangsreglur. Þær gilda um alla notendur. Þú getur tilgreint sérstakar reglur í stillingum fyrir hvern notenda til að ógilda þessar reglur. ",
"allowSignup": "Leyfa nýjum notendum að skrá sig",
"createUserDir": "Auto create user home dir while adding new user",
"insertRegex": "Setja inn reglulega segð",
"insertPath": "Settu inn slóð",
"userUpdated": "Notandastillingar vistaðar!",
"userDefaults": "Sjálfgefnar notendastillingar",
"defaultUserDescription": "Þetta eru sjálfgefnar stillingar fyrir nýja notendur.",
"executeOnShell": "Keyra í skel",
"executeOnShellDescription": "Sjálfgefnar stillingar File Browser eru að keyra skipanir beint með því að sækja binaries. Ef þú villt keyra skipanir í skel (t.d. í Bash eða PowerShell), þá geturðu skilgreint það hér með nauðsynlegum arguments og flags. Ef þetta er stillt, þá verður skipuninni bætt fyrir aftan sem argument. Þetta gildir bæði um skipanir notenda og event hooks.",
"perm": {
"create": "Búa til sköl og möppur",
"delete": "Eyða skjölum og möppum",
"download": "Sækja",
"modify": "Breyta skjölum",
"execute": "Keyra skipanir",
"rename": "Endurnefna eða færa skjöl og möppur",
"share": "Deila skjölum"
}
"username": "Notendanafn",
"users": "Notendur"
},
"sidebar": {
"help": "Hjálp",
"hugoNew": "Hugo New",
"login": "Innskráning",
"signup": "Nýskráning",
"logout": "Útskráning",
"myFiles": "Gögnin mín",
"newFile": "Nýtt skjal",
"newFolder": "Ný mappa",
"preview": "Sýnishorn",
"settings": "Stillingar",
"siteSettings": "Stillingar síðu",
"hugoNew": "Hugo New",
"preview": "Sýnishorn"
"signup": "Nýskráning",
"siteSettings": "Stillingar síðu"
},
"search": {
"images": "Myndir",
"music": "Tónlist",
"pdf": "PDF",
"types": "Skrárgerðir",
"video": "Myndbönd",
"search": "Leita...",
"typeToSearch": "Skrifaðu til að leita...",
"pressToSearch": "Ýttu á Enter til að leita..."
},
"languages": {
"ar": "العربية",
"en": "English",
"it": "Italiano",
"fr": "Français",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ja": "日本語",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"es": "Español",
"de": "Deutsch",
"ru": "Русский",
"pl": "Polski",
"ko": "한국어"
"success": {
"linkCopied": "Hlekkur afritaður!"
},
"time": {
"unit": "Tímastilling",
"seconds": "Sekúndur",
"minutes": "Mínútur",
"days": "Dagar",
"hours": "Klukkutímar",
"days": "Dagar"
},
"download": {
"downloadFile": "Sækja skjal",
"downloadFolder": "Sækja möppu"
"minutes": "Mínútur",
"seconds": "Sekúndur",
"unit": "Tímastilling"
}
}
}

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