diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js
index 908567f36e59b156f2cc6f66216cbd0d326c1308..e038065ac290f36af5dfffef843f24369df6fbdf 100644
--- a/vospace-ui-frontend/src/api/server/index.js
+++ b/vospace-ui-frontend/src/api/server/index.js
@@ -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';
diff --git a/vospace-ui-frontend/src/components/modal/UploadFilesModal.vue b/vospace-ui-frontend/src/components/modal/UploadFilesModal.vue
index 17d68b19e2138ac76e0a1c63e108d8659ec6a5fb..158e6b23940f90875ea88bde704f15d7314ba185 100644
--- a/vospace-ui-frontend/src/components/modal/UploadFilesModal.vue
+++ b/vospace-ui-frontend/src/components/modal/UploadFilesModal.vue
@@ -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>
diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js
index 236a773286a3cbc9db2c1710e0849986d5148f3e..63d0fc70715b88f626eaffaa044d3fd81ac5aef3 100644
--- a/vospace-ui-frontend/src/store.js
+++ b/vospace-ui-frontend/src/store.js
@@ -204,31 +204,22 @@ 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)
-        .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);
-            })
-            .catch(error => {
-              let message = "Unable to upload file"
-              if (error.response && error.response.data) {
-                message += ": " + error.response.data;
-              }
-              main.showError(message);
-            });
-        });
+      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]));
+            }
+            resolve(uploads);
+          })
+          .catch(error => reject(error));
+      });
     },
     deleteNodes({ state, dispatch }) {
       client.deleteNodes(state.nodesToDelete)