/* * 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(); } } }