Compare commits

...

32 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
Ramires Viana
2a1f759e9e chore: remove prompts events 2021-03-04 14:40:18 +00:00
Ramires Viana
edb9e85efd chore: share view logic responsability 2021-03-03 17:46:37 +00:00
Ramires Viana
d8306559fd chore: breadcrumbs component 2021-03-03 12:25:59 +00: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
Ramires Viana
1819377897 feat: improved sharing prompt 2021-02-16 15:39:11 +00:00
73 changed files with 1591 additions and 1646 deletions

2
.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

View File

@@ -3,10 +3,6 @@ project_name: filebrowser
env:
- GO111MODULE=on
before:
hooks:
- make bundle-frontend
build:
env:
- CGO_ENABLED=0

View File

@@ -2,6 +2,38 @@
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)

View File

@@ -37,9 +37,6 @@ $(BIN)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/cmd/golangci-lin
GOIMPORTS = $(BIN)/goimports
$(BIN)/goimports: PACKAGE=golang.org/x/tools/cmd/goimports@v0.1.0
RICE = $(BIN)/rice
$(BIN)/rice: PACKAGE=github.com/GeertJohan/go.rice/rice@v1.0.2
## build: Build
.PHONY: build
build: | build-frontend build-backend ; $(info $(M) building)
@@ -51,12 +48,9 @@ build-frontend: | ; $(info $(M) building frontend…)
## build-backend: Build backend
.PHONY: build-backend
build-backend: bundle-frontend | ; $(info $(M) building backend)
build-backend: | ; $(info $(M) building backend)
$Q $(GO) build -ldflags '$(LDFLAGS)' -o filebrowser
bundle-frontend: | $(RICE) ; $(info $(M) building backend)
$Q cd ./http && rm -rf rice-box.go && $(RICE) embed-go
## test: Run all tests
.PHONY: test
test: | test-frontend test-backend ; $(info $(M) running tests)

View File

