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
	
	 Ramires Viana
						Ramires Viana