feat: download shared subdirectory (#1184)
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>pull/1241/head
parent
677bce376b
commit
fb5b28d9cb
|
@ -58,7 +58,7 @@ export async function put (url, content = '') {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function download (format, ...files) {
|
export function download (format, ...files) {
|
||||||
let url = `${baseURL}/api/raw`
|
let url = store.getters['isSharing'] ? `${baseURL}/api/public/dl/${store.state.hash}` : `${baseURL}/api/raw`
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
url += removePrefix(files[0]) + '?'
|
url += removePrefix(files[0]) + '?'
|
||||||
|
|
|
@ -36,6 +36,8 @@ export async function fetchJSON (url, opts) {
|
||||||
export function removePrefix (url) {
|
export function removePrefix (url) {
|
||||||
if (url.startsWith('/files')) {
|
if (url.startsWith('/files')) {
|
||||||
url = url.slice(6)
|
url = url.slice(6)
|
||||||
|
} else if (store.getters['isSharing']) {
|
||||||
|
url = url.slice(7 + store.state.hash.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url === '') url = '/'
|
if (url === '') url = '/'
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
<search v-if="isLogged"></search>
|
<search v-if="isLogged"></search>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<template v-if="isLogged">
|
<template v-if="isLogged || isSharing">
|
||||||
<button @click="openSearch" :aria-label="$t('buttons.search')" :title="$t('buttons.search')" class="search-button action">
|
<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>
|
<i class="material-icons">search</i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
<!-- Menu that shows on listing AND mobile when there are files selected -->
|
||||||
<div id="file-selection" v-if="isMobile && isListing">
|
<div id="file-selection" v-if="isMobile && isListing && !isSharing">
|
||||||
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
<span v-if="selectedCount > 0">{{ selectedCount }} selected</span>
|
||||||
<share-button v-show="showShareButton"></share-button>
|
<share-button v-show="showShareButton"></share-button>
|
||||||
<rename-button v-show="showRenameButton"></rename-button>
|
<rename-button v-show="showRenameButton"></rename-button>
|
||||||
|
@ -37,13 +37,13 @@
|
||||||
<delete-button v-show="showDeleteButton"></delete-button>
|
<delete-button v-show="showDeleteButton"></delete-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<shell-button v-if="isExecEnabled && user.perm.execute" />
|
<shell-button v-if="isExecEnabled && !isSharing && user.perm.execute" />
|
||||||
<switch-button v-show="isListing"></switch-button>
|
<switch-button v-show="isListing"></switch-button>
|
||||||
<download-button v-show="showDownloadButton"></download-button>
|
<download-button v-show="showDownloadButton"></download-button>
|
||||||
<upload-button v-show="showUpload"></upload-button>
|
<upload-button v-show="showUpload"></upload-button>
|
||||||
<info-button v-show="isFiles"></info-button>
|
<info-button v-show="isFiles"></info-button>
|
||||||
|
|
||||||
<button v-show="isListing" @click="toggleMultipleSelection" :aria-label="$t('buttons.selectMultiple')" :title="$t('buttons.selectMultiple')" class="action" >
|
<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>
|
<i class="material-icons">check_circle</i>
|
||||||
<span>{{ $t('buttons.select') }}</span>
|
<span>{{ $t('buttons.select') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -110,7 +110,8 @@ export default {
|
||||||
'isEditor',
|
'isEditor',
|
||||||
'isPreview',
|
'isPreview',
|
||||||
'isListing',
|
'isListing',
|
||||||
'isLogged'
|
'isLogged',
|
||||||
|
'isSharing'
|
||||||
]),
|
]),
|
||||||
...mapState([
|
...mapState([
|
||||||
'req',
|
'req',
|
||||||
|
@ -128,7 +129,7 @@ export default {
|
||||||
return this.isListing && this.user.perm.create
|
return this.isListing && this.user.perm.create
|
||||||
},
|
},
|
||||||
showDownloadButton () {
|
showDownloadButton () {
|
||||||
return this.isFiles && this.user.perm.download
|
return (this.isFiles && this.user.perm.download) || (this.isSharing && this.selectedCount > 0)
|
||||||
},
|
},
|
||||||
showDeleteButton () {
|
showDeleteButton () {
|
||||||
return this.isFiles && (this.isListing
|
return this.isFiles && (this.isListing
|
||||||
|
@ -156,7 +157,7 @@ export default {
|
||||||
: this.user.perm.create)
|
: this.user.perm.create)
|
||||||
},
|
},
|
||||||
showMore () {
|
showMore () {
|
||||||
return this.isFiles && this.$store.state.show === 'more'
|
return (this.isFiles || this.isSharing) && this.$store.state.show === 'more'
|
||||||
},
|
},
|
||||||
showOverlay () {
|
showOverlay () {
|
||||||
return this.showMore
|
return this.showMore
|
||||||
|
|
|
@ -14,11 +14,11 @@ export default {
|
||||||
name: 'download-button',
|
name: 'download-button',
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['req', 'selected']),
|
...mapState(['req', 'selected']),
|
||||||
...mapGetters(['isListing', 'selectedCount'])
|
...mapGetters(['isListing', 'selectedCount', 'isSharing'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
download: function () {
|
download: function () {
|
||||||
if (!this.isListing) {
|
if (!this.isListing && !this.isSharing) {
|
||||||
api.download(null, this.$route.path)
|
api.download(null, this.$route.path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
:aria-label="name"
|
:aria-label="name"
|
||||||
:aria-selected="isSelected">
|
:aria-selected="isSelected">
|
||||||
<div>
|
<div>
|
||||||
<img v-if="type==='image' && isThumbsEnabled" v-lazy="thumbnailUrl">
|
<img v-if="type==='image' && isThumbsEnabled && !isSharing" v-lazy="thumbnailUrl">
|
||||||
<i v-else class="material-icons">{{ icon }}</i>
|
<i v-else class="material-icons">{{ icon }}</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -47,8 +47,12 @@ export default {
|
||||||
},
|
},
|
||||||
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
props: ['name', 'isDir', 'url', 'type', 'size', 'modified', 'index'],
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['user', 'selected', 'req', 'user', 'jwt']),
|
...mapState(['user', 'selected', 'req', 'jwt']),
|
||||||
...mapGetters(['selectedCount']),
|
...mapGetters(['selectedCount', 'isSharing']),
|
||||||
|
singleClick () {
|
||||||
|
if (this.isSharing) return false
|
||||||
|
return this.user.singleClick
|
||||||
|
},
|
||||||
isSelected () {
|
isSelected () {
|
||||||
return (this.selected.indexOf(this.index) !== -1)
|
return (this.selected.indexOf(this.index) !== -1)
|
||||||
},
|
},
|
||||||
|
@ -60,10 +64,10 @@ export default {
|
||||||
return 'insert_drive_file'
|
return 'insert_drive_file'
|
||||||
},
|
},
|
||||||
isDraggable () {
|
isDraggable () {
|
||||||
return this.user.perm.rename
|
return !this.isSharing && this.user.perm.rename
|
||||||
},
|
},
|
||||||
canDrop () {
|
canDrop () {
|
||||||
if (!this.isDir) return false
|
if (!this.isDir || this.isSharing) return false
|
||||||
|
|
||||||
for (let i of this.selected) {
|
for (let i of this.selected) {
|
||||||
if (this.req.items[i].url === this.url) {
|
if (this.req.items[i].url === this.url) {
|
||||||
|
@ -171,11 +175,11 @@ export default {
|
||||||
action(overwrite, rename)
|
action(overwrite, rename)
|
||||||
},
|
},
|
||||||
itemClick: function(event) {
|
itemClick: function(event) {
|
||||||
if (this.user.singleClick && !this.$store.state.multiple) this.open()
|
if (this.singleClick && !this.$store.state.multiple) this.open()
|
||||||
else this.click(event)
|
else this.click(event)
|
||||||
},
|
},
|
||||||
click: function (event) {
|
click: function (event) {
|
||||||
if (!this.user.singleClick && this.selectedCount !== 0) event.preventDefault()
|
if (!this.singleClick && this.selectedCount !== 0) event.preventDefault()
|
||||||
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
if (this.$store.state.selected.indexOf(this.index) !== -1) {
|
||||||
this.removeSelected(this.index)
|
this.removeSelected(this.index)
|
||||||
return
|
return
|
||||||
|
@ -202,11 +206,11 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.user.singleClick && !event.ctrlKey && !event.metaKey && !this.$store.state.multiple) this.resetSelected()
|
if (!this.singleClick && !event.ctrlKey && !event.metaKey && !this.$store.state.multiple) this.resetSelected()
|
||||||
this.addSelected(this.index)
|
this.addSelected(this.index)
|
||||||
},
|
},
|
||||||
dblclick: function () {
|
dblclick: function () {
|
||||||
if (!this.user.singleClick) this.open()
|
if (!this.singleClick) this.open()
|
||||||
},
|
},
|
||||||
touchstart () {
|
touchstart () {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.share__box__items #listing.list .item {
|
.share__box__items #listing.list .item {
|
||||||
cursor: auto;
|
cursor: pointer;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
@ -57,5 +57,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.share__box__items #listing.list .item .name {
|
.share__box__items #listing.list .item .name {
|
||||||
width: auto;
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__box__items #listing.list .item .modified {
|
||||||
|
width: 25%;
|
||||||
}
|
}
|
|
@ -248,6 +248,7 @@
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Download File",
|
"downloadFile": "Download File",
|
||||||
"downloadFolder": "Download Folder"
|
"downloadFolder": "Download Folder",
|
||||||
|
"downloadSelected": "Download Selected"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,6 +245,7 @@
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "下载文件",
|
"downloadFile": "下载文件",
|
||||||
"downloadFolder": "下载文件夹"
|
"downloadFolder": "下载文件夹",
|
||||||
|
"downloadSelected": "下载已选"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ const getters = {
|
||||||
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
isListing: (state, getters) => getters.isFiles && state.req.isDir,
|
||||||
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
|
||||||
isPreview: state => state.previewMode,
|
isPreview: state => state.previewMode,
|
||||||
|
isSharing: state => !state.loading && state.route.name === 'Share',
|
||||||
selectedCount: state => state.selected.length,
|
selectedCount: state => state.selected.length,
|
||||||
progress : state => {
|
progress : state => {
|
||||||
if (state.upload.progress.length == 0) {
|
if (state.upload.progress.length == 0) {
|
||||||
|
|
|
@ -24,7 +24,8 @@ const state = {
|
||||||
showShell: false,
|
showShell: false,
|
||||||
showMessage: null,
|
showMessage: null,
|
||||||
showConfirm: null,
|
showConfirm: null,
|
||||||
previewMode: false
|
previewMode: false,
|
||||||
|
hash: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
|
|
|
@ -86,7 +86,8 @@ const mutations = {
|
||||||
},
|
},
|
||||||
setPreviewMode(state, value) {
|
setPreviewMode(state, value) {
|
||||||
state.previewMode = value
|
state.previewMode = value
|
||||||
}
|
},
|
||||||
|
setHash: (state, value) => (state.hash = value),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default mutations
|
export default mutations
|
||||||
|
|
|
@ -1,107 +1,223 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="share" v-if="loaded">
|
<div v-if="!loading">
|
||||||
<div class="share__box share__box__info">
|
<div id="breadcrumbs">
|
||||||
<div class="share__box__header">
|
<router-link :to="'/share/' + hash" :aria-label="$t('files.home')" :title="$t('files.home')">
|
||||||
{{ file.isDir ? $t('download.downloadFolder') : $t('download.downloadFile') }}
|
<i class="material-icons">home</i>
|
||||||
</div>
|
</router-link>
|
||||||
<div class="share__box__element share__box__center share__box__icon">
|
|
||||||
<i class="material-icons">{{ file.isDir ? 'folder' : 'insert_drive_file'}}</i>
|
<span v-for="(link, index) in breadcrumbs" :key="index">
|
||||||
</div>
|
<span class="chevron"><i class="material-icons">keyboard_arrow_right</i></span>
|
||||||
<div class="share__box__element">
|
<router-link :to="link.url">{{ link.name }}</router-link>
|
||||||
<strong>{{ $t('prompts.displayName') }}</strong> {{ file.name }}
|
</span>
|
||||||
</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>
|
||||||
<div v-if="file.isDir" class="share__box share__box__items">
|
<div class="share">
|
||||||
<div class="share__box__header" v-if="file.isDir">
|
<div class="share__box share__box__info">
|
||||||
{{ $t('files.files') }}
|
<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>
|
||||||
<div id="listing" class="list">
|
<div v-if="req.isDir && req.items.length > 0" class="share__box share__box__items">
|
||||||
<div class="item" v-for="(item) in file.items.slice(0, this.showLimit)" :key="base64(item.name)">
|
<div class="share__box__header" v-if="req.isDir">
|
||||||
<div>
|
{{ $t('files.files') }}
|
||||||
<i class="material-icons">{{ item.isDir ? 'folder' : (item.type==='image') ? 'insert_photo' : 'insert_drive_file' }}</i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="name">{{ item.name }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="file.items.length > showLimit" class="item">
|
<div id="listing" class="list">
|
||||||
<div>
|
<item v-for="(item) in req.items.slice(0, this.showLimit)"
|
||||||
<p class="name"> + {{ file.items.length - showLimit }} </p>
|
: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>
|
||||||
|
</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>
|
||||||
</div>
|
</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>
|
||||||
</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>
|
||||||
|
<internal-error v-else></internal-error>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {mapState, mapMutations, mapGetters} from 'vuex';
|
||||||
import { share as api } from '@/api'
|
import { share as api } from '@/api'
|
||||||
import { baseURL } from '@/utils/constants'
|
import { baseURL } from '@/utils/constants'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import QrcodeVue from 'qrcode.vue'
|
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 {
|
export default {
|
||||||
name: 'share',
|
name: 'share',
|
||||||
components: {
|
components: {
|
||||||
|
Item,
|
||||||
|
Forbidden,
|
||||||
|
NotFound,
|
||||||
|
InternalError,
|
||||||
QrcodeVue
|
QrcodeVue
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
loaded: false,
|
error: null,
|
||||||
notFound: false,
|
path: '',
|
||||||
file: null,
|
|
||||||
showLimit: 500
|
showLimit: 500
|
||||||
}),
|
}),
|
||||||
watch: {
|
watch: {
|
||||||
'$route': 'fetchData'
|
'$route': 'fetchData'
|
||||||
},
|
},
|
||||||
created: function () {
|
created: async function () {
|
||||||
this.fetchData()
|
const hash = this.$route.params.pathMatch.split('/')[0]
|
||||||
|
this.setHash(hash)
|
||||||
|
await this.fetchData()
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('keydown', this.keyEvent)
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
window.removeEventListener('keydown', this.keyEvent)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hash: function () {
|
...mapState(['hash', 'req', 'loading', 'multiple']),
|
||||||
return this.$route.params.pathMatch
|
...mapGetters(['selectedCount']),
|
||||||
|
icon: function () {
|
||||||
|
if (this.req.isDir) return 'folder'
|
||||||
|
if (this.req.type === 'image') return 'insert_photo'
|
||||||
|
if (this.req.type === 'audio') return 'volume_up'
|
||||||
|
if (this.req.type === 'video') return 'movie'
|
||||||
|
return 'insert_drive_file'
|
||||||
},
|
},
|
||||||
link: function () {
|
link: function () {
|
||||||
return `${baseURL}/api/public/dl/${this.hash}/${encodeURI(this.file.name)}`
|
return `${baseURL}/api/public/dl/${this.hash}${this.path}`
|
||||||
},
|
},
|
||||||
fullLink: function () {
|
fullLink: function () {
|
||||||
return window.location.origin + this.link
|
return window.location.origin + this.link
|
||||||
},
|
},
|
||||||
humanSize: function () {
|
humanSize: function () {
|
||||||
if (this.file.isDir) {
|
if (this.req.isDir) {
|
||||||
return this.file.items.length
|
return this.req.items.length
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesize(this.file.size)
|
return filesize(this.req.size)
|
||||||
},
|
},
|
||||||
humanTime: function () {
|
humanTime: function () {
|
||||||
return moment(this.file.modified).fromNow()
|
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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapMutations([ 'setHash', 'resetSelected', 'updateRequest', 'setLoading' ]),
|
||||||
base64: function (name) {
|
base64: function (name) {
|
||||||
return window.btoa(unescape(encodeURIComponent(name)))
|
return window.btoa(unescape(encodeURIComponent(name)))
|
||||||
},
|
},
|
||||||
fetchData: async function () {
|
fetchData: async function () {
|
||||||
|
// Reset view information.
|
||||||
|
this.$store.commit('setReload', false)
|
||||||
|
this.$store.commit('resetSelected')
|
||||||
|
this.$store.commit('multiple', false)
|
||||||
|
this.$store.commit('closeHovers')
|
||||||
|
|
||||||
|
// Set loading to true and reset the error.
|
||||||
|
this.setLoading(true)
|
||||||
|
this.error = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.file = await api.getHash(this.hash)
|
let file = await api.getHash(encodeURIComponent(this.$route.params.pathMatch))
|
||||||
this.loaded = true
|
this.path = file.path
|
||||||
|
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) {
|
} catch (e) {
|
||||||
this.notFound = true
|
this.error = e
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
keyEvent (event) {
|
||||||
|
// Esc!
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
// If we're on a listing, unselect all
|
||||||
|
// files and folders.
|
||||||
|
if (this.selectedCount > 0) {
|
||||||
|
this.resetSelected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleMultipleSelection () {
|
||||||
|
this.$store.commit('multiple', !this.multiple)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,21 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
)
|
)
|
||||||
|
|
||||||
var withHashFile = func(fn handleFunc) handleFunc {
|
var withHashFile = func(fn handleFunc) handleFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
link, err := d.store.Share.GetByHash(r.URL.Path)
|
id, path := ifPathWithName(r)
|
||||||
|
link, err := d.store.Share.GetByHash(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
link, err = d.store.Share.GetByHash(ifPathWithName(r))
|
return errToStatus(err), err
|
||||||
if err != nil {
|
|
||||||
return errToStatus(err), err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := d.store.Users.Get(d.server.Root, link.UserID)
|
user, err := d.store.Users.Get(d.server.Root, link.UserID)
|
||||||
|
@ -35,6 +37,22 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||||
return errToStatus(err), err
|
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))
|
||||||
|
|
||||||
|
file, err = files.NewFileInfo(files.FileOptions{
|
||||||
|
Fs: d.user.Fs,
|
||||||
|
Path: path,
|
||||||
|
Modify: d.user.Perm.Modify,
|
||||||
|
Expand: true,
|
||||||
|
Checker: d,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errToStatus(err), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
d.raw = file
|
d.raw = file
|
||||||
return fn(w, r, d)
|
return fn(w, r, d)
|
||||||
}
|
}
|
||||||
|
@ -42,15 +60,17 @@ var withHashFile = func(fn handleFunc) handleFunc {
|
||||||
|
|
||||||
// ref to https://github.com/filebrowser/filebrowser/pull/727
|
// ref to https://github.com/filebrowser/filebrowser/pull/727
|
||||||
// `/api/public/dl/MEEuZK-v/file-name.txt` for old browsers to save file with correct name
|
// `/api/public/dl/MEEuZK-v/file-name.txt` for old browsers to save file with correct name
|
||||||
func ifPathWithName(r *http.Request) string {
|
func ifPathWithName(r *http.Request) (id, filePath string) {
|
||||||
pathElements := strings.Split(r.URL.Path, "/")
|
pathElements := strings.Split(r.URL.Path, "/")
|
||||||
// prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name`
|
// prevent maliciously constructed parameters like `/api/public/dl/XZzCDnK2_not_exists_hash_name`
|
||||||
// len(pathElements) will be 1, and golang will panic `runtime error: index out of range`
|
// len(pathElements) will be 1, and golang will panic `runtime error: index out of range`
|
||||||
if len(pathElements) < 2 { //nolint: mnd
|
|
||||||
return r.URL.Path
|
switch len(pathElements) {
|
||||||
|
case 1:
|
||||||
|
return r.URL.Path, "/"
|
||||||
|
default:
|
||||||
|
return pathElements[0], path.Join("/", path.Join(pathElements[1:]...))
|
||||||
}
|
}
|
||||||
id := pathElements[len(pathElements)-2]
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var publicShareHandler = withHashFile(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
|
|
Loading…
Reference in New Issue