Merge pull request #1021 from ramiresviana/upload-queue

This commit is contained in:
Oleg Lobanov 2020-07-13 11:20:59 +02:00 committed by GitHub
commit a47b69bcec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 282 additions and 210 deletions

View File

@ -89,25 +89,17 @@
<script> <script>
import { mapState, mapMutations } from 'vuex' import { mapState, mapMutations } from 'vuex'
import throttle from 'lodash.throttle'
import Item from './ListingItem' import Item from './ListingItem'
import css from '@/utils/css' import css from '@/utils/css'
import { users, files as api } from '@/api' import { users, files as api } from '@/api'
import buttons from '@/utils/buttons' import * as upload from '@/utils/upload'
import url from '@/utils/url'
export default { export default {
name: 'listing', name: 'listing',
components: { Item }, components: { Item },
data: function () { data: function () {
return { return {
showLimit: 50, showLimit: 50
uploading: {
id: 0,
count: 0,
size: 0,
progress: []
}
} }
}, },
computed: { computed: {
@ -194,7 +186,7 @@ export default {
base64: function (name) { base64: function (name) {
return window.btoa(unescape(encodeURIComponent(name))) return window.btoa(unescape(encodeURIComponent(name)))
}, },
keyEvent (event) { keyEvent (event) {
if (this.show !== null) { if (this.show !== null) {
return return
} }
@ -311,7 +303,7 @@ export default {
dragEnd () { dragEnd () {
this.resetOpacity() this.resetOpacity()
}, },
drop: function (event) { drop: async function (event) {
event.preventDefault() event.preventDefault()
this.resetOpacity() this.resetOpacity()
@ -331,65 +323,34 @@ export default {
base = el.querySelector('.name').innerHTML + '/' base = el.querySelector('.name').innerHTML + '/'
} }
if (base === '') { let files = await upload.scanFiles(dt)
this.scanFiles(dt).then((result) => { let path = this.$route.path + base
this.checkConflict(result, this.req.items, base) let items = this.req.items
})
} else { if (base !== '') {
this.scanFiles(dt).then((result) => { try {
api.fetch(this.$route.path + base) items = (await api.fetch(path)).items
.then(req => { } catch (error) {
this.checkConflict(result, req.items, base) this.$showError(error)
}) }
.catch(this.$showError)
})
}
},
checkConflict (files, items, base) {
if (typeof items === 'undefined' || items === null) {
items = []
} }
let folder_upload = false let conflict = upload.checkConflict(files, items)
if (files[0].fullPath !== undefined) {
folder_upload = true
}
let conflict = false if (conflict) {
for (let i = 0; i < files.length; i++) { this.$store.commit('showHover', {
let file = files[i] prompt: 'replace',
let name = file.name confirm: (event) => {
event.preventDefault()
if (folder_upload) { this.$store.commit('closeHovers')
let dirs = file.fullPath.split("/") upload.handleFiles(files, path, true)
if (dirs.length > 1) {
name = dirs[0]
} }
} })
let res = items.findIndex(function hasConflict (element) {
return (element.name === this)
}, name)
if (res >= 0) {
conflict = true
break
}
}
if (!conflict) {
this.handleFiles(files, base)
return return
} }
this.$store.commit('showHover', { upload.handleFiles(files, path)
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
this.handleFiles(files, base, true)
}
})
}, },
uploadInput (event) { uploadInput (event) {
this.$store.commit('closeHovers') this.$store.commit('closeHovers')
@ -404,7 +365,22 @@ export default {
} }
} }
this.checkConflict(files, this.req.items, '') let path = this.$route.path
let conflict = upload.checkConflict(files, this.req.items)
if (conflict) {
this.$store.commit('showHover', {
prompt: 'replace',
confirm: (event) => {
event.preventDefault()
this.$store.commit('closeHovers')
this.handleFiles(files, path, true)
}
})
return
}
upload.handleFiles(files, path)
}, },
resetOpacity () { resetOpacity () {
let items = document.getElementsByClassName('item') let items = document.getElementsByClassName('item')
@ -413,145 +389,6 @@ 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)
}
})
}
})
},
setProgress: throttle(function() {
if (this.uploading.count == 0) {
return
}
let sum = this.uploading.progress.reduce((acc, val) => acc + val)
this.$store.commit('setProgress', Math.ceil(sum / this.uploading.size * 100))
}, 100, {leading: false, trailing: true}),
handleFiles (files, base, overwrite = false) {
if (this.uploading.count == 0) {
buttons.loading('upload')
}
let promises = []
let onupload = (id) => (event) => {
this.uploading.progress[id] = event.loaded
this.setProgress()
}
for (let i = 0; i < files.length; i++) {
let file = files[i]
if (!file.isDir) {
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
let id = this.uploading.id
this.uploading.size += file.size
this.uploading.id++
this.uploading.count++
let promise = api.post(this.$route.path + base + filenameEncoded, file, overwrite, throttle(onupload(id), 100)).finally(() => {
this.uploading.count--
})
promises.push(promise)
} 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 = () => {
if (this.uploading.count > 0) {
return
}
buttons.success('upload')
this.$store.commit('setProgress', 0)
this.$store.commit('setReload', true)
this.uploading.id = 0
this.uploading.sizes = []
this.uploading.progress = []
}
Promise.all(promises)
.then(() => {
finish()
})
.catch(error => {
finish()
this.$showError(error)
})
return false
},
async sort (by) { async sort (by) {
let asc = false let asc = false

View File

@ -3,7 +3,15 @@ const getters = {
isFiles: state => !state.loading && state.route.name === 'Files', isFiles: state => !state.loading && state.route.name === 'Files',
isListing: (state, getters) => getters.isFiles && state.req.isDir, isListing: (state, getters) => getters.isFiles && state.req.isDir,
isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'), isEditor: (state, getters) => getters.isFiles && (state.req.type === 'text' || state.req.type === 'textImmutable'),
selectedCount: state => state.selected.length selectedCount: state => state.selected.length,
progress : state => {
if (state.upload.progress.length == 0) {
return 0;
}
let sum = state.upload.progress.reduce((acc, val) => acc + val)
return Math.ceil(sum / state.upload.size * 100);
}
} }
export default getters export default getters

View File

@ -2,6 +2,7 @@ import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import mutations from './mutations' import mutations from './mutations'
import getters from './getters' import getters from './getters'
import upload from './modules/upload'
Vue.use(Vuex) Vue.use(Vuex)
@ -29,5 +30,6 @@ export default new Vuex.Store({
strict: true, strict: true,
state, state,
getters, getters,
mutations mutations,
modules: { upload }
}) })

