Skip to content

Commit

Permalink
feat: add folder upload (#981)
Browse files Browse the repository at this point in the history
* feat: folder upload
fix #741

* fix: apply gofmt formater

* feat: upload button prompt

* feat: empty folder upload
  • Loading branch information
ramiresviana committed Jun 16, 2020
1 parent 6d899a6 commit 8977344
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 20 deletions.
6 changes: 5 additions & 1 deletion frontend/src/components/buttons/Upload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
}
Expand Down
138 changes: 122 additions & 16 deletions frontend/src/components/files/Listing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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')) {
Expand All @@ -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
Expand All @@ -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')
Expand All @@ -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 = []
Expand All @@ -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 = () => {
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/components/prompts/Prompts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -33,7 +34,8 @@ export default {
NewFile,
NewDir,
Help,
Replace
Replace,
Upload
},
data: function () {
return {
Expand All @@ -58,7 +60,8 @@ export default {
'newDir',
'download',
'replace',
'share'
'share',
'upload'
].indexOf(this.show) >= 0;
return matched && this.show || null;
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/components/prompts/Upload.vue
Original file line number Diff line number Diff line change
@@ -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>
31 changes: 31 additions & 0 deletions frontend/src/css/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
4 changes: 3 additions & 1 deletion frontend/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
7 changes: 7 additions & 0 deletions http/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strings"

"github.com/filebrowser/filebrowser/v2/errors"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8977344

Please sign in to comment.