Select Git revision
uploadsManager.js
-
Sonia Zorba authored
Upload improvements: refactored upload logic into dedicated module (upload manager), handled deletion of node metadata for failed or aborted uploads, displayed proper error messages for the various cases
Sonia Zorba authoredUpload improvements: refactored upload logic into dedicated module (upload manager), handled deletion of node metadata for failed or aborted uploads, displayed proper error messages for the various cases
uploadsManager.js 6.38 KiB
/*
* This file is part of vospace-ui
* Copyright (C) 2021 Istituto Nazionale di Astrofisica
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import Vue from 'vue';
import client from 'api-client';
function resetArray(array) {
array.splice(0, array.length);
}
export default {
state: {
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)
errors: [], // errors happened during downloads
creatingMetadata: false, // true when nodes metadata is being created (pre-upload phase)
uploadInProgress: false, // true when data is being uploaded
},
mutations: {
setFilesToUpload(state, files) {
state.files = files;
},
resetUploadState(state) {
resetArray(state.progress);
resetArray(state.cancellations);
resetArray(state.deletionStatuses);
resetArray(state.errors);
},
setDeletion(state, data) {
Vue.set(state.deletionStatuses, data.index, data.value);
},
setProgress(state, data) {
Vue.set(state.progress, data.index, data.percent);
},
setCreatingMetadata(state, value) {
state.creatingMetadata = value;
},
setUploadInProgress(state, value) {
state.uploadInProgress = value;
},
setPreUploadResults(state, preUploadResults) {
for (let result of preUploadResults) {
if (Object.keys(result).includes('request')) {
state.cancellations.push(result.source); // axios cancellation token
state.deletionStatuses.push(null);
state.errors.push(null);
} else {
state.cancellations.push(null);
state.deletionStatuses.push(null);
state.errors.push(result);
}
}
state.uploadInProgress = true;
},
setError(state, data) {
Vue.set(state.errors, data.index, data.error);
}
},
actions: {
upload({ state, rootState, commit, dispatch }, bvModal) {
// Progress bars initialization
for (let index = 0; index < state.files.length; index++) {
let file = state.files[index];
let reader = new FileReader();
reader.addEventListener('progress', (event) => {
if (event.loaded && event.total) {
let percent = (event.loaded / event.total) * 100;
commit('setProgress', { index, percent });
}
});
reader.readAsDataURL(file);
}
commit('setCreatingMetadata', true);
// Create nodes metadata and obtain upload URLs
dispatch('prepareUpload')
.then((preUploadResults) => {
commit('setCreatingMetadata', false);
let promises = [];
for (let result of preUploadResults) {
if (Object.keys(result).includes('request')) {
promises.push(result.request); // upload http promise
} else {
// fictitious promise for failed pre-upload event
promises.push(new Promise(resolve => resolve()));
}
}
commit('setPreUploadResults', preUploadResults);
// wait until all downloads have been completed (both successfully or not)
return Promise.allSettled(promises);
})
.then((responses) => {
let deletionPromises = [];
for (let index = 0; index < responses.length; index++) {
let error = state.errors[index];
let response = responses[index];
if (response.status === 'fulfilled' && error === null) {
commit('setProgress', { index, percent: 100 });
} else {
if (state.deletionStatuses[index] || error === null) {
// delete node metadata both when user aborted the upload and when it failed
deletionPromises.push(client.deleteNodes(['/' + rootState.path + '/' + state.files[index].name], true));
if (state.deletionStatuses[index] === null) {
// e.g. server became unreachable during the upload
commit('setError', { index, error: 'Unable to upload file' });
}
}
}
}
// if there are some deletion promises, wait them
return Promise.allSettled(deletionPromises);
})
.then(deletionResults => {
// verify if metadata of aborted uploads has been correctly removed
let resultIndex = 0;
for (let i = 0; i < state.deletionStatuses.length; i++) {
let deletionStatus = state.deletionStatuses[i];
if (deletionStatus !== null) {
let deletionResult = deletionResults[resultIndex];
if (deletionResult.status === 'fulfilled') {
commit('setDeletion', { index: i, value: false });
} else {
commit('setError', { index: i, error: 'Failed to delete node metadata. Manual cleanup may be required.' });
}
resultIndex++;
}
}
})
.finally(() => {
// Reload current node when all uploads completed
dispatch('setPath', rootState.path);
commit('setCreatingMetadata', false);
commit('setUploadInProgress', false);
let hasErrors = state.errors.filter(e => e !== null).length > 0;
if (!hasErrors) {
bvModal.hide('upload-files-modal');
}
});
},
prepareUpload({ state, rootState }) {
let names = [];
for (let file of state.files) {
names.push(file.name);
}
return new Promise((resolve, reject) => {
client.prepareForUpload(rootState.path, names)
.then(preUploadResponses => {
let uploads = [];
for (let i = 0; i < state.files.length; i++) {
let uploadUrl = preUploadResponses[i].url;
if (uploadUrl !== null) {
uploads.push(client.uploadFile(uploadUrl, state.files[i]));
} else {
uploads.push(preUploadResponses[i].error);
}
}
resolve(uploads);
})
.catch(error => reject(error));
});
},
cancelUpload({ state, commit }, index) {
commit('setDeletion', { index, value: true });
state.cancellations[index].cancel();
}
}
}