From 89773447a56675b298394149d7a05c5df4039f14 Mon Sep 17 00:00:00 2001 From: Ramires Viana <59319979+ramiresviana@users.noreply.github.com> Date: Tue, 16 Jun 2020 16:56:44 -0300 Subject: [PATCH] feat: add folder upload (#981) * feat: folder upload fix filebrowser/filebrowser#741 * fix: apply gofmt formater * feat: upload button prompt * feat: empty folder upload --- frontend/src/components/buttons/Upload.vue | 6 +- frontend/src/components/files/Listing.vue | 138 +++++++++++++++++--- frontend/src/components/prompts/Prompts.vue | 7 +- frontend/src/components/prompts/Upload.vue | 37 ++++++ frontend/src/css/dashboard.css | 31 +++++ frontend/src/i18n/en.json | 4 +- http/resource.go | 7 + 7 files changed, 210 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/prompts/Upload.vue diff --git a/frontend/src/components/buttons/Upload.vue b/frontend/src/components/buttons/Upload.vue index ff1396d0..9e7f07bd 100644 --- a/frontend/src/components/buttons/Upload.vue +++ b/frontend/src/components/buttons/Upload.vue @@ -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(); + } } } } diff --git a/frontend/src/components/files/Listing.vue b/frontend/src/components/files/Listing.vue index a227cced..ca9b7244 100644 --- a/frontend/src/components/files/Listing.vue +++ b/frontend/src/components/files/Listing.vue @@ -5,6 +5,7 @@ {{ $t('files.lonely') }} +
+

{{ $t('files.multipleSelectionEnabled') }}

@@ -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 = () => { diff --git a/frontend/src/components/prompts/Prompts.vue b/frontend/src/components/prompts/Prompts.vue index 8e530a27..29d4112b 100644 --- a/frontend/src/components/prompts/Prompts.vue +++ b/frontend/src/components/prompts/Prompts.vue @@ -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; diff --git a/frontend/src/components/prompts/Upload.vue b/frontend/src/components/prompts/Upload.vue new file mode 100644 index 00000000..3202ef2c --- /dev/null +++ b/frontend/src/components/prompts/Upload.vue @@ -0,0 +1,37 @@ + + + diff --git a/frontend/src/css/dashboard.css b/frontend/src/css/dashboard.css index d186a727..b29e907f 100644 --- a/frontend/src/css/dashboard.css +++ b/frontend/src/css/dashboard.css @@ -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; +} \ No newline at end of file diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index f42be526..7617caaf 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -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": { diff --git a/http/resource.go b/http/resource.go index 63d7e028..274b3ac5 100644 --- a/http/resource.go +++ b/http/resource.go @@ -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