View File

@ -0,0 +1,95 @@
import Vue from 'vue'
import { files as api } from '@/api'
import throttle from 'lodash.throttle'
import buttons from '@/utils/buttons'
const UPLOADS_LIMIT = 5;
const state = {
id: 0,
size: 0,
progress: [],
queue: [],
uploads: {}
}
const mutations = {
setProgress(state, { id, loaded }) {
Vue.set(state.progress, id, loaded)
},
reset: (state) => {
state.id = 0
state.size = 0
state.progress = []
},
addJob: (state, item) => {
state.queue.push(item)
state.size += item.file.size
state.id++
},
moveJob(state) {
const item = state.queue[0]
state.queue.shift()
Vue.set(state.uploads, item.id, item)
},
removeJob(state, id) {
delete state.uploads[id]
}
}
const actions = {
upload: (context, item) => {
let uploadsCount = Object.keys(context.state.uploads).length;
let isQueueEmpty = context.state.queue.length == 0
let isUploadsEmpty = uploadsCount == 0
if (isQueueEmpty && isUploadsEmpty) {
buttons.loading('upload')
}
context.commit('addJob', item)
context.dispatch('processUploads')
},
finishUpload: (context, item) => {
context.commit('setProgress', { id: item.id, loaded: item.file.size })
context.commit('removeJob', item.id)
context.dispatch('processUploads')
},
processUploads: async (context) => {
let uploadsCount = Object.keys(context.state.uploads).length;
let isBellowLimit = uploadsCount < UPLOADS_LIMIT
let isQueueEmpty = context.state.queue.length == 0
let isUploadsEmpty = uploadsCount == 0
let isFinished = isQueueEmpty && isUploadsEmpty
let canProcess = isBellowLimit && !isQueueEmpty
if (isFinished) {
buttons.success('upload')
context.commit('reset')
context.commit('setReload', true, { root: true })
}
if (canProcess) {
const item = context.state.queue[0];
context.commit('moveJob')
if (item.file.isDir) {
await api.post(item.path).catch(Vue.prototype.$showError)
} else {
let onUpload = throttle(
(event) => context.commit('setProgress', { id: item.id, loaded: event.loaded }),
100, { leading: true, trailing: false }
)
await api.post(item.path, item.file, item.overwrite, onUpload).catch(Vue.prototype.$showError)
}
context.dispatch('finishUpload', item)
}
}
}
export default { state, mutations, actions, namespaced: true }

View File

@ -82,9 +82,6 @@ const mutations = {
resetClipboard: (state) => { resetClipboard: (state) => {
state.clipboard.key = '' state.clipboard.key = ''
state.clipboard.items = [] state.clipboard.items = []
},
setProgress: (state, value) => {
state.progress = value
} }
} }

View File

@ -0,0 +1,133 @@
import store from '@/store'
import url from '@/utils/url'
export function checkConflict(files, items) {
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)
}, name)
if (res >= 0) {
conflict = true
break
}
}
return conflict
}
export function 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,
size: 0,
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)
}
})
}
})
}
export function handleFiles(files, path, overwrite = false) {
for (let i = 0; i < files.length; i++) {
let file = files[i]
let filename = (file.fullPath !== undefined) ? file.fullPath : file.name
let filenameEncoded = url.encodeRFC5987ValueChars(filename)
let id = store.state.upload.id
let itemPath = path + filenameEncoded
if (file.isDir) {
itemPath = path
let folders = file.path.split("/")
for (let i = 0; i < folders.length; i++) {
let folder = folders[i]
let folderEncoded = encodeURIComponent(folder)
itemPath += folderEncoded + "/"
}
}
const item = {
id,
path: itemPath,
file,
overwrite
}
store.dispatch('upload/upload', item);
}
}

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div id="progress"> <div id="progress">
<div v-bind:style="{ width: $store.state.progress + '%' }"></div> <div v-bind:style="{ width: this.progress + '%' }"></div>
</div> </div>
<site-header></site-header> <site-header></site-header>
<sidebar></sidebar> <sidebar></sidebar>
@ -29,7 +29,7 @@ export default {
Shell Shell
}, },
computed: { computed: {
...mapGetters([ 'isLogged' ]), ...mapGetters([ 'isLogged', 'progress' ]),
...mapState([ 'user' ]) ...mapState([ 'user' ])
}, },
watch: { watch: {