Commit d03bdbf9 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Upload improvements: added progress bar and handled upload cancellation

parent 00512880
Loading
Loading
Loading
Loading
Loading
+14 −4
Original line number Diff line number Diff line
@@ -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';
+116 −5
Original line number Diff line number Diff line
@@ -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>
+12 −21
Original line number Diff line number Diff line
@@ -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 }) {