feat: download shared subdirectory (#1184)
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
This commit is contained in:
		
							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