@@ -3,6 +3,7 @@ package cmd
import (
"crypto/tls"
"errors"
"io/fs"
"io/ioutil"
"log"
"net"
@@ -22,6 +23,7 @@ import (
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/diskcache"
"github.com/filebrowser/filebrowser/v2/frontend"
fbhttp "github.com/filebrowser/filebrowser/v2/http"
"github.com/filebrowser/filebrowser/v2/img"
"github.com/filebrowser/filebrowser/v2/settings"
@@ -168,7 +170,12 @@ user created with the credentials from options "username" and "password".`,
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go cleanupHandler(listener, sigc)
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server)
assetsFs, err := fs.Sub(frontend.Assets(), "dist")
if err != nil {
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
checkErr(err)
defer listener.Close()

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

@@ -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": {

View File

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

View File

@@ -58,7 +58,7 @@ export async function put (url, content = '') {
}
export function download (format, ...files) {
let url = store.getters['isSharing'] ? `${baseURL}/api/public/dl/${store.state.hash}` : `${baseURL}/api/raw`
let url = `${baseURL}/api/raw`
if (files.length === 1) {
url += removePrefix(files[0]) + '?'
@@ -74,15 +74,13 @@ export function download (format, ...files) {
url += `/?files=${arg}&`
}
if (format !== null) {
if (format) {
url += `algo=${format}&`
}
if (store.state.jwt !== ''){
if (store.state.jwt){
url += `auth=${store.state.jwt}&`
}
if (store.state.token !== ''){
url += `token=${store.state.token}`
}
window.open(url)
}

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

@@ -4,12 +4,6 @@ export async function list() {
return fetchJSON('/api/shares')
}
export async function getHash(hash, password = "") {
return fetchJSON(`/api/public/share/${hash}`, {
headers: {'X-SHARE-PASSWORD': password},
})
}
export async function get(url) {
url = removePrefix(url)
return fetchJSON(`/api/share${url}`)

View File

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

View File

@@ -1,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', 'isSharing'])
},
methods: {
download: function () {
if (!this.isListing && !this.isSharing) {
api.download(null, this.$route.path)
return
}
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.req.items[this.selected[0]].url)
return
}
this.$store.commit('showHover', 'download')
}
}
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
:aria-label="name"
:aria-selected="isSelected">
<div>
<img v-if="type==='image' && isThumbsEnabled && !isSharing" v-lazy="thumbnailUrl">
<img v-if="readOnly == undefined && type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
<i v-else class="material-icons">{{ icon }}</i>
</div>
@@ -45,13 +45,12 @@ export default {
touches: 0
}
},
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index', 'readOnly'],
computed: {
...mapState(['user', 'selected', 'req', 'jwt']),
...mapGetters(['selectedCount', 'isSharing']),
...mapGetters(['selectedCount']),
singleClick () {
if (this.isSharing) return false
return this.user.singleClick
return this.readOnly == undefined && this.user.singleClick
},
isSelected () {
return (this.selected.indexOf(this.index) !== -1)
@@ -64,10 +63,10 @@ export default {
return 'insert_drive_file'
},
isDraggable () {
return !this.isSharing && this.user.perm.rename
return this.readOnly == undefined && this.user.perm.rename
},
canDrop () {
if (!this.isDir || this.isSharing) return false
if (!this.isDir || this.readOnly !== undefined) return false
for (let i of this.selected) {
if (this.req.items[i].url === this.url) {
@@ -79,7 +78,11 @@ 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
@@ -139,7 +142,7 @@ export default {
to: this.url + this.req.items[i].name,
name: this.req.items[i].name
})
}
}
let base = el.querySelector('.name').innerHTML + '/'
let path = this.$route.path + base

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

@@ -26,7 +26,7 @@ export default {
name: 'delete',
computed: {
...mapGetters(['isListing', 'selectedCount']),
...mapState(['req', 'selected'])
...mapState(['req', 'selected', 'showConfirm'])
},
methods: {
...mapMutations(['closeHovers']),
@@ -38,7 +38,7 @@ export default {
await api.remove(this.$route.path)
buttons.success('delete')
this.$root.$emit('preview-deleted')
this.showConfirm()
this.closeHovers()
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

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

@@ -4,61 +4,82 @@
<h2>{{ $t('buttons.share') }}</h2>
</div>
<div class="card-content">
<ul>
<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 v-if="!hasPermanent">
<div>
<input type="password" :placeholder="$t('prompts.optionalPassword')" v-model="passwordPermalink">
<a @click="getPermalink" :aria-label="$t('buttons.permalink')">{{ $t('buttons.permalink') }}</a>
</div>
</li>
<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>
<input type="password" :placeholder="$t('prompts.optionalPassword')" v-model="password">
<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>
@@ -75,11 +96,10 @@ export default {
return {
time: '',
unit: 'hours',
hasPermanent: false,
links: [],
clip: null,
password: '',
passwordPermalink: ''
listing: true
}
},
computed: {
@@ -104,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)
@@ -125,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.password, 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.passwordPermalink)
this.links.push(res)
this.sort()
this.hasPermanent = true
this.time = ''
this.unit = 'hours'
this.password = ''
this.listing = true
} catch (e) {
this.$showError(e)
}
@@ -149,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)
}
@@ -167,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

@@ -1,7 +1,7 @@
<template>
<div class="card floating">
<div class="card-content">
<p>{{ $t('prompts.deleteMessageShare', {path: hash.path}) }}</p>
<p>{{ $t('prompts.deleteMessageShare', {path: ''}) }}</p>
</div>
<div class="card-action">
<button @click="$store.commit('closeHovers')"
@@ -17,30 +17,16 @@
</template>
<script>
import {mapMutations, mapState} from 'vuex'
import { share as api } from '@/api'
import buttons from '@/utils/buttons'
import {mapState} from 'vuex'
export default {
name: 'share-delete',
computed: {
...mapState(['hash'])
...mapState(['showConfirm'])
},
methods: {
...mapMutations(['closeHovers']),
submit: async function () {
buttons.loading('delete')
try {
await api.remove(this.hash.hash)
buttons.success('delete')
this.$root.$emit('share-deleted', this.hash.hash)
this.closeHovers()
} catch (e) {
buttons.done('delete')
this.$showError(e)
}
submit: function () {
this.showConfirm()
}
}
}

View File

@@ -70,9 +70,4 @@
padding: .5em;
text-align: center;
animation: .2s opac forwards;
}
.share__promt__card {
max-width: max-content !important;
width: auto !important;
}
}

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,45 +118,33 @@
overflow: hidden;
}
#previewer .bar {
width: 100%;
display: flex;
padding: 0.5em;
height: 3.7em;
}
#previewer .bar > * {
flex: 0 0 auto;
}
#previewer .bar .title {
display: block;
flex: 1 1 auto;
padding: 0 1em;
line-height: 2.3em;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.2em;
#previewer header {
background: none;
color: #fff;
}
#previewer .action i {
#previewer header > .action i {
color: #fff;
}
#previewer .action:hover {
@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 {
@@ -170,6 +159,10 @@
margin: 0;
}
#previewer .preview video {
height: 100%;
}
#previewer .pdf {
width: 100%;
height: 100%;
@@ -182,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 {
@@ -199,6 +209,7 @@
#editor-container {
background-color: #fafafa;
position: fixed;
margin-top: 4em;
top: 0;
left: 0;
width: 100%;
@@ -206,43 +217,28 @@
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 .loading {
height: 100%;
width: 100%;
}
#editor-container #editor {
height: calc(100vh - 8.2em);
height: calc(100vh - 8.4em);
}
#editor-container #breadcrumbs {
#editor-container .breadcrumbs {
height: 2.3em;
padding: 0 1em;
}
#editor-container #breadcrumbs span {
#editor-container .breadcrumbs span {
font-size: 12px;
}
#editor-container .breadcrumbs i {
font-size: 16px;
}
/* * * * * * * * * * * * * * * *
* PROMPT *
* * * * * * * * * * * * * * * */

View File

@@ -8,7 +8,7 @@
"create": "创建",
"delete": "删除",
"download": "下载",
"hideDotfiles": "",
"hideDotfiles": "不显示隐藏的文件",
"info": "信息",
"more": "更多",
"move": "移动",
@@ -29,6 +29,7 @@
"selectMultiple": "选择多个",
"share": "分享",
"shell": "激活 shell",
"submit": "提交",
"switchView": "切换显示方式",
"toggleSidebar": "切换侧边栏",
"update": "更新",
@@ -138,11 +139,12 @@
"replace": "替换",
"replaceMessage": "您尝试上传的文件中有一个与现有文件的名称存在冲突。是否替换现有的同名文件?",
"schedule": "计划",
"scheduleMessage": "请选择发布这篇帖子的日期。",
"scheduleMessage": "请选择发布这篇帖子的日期与时间。",
"show": "点击以显示",
"size": "大小",
"upload": "上传",
"uploadMessage": "选择上传项。"
"uploadMessage": "选择上传项。",
"optionalPassword": "密码(选填,不填即无密码)"
},
"search": {
"images": "图像",

View File

@@ -10,9 +10,7 @@ import Settings from '@/views/Settings'
import GlobalSettings from '@/views/settings/Global'
import ProfileSettings from '@/views/settings/Profile'
import Shares from '@/views/settings/Shares'
import Error403 from '@/views/errors/403'
import Error404 from '@/views/errors/404'
import Error500 from '@/views/errors/500'
import Errors from '@/views/Errors'
import store from '@/store'
import { baseURL } from '@/utils/constants'
@@ -102,17 +100,29 @@ const router = new Router({
{
path: '/403',
name: 'Forbidden',
component: Error403
component: Errors,
props: {
errorCode: 403,
showHeader: true
}
},
{
path: '/404',
name: 'Not Found',
component: Error404
component: Errors,
props: {
errorCode: 404,
showHeader: true
}
},
{
path: '/500',
name: 'Internal Server Error',
component: Error500
component: Errors,
props: {
errorCode: 500,
showHeader: true
}
},
{
path: '/files',

View File

@@ -2,9 +2,6 @@ const getters = {
isLogged: state => state.user !== null,
isFiles: state => !state.loading && state.route.name === 'Files',
isListing: (state, getters) => getters.isFiles && state.req.isDir,
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
isPreview: state => state.previewMode,
isSharing: state => !state.loading && state.route.name === 'Share',
selectedCount: state => state.selected.length,
progress : state => {
if (state.upload.progress.length == 0) {

View File

@@ -22,11 +22,7 @@ const state = {
multiple: false,
show: null,
showShell: false,
showMessage: null,
showConfirm: null,
previewMode: false,
hash: '',
token: '',
showConfirm: null
}
export default new Vuex.Store({

View File

@@ -4,7 +4,7 @@ import moment from 'moment'
const mutations = {
closeHovers: state => {
state.show = null
state.showMessage = null
state.showConfirm = null
},
toggleShell: (state) => {
state.showShell = !state.showShell
@@ -16,16 +16,13 @@ const mutations = {
}
state.show = value.prompt
state.showMessage = value.message
state.showConfirm = value.confirm
},
showError: (state, value) => {
showError: (state) => {
state.show = 'error'
state.showMessage = value
},
showSuccess: (state, value) => {
showSuccess: (state) => {
state.show = 'success'
state.showMessage = value
},
setLoading: (state, value) => { state.loading = value },
setReload: (state, value) => { state.reload = value },
@@ -46,12 +43,8 @@ const mutations = {
state.user = value
},
setJWT: (state, value) => (state.jwt = value),
setToken: (state, value ) => (state.token = value),
multiple: (state, value) => (state.multiple = value),
addSelected: (state, value) => (state.selected.push(value)),
addPlugin: (state, value) => {
state.plugins.push(value)
},
removeSelected: (state, value) => {
let i = state.selected.indexOf(value)
if (i === -1) return
@@ -84,11 +77,7 @@ const mutations = {
resetClipboard: (state) => {
state.clipboard.key = ''
state.clipboard.items = []
},
setPreviewMode(state, value) {
state.previewMode = value
},
setHash: (state, value) => (state.hash = value),
}
}
export default mutations

View File

@@ -6,6 +6,10 @@ function loading (button) {
return
}
if (el.innerHTML == 'autorenew' || el.innerHTML == 'done') {
return
}
el.dataset.icon = el.innerHTML
el.style.opacity = 0

View File

@@ -0,0 +1,46 @@
<template>
<div>
<header-bar v-if="showHeader" showMenu showLogo />
<h2 class="message">
<i class="material-icons">{{ icon }}</i>
<span>{{ message }}</span>
</h2>
</div>
</template>
<script>
import HeaderBar from '@/components/header/HeaderBar'
const errors = {
403: {
icon: 'error',
message: 'errors.forbidden'
},
404: {
icon: 'gps_off',
message: 'errors.notFound'
},
500: {
icon: 'error_outline',
message: 'errors.internal'
}
}
export default {
name: 'errors',
components: {
HeaderBar
},
props: [
'errorCode', 'showHeader'
],
data: function () {
return {
icon: errors[this.errorCode].icon,
message: this.$t(errors[this.errorCode].message)
}
}
}
</script>

View File

@@ -1,24 +1,11 @@
<template>
<div>
<div id="breadcrumbs" v-if="isListing || error">
<router-link to="/files/" :aria-label="$t('files.home')" :title="$t('files.home')">
<i class="material-icons">home</i>
</router-link>
<header-bar v-if="error || !req.type" showMenu showLogo />
<span v-for="(link, index) in breadcrumbs" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<router-link :to="link.url">{{ link.name }}</router-link>
</span>
</div>
<breadcrumbs base="/files" />
<div v-if="error">
<not-found v-if="error.message === '404'"></not-found>
<forbidden v-else-if="error.message === '403'"></forbidden>
<internal-error v-else></internal-error>
</div>
<preview v-else-if="isPreview"></preview>
<editor v-else-if="isEditor"></editor>
<listing :class="{ multiple }" v-else-if="isListing"></listing>
<errors v-if="error" :errorCode="errorCode" />
<component v-else-if="currentView" :is="currentView"></component>
<div v-else>
<h2 class="message">
<span>{{ $t('files.loading') }}</span>
@@ -28,13 +15,14 @@
</template>
<script>
import Forbidden from './errors/403'
import NotFound from './errors/404'
import InternalError from './errors/500'
import Preview from '@/components/files/Preview'
import Listing from '@/components/files/Listing'
import { files as api } from '@/api'
import { mapGetters, mapState, mapMutations } from 'vuex'
import { mapState, mapMutations } from 'vuex'
import HeaderBar from '@/components/header/HeaderBar'
import Breadcrumbs from '@/components/Breadcrumbs'
import Errors from '@/views/Errors'
import Preview from '@/views/files/Preview'
import Listing from '@/views/files/Listing'
function clean (path) {
return path.endsWith('/') ? path.slice(0, -1) : path
@@ -43,68 +31,41 @@ function clean (path) {
export default {
name: 'files',
components: {
Forbidden,
NotFound,
InternalError,
HeaderBar,
Breadcrumbs,
Errors,
Preview,
Listing,
Editor: () => import('@/components/files/Editor')
},
computed: {
...mapGetters([
'selectedCount',
'isListing',
'isEditor',
'isFiles'
]),
...mapState([
'req',
'user',
'reload',
'multiple',
'loading',
'show'
]),
isPreview () {
return !this.loading && !this.isListing && !this.isEditor || this.loading && this.$store.state.previewMode
},
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++) {
if (i === 0) {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: '/' + parts[i] + '/' })
} else {
breadcrumbs.push({ name: decodeURIComponent(parts[i]), url: breadcrumbs[i - 1].url + parts[i] + '/' })
}
}
breadcrumbs.shift()
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift()
}
breadcrumbs[0].name = '...'
}
return breadcrumbs
}
Editor: () => import('@/views/files/Editor'),
},
data: function () {
return {
error: null
error: null,
width: window.innerWidth
}
},
computed: {
...mapState([
'req',
'reload',
'loading',
'show'
]),
currentView () {
if (this.req.type == undefined) {
return null
}
if (this.req.isDir) {
return 'listing'
} else if(this.req.type === 'text' || this.req.type === 'textImmutable') {
return 'editor'
} else {
return 'preview'
}
},
errorCode() {
return (this.error.message === '404' || this.error.message === '403') ? parseInt(this.error.message) : 500
}
},
created () {
@@ -120,13 +81,14 @@ export default {
},
mounted () {
window.addEventListener('keydown', this.keyEvent)
window.addEventListener('scroll', this.scroll)
},
beforeDestroy () {
window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('scroll', this.scroll)
},
destroyed () {
if (this.$store.state.showShell) {
this.$store.commit('toggleShell')
}
this.$store.commit('updateRequest', {})
},
methods: {
@@ -171,74 +133,11 @@ export default {
return
}
// Esc!
if (event.keyCode === 27) {
// If we're on a listing, unselect all
// files and folders.
if (this.isListing) {
this.$store.commit('resetSelected')
}
}
// Del!
if (event.keyCode === 46) {
if (this.isEditor ||
!this.isFiles ||
this.loading ||
!this.user.perm.delete ||
(this.isListing && this.selectedCount === 0) ||
this.$store.state.show != null) return
this.$store.commit('showHover', 'delete')
}
// F1!
if (event.keyCode === 112) {
event.preventDefault()
this.$store.commit('showHover', 'help')
}
// F2!
if (event.keyCode === 113) {
if (this.isEditor ||
!this.isFiles ||
this.loading ||
!this.user.perm.rename ||
(this.isListing && this.selectedCount === 0) ||
(this.isListing && this.selectedCount > 1)) return
this.$store.commit('showHover', 'rename')
}
// CTRL + S
if (event.ctrlKey || event.metaKey) {
if (this.isEditor) return
if (String.fromCharCode(event.which).toLowerCase() === 's') {
event.preventDefault()
if (this.req.kind !== 'editor') {
document.getElementById('download-button').click()
}
}
}
},
scroll () {
if (this.req.kind !== 'listing' || this.$store.state.user.viewMode === 'mosaic') return
let top = 112 - window.scrollY
if (top < 64) {
top = 64
}
document.querySelector('#listing.list .item.header').style.top = top + 'px'
},
openSidebar () {
this.$store.commit('showHover', 'sidebar')
},
openSearch () {
this.$store.commit('showHover', 'search')
}
}
}

View File

@@ -3,7 +3,6 @@
<div id="progress">
<div v-bind:style="{ width: this.progress + '%' }"></div>
</div>
<site-header></site-header>
<sidebar></sidebar>
<main>
<router-view></router-view>
@@ -17,7 +16,6 @@
import { mapState, mapGetters } from 'vuex'
import Sidebar from '@/components/Sidebar'
import Prompts from '@/components/prompts/Prompts'
import SiteHeader from '@/components/Header'
import Shell from '@/components/Shell'
import { enableExec } from '@/utils/constants'
@@ -25,7 +23,6 @@ export default {
name: 'layout',
components: {
Sidebar,
SiteHeader,
Prompts,
Shell
},

View File

@@ -1,11 +1,17 @@
<template>
<div class="dashboard">
<ul id="nav">
<li :class="{ active: $route.path === '/settings/profile' }"><router-link to="/settings/profile">{{ $t('settings.profileSettings') }}</router-link></li>
<li :class="{ active: $route.path === '/settings/shares' }"><router-link to="/settings/shares">{{ $t('settings.shareManagement') }}</router-link></li>
<li v-if="user.perm.admin" :class="{ active: $route.path === '/settings/global' }"><router-link to="/settings/global">{{ $t('settings.globalSettings') }}</router-link></li>
<li v-if="user.perm.admin" :class="{ active: $route.path === '/settings/users' }"><router-link to="/settings/users">{{ $t('settings.userManagement') }}</router-link></li>
</ul>
<header-bar showMenu showLogo />
<div id="nav">
<div class="wrapper">
<ul>
<router-link to="/settings/profile"><li :class="{ active: $route.path === '/settings/profile' }">{{ $t('settings.profileSettings') }}</li></router-link>
<router-link to="/settings/shares"><li :class="{ active: $route.path === '/settings/shares' }">{{ $t('settings.shareManagement') }}</li></router-link>
<router-link to="/settings/global"><li :class="{ active: $route.path === '/settings/global' }" v-if="user.perm.admin">{{ $t('settings.globalSettings') }}</li></router-link>
<router-link to="/settings/users"><li :class="{ active: $route.path === '/settings/users' || $route.name === 'User' }" v-if="user.perm.admin">{{ $t('settings.userManagement') }}</li></router-link>
</ul>
</div>
</div>
<router-view></router-view>
</div>
@@ -14,8 +20,15 @@
<script>
import { mapState } from 'vuex'
import HeaderBar from '@/components/header/HeaderBar'
export default {
name: 'settings',
computed: mapState([ 'user' ])
components: {
HeaderBar
},
computed: {
...mapState([ 'user' ])
}
}
</script>

View File

@@ -1,135 +1,139 @@
<template>
<div v-if="!loading">
<div id="breadcrumbs">
<router-link :to="'/share/' + hash" :aria-label="$t('files.home')" :title="$t('files.home')">
<i class="material-icons">home</i>
</router-link>
<div>
<header-bar showMenu showLogo>
<title />
<span v-for="(link, index) in breadcrumbs" :key="index">
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
<router-link :to="link.url">{{ link.name }}</router-link>
</span>
</div>
<div class="share">
<div class="share__box share__box__info">
<div class="share__box__header">
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
</div>
<div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i>
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.name }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }}
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="fullLink" size="200" level="M"></qrcode-vue>
</div>
</div>
<div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items">
<div class="share__box__header" v-if="req.isDir">
{{ $t('files.files') }}
</div>
<div id="listing" class="list">
<item v-for="(item) in req.items.slice(0, this.showLimit)"
: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 v-if="req.items.length > showLimit" class="item">
<div>
<p class="name"> + {{ req.items.length - showLimit }} </p>
<action v-if="selectedCount" icon="file_download" :label="$t('buttons.download')" @action="download" :counter="selectedCount" />
<action icon="check_circle" :label="$t('buttons.selectMultiple')" @action="toggleMultipleSelection" />
</header-bar>
<breadcrumbs :base="'/share/' + hash" />
<div v-if="!loading">
<div class="share">
<div class="share__box share__box__info">
<div class="share__box__header">
{{ req.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
</div>
<div class="share__box__element share__box__center share__box__icon">
<i class="material-icons">{{ icon }}</i>
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.displayName') }}</strong> {{ req.name }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.lastModified') }}:</strong> {{ humanTime }}
</div>
<div class="share__box__element">
<strong>{{ $t('prompts.size') }}:</strong> {{ humanSize }}
</div>
<div class="share__box__element share__box__center">
<a target="_blank" :href="link" class="button button--flat">{{ $t('buttons.download') }}</a>
</div>
<div class="share__box__element share__box__center">
<qrcode-vue :value="link" size="200" level="M"></qrcode-vue>
</div>
</div>
<div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items">
<div class="share__box__header" v-if="req.isDir">
{{ $t('files.files') }}
</div>
<div id="listing" class="list">
<item v-for="(item) in req.items.slice(0, this.showLimit)"
: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"
readOnly>
</item>
<div v-if="req.items.length > showLimit" class="item">
<div>
<p class="name"> + {{ req.items.length - showLimit }} </p>
</div>
</div>
<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 :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>
</div>
</div>
<div v-else-if="req.isDir && req.items.length === 0" class="share__box share__box__items">
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
</h2>
<div v-else-if="req.isDir && req.items.length === 0" class="share__box share__box__items">
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
</h2>
</div>
</div>
</div>
</div>
<div v-else-if="error">
<not-found v-if="error.message === '404'"></not-found>
<forbidden v-else-if="error.message === '403'"></forbidden>
<div v-else-if="error.message === '401'">
<div class="card floating" id="password">
<div v-if="attemptedPasswordLogin" class="share__wrong__password">{{ $t('login.wrongCredentials') }}</div>
<div class="card-title">
<h2>{{ $t('login.password') }}</h2>
</div>
<div v-if="error">
<div v-if="error.message === '401'">
<div class="card floating" id="password">
<div v-if="attemptedPasswordLogin" class="share__wrong__password">{{ $t('login.wrongCredentials') }}</div>
<div class="card-title">
<h2>{{ $t('login.password') }}</h2>
</div>
<div class="card-content">
<input v-focus type="password" :placeholder="$t('login.password')" v-model="password" @keyup.enter="fetchData">
</div>
<div class="card-action">
<button class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')">{{ $t('buttons.submit') }}</button>
<div class="card-content">
<input v-focus type="password" :placeholder="$t('login.password')" v-model="password" @keyup.enter="fetchData">
</div>
<div class="card-action">
<button class="button button--flat"
@click="fetchData"
:aria-label="$t('buttons.submit')"
:title="$t('buttons.submit')">{{ $t('buttons.submit') }}</button>
</div>
</div>
</div>
<errors v-else :errorCode="errorCode" />
</div>
<internal-error v-else></internal-error>
</div>
</template>
<script>
import {mapState, mapMutations, mapGetters} from 'vuex';
import { share as api } from '@/api'
import { pub as api } from '@/api'
import { baseURL } from '@/utils/constants'
import filesize from 'filesize'
import moment from 'moment'
import HeaderBar from '@/components/header/HeaderBar'
import Action from '@/components/header/Action'
import Breadcrumbs from '@/components/Breadcrumbs'
import Errors from '@/views/Errors'
import QrcodeVue from 'qrcode.vue'
import Item from "@/components/files/ListingItem"
import Forbidden from './errors/403'
import NotFound from './errors/404'
import InternalError from './errors/500'
export default {
name: 'share',
components: {
HeaderBar,
Action,
Breadcrumbs,
Item,
Forbidden,
NotFound,
InternalError,
QrcodeVue
QrcodeVue,
Errors
},
data: () => ({
error: null,
path: '',
showLimit: 500,
password: '',
attemptedPasswordLogin: false
attemptedPasswordLogin: false,
hash: null,
token: null
}),
watch: {
'$route': 'fetchData'
},
created: async function () {
const hash = this.$route.params.pathMatch.split('/')[0]
this.setHash(hash)
this.hash = hash
await this.fetchData()
},
mounted () {
@@ -139,8 +143,8 @@ export default {
window.removeEventListener('keydown', this.keyEvent)
},
computed: {
...mapState(['hash', 'req', 'loading', 'multiple']),
...mapGetters(['selectedCount']),
...mapState(['req', 'loading', 'multiple', 'selected']),
...mapGetters(['selectedCount', 'selectedCount']),
icon: function () {
if (this.req.isDir) return 'folder'
if (this.req.type === 'image') return 'insert_photo'
@@ -153,10 +157,9 @@ export default {
if (this.token !== ''){
queryArg = `?token=${this.token}`
}
return `${baseURL}/api/public/dl/${this.hash}${this.path}${queryArg}`
},
fullLink: function () {
return window.location.origin + this.link
const path = this.$route.path.split('/').splice(2).join('/')
return `${baseURL}/api/public/dl/${path}${queryArg}`
},
humanSize: function () {
if (this.req.isDir) {
@@ -168,40 +171,12 @@ export default {
humanTime: function () {
return moment(this.req.modified).fromNow()
},
breadcrumbs () {
let parts = this.path.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: '/share/' + this.hash + '/' + 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
errorCode() {
return (this.error.message === '404' || this.error.message === '403') ? parseInt(this.error.message) : 500
}
},
methods: {
...mapMutations([ 'setHash', 'resetSelected', 'updateRequest', 'setLoading' ]),
...mapMutations([ 'resetSelected', 'updateRequest', 'setLoading' ]),
base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name)))
},
@@ -216,19 +191,19 @@ export default {
this.setLoading(true)
this.error = null
if (this.password !== ''){
this.attemptedPasswordLogin = true
}
let url = this.$route.path
if (url === '') url = '/'
if (url[0] !== '/') url = '/' + url
try {
if (this.password !== ''){
this.attemptedPasswordLogin = true
}
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch), this.password)
this.path = file.path
let file = await api.fetch(url, this.password)
this.token = file.token || ''
this.$store.commit('setToken', this.token)
if (file.isDir) file.items = file.items.map((item, index) => {
item.index = index
item.url = `/share/${this.hash}${this.path}/${encodeURIComponent(item.name)}`
return item
})
this.updateRequest(file)
this.setLoading(false)
} catch (e) {
@@ -247,6 +222,27 @@ export default {
},
toggleMultipleSelection () {
this.$store.commit('multiple', !this.multiple)
},
download () {
if (this.selectedCount === 1 && !this.req.items[this.selected[0]].isDir) {
api.download(null, this.hash, this.token, this.req.items[this.selected[0]].path)
return
}
this.$store.commit('showHover', {
prompt: 'download',
confirm: (format) => {
this.$store.commit('closeHovers')
let files = []
for (let i of this.selected) {
files.push(this.req.items[i].path)
}
api.download(format, this.hash, this.token, ...files)
}
})
}
}
}

View File

@@ -1,13 +0,0 @@
<template>
<div>
<h2 class="message">
<i class="material-icons">error</i>
<span>{{ $t('errors.forbidden') }}</span>
</h2>
</div>
</template>
<script>
export default {name: 'forbidden'}
</script>

View File

@@ -1,13 +0,0 @@
<template>
<div>
<h2 class="message">
<i class="material-icons">gps_off</i>
<span>{{ $t('errors.notFound') }}</span>
</h2>
</div>
</template>
<script>
export default {name: 'not-found'}
</script>

View File

@@ -1,13 +0,0 @@
<template>
<div>
<h2 class="message">
<i class="material-icons">error_outline</i>
<span>{{ $t('errors.internal') }}</span>
</h2>
</div>
</template>
<script>
export default {name: 'internal-error'}
</script>

View File

@@ -1,27 +1,15 @@
<template>
<div id="editor-container">
<div class="bar">
<button @click="back" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close" class="action">
<i class="material-icons">close</i>
</button>
<header-bar>
<action icon="close" :label="$t('buttons.close')" @action="close()" />
<title>{{ req.name }}</title>
<div class="title">
<span>{{ req.name }}</span>
</div>
<template #actions>
<action id="save-button" icon="save" :label="$t('buttons.save')" @action="save()" />
</template>
</header-bar>
<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>
<breadcrumbs base="/files" noLink />
<form id="editor"></form>
</div>
@@ -30,16 +18,25 @@
<script>
import { mapState } from 'vuex'
import { files as api } from '@/api'
import { theme } from '@/utils/constants'
import buttons from '@/utils/buttons'
import url from '@/utils/url'
import ace from 'ace-builds/src-min-noconflict/ace.js'
import modelist from 'ace-builds/src-min-noconflict/ext-modelist.js'
import 'ace-builds/webpack-resolver'
import { theme } from '@/utils/constants'
import HeaderBar from '@/components/header/HeaderBar'
import Action from '@/components/header/Action'
import Breadcrumbs from '@/components/Breadcrumbs'
export default {
name: 'editor',
components: {
HeaderBar,
Action,
Breadcrumbs
},
data: function () {
return {}
},
@@ -82,7 +79,7 @@ export default {
window.removeEventListener('keydown', this.keyEvent)
this.editor.destroy();
},
mounted: function () {
mounted: function () {
const fileContent = this.req.content || '';
this.editor = ace.edit('editor', {
@@ -126,6 +123,12 @@ export default {
buttons.done(button)
this.$showError(e)
}
},
close () {
this.$store.commit('updateRequest', {})
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
}
}
}

View File

@@ -1,108 +1,169 @@
<template>
<div v-if="(req.numDirs + req.numFiles) == 0">
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
</h2>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
</div>
<div v-else id="listing"
:class="user.viewMode">
<div>
<div class="item header">
<div></div>
<div>
<p :class="{ active: nameSorted }" class="name"
role="button"
tabindex="0"
@click="sort('name')"
:title="$t('files.sortByName')"
:aria-label="$t('files.sortByName')">
<span>{{ $t('files.name') }}</span>
<i class="material-icons">{{ nameIcon }}</i>
</p>
<div>
<header-bar showMenu showLogo>
<search /> <title />
<action class="search-button" icon="search" :label="$t('buttons.search')" @action="openSearch()" />
<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>
<template #actions>
<template v-if="!isMobile">
<action v-if="headerButtons.share" icon="share" :label="$t('buttons.share')" show="share" />
<action v-if="headerButtons.rename" icon="mode_edit" :label="$t('buttons.rename')" show="rename" />
<action v-if="headerButtons.copy" icon="content_copy" :label="$t('buttons.copyFile')" show="copy" />
<action v-if="headerButtons.move" icon="forward" :label="$t('buttons.moveFile')" show="move" />
<action v-if="headerButtons.delete" icon="delete" :label="$t('buttons.delete')" show="delete" />
</template>
<action v-if="headerButtons.shell" icon="code" :label="$t('buttons.shell')" @action="$store.commit('toggleShell')" />
<action :icon="user.viewMode === 'mosaic' ? 'view_list' : 'view_module'" :label="$t('buttons.switchView')" @action="switchView" />
<action icon="file_download" :label="$t('buttons.download')" @action="download" :counter="selectedCount" />
<action icon="file_upload" :label="$t('buttons.upload')" @action="upload" />
<action icon="info" :label="$t('buttons.info')" show="info" />
<action icon="check_circle" :label="$t('buttons.selectMultiple')" @action="toggleMultipleSelection" />
</template>
</header-bar>
<div v-if="isMobile" id="file-selection">
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
<action v-if="headerButtons.share" icon="share" :label="$t('buttons.share')" show="share" />
<action v-if="headerButtons.rename" icon="mode_edit" :label="$t('buttons.rename')" show="rename" />
<action v-if="headerButtons.copy" icon="content_copy" :label="$t('buttons.copyFile')" show="copy" />
<action v-if="headerButtons.move" icon="forward" :label="$t('buttons.moveFile')" show="move" />
<action v-if="headerButtons.delete" icon="delete" :label="$t('buttons.delete')" show="delete" />
</div>
<div v-if="$store.state.loading">
<h2 class="message">
<span>{{ $t('files.loading') }}</span>
</h2>
</div>
<template v-else>
<div v-if="(req.numDirs + req.numFiles) == 0">
<h2 class="message">
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
</h2>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
</div>
<div v-else id="listing"
:class="user.viewMode">
<div>
<div class="item header">
<div></div>
<div>
<p :class="{ active: nameSorted }" class="name"
role="button"
tabindex="0"
@click="sort('name')"
:title="$t('files.sortByName')"
:aria-label="$t('files.sortByName')">
<span>{{ $t('files.name') }}</span>
<i class="material-icons">{{ nameIcon }}</i>
</p>
<p :class="{ active: sizeSorted }" class="size"
role="button"
tabindex="0"
@click="sort('size')"
:title="$t('files.sortBySize')"
:aria-label="$t('files.sortBySize')">
<span>{{ $t('files.size') }}</span>
<i class="material-icons">{{ sizeIcon }}</i>
</p>
<p :class="{ active: modifiedSorted }" class="modified"
role="button"
tabindex="0"
@click="sort('modified')"
:title="$t('files.sortByLastModified')"
:aria-label="$t('files.sortByLastModified')">
<span>{{ $t('files.lastModified') }}</span>
<i class="material-icons">{{ modifiedIcon }}</i>
</p>
</div>
</div>
</div>
<h2 v-if="req.numDirs > 0">{{ $t('files.folders') }}</h2>
<div v-if="req.numDirs > 0">
<item v-for="(item) in dirs"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.isDir"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size">
</item>
</div>
<h2 v-if="req.numFiles > 0">{{ $t('files.files') }}</h2>
<div v-if="req.numFiles > 0">
<item v-for="(item) in files"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.isDir"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size">
</item>
</div>
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
<input style="display:none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory multiple>
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
<div @click="$store.commit('multiple', false)" tabindex="0" role="button" :title="$t('files.clear')" :aria-label="$t('files.clear')" class="action">
<i class="material-icons">clear</i>
</div>
</div>
</div>
</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>
</template>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import Item from './ListingItem'
import css from '@/utils/css'
import { mapState, mapGetters, mapMutations } from 'vuex'
import { users, files as api } from '@/api'
import { enableExec } from '@/utils/constants'
import * as upload from '@/utils/upload'
import css from '@/utils/css'
import HeaderBar from '@/components/header/HeaderBar'
import Action from '@/components/header/Action'
import Search from '@/components/Search'
import Item from '@/components/files/ListingItem'
export default {
name: 'listing',
components: { Item },
components: {
HeaderBar,
Action,
Search,
Item
},
data: function () {
return {
showLimit: 50,
dragCounter: 0
dragCounter: 0,
width: window.innerWidth
}
},
computed: {
...mapState(['req', 'selected', 'user', 'show']),
...mapState([
'req',
'selected',
'user',
'show',
'multiple',
'selected'
]),
...mapGetters([
'selectedCount'
]),
nameSorted () {
return (this.req.sorting.by === 'name')
},
@@ -159,6 +220,21 @@ export default {
}
return 'arrow_upward'
},
headerButtons() {
return {
upload: this.user.perm.create,
download: this.user.perm.download,
shell: this.user.perm.execute && enableExec,
delete: this.selectedCount > 0 && this.user.perm.delete,
rename: this.selectedCount === 1 && this.user.perm.rename,
share: this.selectedCount === 1 && this.user.perm.share,
move: this.selectedCount > 0 && this.user.perm.rename,
copy: this.selectedCount > 0 && this.user.perm.create,
}
},
isMobile () {
return this.width <= 736
}
},
mounted: function () {
@@ -169,6 +245,7 @@ export default {
window.addEventListener('keydown', this.keyEvent)
window.addEventListener('resize', this.resizeEvent)
window.addEventListener('scroll', this.scrollEvent)
window.addEventListener('resize', this.windowsResize)
document.addEventListener('dragover', this.preventDefault)
document.addEventListener('dragenter', this.dragEnter)
document.addEventListener('dragleave', this.dragLeave)
@@ -179,6 +256,7 @@ export default {
window.removeEventListener('keydown', this.keyEvent)
window.removeEventListener('resize', this.resizeEvent)
window.removeEventListener('scroll', this.scrollEvent)
window.removeEventListener('resize', this.windowsResize)
document.removeEventListener('dragover', this.preventDefault)
document.removeEventListener('dragenter', this.dragEnter)
document.removeEventListener('dragleave', this.dragLeave)
@@ -190,10 +268,34 @@ export default {
return window.btoa(unescape(encodeURIComponent(name)))
},
keyEvent (event) {
// No prompts are shown
if (this.show !== null) {
return
}
// Esc!
if (event.keyCode === 27) {
// Reset files selection.
this.$store.commit('resetSelected')
}
// Del!
if (event.keyCode === 46) {
if (!this.user.perm.delete || this.selectedCount == 0) return
// Show delete prompt.
this.$store.commit('showHover', 'delete')
}
// F2!
if (event.keyCode === 113) {
if (!this.user.perm.rename || this.selectedCount !== 1) return
// Show rename prompt.
this.$store.commit('showHover', 'rename')
}
// Ctrl is pressed
if (!event.ctrlKey && !event.metaKey) {
return
}
@@ -207,7 +309,7 @@ export default {
break
case 'c':
case 'x':
this.copyCut(event, key)
this.copyCut(event, key)
break
case 'v':
this.paste(event)
@@ -225,6 +327,10 @@ export default {
}
}
break
case 's':
event.preventDefault()
document.getElementById('download-button').click()
break
}
},
preventDefault (event) {
@@ -458,6 +564,63 @@ export default {
}
this.$store.commit('setReload', true)
},
openSearch () {
this.$store.commit('showHover', 'search')
},
toggleMultipleSelection () {
this.$store.commit('multiple', !this.multiple)
this.$store.commit('closeHovers')
},
windowsResize () {
this.width = window.innerWidth
},
download() {
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', {
prompt: 'download',
confirm: (format) => {
this.$store.commit('closeHovers')
let files = []
if (this.selectedCount > 0) {
for (let i of this.selected) {
files.push(this.req.items[i].url)
}
} else {
files.push(this.$route.path)
}
api.download(format, ...files)
}
})
},
switchView: async function () {
this.$store.commit('closeHovers')
const data = {
id: this.user.id,
viewMode: (this.user.viewMode === 'mosaic') ? 'list' : 'mosaic'
}
try {
await users.update(data, ['viewMode'])
this.$store.commit('updateUser', data)
} catch (e) {
this.$showError(e)
}
},
upload: function () {
if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
this.$store.commit('showHover', 'upload')
} else {
document.getElementById('upload-input').click();
}
}
}
}

View File

@@ -1,24 +1,17 @@
<template>
<div id="previewer">
<div class="bar">
<button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
<i class="material-icons">close</i>
</button>
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
<header-bar>
<action icon="close" :label="$t('buttons.close')" @action="close()" />
<title>{{ name }}</title>
<action :disabled="loading" v-if="isResizeEnabled && req.type === 'image'" :icon="fullSize ? 'photo_size_select_large' : 'hd'" @action="toggleSize" />
<div class="title">{{ this.name }}</div>
<preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
<button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
<i class="material-icons">more_vert</i>
</button>
<div id="dropdown" :class="{ active : showMore }">
<rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
<delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
<download-button :disabled="loading" v-if="user.perm.download"></download-button>
<info-button :disabled="loading"></info-button>
</div>
</div>
<template #actions>
<action :disabled="loading" icon="mode_edit" :label="$t('buttons.rename')" show="rename" />
<action :disabled="loading" icon="delete" :label="$t('buttons.delete')" @action="deleteFile" id="delete-button" />
<action :disabled="loading" icon="file_download" :label="$t('buttons.download')" @action="download" />
<action :disabled="loading" icon="info" :label="$t('buttons.info')" show="info" />
</template>
</header-bar>
<div class="loading" v-if="loading">
<div class="spinner">
@@ -28,18 +21,11 @@
</div>
</div>
<button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
<i class="material-icons">chevron_left</i>
</button>
<button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<i class="material-icons">chevron_right</i>
</button>
<template v-if="!loading">
<div class="preview">
<ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
<audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
<video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
<audio v-else-if="req.type == 'audio'" :src="raw" controls></audio>
<video v-else-if="req.type == 'video'" :src="raw" controls>
<track
kind="captions"
v-for="(sub, index) in subtitles"
@@ -47,31 +33,35 @@
: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>
but don't worry, you can <a :href="downloadUrl">download it</a>
and watch it with your favorite video player!
</video>
<object v-else-if="req.extension.toLowerCase() == '.pdf'" class="pdf" :data="raw"></object>
<a v-else-if="req.type == 'blob'" :href="download">
<a v-else-if="req.type == 'blob'" :href="downloadUrl">
<h2 class="message">{{ $t('buttons.download') }} <i class="material-icons">file_download</i></h2>
</a>
</div>
</template>
<div v-show="showMore" @click="resetPrompts" class="overlay"></div>
<button @click="prev" @mouseover="hoverNav = true" @mouseleave="hoverNav = false" :class="{ hidden: !hasPrevious || !showNav }" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
<i class="material-icons">chevron_left</i>
</button>
<button @click="next" @mouseover="hoverNav = true" @mouseleave="hoverNav = false" :class="{ hidden: !hasNext || !showNav }" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<i class="material-icons">chevron_right</i>
</button>
</div>
</template>
<script>
import { mapState } from 'vuex'
import url from '@/utils/url'
import { baseURL, resizePreview } from '@/utils/constants'
import { files as api } from '@/api'
import PreviewSizeButton from '@/components/buttons/PreviewSize'
import InfoButton from '@/components/buttons/Info'
import DeleteButton from '@/components/buttons/Delete'
import RenameButton from '@/components/buttons/Rename'
import DownloadButton from '@/components/buttons/Download'
import ExtendedImage from './ExtendedImage'
import { baseURL, resizePreview } from '@/utils/constants'
import url from '@/utils/url'
import throttle from 'lodash.throttle'
import HeaderBar from '@/components/header/HeaderBar'
import Action from '@/components/header/Action'
import ExtendedImage from '@/components/files/ExtendedImage'
const mediaTypes = [
"image",
@@ -83,11 +73,8 @@ const mediaTypes = [
export default {
name: 'preview',
components: {
PreviewSizeButton,
InfoButton,
DeleteButton,
RenameButton,
DownloadButton,
HeaderBar,
Action,
ExtendedImage
},
data: function () {
@@ -97,7 +84,10 @@ export default {
listing: null,
name: '',
subtitles: [],
fullSize: false
fullSize: false,
showNav: true,
navTimeout: null,
hoverNav: false
}
},
computed: {
@@ -108,14 +98,17 @@ export default {
hasNext () {
return (this.nextLink !== '')
},
download () {
downloadUrl () {
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
},
previewUrl () {
// reload the image when the file is replaced
const key = Date.parse(this.req.modified)
if (this.req.type === 'image' && !this.fullSize) {
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}`
return `${baseURL}/api/preview/big${url.encodePath(this.req.path)}?auth=${this.jwt}&k=${key}`
}
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}`
return `${baseURL}/api/raw${url.encodePath(this.req.path)}?auth=${this.jwt}&k=${key}`
},
raw () {
return `${this.previewUrl}&inline=true`
@@ -130,41 +123,40 @@ export default {
watch: {
$route: function () {
this.updatePreview()
this.toggleNavigation()
}
},
async mounted () {
window.addEventListener('keydown', this.key)
this.$store.commit('setPreviewMode', true)
this.listing = this.oldReq.items
this.$root.$on('preview-deleted', this.deleted)
this.updatePreview()
},
beforeDestroy () {
window.removeEventListener('keydown', this.key)
this.$store.commit('setPreviewMode', false)
this.$root.$off('preview-deleted', this.deleted)
},
methods: {
deleted () {
this.listing = this.listing.filter(item => item.name !== this.name)
deleteFile () {
this.$store.commit('showHover', {
prompt: 'delete',
confirm: () => {
this.listing = this.listing.filter(item => item.name !== this.name)
if (this.hasNext) {
this.next()
} else if (!this.hasPrevious && !this.hasNext) {
this.back()
} else {
this.prev()
}
},
back () {
this.$store.commit('setPreviewMode', false)
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
if (this.hasNext) {
this.next()
} else if (!this.hasPrevious && !this.hasNext) {
this.close()
} else {
this.prev()
}
}
})
},
prev () {
this.hoverNav = false
this.$router.push({ path: this.previousLink })
},
next () {
this.hoverNav = false
this.$router.push({ path: this.nextLink })
},
key (event) {
@@ -178,7 +170,7 @@ export default {
} else if (event.which === 37) { // left arrow
if (this.hasPrevious) this.prev()
} else if (event.which === 27) { // esc
this.back()
this.close()
}
},
async updatePreview () {
@@ -232,6 +224,27 @@ export default {
},
toggleSize () {
this.fullSize = !this.fullSize
},
toggleNavigation: throttle(function() {
this.showNav = true
if (this.navTimeout) {
clearTimeout(this.navTimeout)
}
this.navTimeout = setTimeout(() => {
this.showNav = false || this.hoverNav
this.navTimeout = null
}, 1500);
}, 500),
close () {
this.$store.commit('updateRequest', {})
let uri = url.removeLastDir(this.$route.path) + '/'
this.$router.push({ path: uri })
},
download() {
api.download(null, this.$route.path)
}
}
}

View File

@@ -1,102 +1,108 @@
<template>
<div class="dashboard" v-if="settings !== null">
<form class="card" @submit.prevent="save">
<div class="card-title">
<h2>{{ $t('settings.globalSettings') }}</h2>
</div>
<div class="card-content">
<p><input type="checkbox" v-model="settings.signup"> {{ $t('settings.allowSignup') }}</p>
<p><input type="checkbox" v-model="settings.createUserDir"> {{ $t('settings.createUserDir') }}</p>
<h3>{{ $t('settings.rules') }}</h3>
<p class="small">{{ $t('settings.globalRules') }}</p>
<rules :rules.sync="settings.rules" />
<div v-if="isExecEnabled">
<h3>{{ $t('settings.executeOnShell') }}</h3>
<p class="small">{{ $t('settings.executeOnShellDescription') }}</p>
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" />
<div class="row" v-if="settings !== null">
<div class="column">
<form class="card" @submit.prevent="save">
<div class="card-title">
<h2>{{ $t('settings.globalSettings') }}</h2>
</div>
<h3>{{ $t('settings.branding') }}</h3>
<div class="card-content">
<p><input type="checkbox" v-model="settings.signup"> {{ $t('settings.allowSignup') }}</p>
<i18n path="settings.brandingHelp" tag="p" class="small">
<a class="link" target="_blank" href="https://filebrowser.org/configuration/custom-branding">{{ $t('settings.documentation') }}</a>
</i18n>
<p><input type="checkbox" v-model="settings.createUserDir"> {{ $t('settings.createUserDir') }}</p>
<p>
<input type="checkbox" v-model="settings.branding.disableExternal" id="branding-links" />
{{ $t('settings.disableExternalLinks') }}
</p>
<h3>{{ $t('settings.rules') }}</h3>
<p class="small">{{ $t('settings.globalRules') }}</p>
<rules :rules.sync="settings.rules" />
<p>
<label for="theme">{{ $t('settings.themes.title') }}</label>
<themes class="input input--block" :theme.sync="settings.branding.theme" id="theme"></themes>
</p>
<div v-if="isExecEnabled">
<h3>{{ $t('settings.executeOnShell') }}</h3>
<p class="small">{{ $t('settings.executeOnShellDescription') }}</p>
<input class="input input--block" type="text" placeholder="bash -c, cmd /c, ..." v-model="settings.shell" />
</div>
<p>
<label for="branding-name">{{ $t('settings.instanceName') }}</label>
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" />
</p>
<h3>{{ $t('settings.branding') }}</h3>
<p>
<label for="branding-files">{{ $t('settings.brandingDirectoryPath') }}</label>
<input class="input input--block" type="text" v-model="settings.branding.files" id="branding-files" />
</p>
<i18n path="settings.brandingHelp" tag="p" class="small">
<a class="link" target="_blank" href="https://filebrowser.org/configuration/custom-branding">{{ $t('settings.documentation') }}</a>
</i18n>
</div>
<p>
<input type="checkbox" v-model="settings.branding.disableExternal" id="branding-links" />
{{ $t('settings.disableExternalLinks') }}
</p>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
<p>
<label for="theme">{{ $t('settings.themes.title') }}</label>
<themes class="input input--block" :theme.sync="settings.branding.theme" id="theme"></themes>
</p>
<form class="card" @submit.prevent="save">
<div class="card-title">
<h2>{{ $t('settings.userDefaults') }}</h2>
</div>
<p>
<label for="branding-name">{{ $t('settings.instanceName') }}</label>
<input class="input input--block" type="text" v-model="settings.branding.name" id="branding-name" />
</p>
<div class="card-content">
<p class="small">{{ $t('settings.defaultUserDescription') }}</p>
<p>
<label for="branding-files">{{ $t('settings.brandingDirectoryPath') }}</label>
<input class="input input--block" type="text" v-model="settings.branding.files" id="branding-files" />
</p>
<user-form :isNew="false" :isDefault="true" :user.sync="settings.defaults" />
</div>
</div>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
</div>
<form v-if="isExecEnabled" class="card" @submit.prevent="save">
<div class="card-title">
<h2>{{ $t('settings.commandRunner') }}</h2>
</div>
<div class="column">
<form class="card" @submit.prevent="save">
<div class="card-title">
<h2>{{ $t('settings.userDefaults') }}</h2>
</div>
<div class="card-content">
<i18n path="settings.commandRunnerHelp" tag="p" class="small">
<code>FILE</code>
<code>SCOPE</code>
<a class="link" target="_blank" href="https://filebrowser.org/configuration/command-runner">{{ $t('settings.documentation') }}</a>
</i18n>
<div class="card-content">
<p class="small">{{ $t('settings.defaultUserDescription') }}</p>
<div v-for="command in settings.commands" :key="command.name" class="collapsible">
<input :id="command.name" type="checkbox">
<label :for="command.name">
<p>{{ capitalize(command.name) }}</p>
<i class="material-icons">arrow_drop_down</i>
</label>
<div class="collapse">
<textarea class="input input--block input--textarea" v-model.trim="command.value"></textarea>
<user-form :isNew="false" :isDefault="true" :user.sync="settings.defaults" />
</div>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
</div>
<div class="column">
<form v-if="isExecEnabled" class="card" @submit.prevent="save">
<div class="card-title">
<h2>{{ $t('settings.commandRunner') }}</h2>
</div>
<div class="card-content">
<i18n path="settings.commandRunnerHelp" tag="p" class="small">
<code>FILE</code>
<code>SCOPE</code>
<a class="link" target="_blank" href="https://filebrowser.org/configuration/command-runner">{{ $t('settings.documentation') }}</a>
</i18n>
<div v-for="command in settings.commands" :key="command.name" class="collapsible">
<input :id="command.name" type="checkbox">
<label :for="command.name">
<p>{{ capitalize(command.name) }}</p>
<i class="material-icons">arrow_drop_down</i>
</label>
<div class="collapse">
<textarea class="input input--block input--textarea" v-model.trim="command.value"></textarea>
</div>
</div>
</div>
</div>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
</div>
</div>
</template>

View File

@@ -1,36 +1,40 @@
<template>
<div class="dashboard">
<form class="card" @submit="updateSettings">
<div class="card-title">
<h2>{{ $t('settings.profileSettings') }}</h2>
</div>
<div class="row">
<div class="column">
<form class="card" @submit="updateSettings">
<div class="card-title">
<h2>{{ $t('settings.profileSettings') }}</h2>
</div>
<div class="card-content">
<p><input type="checkbox" v-model="hideDotfiles"> {{ $t('settings.hideDotfiles') }}</p>
<p><input type="checkbox" v-model="singleClick"> {{ $t('settings.singleClick') }}</p>
<h3>{{ $t('settings.language') }}</h3>
<languages class="input input--block" :locale.sync="locale"></languages>
</div>
<div class="card-content">
<p><input type="checkbox" v-model="hideDotfiles"> {{ $t('settings.hideDotfiles') }}</p>
<p><input type="checkbox" v-model="singleClick"> {{ $t('settings.singleClick') }}</p>
<h3>{{ $t('settings.language') }}</h3>
<languages class="input input--block" :locale.sync="locale"></languages>
</div>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
</div>
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
<div class="card-title">
<h2>{{ $t('settings.changePassword') }}</h2>
</div>
<div class="column">
<form class="card" v-if="!user.lockPassword" @submit="updatePassword">
<div class="card-title">
<h2>{{ $t('settings.changePassword') }}</h2>
</div>
<div class="card-content">
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password">
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password">
</div>
<div class="card-content">
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPassword')" v-model="password" name="password">
<input :class="passwordClass" type="password" :placeholder="$t('settings.newPasswordConfirm')" v-model="passwordConf" name="password">
</div>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
<div class="card-action">
<input class="button button--flat" type="submit" :value="$t('buttons.update')">
</div>
</form>
</div>
</div>
</template>

View File

@@ -1,40 +1,44 @@
<template>
<div class="card">
<div class="card-title">
<h2>{{ $t('settings.shareManagement') }}</h2>
</div>
<div class="row">
<div class="column">
<div class="card">
<div class="card-title">
<h2>{{ $t('settings.shareManagement') }}</h2>
</div>
<div class="card-content full">
<table>
<tr>
<th>{{ $t('settings.path') }}</th>
<th>{{ $t('settings.shareDuration') }}</th>
<th v-if="user.perm.admin">{{ $t('settings.username') }}</th>
<th></th>
<th></th>
</tr>
<div class="card-content full">
<table>
<tr>
<th>{{ $t('settings.path') }}</th>
<th>{{ $t('settings.shareDuration') }}</th>
<th v-if="user.perm.admin">{{ $t('settings.username') }}</th>
<th></th>
<th></th>
</tr>
<tr v-for="link in links" :key="link.hash">
<td><a :href="buildLink(link.hash)" target="_blank">{{ link.path }}</a></td>
<td>
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
<template v-else>{{ $t('permanent') }}</template>
</td>
<td v-if="user.perm.admin">{{ link.username }}</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>
<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>
</tr>
</table>
<tr v-for="link in links" :key="link.hash">
<td><a :href="buildLink(link.hash)" target="_blank">{{ link.path }}</a></td>
<td>
<template v-if="link.expire !== 0">{{ humanTime(link.expire) }}</template>
<template v-else>{{ $t('permanent') }}</template>
</td>
<td v-if="user.perm.admin">{{ link.username }}</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>
<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>
</tr>
</table>
</div>
</div>
</div>
</div>
</template>
@@ -73,23 +77,23 @@ export default {
this.clip.on('success', () => {
this.$showSuccess(this.$t('success.linkCopied'))
})
this.$root.$on('share-deleted', this.deleted)
},
beforeDestroy () {
this.clip.destroy()
this.$root.$off('share-deleted', this.deleted)
},
methods: {
deleteLink: async function (event, link) {
event.preventDefault()
this.$store.commit('setHash', {
hash: link.hash,
path: link.path
this.$store.commit('showHover', {
prompt: 'share-delete',
confirm: () => {
this.$store.commit('closeHovers')
api.remove(link.hash)
this.links = this.links.filter(item => item.hash !== link.hash)
}
})
this.$store.commit('showHover', 'share-delete')
},
deleted (hash) {
this.links = this.links.filter(item => item.hash !== hash)
},
humanTime (time) {
return moment(time * 1000).fromNow()

View File

@@ -1,29 +1,31 @@
<template>
<div>
<form v-if="loaded" @submit="save" class="card">
<div class="card-title">
<h2 v-if="user.id === 0">{{ $t('settings.newUser') }}</h2>
<h2 v-else>{{ $t('settings.user') }} {{ user.username }}</h2>
</div>
<div class="row">
<div class="column">
<form v-if="loaded" @submit="save" class="card">
<div class="card-title">
<h2 v-if="user.id === 0">{{ $t('settings.newUser') }}</h2>
<h2 v-else>{{ $t('settings.user') }} {{ user.username }}</h2>
</div>
<div class="card-content">
<user-form :user.sync="user" :isDefault="false" :isNew="isNew" />
</div>
<div class="card-content">
<user-form :user.sync="user" :isDefault="false" :isNew="isNew" />
</div>
<div class="card-action">
<button
v-if="!isNew"
@click.prevent="deletePrompt"
type="button"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
<input
class="button button--flat"
type="submit"
:value="$t('buttons.save')">
</div>
</form>
<div class="card-action">
<button
v-if="!isNew"
@click.prevent="deletePrompt"
type="button"
class="button button--flat button--red"
:aria-label="$t('buttons.delete')"
:title="$t('buttons.delete')">{{ $t('buttons.delete') }}</button>
<input
class="button button--flat"
type="submit"
:value="$t('buttons.save')">
</div>
</form>
</div>
<div v-if="$store.state.show === 'deleteUser'" class="card floating">
<div class="card-content">

View File

@@ -1,28 +1,32 @@
<template>
<div class="card">
<div class="card-title">
<h2>{{ $t('settings.users') }}</h2>
<router-link to="/settings/users/new"><button class="button">{{ $t('buttons.new') }}</button></router-link>
</div>
<div class="row">
<div class="column">
<div class="card">
<div class="card-title">
<h2>{{ $t('settings.users') }}</h2>
<router-link to="/settings/users/new"><button class="button">{{ $t('buttons.new') }}</button></router-link>
</div>
<div class="card-content full">
<table>
<tr>
<th>{{ $t('settings.username') }}</th>
<th>{{ $t('settings.admin') }}</th>
<th>{{ $t('settings.scope') }}</th>
<th></th>
</tr>
<div class="card-content full">
<table>
<tr>
<th>{{ $t('settings.username') }}</th>
<th>{{ $t('settings.admin') }}</th>
<th>{{ $t('settings.scope') }}</th>
<th></th>
</tr>
<tr v-for="user in users" :key="user.id">
<td>{{ user.username }}</td>
<td><i v-if="user.perm.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
<td>{{ user.scope }}</td>
<td class="small">
<router-link :to="'/settings/users/' + user.id"><i class="material-icons">mode_edit</i></router-link>
</td>
</tr>
</table>
<tr v-for="user in users" :key="user.id">
<td>{{ user.username }}</td>
<td><i v-if="user.perm.admin" class="material-icons">done</i><i v-else class="material-icons">close</i></td>
<td>{{ user.scope }}</td>
<td class="small">
<router-link :to="'/settings/users/' + user.id"><i class="material-icons">mode_edit</i></router-link>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</template>

6
go.mod
View File

@@ -1,12 +1,12 @@
module github.com/filebrowser/filebrowser/v2
go 1.16
require (
github.com/DataDog/zstd v1.4.0 // indirect
github.com/GeertJohan/go.rice v1.0.0
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56 // indirect
github.com/asdine/storm v2.1.2+incompatible
github.com/caddyserver/caddy v1.0.3
github.com/daaku/go.zipexe v1.0.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.2
github.com/dsnet/compress v0.0.1 // indirect
@@ -39,5 +39,3 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.2.7
)
go 1.14

108
go.sum
View File

@@ -1,107 +1,68 @@
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.4.0 h1:vhoV+DUHnRZdKW1i5UMjAk2G4JY8wN4ayRfYDNdEhwo=
github.com/DataDog/zstd v1.4.0/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56 h1:3trCIB5GsAOIY8NxlfMztCYIhVsW9V5sZ+brsecjaiI=
github.com/Sereal/Sereal v0.0.0-20190430203904-6faf9605eb56/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115 h1:fUjoj2bT6dG8LoEe+uNsKk8J+sLkDbQkJnB6Z1F02Bc=
github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU=
github.com/caddyserver/caddy v1.0.3 h1:i9gRhBgvc5ifchwWtSe7pDpsdS9+Q0Rw9oYQmYUTw1w=
github.com/caddyserver/caddy v1.0.3/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E=
github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 h1:a1zrFsLFac2xoM6zG1u72DWJwZG3ayttYLfmLbxVETk=
github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M=
github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954 h1:RMLoZVzv4GliuWafOuPuQDKSm1SJph7uCRnnS61JAn4=
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/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780 h1:tFh1tRc4CA31yP6qDcu+Trax5wW5GuMxvkIba07qVLY=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-acme/lego v2.5.0+incompatible h1:5fNN9yRQfv8ymH3DSsxla+4aYeQt2IgfZqHKVnK8f0s=
github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -112,70 +73,46 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 h1:UnszMmmmm5vLwWzDjTFVIkfhvWF1NdrmChl8L2NUDCw=
github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a h1:BcF8coBl0QFVhe8vAMMlD+CV8EISiu9MGKLoj6ZEyJA=
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0 h1:ZqfnKyx9KGpRcW04j5nnPDgRgoXUeLh2YFBeFzphcA0=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1 h1:8VMb5+0wMgdBykOV96DwNwKFQ+WTI4pzYURP99CcB9E=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f h1:sSeNEkJrs+0F9TUau0CgWTTNEwF23HST3Eq0A+QIx+A=
github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04=
github.com/lucas-clemente/quic-clients v0.1.0 h1:/P9n0nICT/GnQJkZovtBqridjxU0ao34m7DpMts79qY=
github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk=
github.com/lucas-clemente/quic-go v0.10.2 h1:iQtTSZVbd44k94Lu0U16lLBIG3lrnjDvQongjPd4B/s=
github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao=
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced h1:zqEC1GJZFbGZA0tRyNZqRjep92K5fujFtFsu5ZW7Aug=
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1 h1:PEhRT94KBTY4E0KdCYmhvDGWjSFBxc68j2M6PMRix8U=
github.com/maruel/natural v0.0.0-20180416170133-dbcb3e2e8cf1/go.mod h1:wI697HNhDFM/vBruYM3ckbszQ2+DOIeH9qdBKMdf288=
github.com/marusama/semaphore/v2 v2.4.1 h1:Y29DhhFMvreVgoqF9EtaSJAF9t2E7Sk7i5VW81sqB8I=
github.com/marusama/semaphore/v2 v2.4.1/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
@@ -187,62 +124,42 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8=
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1 h1:0utzB5Mn6QyMzIeOn+oD7pjKQLjJwfM9bz6TkPPdxcw=
github.com/pierrec/lz4 v0.0.0-20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
@@ -261,7 +178,6 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -269,36 +185,24 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -310,7 +214,6 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
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-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -324,7 +227,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b h1:IYiJPiJfzktmDAO1HQiwjMjwjlYKHAL7KzeD544RJPs=
golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -346,40 +248,31 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mcuadros/go-syslog.v2 v2.2.1 h1:60g8zx1BijSVSgLTzLCW9UC4/+i1Ih9jJ1DR5Tgp9vE=
gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -389,5 +282,4 @@ gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099 h1:XJP7lxbSxWLOMNdBE4B/STaqVy6L73o0knwj2vIlxnw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -1,6 +1,7 @@
package http
import (
"io/fs"
"net/http"
"github.com/gorilla/mux"
@@ -14,11 +15,17 @@ type modifyRequest struct {
Which []string `json:"which"` // Answer to: which fields?
}
func NewHandler(imgSvc ImgService, fileCache FileCache, store *storage.Storage, server *settings.Server) (http.Handler, error) {
func NewHandler(
imgSvc ImgService,
fileCache FileCache,
store *storage.Storage,
server *settings.Server,
assetsFs fs.FS,
) (http.Handler, error) {
server.Clean()
r := mux.NewRouter()
index, static := getStaticHandlers(store, server)
index, static := getStaticHandlers(store, server, assetsFs)
// NOTE: This fixes the issue where it would redirect if people did not put a
// trailing slash in the end. I hate this decision since this allows some awful
@@ -47,8 +54,8 @@ func NewHandler(imgSvc ImgService, fileCache FileCache, store *storage.Storage,
api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET")
api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler(fileCache), "/api/resources")).Methods("DELETE")
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("POST")
api.PathPrefix("/resources").Handler(monkey(resourcePostPutHandler, "/api/resources")).Methods("PUT")
api.PathPrefix("/resources").Handler(monkey(resourcePostHandler(fileCache), "/api/resources")).Methods("POST")
api.PathPrefix("/resources").Handler(monkey(resourcePutHandler, "/api/resources")).Methods("PUT")
api.PathPrefix("/resources").Handler(monkey(resourcePatchHandler, "/api/resources")).Methods("PATCH")
api.Path("/shares").Handler(monkey(shareListHandler, "/api/shares")).Methods("GET")

View File

@@ -79,7 +79,7 @@ func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgServic
return errToStatus(err), err
}
cacheKey := previewCacheKey(file.Path, previewSize)
cacheKey := previewCacheKey(file.Path, file.ModTime.Unix(), previewSize)
cachedFile, ok, err := fileCache.Load(r.Context(), cacheKey)
if err != nil {
return errToStatus(err), err
@@ -133,6 +133,6 @@ func handleImagePreview(w http.ResponseWriter, r *http.Request, imgSvc ImgServic
return 0, nil
}
func previewCacheKey(fPath string, previewSize PreviewSize) string {
return fPath + previewSize.String()
func previewCacheKey(fPath string, fTime int64, previewSize PreviewSize) string {
return fmt.Sprintf("%x%x%x", fPath, fTime, previewSize)
}

View File

@@ -16,7 +16,7 @@ import (
var withHashFile = func(fn handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
id, path := ifPathWithName(r)
id, ifPath := ifPathWithName(r)
link, err := d.store.Share.GetByHash(id)
if err != nil {
return errToStatus(err), err
@@ -47,21 +47,30 @@ var withHashFile = func(fn handleFunc) handleFunc {
return errToStatus(err), err
}
if file.IsDir {
// set fs root to the shared folder
d.user.Fs = afero.NewBasePathFs(d.user.Fs, filepath.Dir(link.Path))
// share base path
basePath := link.Path
file, err = files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: path,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
Token: link.Token,
})
if err != nil {
return errToStatus(err), err
}
// file relative path
filePath := ""
if file.IsDir {
basePath = filepath.Dir(basePath)
filePath = ifPath
}
// set fs root to the shared file/folder
d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath)
file, err = files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: filePath,
Modify: d.user.Perm.Modify,
Expand: true,
Checker: d,
Token: link.Token,
})
if err != nil {
return errToStatus(err), err
}
d.raw = file

View File

@@ -108,8 +108,6 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
})
func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
// Checks are always done with paths with "/" as path separator.
path = strings.Replace(path, "\\", "/", -1)
if !d.Check(path) {
return nil
}
@@ -134,7 +132,7 @@ func addFile(ar archiver.Writer, d *data, path, commonPath string) error {
if path != commonPath {
filename := strings.TrimPrefix(path, commonPath)
filename = strings.TrimPrefix(filename, "/")
filename = strings.TrimPrefix(filename, string(filepath.Separator))
err = ar.Write(archiver.File{
FileInfo: archiver.FileInfo{
FileInfo: info,
@@ -175,20 +173,25 @@ func rawDirHandler(w http.ResponseWriter, r *http.Request, d *data, file *files.
return http.StatusInternalServerError, err
}
name := file.Name
if name == "." || name == "" {
name = "archive"
}
name += extension
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
err = ar.Create(w)
if err != nil {
return http.StatusInternalServerError, err
}
defer ar.Close()
commonDir := fileutils.CommonPrefix('/', filenames...)
commonDir := fileutils.CommonPrefix(filepath.Separator, filenames...)
var name string
if len(filenames) > 1 {
name = "_" + filepath.Base(commonDir)
} else {
name = file.Name
}
if name == "." || name == "" {
name = "archive"
}
name += extension
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(name))
for _, fname := range filenames {
err = addFile(ar, d, fname, commonDir)

View File

@@ -1,6 +1,7 @@
package http
import (
"context"
"fmt"
"io"
"io/ioutil"
@@ -71,11 +72,9 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
}
// delete thumbnails
for _, previewSizeName := range PreviewSizeNames() {
size, _ := ParsePreviewSize(previewSizeName)
if err := fileCache.Delete(r.Context(), previewCacheKey(file.Path, size)); err != nil { //nolint:govet
return errToStatus(err), err
}
err = delThumbs(r.Context(), fileCache, file)
if err != nil {
return errToStatus(err), err
}
err = d.RunHook(func() error {
@@ -90,12 +89,59 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
})
}
var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Create && r.Method == http.MethodPost {
return http.StatusForbidden, nil
}
func resourcePostHandler(fileCache FileCache) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
if !d.user.Perm.Modify && r.Method == http.MethodPut {
defer func() {
_, _ = io.Copy(ioutil.Discard, r.Body)
}()
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err := d.user.Fs.MkdirAll(r.URL.Path, 0775)
return errToStatus(err), err
}
file, err := files.NewFileInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err == nil {
if r.URL.Query().Get("override") != "true" {
return http.StatusConflict, nil
}
err = delThumbs(r.Context(), fileCache, file)
if err != nil {
return errToStatus(err), err
}
}
err = d.RunHook(func() error {
info, _ := writeFile(d.user.Fs, r.URL.Path, r.Body)
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, "upload", r.URL.Path, "", d.user)
if err != nil {
_ = d.user.Fs.RemoveAll(r.URL.Path)
}
return errToStatus(err), err
})
}
var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Modify || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
@@ -103,55 +149,18 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
_, _ = io.Copy(ioutil.Discard, r.Body)
}()
// For directories, only allow POST for creation.
// Only allow PUT for files.
if strings.HasSuffix(r.URL.Path, "/") {
if r.Method == http.MethodPut {
return http.StatusMethodNotAllowed, nil
}
err := d.user.Fs.MkdirAll(r.URL.Path, 0775)
return errToStatus(err), err
}
if r.Method == http.MethodPost && r.URL.Query().Get("override") != "true" {
if _, err := d.user.Fs.Stat(r.URL.Path); err == nil {
return http.StatusConflict, nil
}
}
action := "upload"
if r.Method == http.MethodPut {
action = "save"
return http.StatusMethodNotAllowed, nil
}
err := d.RunHook(func() error {
dir, _ := path.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)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, r.Body)
if err != nil {
return err
}
// Gets the info about the file.
info, err := file.Stat()
if err != nil {
return err
}
info, _ := writeFile(d.user.Fs, r.URL.Path, r.Body)
etag := fmt.Sprintf(`"%x%x"`, info.ModTime().UnixNano(), info.Size())
w.Header().Set("ETag", etag)
return nil
}, action, r.URL.Path, "", d.user)
}, "save", r.URL.Path, "", d.user)
if err != nil {
_ = d.user.Fs.RemoveAll(r.URL.Path)
@@ -165,6 +174,9 @@ var resourcePatchHandler = withUser(func(w http.ResponseWriter, r *http.Request,
dst := r.URL.Query().Get("destination")
action := r.URL.Query().Get("action")
dst, err := url.QueryUnescape(dst)
if !d.Check(src) || !d.Check(dst) {
return http.StatusForbidden, nil
}
if err != nil {
return errToStatus(err), err
}
@@ -242,3 +254,41 @@ func addVersionSuffix(source string, fs afero.Fs) string {
return source
}
func writeFile(fs afero.Fs, dst string, in io.Reader) (os.FileInfo, error) {
dir, _ := path.Split(dst)
err := fs.MkdirAll(dir, 0775)
if err != nil {
return nil, err
}
file, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
if err != nil {
return nil, err
}
defer file.Close()
_, err = io.Copy(file, in)
if err != nil {
return nil, err
}
// Gets the info about the file.
info, err := file.Stat()
if err != nil {
return nil, err
}
return info, nil
}
func delThumbs(ctx context.Context, fileCache FileCache, file *files.FileInfo) error {
for _, previewSizeName := range PreviewSizeNames() {
size, _ := ParsePreviewSize(previewSizeName)
if err := fileCache.Delete(ctx, previewCacheKey(file.Path, file.ModTime.Unix(), size)); err != nil {
return err
}
}
return nil
}

View File

@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"path"
"sort"
"strconv"
"strings"
@@ -91,17 +90,6 @@ var sharePostHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
defer r.Body.Close()
}
if body.Expires == "" {
var err error
s, err = d.store.Share.GetPermanent(r.URL.Path, d.user.ID)
if err == nil {
if _, err := w.Write([]byte(path.Join(d.server.BaseURL, "/share/", s.Hash))); err != nil {
return http.StatusInternalServerError, err
}
return 0, nil
}
}
bytes := make([]byte, 6)
_, err := rand.Read(bytes)
if err != nil {

View File

@@ -2,6 +2,7 @@ package http
import (
"encoding/json"
"io/fs"
"log"
"net/http"
"os"
@@ -10,15 +11,13 @@ import (
"strings"
"text/template"
rice "github.com/GeertJohan/go.rice"
"github.com/filebrowser/filebrowser/v2/auth"
"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/storage"
"github.com/filebrowser/filebrowser/v2/version"
)
func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *rice.Box, file, contentType string) (int, error) {
func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, fSys fs.FS, file, contentType string) (int, error) {
w.Header().Set("Content-Type", contentType)
auther, err := d.store.Auth.Get(d.settings.AuthMethod)
@@ -79,14 +78,14 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *
data["Json"] = string(b)
fileContents, err := box.String(file)
fileContents, err := fs.ReadFile(fSys, file)
if err != nil {
if err == os.ErrNotExist {
return http.StatusNotFound, err
}
return http.StatusInternalServerError, err
}
index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(fileContents))
index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(string(fileContents)))
err = index.Execute(w, data)
if err != nil {
return http.StatusInternalServerError, err
@@ -95,17 +94,14 @@ func handleWithStaticData(w http.ResponseWriter, _ *http.Request, d *data, box *
return 0, nil
}
func getStaticHandlers(store *storage.Storage, server *settings.Server) (index, static http.Handler) {
box := rice.MustFindBox("../frontend/dist")
handler := http.FileServer(box.HTTPBox())
func getStaticHandlers(store *storage.Storage, server *settings.Server, assetsFs fs.FS) (index, static http.Handler) {
index = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if r.Method != http.MethodGet {
return http.StatusNotFound, nil
}
w.Header().Set("x-xss-protection", "1; mode=block")
return handleWithStaticData(w, r, d, box, "index.html", "text/html; charset=utf-8")
return handleWithStaticData(w, r, d, assetsFs, "index.html", "text/html; charset=utf-8")
}, "", store, server)
static = handle(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
@@ -127,11 +123,11 @@ func getStaticHandlers(store *storage.Storage, server *settings.Server) (index,
}
if !strings.HasSuffix(r.URL.Path, ".js") {
handler.ServeHTTP(w, r)
http.FileServer(http.FS(assetsFs)).ServeHTTP(w, r)
return 0, nil
}
return handleWithStaticData(w, r, d, box, r.URL.Path, "application/javascript; charset=utf-8")
return handleWithStaticData(w, r, d, assetsFs, r.URL.Path, "application/javascript; charset=utf-8")
}, "/static/", store, server)
return index, static

View File

@@ -102,6 +102,14 @@ func (r *Runner) exec(raw, evt, path, dst string, user *users.User) error {
if !blocking {
log.Printf("[INFO] Nonblocking Command: \"%s\"", strings.Join(command, " "))
defer func() {
go func() {
err := cmd.Wait()
if err != nil {
log.Printf("[INFO] Nonblocking Command \"%s\" failed: %s", strings.Join(command, " "), err)
}
}()
}()
return cmd.Start()
}