feat: add folder upload (#981)
* feat: folder upload fix filebrowser/filebrowser#741 * fix: apply gofmt formater * feat: upload button prompt * feat: empty folder uploadpull/987/head
							parent
							
								
									6d899a6335
								
							
						
					
					
						commit
						89773447a5
					
				| 
						 | 
				
			
			@ -10,7 +10,11 @@ export default {
 | 
			
		|||
  name: 'upload-button',
 | 
			
		||||
  methods: {
 | 
			
		||||
    upload: function () {
 | 
			
		||||
      document.getElementById('upload-input').click()
 | 
			
		||||
      if (typeof(DataTransferItem.prototype.webkitGetAsEntry) !== 'undefined') {
 | 
			
		||||
        this.$store.commit('showHover', 'upload')
 | 
			
		||||
      } else {
 | 
			
		||||
        document.getElementById('upload-input').click();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
      <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"
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +76,7 @@
 | 
			
		|||
    </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>
 | 
			
		||||
| 
						 | 
				
			
			@ -290,10 +292,9 @@ export default {
 | 
			
		|||
      this.resetOpacity()
 | 
			
		||||
 | 
			
		||||
      let dt = event.dataTransfer
 | 
			
		||||
      let files = dt.files
 | 
			
		||||
      let el = event.target
 | 
			
		||||
 | 
			
		||||
      if (files.length <= 0) return
 | 
			
		||||
      if (dt.files.length <= 0) return
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < 5; i++) {
 | 
			
		||||
        if (el !== null && !el.classList.contains('item')) {
 | 
			
		||||
| 
						 | 
				
			
			@ -306,28 +307,45 @@ export default {
 | 
			
		|||
        base = el.querySelector('.name').innerHTML + '/'
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (base !== '') {
 | 
			
		||||
        api.fetch(this.$route.path + base)
 | 
			
		||||
          .then(req => {
 | 
			
		||||
            this.checkConflict(files, req.items, base)
 | 
			
		||||
          })
 | 
			
		||||
          .catch(this.$showError)
 | 
			
		||||
 | 
			
		||||
        return
 | 
			
		||||
      if (base === '') {
 | 
			
		||||
        this.scanFiles(dt).then((result) => {
 | 
			
		||||
          this.checkConflict(result, this.req.items, base)
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        this.scanFiles(dt).then((result) => {
 | 
			
		||||
          api.fetch(this.$route.path + base)
 | 
			
		||||
            .then(req => {
 | 
			
		||||
                this.checkConflict(result, req.items, base)
 | 
			
		||||
            })
 | 
			
		||||
            .catch(this.$showError)
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.checkConflict(files, this.req.items, base)
 | 
			
		||||
    },
 | 
			
		||||
    checkConflict (files, items, base) {
 | 
			
		||||
      if (typeof items === 'undefined' || items === null) {
 | 
			
		||||
        items = []
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let folder_upload = false
 | 
			
		||||
      if (files[0].fullPath !== undefined) {
 | 
			
		||||
        folder_upload = true
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let conflict = false
 | 
			
		||||
      for (let i = 0; i < files.length; i++) {
 | 
			
		||||
        let file = files[i]
 | 
			
		||||
        let name = file.name
 | 
			
		||||
 | 
			
		||||
        if (folder_upload) {
 | 
			
		||||
          let dirs = file.fullPath.split("/")
 | 
			
		||||
          if (dirs.length > 1) {
 | 
			
		||||
            name = dirs[0]
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let res = items.findIndex(function hasConflict (element) {
 | 
			
		||||
          return (element.name === this)
 | 
			
		||||
        }, files[i].name)
 | 
			
		||||
        }, name)
 | 
			
		||||
 | 
			
		||||
        if (res >= 0) {
 | 
			
		||||
          conflict = true
 | 
			
		||||
| 
						 | 
				
			
			@ -350,7 +368,19 @@ export default {
 | 
			
		|||
      })
 | 
			
		||||
    },
 | 
			
		||||
    uploadInput (event) {
 | 
			
		||||
      this.checkConflict(event.currentTarget.files, this.req.items, '')
 | 
			
		||||
      this.$store.commit('closeHovers')
 | 
			
		||||
 | 
			
		||||
      let files = event.currentTarget.files
 | 
			
		||||
      let folder_upload = files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== ''
 | 
			
		||||
 | 
			
		||||
      if (folder_upload) {
 | 
			
		||||
        for (let i = 0; i < files.length; i++) {
 | 
			
		||||
          let file = files[i]
 | 
			
		||||
          files[i].fullPath = file.webkitRelativePath
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.checkConflict(files, this.req.items, '')
 | 
			
		||||
    },
 | 
			
		||||
    resetOpacity () {
 | 
			
		||||
      let items = document.getElementsByClassName('item')
 | 
			
		||||
| 
						 | 
				
			
			@ -359,6 +389,67 @@ export default {
 | 
			
		|||
        file.style.opacity = 1
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    scanFiles(dt) {
 | 
			
		||||
        return new Promise((resolve) => {
 | 
			
		||||
            let reading = 0
 | 
			
		||||
            const contents = []
 | 
			
		||||
 | 
			
		||||
            if (dt.items !== undefined) {
 | 
			
		||||
              for (let item of dt.items) {
 | 
			
		||||
                if (item.kind === "file" && typeof item.webkitGetAsEntry === "function") {
 | 
			
		||||
                  const entry = item.webkitGetAsEntry()
 | 
			
		||||
                  readEntry(entry)
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              resolve(dt.files)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            function readEntry(entry, directory = "") {
 | 
			
		||||
                if (entry.isFile) {
 | 
			
		||||
                    reading++
 | 
			
		||||
                    entry.file(file => {
 | 
			
		||||
                        reading--
 | 
			
		||||
 | 
			
		||||
                        file.fullPath = `${directory}${file.name}`
 | 
			
		||||
                        contents.push(file)
 | 
			
		||||
 | 
			
		||||
                        if (reading === 0) {
 | 
			
		||||
                            resolve(contents)
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                } else if (entry.isDirectory) {
 | 
			
		||||
                    const dir = {
 | 
			
		||||
                      isDir: true,
 | 
			
		||||
                      path: `${directory}${entry.name}`
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    contents.push(dir)
 | 
			
		||||
 | 
			
		||||
                    readReaderContent(entry.createReader(), `${directory}${entry.name}`)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            function readReaderContent(reader, directory) {
 | 
			
		||||
                reading++
 | 
			
		||||
 | 
			
		||||
                reader.readEntries(function (entries) {
 | 
			
		||||
                    reading--
 | 
			
		||||
                    if (entries.length > 0) {
 | 
			
		||||
                        for (const entry of entries) {
 | 
			
		||||
                            readEntry(entry, `${directory}/`)
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        readReaderContent(reader, `${directory}/`)
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (reading === 0) {
 | 
			
		||||
                        resolve(contents)
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    handleFiles (files, base, overwrite = false) {
 | 
			
		||||
      buttons.loading('upload')
 | 
			
		||||
      let promises = []
 | 
			
		||||
| 
						 | 
				
			
			@ -377,8 +468,23 @@ export default {
 | 
			
		|||
 | 
			
		||||
      for (let i = 0; i < files.length; i++) {
 | 
			
		||||
        let file = files[i]
 | 
			
		||||
        let filenameEncoded = url.encodeRFC5987ValueChars(file.name)
 | 
			
		||||
        promises.push(api.post(this.$route.path + base + filenameEncoded, file, overwrite, onupload(i)))
 | 
			
		||||
 | 
			
		||||
        if (!file.isDir) {
 | 
			
		||||
          let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
 | 
			
		||||
          let filenameEncoded = url.encodeRFC5987ValueChars(filename)
 | 
			
		||||
          promises.push(api.post(this.$route.path + base + filenameEncoded, file, overwrite, onupload(i)))
 | 
			
		||||
        } else {
 | 
			
		||||
          let uri = this.$route.path + base;
 | 
			
		||||
          let folders = file.path.split("/");
 | 
			
		||||
 | 
			
		||||
          for (let i = 0; i < folders.length; i++) {
 | 
			
		||||
            let folder = folders[i];
 | 
			
		||||
            let folderEncoded = encodeURIComponent(folder);
 | 
			
		||||
            uri += folderEncoded + "/"
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          api.post(uri);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let finish = () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ import NewFile from './NewFile'
 | 
			
		|||
import NewDir from './NewDir'
 | 
			
		||||
import Replace from './Replace'
 | 
			
		||||
import Share from './Share'
 | 
			
		||||
import Upload from './Upload'
 | 
			
		||||
import { mapState } from 'vuex'
 | 
			
		||||
import buttons from '@/utils/buttons'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +34,8 @@ export default {
 | 
			
		|||
    NewFile,
 | 
			
		||||
    NewDir,
 | 
			
		||||
    Help,
 | 
			
		||||
    Replace
 | 
			
		||||
    Replace,
 | 
			
		||||
    Upload
 | 
			
		||||
  },
 | 
			
		||||
  data: function () {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			@ -58,7 +60,8 @@ export default {
 | 
			
		|||
        'newDir',
 | 
			
		||||
        'download',
 | 
			
		||||
        'replace',
 | 
			
		||||
        'share'
 | 
			
		||||
        'share',
 | 
			
		||||
        'upload'
 | 
			
		||||
      ].indexOf(this.show) >= 0;
 | 
			
		||||
 | 
			
		||||
      return matched && this.show || null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
<template>
 | 
			
		||||
  <div class="card floating">
 | 
			
		||||
    <div class="card-title">
 | 
			
		||||
      <h2>{{ $t('prompts.upload') }}</h2>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="card-content">
 | 
			
		||||
      <p>{{ $t('prompts.uploadMessage') }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="card-action full">
 | 
			
		||||
      <div @click="uploadFile" class="action">
 | 
			
		||||
        <i class="material-icons">insert_drive_file</i>
 | 
			
		||||
        <div class="title">File</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div @click="uploadFolder" class="action">
 | 
			
		||||
        <i class="material-icons">folder</i>
 | 
			
		||||
        <div class="title">Folder</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'upload',
 | 
			
		||||
  methods: {
 | 
			
		||||
    uploadFile: function () {
 | 
			
		||||
      document.getElementById('upload-input').click()
 | 
			
		||||
    },
 | 
			
		||||
    uploadFolder: function () {
 | 
			
		||||
      document.getElementById('upload-folder-input').click()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +96,7 @@ table tr>*:last-child {
 | 
			
		|||
  background-color: #fff;
 | 
			
		||||
  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);
 | 
			
		||||
  overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card.floating {
 | 
			
		||||
| 
						 | 
				
			
			@ -366,3 +367,33 @@ table tr>*:last-child {
 | 
			
		|||
.card .collapsible .collapse {
 | 
			
		||||
  padding: 0 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card .card-action.full {
 | 
			
		||||
  padding-top: 0;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card .card-action.full .action {
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  padding: 2em;
 | 
			
		||||
  border-radius: 0.2em;
 | 
			
		||||
  border: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card .card-action.full .action {
 | 
			
		||||
  margin: 0 0.25em 0.50em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card .card-action.full .action i {
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin-bottom: 0.25em;
 | 
			
		||||
  font-size: 4em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.card .card-action.full .action .title {
 | 
			
		||||
  font-size: 1.5em;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -116,7 +116,9 @@
 | 
			
		|||
    "size": "Size",
 | 
			
		||||
    "schedule": "Schedule",
 | 
			
		||||
    "scheduleMessage": "Pick a date and time to schedule the publication of this post.",
 | 
			
		||||
    "newArchetype": "Create a new post based on an archetype. Your file will be created on content folder."
 | 
			
		||||
    "newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
 | 
			
		||||
    "upload": "Upload",
 | 
			
		||||
    "uploadMessage": "Select an option to upload."
 | 
			
		||||
  },
 | 
			
		||||
  "settings": {
 | 
			
		||||
    "themes": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import (
 | 
			
		|||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/filebrowser/filebrowser/v2/errors"
 | 
			
		||||
| 
						 | 
				
			
			@ -93,6 +94,12 @@ var resourcePostPutHandler = withUser(func(w http.ResponseWriter, r *http.Reques
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	err := d.RunHook(func() error {
 | 
			
		||||
		dir, _ := filepath.Split(r.URL.Path)
 | 
			
		||||
		err := d.user.Fs.MkdirAll(dir, 0775)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue