diff --git a/src/constants/config.js b/src/constants/config.js index 68f3c231be..f00a8c9f44 100644 --- a/src/constants/config.js +++ b/src/constants/config.js @@ -11,3 +11,5 @@ export const TRASH_DIR_PATH = '/.cozy_trash' export const KONNECTORS_DIR_PATH = '/.cozy_konnectors' export const FILES_FETCH_LIMIT = 100 export const HOME_LINK_HREF = 'https://manager.cozycloud.cc/cozy/create' +export const MAX_PAYLOAD_SIZE_IN_GB = 5 +export const MAX_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE_IN_GB * 1024 * 1024 * 1024 diff --git a/src/locales/en.json b/src/locales/en.json index 2774a20de1..a79cd271b6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -515,7 +515,8 @@ "updated": "%{smart_count} %{type} updated. |||| %{smart_count} %{type} updated.", "updated_conflicts": "%{smart_count} %{type} updated with %{conflictCount} conflict(s). |||| %{smart_count} %{type}s updated with %{conflictCount} conflict(s).", "errors": "Errors occurred during the %{type} upload.", - "network": "You are currenly offline. Please try again once you're connected." + "network": "You are currenly offline. Please try again once you're connected.", + "fileTooLargeErrors": "File too large. Maximum file size: %{max_size_value} GB" } }, "intents": { diff --git a/src/locales/fr.json b/src/locales/fr.json index 2d6110a53d..08dbbfd9d4 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -515,7 +515,8 @@ "updated": "%{smart_count} %{type} mis à jour. |||| %{smart_count} %{type}s mis à jour.", "updated_conflicts": "%{smart_count} %{type} mis à jour avec %{conflictCount} conflit(s). |||| %{smart_count} %{type}s mis à jour avec %{conflictCount} conflit(s).", "errors": "Une erreur est survenue lors de l’import du %{type}, merci de réessayer plus tard.", - "network": "Vous ne disposez pas d'une connexion internet. Merci de réessayer quand ce sera le cas." + "network": "Vous ne disposez pas d'une connexion internet. Merci de réessayer quand ce sera le cas.", + "fileTooLargeErrors": "Fichier trop volumineux. Taille maximale autorisée par fichier : %{max_size_value} Go" } }, "intents": { diff --git a/src/modules/navigation/duck/actions.jsx b/src/modules/navigation/duck/actions.jsx index a29e43a2b4..34e38a2c70 100644 --- a/src/modules/navigation/duck/actions.jsx +++ b/src/modules/navigation/duck/actions.jsx @@ -4,6 +4,7 @@ import { showModal } from 'react-cozy-helpers' import { isDirectory } from 'cozy-client/dist/models/file' import Alerter from 'cozy-ui/transpiled/react/deprecated/Alerter' +import { MAX_PAYLOAD_SIZE_IN_GB } from 'constants/config' import { createEncryptedDir } from 'lib/encryption' import { getEntriesTypeTranslated } from 'lib/entries' import logger from 'lib/logger' @@ -48,7 +49,15 @@ export const uploadFiles = dirId, sharingState, // used to know if files are shared for conflicts management fileUploadedCallback, - (loaded, quotas, conflicts, networkErrors, errors, updated) => + ( + loaded, + quotas, + conflicts, + networkErrors, + errors, + updated, + fileTooLargeErrors + ) => dispatch( uploadQueueProcessed( loaded, @@ -57,7 +66,8 @@ export const uploadFiles = networkErrors, errors, updated, - t + t, + fileTooLargeErrors ) ), { client, vaultClient } @@ -66,7 +76,16 @@ export const uploadFiles = } const uploadQueueProcessed = - (created, quotas, conflicts, networkErrors, errors, updated, t) => + ( + created, + quotas, + conflicts, + networkErrors, + errors, + updated, + t, + fileTooLargeErrors + ) => dispatch => { const conflictCount = conflicts.length const createdCount = created.length @@ -119,6 +138,10 @@ const uploadQueueProcessed = smart_count: updatedCount, type }) + } else if (fileTooLargeErrors.length > 0) { + Alerter.error('upload.alert.fileTooLargeErrors', { + max_size_value: MAX_PAYLOAD_SIZE_IN_GB + }) } else { Alerter.success('upload.alert.success', { smart_count: createdCount, diff --git a/src/modules/upload/index.js b/src/modules/upload/index.js index fcb618deb6..c04b37635f 100644 --- a/src/modules/upload/index.js +++ b/src/modules/upload/index.js @@ -5,6 +5,7 @@ import { models } from 'cozy-client' import flag from 'cozy-flags' import UploadQueue from './UploadQueue' +import { MAX_PAYLOAD_SIZE } from 'constants/config' import { DOCTYPE_FILES } from 'lib/doctypes' import { encryptAndUploadNewFile, @@ -36,6 +37,7 @@ const FAILED = 'failed' const CONFLICT = 'conflict' const QUOTA = 'quota' const NETWORK = 'network' +const FILE_TOO_LARGE_ERROR = 'Request Entity Too Large' const DONE_STATUSES = [CREATED, UPDATED] const ERROR_STATUSES = [CONFLICT, NETWORK, QUOTA] @@ -50,7 +52,8 @@ export const status = { QUOTA, NETWORK, DONE_STATUSES, - ERROR_STATUSES + ERROR_STATUSES, + FILE_TOO_LARGE_ERROR } const CONFLICT_ERROR = 409 @@ -233,19 +236,33 @@ export const processNextFile = } if (error) { logger.warn(error) + logException( `Upload module catches an error when executing processNextFile(): ${error}` ) + + // Define mapping for specific status codes to our constants const statusError = { 409: CONFLICT, 413: QUOTA } - const status = - statusError[error.status] || - (/Failed to fetch$/.exec(error.toString()) && NETWORK) || - FAILED + // Determine the status based on the error details + let status + if ( + error.title === FILE_TOO_LARGE_ERROR || + error.message === FILE_TOO_LARGE_ERROR + ) { + status = FILE_TOO_LARGE_ERROR // File size exceeded maximum size allowed by the server + } else if (error.status in statusError) { + status = statusError[error.status] + } else if (/Failed to fetch$/.exec(error.toString())) { + status = NETWORK + } else { + status = FAILED + } + // Dispatch an action to handle the upload error with the determined status dispatch({ type: RECEIVE_UPLOAD_ERROR, file, status }) } } @@ -314,16 +331,30 @@ const uploadFile = async (client, file, dirID, options = {}) => { * We don't need to do that work on other browser (window.chrome * should be available on new Edge, Chrome, Chromium, Brave, Opera...) */ + + // Check if running in a Chrome browser if (window.chrome) { + // Convert file size to integer for comparison + const fileSize = parseInt(file.size, 10) + + // Check if the file size exceeds the server's maximum payload size + if (fileSize > MAX_PAYLOAD_SIZE) { + // Create a new error for exceeding the maximum payload size + const error = new Error(FILE_TOO_LARGE_ERROR) + throw error + } + + // Proceed to check disk usage const { data: diskUsage } = await client .getStackClient() .fetchJSON('GET', '/settings/disk-usage') if (diskUsage.attributes.quota) { - if ( - parseInt(diskUsage.attributes.used) + parseInt(file.size) > - parseInt(diskUsage.attributes.quota) - ) { - const error = new Error('Payload Too Large') + const usedSpace = parseInt(diskUsage.attributes.used, 10) + const totalQuota = parseInt(diskUsage.attributes.quota, 10) + const availableSpace = totalQuota - usedSpace + + if (fileSize > availableSpace) { + const error = new Error('Insufficient Disk Space') error.status = 413 throw error } @@ -456,8 +487,17 @@ export const onQueueEmpty = callback => (dispatch, getState) => { const updated = getUpdated(queue) const networkErrors = getNetworkErrors(queue) const errors = getErrors(queue) - - return callback(created, quotas, conflicts, networkErrors, errors, updated) + const fileTooLargeErrors = getfileTooLargeErrors(queue) + + return callback( + created, + quotas, + conflicts, + networkErrors, + errors, + updated, + fileTooLargeErrors + ) } // selectors @@ -468,6 +508,8 @@ const getQuotaErrors = queue => filterByStatus(queue, QUOTA) const getNetworkErrors = queue => filterByStatus(queue, NETWORK) const getCreated = queue => filterByStatus(queue, CREATED) const getUpdated = queue => filterByStatus(queue, UPDATED) +const getfileTooLargeErrors = queue => + filterByStatus(queue, FILE_TOO_LARGE_ERROR) export const getUploadQueue = state => state[SLUG].queue diff --git a/src/modules/upload/index.spec.js b/src/modules/upload/index.spec.js index beeda4b116..d852baaade 100644 --- a/src/modules/upload/index.spec.js +++ b/src/modules/upload/index.spec.js @@ -171,6 +171,7 @@ describe('processNextFile function', () => { [], [], [], + [], [] ) })