Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b359561f | ||
|
|
453636dfe2 | ||
|
|
b1605aa6d3 | ||
|
|
23503b80a4 | ||
|
|
0d69fbd9a3 | ||
|
|
0d665e528f | ||
|
|
de0b8bb7b2 | ||
|
|
84da110085 | ||
|
|
6b0d49b1fc | ||
|
|
4c20772e11 | ||
|
|
68f8348dde | ||
|
|
5023e77296 | ||
|
|
95316cbe8c | ||
|
|
cd454bae51 | ||
|
|
241201657c | ||
|
|
9eefaddd9b | ||
|
|
d6d47bbd6b | ||
|
|
82c883f95e | ||
|
|
dd40b0d9b9 | ||
|
|
963837ef1d | ||
|
|
66863b72f7 | ||
|
|
89773447a5 | ||
|
|
6d899a6335 | ||
|
|
28672c0114 | ||
|
|
b8300b7121 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ _old
|
|||||||
rice-box.go
|
rice-box.go
|
||||||
.idea/
|
.idea/
|
||||||
filebrowser
|
filebrowser
|
||||||
|
dist/
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
@@ -79,3 +79,29 @@ dockers:
|
|||||||
- "filebrowser/filebrowser:v{{ .Major }}-pi"
|
- "filebrowser/filebrowser:v{{ .Major }}-pi"
|
||||||
extra_files:
|
extra_files:
|
||||||
- .docker.json
|
- .docker.json
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile.alpine
|
||||||
|
binaries:
|
||||||
|
- filebrowser
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
goarm: ''
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:alpine"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-alpine"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-alpine"
|
||||||
|
extra_files:
|
||||||
|
- .docker.json
|
||||||
|
-
|
||||||
|
dockerfile: Dockerfile.debian
|
||||||
|
binaries:
|
||||||
|
- filebrowser
|
||||||
|
goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
goarm: ''
|
||||||
|
image_templates:
|
||||||
|
- "filebrowser/filebrowser:debian"
|
||||||
|
- "filebrowser/filebrowser:{{ .Tag }}-debian"
|
||||||
|
- "filebrowser/filebrowser:v{{ .Major }}-debian"
|
||||||
|
extra_files:
|
||||||
|
- .docker.json
|
||||||
|
|||||||
47
CHANGELOG.md
Normal file
47
CHANGELOG.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.4.0](https://github.com/filebrowser/filebrowser/compare/v2.3.0...v2.4.0) (2020-07-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* full screen editor ([0d665e5](https://github.com/filebrowser/filebrowser/commit/0d665e528f880ceda0976ceed66070ac34de7969))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add preview bypass for .gif files ([#1012](https://github.com/filebrowser/filebrowser/issues/1012)) ([453636d](https://github.com/filebrowser/filebrowser/commit/453636dfe2bbf177c74617862eb763485d4774bf))
|
||||||
|
* prompt key shortcut conflict ([0d69fbd](https://github.com/filebrowser/filebrowser/commit/0d69fbd9a342aa2695859021df0c423e3ae4a4fa))
|
||||||
|
|
||||||
|
## [2.3.0](https://github.com/filebrowser/filebrowser/compare/v2.2.0...v2.3.0) (2020-06-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add image thumbnails support ([#980](https://github.com/filebrowser/filebrowser/issues/980)) ([6b0d49b](https://github.com/filebrowser/filebrowser/commit/6b0d49b1fc8bdce89576ba91cc0b8ec594fcd625))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* typo in image_templates (apline -> alpine) ([#1005](https://github.com/filebrowser/filebrowser/issues/1005)) ([84da110](https://github.com/filebrowser/filebrowser/commit/84da11008516a371fc0446d97863dc14d337aa25))
|
||||||
|
|
||||||
|
## [2.2.0](https://github.com/filebrowser/filebrowser/compare/v2.1.2...v2.2.0) (2020-06-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add alpine and debian docker images ([66863b7](https://github.com/filebrowser/filebrowser/commit/66863b72f7664e6cb9417f7da542a92fa77ca635))
|
||||||
|
* add folder upload ([#981](https://github.com/filebrowser/filebrowser/issues/981)) ([8977344](https://github.com/filebrowser/filebrowser/commit/89773447a56675b298394149d7a05c5df4039f14)), closes [filebrowser/filebrowser#741](https://github.com/filebrowser/filebrowser/issues/741)
|
||||||
|
* add key shortcuts ([95316cb](https://github.com/filebrowser/filebrowser/commit/95316cbe8c8ac3dbb28310bc11ec347c0caf699b))
|
||||||
|
* upload progress based on total size ([#993](https://github.com/filebrowser/filebrowser/issues/993)) ([cd454ba](https://github.com/filebrowser/filebrowser/commit/cd454bae51f40b1249e6fa6133c2949970eb3018))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add a workaround to fix window freezing when viewing a large file [#992](https://github.com/filebrowser/filebrowser/issues/992) ([2412016](https://github.com/filebrowser/filebrowser/commit/241201657c2bf01806d02a297eb846b26102a479))
|
||||||
|
* apply all fs user rulles ([68f8348](https://github.com/filebrowser/filebrowser/commit/68f8348ddeecba570a361e7aba4546052cc3e356))
|
||||||
|
* frontend token validation ([dd40b0d](https://github.com/filebrowser/filebrowser/commit/dd40b0d9b9cc6268a611306ac4684a1af852b79d)), closes [filebrowser/filebrowser#638](https://github.com/filebrowser/filebrowser/issues/638)
|
||||||
|
* multiple selection count ([963837e](https://github.com/filebrowser/filebrowser/commit/963837ef1dc6e2e84fcf924606ce388ac30f3891))
|
||||||
|
* save event hook ([82c883f](https://github.com/filebrowser/filebrowser/commit/82c883f95eead9eebe215e230f74773c945f864a)), closes [filebrowser/filebrowser#696](https://github.com/filebrowser/filebrowser/issues/696)
|
||||||
11
Dockerfile.alpine
Normal file
11
Dockerfile.alpine
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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" ]
|
||||||
9
Dockerfile.debian
Normal file
9
Dockerfile.debian
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM debian:buster
|
||||||
|
|
||||||
|
VOLUME /srv
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
COPY .docker.json /.filebrowser.json
|
||||||
|
COPY filebrowser /filebrowser
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/filebrowser" ]
|
||||||
14
README.md
14
README.md
@@ -14,16 +14,20 @@ filebrowser provides a file managing interface within a specified directory and
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/features](https://github.com/filebrowser/docs/tree/master/features)
|
Please refer to our docs at [https://filebrowser.org/features](https://filebrowser.org/features)
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz](https://github.com/filebrowser/docs/tree/master/).
|
For installation instructions please refer to our docs at [https://filebrowser.org/installation](https://filebrowser.org/installation).
|
||||||
|
|
||||||
## Usage
|
## Configuration
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/usage](https://github.com/filebrowser/docs/tree/master/usage).
|
[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.
|
||||||
|
|
||||||
|
[Custom Branding](https://filebrowser.org/configuration/custom-branding) - You can customize your File Browser installation by change its name to any other you want, by adding a global custom style sheet and by using your own logotype if you want.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please refer to our docs at [filebrowser.xyz/contributing](https://github.com/filebrowser/docs/tree/master/contributing).
|
If you're interested in contributing to this project, our docs are best places to start [https://filebrowser.org/contributing](https://filebrowser.org/contributing).
|
||||||
|
|||||||
@@ -3,15 +3,17 @@ package errors
|
|||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrEmptyKey = errors.New("empty key")
|
ErrEmptyKey = errors.New("empty key")
|
||||||
ErrExist = errors.New("the resource already exists")
|
ErrExist = errors.New("the resource already exists")
|
||||||
ErrNotExist = errors.New("the resource does not exist")
|
ErrNotExist = errors.New("the resource does not exist")
|
||||||
ErrEmptyPassword = errors.New("password is empty")
|
ErrEmptyPassword = errors.New("password is empty")
|
||||||
ErrEmptyUsername = errors.New("username is empty")
|
ErrEmptyUsername = errors.New("username is empty")
|
||||||
ErrEmptyRequest = errors.New("empty request")
|
ErrEmptyRequest = errors.New("empty request")
|
||||||
ErrScopeIsRelative = errors.New("scope is a relative path")
|
ErrScopeIsRelative = errors.New("scope is a relative path")
|
||||||
ErrInvalidDataType = errors.New("invalid data type")
|
ErrInvalidDataType = errors.New("invalid data type")
|
||||||
ErrIsDirectory = errors.New("file is directory")
|
ErrIsDirectory = errors.New("file is directory")
|
||||||
ErrInvalidOption = errors.New("invalid option")
|
ErrInvalidOption = errors.New("invalid option")
|
||||||
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
ErrInvalidAuthMethod = errors.New("invalid auth method")
|
||||||
|
ErrPermissionDenied = errors.New("permission denied")
|
||||||
|
ErrInvalidRequestParams = errors.New("invalid request params")
|
||||||
)
|
)
|
||||||
|
|||||||
5
frontend/package-lock.json
generated
5
frontend/package-lock.json
generated
@@ -8802,6 +8802,11 @@
|
|||||||
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
|
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"lodash.throttle": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||||
|
},
|
||||||
"lodash.transform": {
|
"lodash.transform": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"clipboard": "^2.0.4",
|
"clipboard": "^2.0.4",
|
||||||
"js-base64": "^2.5.1",
|
"js-base64": "^2.5.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ nav > div {
|
|||||||
color: var(--textPrimary);
|
color: var(--textPrimary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#editor-container {
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .bar {
|
||||||
|
background: var(--surfacePrimary);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 736px) {
|
@media (max-width: 736px) {
|
||||||
#file-selection {
|
#file-selection {
|
||||||
background: var(--surfaceSecondary) !important;
|
background: var(--surfaceSecondary) !important;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<header v-if="!isEditor">
|
||||||
<div>
|
<div>
|
||||||
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
<button @click="openSidebar" :aria-label="$t('buttons.toggleSidebar')" :title="$t('buttons.toggleSidebar')" class="action">
|
||||||
<i class="material-icons">menu</i>
|
<i class="material-icons">menu</i>
|
||||||
@@ -13,10 +13,6 @@
|
|||||||
<i class="material-icons">search</i>
|
<i class="material-icons">search</i>
|
||||||
</button>
|
</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">
|
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
|
||||||
<i class="material-icons">more_vert</i>
|
<i class="material-icons">more_vert</i>
|
||||||
</button>
|
</button>
|
||||||
@@ -129,9 +125,6 @@ export default {
|
|||||||
showUpload () {
|
showUpload () {
|
||||||
return this.isListing && this.user.perm.create
|
return this.isListing && this.user.perm.create
|
||||||
},
|
},
|
||||||
showSaveButton () {
|
|
||||||
return this.isEditor && this.user.perm.modify
|
|
||||||
},
|
|
||||||
showDownloadButton () {
|
showDownloadButton () {
|
||||||
return this.isFiles && this.user.perm.download
|
return this.isFiles && this.user.perm.download
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -136,12 +136,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener("keydown", event => {
|
|
||||||
if (event.keyCode === 27) {
|
|
||||||
this.closeHovers()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$refs.result.addEventListener('scroll', event => {
|
this.$refs.result.addEventListener('scroll', event => {
|
||||||
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight - 100) {
|
||||||
this.resultsCount += 50
|
this.resultsCount += 50
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ export default {
|
|||||||
name: 'upload-button',
|
name: 'upload-button',
|
||||||
methods: {
|
methods: {
|
||||||
upload: function () {
|
upload: function () {
|
||||||
document.getElementById('upload-input').click()
|
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
|
||||||
|
this.$store.commit('showHover', 'upload')
|
||||||
|
} else {
|
||||||
|
document.getElementById('upload-input').click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,37 @@
|
|||||||
<template>
|
<template>
|
||||||
<form id="editor"></form>
|
<div id="editor-container">
|
||||||
|
<div class="bar">
|
||||||
|
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
|
||||||
|
<i class="material-icons">close</i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="title">
|
||||||
|
<span>{{ req.name }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="save" v-show="user.perm.modify" :aria-label="$t('buttons.save')" :title="$t('buttons.save')" id="save-button" class="action">
|
||||||
|
<i class="material-icons">save</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="breadcrumbs">
|
||||||
|
<span><i class="material-icons">home</i></span>
|
||||||
|
|
||||||
|
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||||
|
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||||
|
<span>{{ link.name }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="editor"></form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { files as api } from '@/api'
|
import { files as api } from '@/api'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
import url from '@/utils/url'
|
||||||
|
|
||||||
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
import ace from 'ace-builds/src-min-noconflict/ace.js'
|
||||||
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
|
||||||
@@ -14,27 +40,52 @@ import { theme } from '@/utils/constants'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'editor',
|
name: 'editor',
|
||||||
computed: {
|
|
||||||
...mapState(['req'])
|
|
||||||
},
|
|
||||||
data: function () {
|
data: function () {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['req', 'user']),
|
||||||
|
breadcrumbs () {
|
||||||
|
let parts = this.$route.path.split('/')
|
||||||
|
|
||||||
|
if (parts[0] === '') {
|
||||||
|
parts.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts[parts.length - 1] === '') {
|
||||||
|
parts.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
let breadcrumbs = []
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
breadcrumbs.push({ name: decodeURIComponent(parts[i]) })
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbs.shift()
|
||||||
|
|
||||||
|
if (breadcrumbs.length > 3) {
|
||||||
|
while (breadcrumbs.length !== 4) {
|
||||||
|
breadcrumbs.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
breadcrumbs[0].name = '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumbs
|
||||||
|
}
|
||||||
|
},
|
||||||
created () {
|
created () {
|
||||||
window.addEventListener('keydown', this.keyEvent)
|
window.addEventListener('keydown', this.keyEvent)
|
||||||
document.getElementById('save-button').addEventListener('click', this.save)
|
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
window.removeEventListener('keydown', this.keyEvent)
|
window.removeEventListener('keydown', this.keyEvent)
|
||||||
document.getElementById('save-button').removeEventListener('click', this.save)
|
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
const fileContent = this.req.content || '';
|
const fileContent = this.req.content || '';
|
||||||
|
|
||||||
this.editor = ace.edit('editor', {
|
this.editor = ace.edit('editor', {
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 20,
|
|
||||||
value: fileContent,
|
value: fileContent,
|
||||||
showPrintMargin: false,
|
showPrintMargin: false,
|
||||||
readOnly: this.req.type === 'textImmutable',
|
readOnly: this.req.type === 'textImmutable',
|
||||||
@@ -48,6 +99,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
back () {
|
||||||
|
let uri = url.removeLastDir(this.$route.path) + '/'
|
||||||
|
this.$router.push({ path: uri })
|
||||||
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
if (!event.ctrlKey && !event.metaKey) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<span>{{ $t('files.lonely') }}</span>
|
<span>{{ $t('files.lonely') }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
<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>
|
||||||
<div v-else id="listing"
|
<div v-else id="listing"
|
||||||
:class="user.viewMode"
|
:class="user.viewMode"
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
<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">
|
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||||
@@ -87,6 +89,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapMutations } from 'vuex'
|
import { mapState, mapMutations } from 'vuex'
|
||||||
|
import throttle from 'lodash.throttle'
|
||||||
import Item from './ListingItem'
|
import Item from './ListingItem'
|
||||||
import css from '@/utils/css'
|
import css from '@/utils/css'
|
||||||
import { users, files as api } from '@/api'
|
import { users, files as api } from '@/api'
|
||||||
@@ -98,11 +101,17 @@ export default {
|
|||||||
components: { Item },
|
components: { Item },
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
show: 50
|
showLimit: 50,
|
||||||
|
uploading: {
|
||||||
|
id: 0,
|
||||||
|
count: 0,
|
||||||
|
size: 0,
|
||||||
|
progress: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req', 'selected', 'user']),
|
...mapState(['req', 'selected', 'user', 'show']),
|
||||||
nameSorted () {
|
nameSorted () {
|
||||||
return (this.req.sorting.by === 'name')
|
return (this.req.sorting.by === 'name')
|
||||||
},
|
},
|
||||||
@@ -130,14 +139,14 @@ export default {
|
|||||||
return { dirs, files }
|
return { dirs, files }
|
||||||
},
|
},
|
||||||
dirs () {
|
dirs () {
|
||||||
return this.items.dirs.slice(0, this.show)
|
return this.items.dirs.slice(0, this.showLimit)
|
||||||
},
|
},
|
||||||
files () {
|
files () {
|
||||||
let show = this.show - this.items.dirs.length
|
let showLimit = this.showLimit - this.items.dirs.length
|
||||||
|
|
||||||
if (show < 0) show = 0
|
if (showLimit < 0) showLimit = 0
|
||||||
|
|
||||||
return this.items.files.slice(0, show)
|
return this.items.files.slice(0, showLimit)
|
||||||
},
|
},
|
||||||
nameIcon () {
|
nameIcon () {
|
||||||
if (this.nameSorted && !this.ascOrdered) {
|
if (this.nameSorted && !this.ascOrdered) {
|
||||||
@@ -181,11 +190,15 @@ export default {
|
|||||||
document.removeEventListener('drop', this.drop)
|
document.removeEventListener('drop', this.drop)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations([ 'updateUser' ]),
|
...mapMutations([ 'updateUser', 'addSelected' ]),
|
||||||
base64: function (name) {
|
base64: function (name) {
|
||||||
return window.btoa(unescape(encodeURIComponent(name)))
|
return window.btoa(unescape(encodeURIComponent(name)))
|
||||||
},
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
|
if (this.show !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.ctrlKey && !event.metaKey) {
|
if (!event.ctrlKey && !event.metaKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -204,6 +217,19 @@ export default {
|
|||||||
case 'v':
|
case 'v':
|
||||||
this.paste(event)
|
this.paste(event)
|
||||||
break
|
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) {
|
preventDefault (event) {
|
||||||
@@ -270,7 +296,7 @@ export default {
|
|||||||
},
|
},
|
||||||
scrollEvent () {
|
scrollEvent () {
|
||||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||||
this.show += 50
|
this.showLimit += 50
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dragEnter () {
|
dragEnter () {
|
||||||
@@ -290,10 +316,9 @@ export default {
|
|||||||
this.resetOpacity()
|
this.resetOpacity()
|
||||||
|
|
||||||
let dt = event.dataTransfer
|
let dt = event.dataTransfer
|
||||||
let files = dt.files
|
|
||||||
let el = event.target
|
let el = event.target
|
||||||
|
|
||||||
if (files.length <= 0) return
|
if (dt.files.length <= 0) return
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
if (el !== null && !el.classList.contains('item')) {
|
if (el !== null && !el.classList.contains('item')) {
|
||||||
@@ -306,28 +331,45 @@ export default {
|
|||||||
base = el.querySelector('.name').innerHTML + '/'
|
base = el.querySelector('.name').innerHTML + '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (base !== '') {
|
if (base === '') {
|
||||||
api.fetch(this.$route.path + base)
|
this.scanFiles(dt).then((result) => {
|
||||||
.then(req => {
|
this.checkConflict(result, this.req.items, base)
|
||||||
this.checkConflict(files, req.items, base)
|
})
|
||||||
})
|
} else {
|
||||||
.catch(this.$showError)
|
this.scanFiles(dt).then((result) => {
|
||||||
|
api.fetch(this.$route.path + base)
|
||||||
return
|
.then(req => {
|
||||||
|
this.checkConflict(result, req.items, base)
|
||||||
|
})
|
||||||
|
.catch(this.$showError)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkConflict(files, this.req.items, base)
|
|
||||||
},
|
},
|
||||||
checkConflict (files, items, base) {
|
checkConflict (files, items, base) {
|
||||||
if (typeof items === 'undefined' || items === null) {
|
if (typeof items === 'undefined' || items === null) {
|
||||||
items = []
|
items = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let folder_upload = false
|
||||||
|
if (files[0].fullPath !== undefined) {
|
||||||
|
folder_upload = true
|
||||||
|
}
|
||||||
|
|
||||||
let conflict = false
|
let conflict = false
|
||||||
for (let i = 0; i < files.length; i++) {
|
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) {
|
let res = items.findIndex(function hasConflict (element) {
|
||||||
return (element.name === this)
|
return (element.name === this)
|
||||||
}, files[i].name)
|
}, name)
|
||||||
|
|
||||||
if (res >= 0) {
|
if (res >= 0) {
|
||||||
conflict = true
|
conflict = true
|
||||||
@@ -350,7 +392,19 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
uploadInput (event) {
|
uploadInput (event) {
|
||||||
this.checkConflict(event.currentTarget.files, this.req.items, '')
|
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 () {
|
resetOpacity () {
|
||||||
let items = document.getElementsByClassName('item')
|
let items = document.getElementsByClassName('item')
|
||||||
@@ -359,37 +413,137 @@ export default {
|
|||||||
file.style.opacity = 1
|
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) {
|
handleFiles (files, base, overwrite = false) {
|
||||||
buttons.loading('upload')
|
if (this.uploading.count == 0) {
|
||||||
|
buttons.loading('upload')
|
||||||
|
}
|
||||||
|
|
||||||
let promises = []
|
let promises = []
|
||||||
let progress = new Array(files.length).fill(0)
|
|
||||||
|
|
||||||
let onupload = (id) => (event) => {
|
let onupload = (id) => (event) => {
|
||||||
progress[id] = (event.loaded / event.total) * 100
|
this.uploading.progress[id] = event.loaded
|
||||||
|
this.setProgress()
|
||||||
let sum = 0
|
|
||||||
for (let i = 0; i < progress.length; i++) {
|
|
||||||
sum += progress[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$store.commit('setProgress', Math.ceil(sum / progress.length))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
let file = files[i]
|
let file = files[i]
|
||||||
let filenameEncoded = url.encodeRFC5987ValueChars(file.name)
|
|
||||||
promises.push(api.post(this.$route.path + base + filenameEncoded, file, overwrite, onupload(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 = () => {
|
let finish = () => {
|
||||||
|
if (this.uploading.count > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
buttons.success('upload')
|
buttons.success('upload')
|
||||||
|
|
||||||
this.$store.commit('setProgress', 0)
|
this.$store.commit('setProgress', 0)
|
||||||
|
this.$store.commit('setReload', true)
|
||||||
|
|
||||||
|
this.uploading.id = 0
|
||||||
|
this.uploading.sizes = []
|
||||||
|
this.uploading.progress = []
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
finish()
|
finish()
|
||||||
this.$store.commit('setReload', true)
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
finish()
|
finish()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="item"
|
<div class="item"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
draggable="true"
|
:draggable="isDraggable"
|
||||||
@dragstart="dragStart"
|
@dragstart="dragStart"
|
||||||
@dragover="dragOver"
|
@dragover="dragOver"
|
||||||
@drop="drop"
|
@drop="drop"
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected">
|
:aria-selected="isSelected">
|
||||||
<div>
|
<div>
|
||||||
<i class="material-icons">{{ icon }}</i>
|
<img v-if="type==='image'" :src="thumbnailUrl">
|
||||||
|
<i v-else class="material-icons">{{ icon }}</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { baseURL } from '@/utils/constants'
|
||||||
import { mapMutations, mapGetters, mapState } from 'vuex'
|
import { mapMutations, mapGetters, mapState } from 'vuex'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
@@ -44,7 +46,7 @@ export default {
|
|||||||
},
|
},
|
||||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['selected', 'req']),
|
...mapState(['selected', 'req', 'user', 'jwt']),
|
||||||
...mapGetters(['selectedCount']),
|
...mapGetters(['selectedCount']),
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return (this.selected.indexOf(this.index) !== -1)
|
return (this.selected.indexOf(this.index) !== -1)
|
||||||
@@ -56,6 +58,9 @@ export default {
|
|||||||
if (this.type === 'video') return 'movie'
|
if (this.type === 'video') return 'movie'
|
||||||
return 'insert_drive_file'
|
return 'insert_drive_file'
|
||||||
},
|
},
|
||||||
|
isDraggable () {
|
||||||
|
return this.user.perm.rename
|
||||||
|
},
|
||||||
canDrop () {
|
canDrop () {
|
||||||
if (!this.isDir) return false
|
if (!this.isDir) return false
|
||||||
|
|
||||||
@@ -66,6 +71,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
},
|
||||||
|
thumbnailUrl () {
|
||||||
|
const path = this.url.replace(/^\/files\//, '')
|
||||||
|
return `${baseURL}/api/preview/thumb/${path}?auth=${this.jwt}&inline=true`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -129,7 +138,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey && this.selected.length > 0) {
|
||||||
let fi = 0
|
let fi = 0
|
||||||
let la = 0
|
let la = 0
|
||||||
|
|
||||||
@@ -142,7 +151,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (; fi <= la; fi++) {
|
for (; fi <= la; fi++) {
|
||||||
this.addSelected(fi)
|
if (this.$store.state.selected.indexOf(fi) == -1) {
|
||||||
|
this.addSelected(fi)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -86,8 +86,14 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
return `${baseURL}/api/raw${this.req.path}?auth=${this.jwt}`
|
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 () {
|
raw () {
|
||||||
return `${this.download}&inline=true`
|
return `${this.previewUrl}&inline=true`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<component :is="currentComponent"></component>
|
<component ref="currentComponent" :is="currentComponent"></component>
|
||||||
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,6 +17,7 @@ import NewFile from './NewFile'
|
|||||||
import NewDir from './NewDir'
|
import NewDir from './NewDir'
|
||||||
import Replace from './Replace'
|
import Replace from './Replace'
|
||||||
import Share from './Share'
|
import Share from './Share'
|
||||||
|
import Upload from './Upload'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
@@ -33,7 +34,8 @@ export default {
|
|||||||
NewFile,
|
NewFile,
|
||||||
NewDir,
|
NewDir,
|
||||||
Help,
|
Help,
|
||||||
Replace
|
Replace,
|
||||||
|
Upload
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
@@ -44,6 +46,33 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
window.addEventListener('keydown', (event) => {
|
||||||
|
if (this.show == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
let prompt = this.$refs.currentComponent;
|
||||||
|
|
||||||
|
// Enter
|
||||||
|
if (event.keyCode == 13) {
|
||||||
|
switch (this.show) {
|
||||||
|
case 'delete':
|
||||||
|
prompt.submit()
|
||||||
|
break;
|
||||||
|
case 'copy':
|
||||||
|
prompt.copy(event)
|
||||||
|
break;
|
||||||
|
case 'move':
|
||||||
|
prompt.move(event)
|
||||||
|
break;
|
||||||
|
case 'replace':
|
||||||
|
prompt.showConfirm(event)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['show', 'plugins']),
|
...mapState(['show', 'plugins']),
|
||||||
currentComponent: function () {
|
currentComponent: function () {
|
||||||
@@ -58,7 +87,8 @@ export default {
|
|||||||
'newDir',
|
'newDir',
|
||||||
'download',
|
'download',
|
||||||
'replace',
|
'replace',
|
||||||
'share'
|
'share',
|
||||||
|
'upload'
|
||||||
].indexOf(this.show) >= 0;
|
].indexOf(this.show) >= 0;
|
||||||
|
|
||||||
return matched && this.show || null;
|
return matched && this.show || null;
|
||||||
|
|||||||
37
frontend/src/components/prompts/Upload.vue
Normal file
37
frontend/src/components/prompts/Upload.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card floating">
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>{{ $t('prompts.upload') }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p>{{ $t('prompts.uploadMessage') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-action full">
|
||||||
|
<div @click="uploadFile" class="action">
|
||||||
|
<i class="material-icons">insert_drive_file</i>
|
||||||
|
<div class="title">File</div>
|
||||||
|
</div>
|
||||||
|
<div @click="uploadFolder" class="action">
|
||||||
|
<i class="material-icons">folder</i>
|
||||||
|
<div class="title">Folder</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'upload',
|
||||||
|
methods: {
|
||||||
|
uploadFile: function () {
|
||||||
|
document.getElementById('upload-input').click()
|
||||||
|
},
|
||||||
|
uploadFolder: function () {
|
||||||
|
document.getElementById('upload-folder-input').click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -96,6 +96,7 @@ table tr>*:last-child {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 2px;
|
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);
|
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);
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card.floating {
|
.card.floating {
|
||||||
@@ -366,3 +367,33 @@ table tr>*:last-child {
|
|||||||
.card .collapsible .collapse {
|
.card .collapsible .collapse {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card .card-action.full {
|
||||||
|
padding-top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action.full .action {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action.full .action {
|
||||||
|
margin: 0 0.25em 0.50em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action.full .action i {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
font-size: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .card-action.full .action .title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -52,6 +52,13 @@
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#listing .item img {
|
||||||
|
width: 4em;
|
||||||
|
height: 4em;
|
||||||
|
margin-right: 0.1em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
@@ -129,6 +136,11 @@
|
|||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#listing.list .item div:first-of-type img {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
#listing.list .item div:last-of-type {
|
#listing.list .item div:last-of-type {
|
||||||
width: calc(100% - 3em);
|
width: calc(100% - 3em);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -184,6 +184,53 @@
|
|||||||
right: 0.5em;
|
right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* EDITOR */
|
||||||
|
|
||||||
|
#editor-container {
|
||||||
|
background-color: #fafafa;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 9999;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .bar {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 3.7em;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.075);
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container .title {
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
line-height: 2.7em;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#previewer .title span {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #editor {
|
||||||
|
height: calc(100vh - 8.2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #breadcrumbs {
|
||||||
|
height: 2.3em;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-container #breadcrumbs span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* * * * * * * * * * * * * * * *
|
/* * * * * * * * * * * * * * * *
|
||||||
* PROMPT *
|
* PROMPT *
|
||||||
|
|||||||
@@ -116,7 +116,9 @@
|
|||||||
"size": "Size",
|
"size": "Size",
|
||||||
"schedule": "Schedule",
|
"schedule": "Schedule",
|
||||||
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
|
"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."
|
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
|
||||||
|
"upload": "Upload",
|
||||||
|
"uploadMessage": "Select an option to upload."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"themes": {
|
"themes": {
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ export function parseToken (token) {
|
|||||||
|
|
||||||
const data = JSON.parse(Base64.decode(parts[1]))
|
const data = JSON.parse(Base64.decode(parts[1]))
|
||||||
|
|
||||||
if (Math.round(new Date().getTime() / 1000) > data.exp) {
|
|
||||||
throw new Error('token expired')
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('jwt', token)
|
localStorage.setItem('jwt', token)
|
||||||
store.commit('setJWT', token)
|
store.commit('setJWT', token)
|
||||||
store.commit('setUser', data.user)
|
store.commit('setUser', data.user)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="breadcrumbs">
|
<div id="breadcrumbs" v-if="isListing || error">
|
||||||
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||||
<i class="material-icons">home</i>
|
<i class="material-icons">home</i>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -61,7 +61,8 @@ export default {
|
|||||||
'user',
|
'user',
|
||||||
'reload',
|
'reload',
|
||||||
'multiple',
|
'multiple',
|
||||||
'loading'
|
'loading',
|
||||||
|
'show'
|
||||||
]),
|
]),
|
||||||
isPreview () {
|
isPreview () {
|
||||||
return !this.loading && !this.isListing && !this.isEditor
|
return !this.loading && !this.isListing && !this.isEditor
|
||||||
@@ -158,10 +159,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
keyEvent (event) {
|
keyEvent (event) {
|
||||||
// Esc!
|
if (this.show !== null) {
|
||||||
if (event.keyCode === 27) {
|
// Esc!
|
||||||
this.$store.commit('closeHovers')
|
if (event.keyCode === 27) {
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Esc!
|
||||||
|
if (event.keyCode === 27) {
|
||||||
// If we're on a listing, unselect all
|
// If we're on a listing, unselect all
|
||||||
// files and folders.
|
// files and folders.
|
||||||
if (this.isListing) {
|
if (this.isListing) {
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/caddyserver/caddy v1.0.3
|
github.com/caddyserver/caddy v1.0.3
|
||||||
github.com/daaku/go.zipexe v1.0.1 // indirect
|
github.com/daaku/go.zipexe v1.0.1 // indirect
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
|
github.com/disintegration/imaging v1.6.2
|
||||||
github.com/dsnet/compress v0.0.1 // indirect
|
github.com/dsnet/compress v0.0.1 // indirect
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/gorilla/mux v1.7.3
|
github.com/gorilla/mux v1.7.3
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -43,6 +43,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
@@ -239,6 +241,8 @@ golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
|
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
|
||||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
|
||||||
|
|||||||
17
http/data.go
17
http/data.go
@@ -26,19 +26,20 @@ type data struct {
|
|||||||
|
|
||||||
// Check implements rules.Checker.
|
// Check implements rules.Checker.
|
||||||
func (d *data) Check(path string) bool {
|
func (d *data) Check(path string) bool {
|
||||||
for _, rule := range d.user.Rules {
|
allow := true
|
||||||
if rule.Matches(path) {
|
|
||||||
return rule.Allow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rule := range d.settings.Rules {
|
for _, rule := range d.settings.Rules {
|
||||||
if rule.Matches(path) {
|
if rule.Matches(path) {
|
||||||
return rule.Allow
|
allow = rule.Allow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
for _, rule := range d.user.Rules {
|
||||||
|
if rule.Matches(path) {
|
||||||
|
allow = rule.Allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allow
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
|
func handle(fn handleFunc, prefix string, store *storage.Storage, server *settings.Server) http.Handler {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ func NewHandler(store *storage.Storage, server *settings.Server) (http.Handler,
|
|||||||
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
|
api.Handle("/settings", monkey(settingsPutHandler, "")).Methods("PUT")
|
||||||
|
|
||||||
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
|
api.PathPrefix("/raw").Handler(monkey(rawHandler, "/api/raw")).Methods("GET")
|
||||||
|
api.PathPrefix("/preview/{size}/{path:.*}").Handler(monkey(previewHandler, "/api/preview")).Methods("GET")
|
||||||
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
|
api.PathPrefix("/command").Handler(monkey(commandsHandler, "/api/command")).Methods("GET")
|
||||||
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
|
api.PathPrefix("/search").Handler(monkey(searchHandler, "/api/search")).Methods("GET")
|
||||||
|
|
||||||
|
|||||||
101
http/preview.go
Normal file
101
http/preview.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sizeThumb = "thumb"
|
||||||
|
sizeBig = "big"
|
||||||
|
)
|
||||||
|
|
||||||
|
type imageProcessor func(src image.Image) (image.Image, error)
|
||||||
|
|
||||||
|
var previewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
|
if !d.user.Perm.Download {
|
||||||
|
return http.StatusAccepted, nil
|
||||||
|
}
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
size := vars["size"]
|
||||||
|
if size != sizeBig && size != sizeThumb {
|
||||||
|
return http.StatusNotImplemented, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := files.NewFileInfo(files.FileOptions{
|
||||||
|
Fs: d.user.Fs,
|
||||||
|
Path: "/" + vars["path"],
|
||||||
|
Modify: d.user.Perm.Modify,
|
||||||
|
Expand: true,
|
||||||
|
Checker: d,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentDisposition(w, r, file)
|
||||||
|
|
||||||
|
switch file.Type {
|
||||||
|
case "image":
|
||||||
|
return handleImagePreview(w, r, file, size)
|
||||||
|
default:
|
||||||
|
return http.StatusNotImplemented, fmt.Errorf("can't create preview for %s type", file.Type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
func handleImagePreview(w http.ResponseWriter, r *http.Request, file *files.FileInfo, size string) (int, error) {
|
||||||
|
format, err := imaging.FormatFromExtension(file.Extension)
|
||||||
|
if err != nil {
|
||||||
|
// Unsupported extensions directly return the raw data
|
||||||
|
if err == imaging.ErrUnsupportedFormat {
|
||||||
|
return rawFileHandler(w, r, file)
|
||||||
|
}
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := file.Fs.Open(file.Path)
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
if format == imaging.GIF && size == sizeBig {
|
||||||
|
if _, err := rawFileHandler(w, r, file); err != nil { //nolint: govet
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var imgProcessor imageProcessor
|
||||||
|
switch size {
|
||||||
|
case sizeBig:
|
||||||
|
imgProcessor = func(img image.Image) (image.Image, error) {
|
||||||
|
return imaging.Fit(img, 1080, 1080, imaging.Lanczos), nil
|
||||||
|
}
|
||||||
|
case sizeThumb:
|
||||||
|
imgProcessor = func(img image.Image) (image.Image, error) {
|
||||||
|
return imaging.Thumbnail(img, 128, 128, imaging.Box), nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return http.StatusBadRequest, fmt.Errorf("unsupported preview size %s", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := imaging.Decode(fd, imaging.AutoOrientation(true))
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
img, err = imgProcessor(img)
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
if imaging.Encode(w, img, format) != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
16
http/raw.go
16
http/raw.go
@@ -58,6 +58,15 @@ func parseQueryAlgorithm(r *http.Request) (string, archiver.Writer, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setContentDisposition(w http.ResponseWriter, r *http.Request, file *files.FileInfo) {
|
||||||
|
if r.URL.Query().Get("inline") == "true" {
|
||||||
|
w.Header().Set("Content-Disposition", "inline")
|
||||||
|
} else {
|
||||||
|
// As per RFC6266 section 4.3
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
if !d.user.Perm.Download {
|
if !d.user.Perm.Download {
|
||||||
return http.StatusAccepted, nil
|
return http.StatusAccepted, nil
|
||||||
@@ -168,12 +177,7 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file *files.FileInfo
|
|||||||
}
|
}
|
||||||
defer fd.Close()
|
defer fd.Close()
|
||||||
|
|
||||||
if r.URL.Query().Get("inline") == "true" {
|
setContentDisposition(w, r, file)
|
||||||
w.Header().Set("Content-Disposition", "inline")
|
|
||||||
} else {
|
|
||||||
// As per RFC6266 section 4.3
|
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(file.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
http.ServeContent(w, r, file.Name, file.ModTime, fd)
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
"github.com/filebrowser/filebrowser/v2/errors"
|
||||||
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
"github.com/filebrowser/filebrowser/v2/fileutils"
|
"github.com/filebrowser/filebrowser/v2/fileutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,7 +93,18 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
action := "upload"
|
||||||
|
if r.Method == http.MethodPut {
|
||||||
|
action = "save"
|
||||||
|
}
|
||||||
|
|
||||||
err := d.RunHook(func() error {
|
err := d.RunHook(func() error {
|
||||||
|
dir, _ := filepath.Split(r.URL.Path)
|
||||||
|
err := d.user.Fs.MkdirAll(dir, 0775)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -114,12 +125,11 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
|
|||||||
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
|
||||||
w.Header().Set("ETag", etag)
|
w.Header().Set("ETag", etag)
|
||||||
return nil
|
return nil
|
||||||
}, "upload", r.URL.Path, "", d.user)
|
}, action, r.URL.Path, "", d.user)
|
||||||
|
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
})
|
})
|
||||||
|
|
||||||
//nolint: goconst
|
|
||||||
var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
src := r.URL.Path
|
src := r.URL.Path
|
||||||
dst := r.URL.Query().Get("destination")
|
dst := r.URL.Query().Get("destination")
|
||||||
@@ -134,26 +144,22 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
|
|||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch action {
|
|
||||||
// TODO: use enum
|
|
||||||
case "copy":
|
|
||||||
if !d.user.Perm.Create {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
case "rename":
|
|
||||||
default:
|
|
||||||
action = "rename"
|
|
||||||
if !d.user.Perm.Rename {
|
|
||||||
return http.StatusForbidden, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = d.RunHook(func() error {
|
err = d.RunHook(func() error {
|
||||||
if action == "copy" {
|
switch action {
|
||||||
|
// TODO: use enum
|
||||||
|
case "copy":
|
||||||
|
if !d.user.Perm.Create {
|
||||||
|
return errors.ErrPermissionDenied
|
||||||
|
}
|
||||||
return fileutils.Copy(d.user.Fs, src, dst)
|
return fileutils.Copy(d.user.Fs, src, dst)
|
||||||
|
case "rename":
|
||||||
|
if !d.user.Perm.Rename {
|
||||||
|
return errors.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
return d.user.Fs.Rename(src, dst)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.user.Fs.Rename(src, dst)
|
|
||||||
}, action, src, dst, d.user)
|
}, action, src, dst, d.user)
|
||||||
|
|
||||||
return errToStatus(err), err
|
return errToStatus(err), err
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
libErrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func renderJSON(w http.ResponseWriter, _ *http.Request, data interface{}) (int, error) {
|
func renderJSON(w http.ResponseWriter, _ *http.Request, data interface{}) (int, error) {
|
||||||
@@ -31,10 +32,14 @@ func errToStatus(err error) int {
|
|||||||
return http.StatusOK
|
return http.StatusOK
|
||||||
case os.IsPermission(err):
|
case os.IsPermission(err):
|
||||||
return http.StatusForbidden
|
return http.StatusForbidden
|
||||||
case os.IsNotExist(err), err == errors.ErrNotExist:
|
case os.IsNotExist(err), err == libErrors.ErrNotExist:
|
||||||
return http.StatusNotFound
|
return http.StatusNotFound
|
||||||
case os.IsExist(err), err == errors.ErrExist:
|
case os.IsExist(err), err == libErrors.ErrExist:
|
||||||
return http.StatusConflict
|
return http.StatusConflict
|
||||||
|
case errors.Is(err, libErrors.ErrPermissionDenied):
|
||||||
|
return http.StatusForbidden
|
||||||
|
case errors.Is(err, libErrors.ErrInvalidRequestParams):
|
||||||
|
return http.StatusBadRequest
|
||||||
default:
|
default:
|
||||||
return http.StatusInternalServerError
|
return http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user