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',
|
name: 'upload-button',
|
||||||
methods: {
|
methods: {
|
||||||
upload: function () {
|
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>
|
<span>{{ $t('files.lonely') }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
<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>
|
||||||
<div v-else id="listing"
|
<div v-else id="listing"
|
||||||
:class="user.viewMode"
|
:class="user.viewMode"
|
||||||
|
@ -75,6 +76,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input style="display:none" type="file" id="upload-input" @change="uploadInput($event)" multiple>
|
<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">
|
<div :class="{ active: $store.state.multiple }" id="multiple-selection">
|
||||||
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
<p>{{ $t('files.multipleSelectionEnabled') }}</p>
|
||||||
|
@ -290,10 +292,9 @@ export default {
|
||||||
this.resetOpacity()
|
this.resetOpacity()
|
||||||
|
|
||||||
let dt = event.dataTransfer
|
let dt = event.dataTransfer
|
||||||
let files = dt.files
|
|
||||||
let el = event.target
|
let el = event.target
|
||||||
|
|
||||||
if (files.length <= 0) return
|
if (dt.files.length <= 0) return
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
if (el !== null && !el.classList.contains('item')) {
|
if (el !== null && !el.classList.contains('item')) {
|
||||||
|
@ -306,28 +307,45 @@ export default {
|
||||||
base = el.querySelector('.name').innerHTML + '/'
|
base = el.querySelector('.name').innerHTML + '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (base !== '') {
|
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)
|
api.fetch(this.$route.path + base)
|
||||||
.then(req => {
|
.then(req => {
|
||||||
this.checkConflict(files, req.items, base)
|
this.checkConflict(result, req.items, base)
|
||||||
})
|
})
|
||||||
.catch(this.$showError)
|
.catch(this.$showError)
|
||||||
|
})
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkConflict(files, this.req.items, base)
|
|
||||||
},
|
},
|
||||||
checkConflict (files, items, base) {
|
checkConflict (files, items, base) {
|
||||||
if (typeof items === 'undefined' || items === null) {
|
if (typeof items === 'undefined' || items === null) {
|
||||||
items = []
|
items = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let folder_upload = false
|
||||||
|
if (files[0].fullPath !== undefined) {
|
||||||
|
folder_upload = true
|
||||||
|
}
|
||||||
|
|
||||||
let conflict = false
|
let conflict = false
|
||||||
for (let i = 0; i < files.length; i++) {
|
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) {
|
let res = items.findIndex(function hasConflict (element) {
|
||||||
return (element.name === this)
|
return (element.name === this)
|
||||||
}, files[i].name)
|
}, name)
|
||||||
|
|
||||||
if (res >= 0) {
|
if (res >= 0) {
|
||||||
conflict = true
|
conflict = true
|
||||||
|
@ -350,7 +368,19 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
uploadInput (event) {
|
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 () {
|
resetOpacity () {
|
||||||
let items = document.getElementsByClassName('item')
|
let items = document.getElementsByClassName('item')
|
||||||
|
@ -359,6 +389,67 @@ export default {
|
||||||
file.style.opacity = 1
|
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) {
|
handleFiles (files, base, overwrite = false) {
|
||||||
buttons.loading('upload')
|
buttons.loading('upload')
|
||||||
let promises = []
|
let promises = []
|
||||||
|
@ -377,8 +468,23 @@ export default {
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
let file = files[i]
|
let file = files[i]
|
||||||
let filenameEncoded = url.encodeRFC5987ValueChars(file.name)
|
|
||||||
|
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)))
|
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 = () => {
|
let finish = () => {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import NewFile from './NewFile'
|
||||||
import NewDir from './NewDir'
|
import NewDir from './NewDir'
|
||||||
import Replace from './Replace'
|
import Replace from './Replace'
|
||||||
import Share from './Share'
|
import Share from './Share'
|
||||||
|
import Upload from './Upload'
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import buttons from '@/utils/buttons'
|
import buttons from '@/utils/buttons'
|
||||||
|
|
||||||
|
@ -33,7 +34,8 @@ export default {
|
||||||
NewFile,
|
NewFile,
|
||||||
NewDir,
|
NewDir,
|
||||||
Help,
|
Help,
|
||||||
Replace
|
Replace,
|
||||||
|
Upload
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
@ -58,7 +60,8 @@ export default {
|
||||||
'newDir',
|
'newDir',
|
||||||
'download',
|
'download',
|
||||||
'replace',
|
'replace',
|
||||||
'share'
|
'share',
|
||||||
|
'upload'
|
||||||
].indexOf(this.show) >= 0;
|
].indexOf(this.show) >= 0;
|
||||||
|
|
||||||
return matched && this.show || null;
|
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;
|
background-color: #fff;
|
||||||
border-radius: 2px;
|
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);
|
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 {
|
.card.floating {
|
||||||
|
@ -366,3 +367,33 @@ table tr>*:last-child {
|
||||||
.card .collapsible .collapse {
|
.card .collapsible .collapse {
|
||||||
padding: 0 1em;
|
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",
|
"size": "Size",
|
||||||
"schedule": "Schedule",
|
"schedule": "Schedule",
|
||||||
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
|
"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": {
|
"settings": {
|
||||||
"themes": {
|
"themes": {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
"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 {
|
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)
|
file, err := d.user.Fs.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
Loading…
Reference in New Issue