Skip to content
Snippets Groups Projects
Commit d03bdbf9 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Upload improvements: added progress bar and handled upload cancellation

parent 00512880
No related branches found
No related tags found
No related merge requests found
Pipeline #2220 passed
......@@ -127,15 +127,25 @@ export default {
}, false);
},
uploadFile(url, file) {
let CancelToken = axios.CancelToken;
let source = CancelToken.source();
let formData = new FormData();
formData.append('file', file);
return axios.put(url, formData, {
let request = axios.put(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
},
cancelToken: source.token
});
return {
request,
source
};
},
deleteNodes(paths) {
deleteNodes(paths, showLoading = true) {
let url = BASE_API_URL + 'delete';
return apiRequest({
method: 'POST',
......@@ -145,7 +155,7 @@ export default {
'Cache-Control': 'no-cache'
},
data: paths
}, true, true);
}, showLoading, true);
},
keepalive() {
let url = BASE_API_URL + 'keepalive';
......
......@@ -4,20 +4,46 @@
SPDX-License-Identifier: GPL-3.0-or-later
-->
<template>
<b-modal id="upload-files-modal" title="Upload file" okTitle="Upload" @show="reset" @ok.prevent="uploadFiles">
<b-form-file v-model="files" :multiple="true" :state="fileState" placeholder="Choose your files or drop them here..." drop-placeholder="Drop files here..." @change="resetError"></b-form-file>
<b-modal id="upload-files-modal" title="Upload file" okTitle="Upload" @show="reset" @ok.prevent="uploadFiles" :no-close-on-backdrop="blockModal" :no-close-on-esc="blockModal" :hide-header-close="blockModal" :cancel-disabled="blockModal"
:ok-disabled="blockModal" size="lg">
<b-form-file v-model="files" :multiple="true" :state="fileState" placeholder="Choose your files or drop them here..." drop-placeholder="Drop files here..." @change="selectionChanged"></b-form-file>
<b-form-invalid-feedback id="upload-file-input-feedback" class="text-right">{{uploadFileError}}</b-form-invalid-feedback>
<div class="mt-3">Selected files: {{ selectedFiles }}</div>
<div class="mt-3" v-if="!blockModal">Selected files: {{ selectedFiles }}</div>
<div v-if="creatingMetadata" class="mt-3">
<b-spinner small variant="primary" label="Spinning"></b-spinner>
Creating metadata...
</div>
<div v-if="uploadInProgress" class="mt-3">
<div v-for="(file, index) in files" :key="index" class="upload-progress-container mt-1">
<div v-if="!deletionStatuses[index]">
{{ file.name }}<br />
<span class="text-danger cancel-upload" @click="cancelUpload(index)">&times;</span>
<b-progress :value="progress[index]" :max="100" show-progress animated></b-progress>
</div>
<div v-if="deletionStatuses[index]">
<b-spinner small variant="primary" label="Spinning"></b-spinner>
Upload of {{ file.name }} has been canceled. Waiting for metadata deletion...
</div>
</div>
</div>
</b-modal>
</template>
<script>
const maxUploadSize = process.env.VUE_APP_MAX_UPLOAD_SIZE;
import main from '../../main';
import client from 'api-client';
import Vue from 'vue';
export default {
data() {
return {
files: [],
files: [], // array of selected files
progress: [], // array of upload progress percentages (for tracking upload progress)
cancellations: [], // array of axios cancel tokens (for aborting uploads)
deletionStatuses: [], // status of upload deletions (true if request is being aborted, false otherwise)
creatingMetadata: false, // true when nodes metadata is being created (pre-upload phase)
uploadInProgress: false, // true when data is being uploaded
uploadFileError: null
}
},
......@@ -37,6 +63,9 @@ export default {
names.push(file.name);
}
return names.join(', ');
},
blockModal() {
return this.creatingMetadata || this.uploadInProgress;
}
},
methods: {
......@@ -47,7 +76,13 @@ export default {
resetError() {
this.uploadFileError = null;
},
selectionChanged() {
this.resetError();
},
uploadFiles() {
if (this.uploadInProgress || this.creatingMetadata) {
return;
}
if (this.files.length === 0) {
this.uploadFileError = "Select at least one file";
} else {
......@@ -67,13 +102,89 @@ export default {
}
}
for (let i = 0; i < this.files.length; i++) {
let file = this.files[i];
let reader = new FileReader();
reader.addEventListener('progress', (event) => {
if (event.loaded && event.total) {
let percent = (event.loaded / event.total) * 100;
this.progress[i] = percent;
}
});
reader.readAsDataURL(file);
}
// reset status arrays
Vue.set(this, 'progress', []);
Vue.set(this, 'cancellations', []);
Vue.set(this, 'deletionStatuses', []);
this.creatingMetadata = true;
// Upload
this.$store.dispatch('uploadFiles', this.files)
.then(() => {
.then((uploads) => {
this.creatingMetadata = false;
let promises = [];
for (let upload of uploads) {
promises.push(upload.request);
this.cancellations.push(upload.source);
this.deletionStatuses.push(false);
}
this.uploadInProgress = true;
// wait until all downloads have been completed (both successfully or not)
return Promise.allSettled(promises);
})
.then((responses) => {
let deletionPromises = [];
for (let i = 0; i < responses.length; i++) {
let response = responses[i];
if (response.status !== 'fulfilled') {
if (this.deletionStatuses[i]) {
deletionPromises.push(client.deleteNodes(['/' + this.$store.state.path + '/' + this.files[i].name], false));
} else {
let message = "Unable to upload file " + this.files[i].name;
main.showError(message);
}
}
}
return Promise.allSettled(deletionPromises);
})
.finally(() => {
// Reload current node when all uploads completed
this.$store.dispatch('setPath', this.$store.state.path);
this.$bvModal.hide('upload-files-modal');
this.creatingMetadata = false;
this.uploadInProgress = false;
});
}
},
cancelUpload(index) {
Vue.set(this.deletionStatuses, index, true);
this.cancellations[index].cancel();
}
}
}
</script>
<style>
.upload-progress-container {
position: relative;
}
.cancel-upload {
cursor: pointer;
position: absolute;
right: 3px;
top: 12px;
font-size: 170%;
}
</style>
......@@ -204,30 +204,21 @@ export default new Vuex.Store({
dispatch('setPath', state.path);
});
},
uploadFiles({ state, commit, dispatch }, files) {
commit('setLoading', true);
uploadFiles({ state }, files) {
let names = [];
for (let file of files) {
names.push(file.name);
}
return client.prepareForUpload(state.path, names)
return new Promise((resolve, reject) => {
client.prepareForUpload(state.path, names)
.then(uploadUrls => {
let uploads = [];
for (let i = 0; i < files.length; i++) {
uploads.push(client.uploadFile(uploadUrls[i], files[i]));
}
Promise.all(uploads)
.then(() => {
// Reload current node when all uploads completed
dispatch('setPath', state.path);
resolve(uploads);
})
.catch(error => {
let message = "Unable to upload file"
if (error.response && error.response.data) {
message += ": " + error.response.data;
}
main.showError(message);
});
.catch(error => reject(error));
});
},
deleteNodes({ state, dispatch }) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment