diff --git a/README.rst b/README.rst index 96fade1c60..e180fc6026 100644 --- a/README.rst +++ b/README.rst @@ -126,11 +126,15 @@ The following are requirements for this feature to function correctly: Configuration ------------- -In additional to the standard settings, the following local configuration items are required: +In addition to the standard settings, the following local configuration items are required: * ``LEARNING_BASE_URL``: points to Learning MFE; necessary so that the `View Live` button works * ``ENABLE_PROGRESS_GRAPH_SETTINGS``: allow enabling or disabling the learner progress graph course-wide +In addition to the standard settings, the following local configuration items are optional: + +* ``OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB``: specifies a positive (non-zero) integer representing a value in MegaBytes, used to override the maxSize attribute when uploading textbooks. For more information and examples of usage, please see `OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB `_ in the how_tos docs. + Feature Description ------------------- @@ -212,6 +216,14 @@ Feature: Files & Uploads In Studio, the "Files & Uploads" page for each enabled course will now be served by this frontend, instead of the UI built into edx-platform. This page allows managing static asset files like PDFs, images, etc. used for the course. +Configuration +------------- + +In addition to the standard settings, the following local configuration items are optional: + +* ``OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB``: specifies a positive (non-zero) integer representing a value in MegaBytes, used to override the maxSize attribute when uploading files. For more information and examples of usage, please see `OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB `_ in the how_tos docs. + + Feature: Course Updates ========================== diff --git a/docs/how_tos/OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB.rst b/docs/how_tos/OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB.rst new file mode 100644 index 0000000000..36b659b0b4 --- /dev/null +++ b/docs/how_tos/OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB.rst @@ -0,0 +1,65 @@ +#################### +OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB +#################### +This document provides information related to overriding the maxFileSize values allowed when uploading files into the Studio. +Currently the override affects the following areas of the platform: + +* ``Content -> Files``: This is the general location for files to be uploaded into Studio. +* ``Content -> Pages & Resources -> Textbooks``: This is the location specifically for uploading textbooks into a course. + +In addition to overriding the value in ``openedx-lms-production-settings`` it is also necessary to modify Caddy's ``max_size`` handling in the ``request_body`` otherwise Caddy will fail to process your file submission(s). + +The following Tutor plugin can be used as a template to configure the override value. +In the example provided, override_value = "1024" means 1024MB or equivalently 1GB. Replace this with your preferred value. + +.. code-block:: python + + from tutor import hooks + + # Instructions / info + # override_value is defined as a string so bad formats (incorrectly entered values) don't crash Python immediately + # User MUST enter only digits as a POSITIVE integer representing a value in MegaBytes (MB), e.g. "1024" for 1GB + # This adds the value to the MFE_Config API as well as the CaddyFile CMS block + + override_value = "1024" + + # --- Validation --- + try: + override_int = int(override_value) + if override_int <= 0: + raise ValueError + except ValueError: + raise ValueError( + f"Invalid override_value: {override_value}. " + "It must be a positive integer without units (e.g., 1048)." + ) + + # --- Config patches --- + hooks.Filters.ENV_PATCHES.add_items([ + ( + "openedx-lms-production-settings", + f""" + MFE_CONFIG["OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB"] = "{override_int}" + """ + ), + ]) + + hooks.Filters.ENV_PATCHES.add_item( + ( + "caddyfile-cms", + f""" + # Maximum asset upload size in CMS/Studio + handle /assets/* {{ + request_body {{ + max_size {override_int}MB + }} + }} + """ + ) + ) + +Assuming your plugin is named ``override_max_asset_upload_size.py``: + +* activate your plugin: ``tutor plugins enable override_max_asset_upload_size`` +* restart your server instance: ``tutor local stop && tutor local start -d`` +* validation: open the ``Files & Uploads`` page and confirm that your new override value is displayed instead of the default 20MB limit diff --git a/src/constants.js b/src/constants.js index beb4a74d1f..c3920db7b2 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,3 +1,5 @@ +import { getConfig } from '@edx/frontend-platform'; + export const DATE_FORMAT = 'MM/dd/yyyy'; export const TIME_FORMAT = 'HH:mm'; export const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss\\Z'; @@ -52,7 +54,16 @@ export const DECODED_ROUTES = { ], }; -export const UPLOAD_FILE_MAX_SIZE = 20 * 1024 * 1024; // 100mb +// FilesUpload page - Default max size: 20MB else use env override if exists and valid number +const DEFAULT_UPLOAD_FILE_MAX_SIZE = 20 * 1024 * 1024; // 20 MB + +export const getUploadFileMaxSize = () => { + const config = getConfig(); + const overrideMaxFileSizeMB = parseInt(config.OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB, 10); + return !Number.isNaN(overrideMaxFileSizeMB) && overrideMaxFileSizeMB > 0 + ? overrideMaxFileSizeMB * 1024 * 1024 + : DEFAULT_UPLOAD_FILE_MAX_SIZE; +}; export const COURSE_BLOCK_NAMES = ({ chapter: { id: 'chapter', name: 'Section' }, diff --git a/src/constants.test.ts b/src/constants.test.ts new file mode 100644 index 0000000000..c54dd4a299 --- /dev/null +++ b/src/constants.test.ts @@ -0,0 +1,35 @@ +import { mergeConfig } from '@edx/frontend-platform'; +import { getUploadFileMaxSize } from '@src/constants'; + +const DEFAULT_MAX = 20 * 1024 * 1024; + +describe('getUploadFileMaxSize()', () => { + afterEach(() => { + // Reset config after each test to avoid leaks + mergeConfig({}); + }); + + it('returns the global default when no config value is set', () => { + expect(getUploadFileMaxSize()).toEqual(DEFAULT_MAX); + }); + + it('returns OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB when set to a valid positive integer', () => { + mergeConfig({ OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB: 7 }); + expect(getUploadFileMaxSize()).toEqual(7 * 1024 * 1024); + }); + + it('falls back to default when override is not a number', () => { + mergeConfig({ OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB: 'not-a-number' as any }); + expect(getUploadFileMaxSize()).toEqual(DEFAULT_MAX); + }); + + it('falls back to default when override is 0', () => { + mergeConfig({ OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB: 0 }); + expect(getUploadFileMaxSize()).toEqual(DEFAULT_MAX); + }); + + it('falls back to default when override is negative', () => { + mergeConfig({ OVERRIDE_UPLOAD_FILE_MAX_SIZE_IN_MB: -5 }); + expect(getUploadFileMaxSize()).toEqual(DEFAULT_MAX); + }); +}); diff --git a/src/files-and-videos/files-page/FilesPage.jsx b/src/files-and-videos/files-page/FilesPage.jsx index 0976cc9bb6..df6c63fc3e 100644 --- a/src/files-and-videos/files-page/FilesPage.jsx +++ b/src/files-and-videos/files-page/FilesPage.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { CheckboxFilter, Container } from '@openedx/paragon'; +import { getUploadFileMaxSize } from '@src/constants'; import Placeholder from '../../editors/Placeholder'; import { RequestStatus } from '../../data/constants'; @@ -90,7 +91,7 @@ const FilesPage = ({ usageErrorMessages: errorMessages.usageMetrics, fileType: 'file', }; - const maxFileSize = 20 * 1048576; + const maxFileSize = getUploadFileMaxSize(); const activeColumn = { id: 'activeStatus', diff --git a/src/generic/modal-dropzone/ModalDropzone.jsx b/src/generic/modal-dropzone/ModalDropzone.jsx index 8133910785..e8457fe288 100644 --- a/src/generic/modal-dropzone/ModalDropzone.jsx +++ b/src/generic/modal-dropzone/ModalDropzone.jsx @@ -13,9 +13,9 @@ import { } from '@openedx/paragon'; import { FileUpload as FileUploadIcon } from '@openedx/paragon/icons'; +import { getUploadFileMaxSize } from '@src/constants'; import useModalDropzone from './useModalDropzone'; import messages from './messages'; -import { UPLOAD_FILE_MAX_SIZE } from '../../constants'; const ModalDropzone = ({ fileTypes, @@ -30,7 +30,7 @@ const ModalDropzone = ({ onChange, onSavingStatus, onSelectFile, - maxSize = UPLOAD_FILE_MAX_SIZE, + maxSize, }) => { const { intl, @@ -48,7 +48,7 @@ const ModalDropzone = ({ const invalidSizeMore = invalidFileSizeMore || intl.formatMessage( messages.uploadImageDropzoneInvalidSizeMore, - { maxSize: maxSize / (1000 * 1000) }, + { maxSize: (maxSize || getUploadFileMaxSize()) / (1024 * 1024) }, ); const inputComponent = previewUrl ? ( @@ -129,7 +129,7 @@ ModalDropzone.defaultProps = { imageHelpText: '', previewComponent: null, imageDropzoneText: '', - maxSize: UPLOAD_FILE_MAX_SIZE, + maxSize: undefined, invalidFileSizeMore: '', onSelectFile: null, }; diff --git a/src/textbooks/textbook-form/TextbookForm.jsx b/src/textbooks/textbook-form/TextbookForm.jsx index dfc472ce4f..db92b6a8d4 100644 --- a/src/textbooks/textbook-form/TextbookForm.jsx +++ b/src/textbooks/textbook-form/TextbookForm.jsx @@ -17,11 +17,11 @@ import { useToggle, } from '@openedx/paragon'; +import { getUploadFileMaxSize } from '@src/constants'; import FormikControl from '../../generic/FormikControl'; import PromptIfDirty from '../../generic/prompt-if-dirty/PromptIfDirty'; import ModalDropzone from '../../generic/modal-dropzone/ModalDropzone'; import { useModel } from '../../generic/model-store'; -import { UPLOAD_FILE_MAX_SIZE } from '../../constants'; import textbookFormValidationSchema from './validations'; import messages from './messages'; @@ -171,7 +171,7 @@ const TextbookForm = ({ onSavingStatus={onSavingStatus} invalidFileSizeMore={intl.formatMessage( messages.uploadModalFileInvalidSizeText, - { maxSize: UPLOAD_FILE_MAX_SIZE / (1000 * 1000) }, + { maxSize: getUploadFileMaxSize() / (1024 * 1024) }, )} onSelectFile={setSelectedFile} previewComponent={( @@ -180,7 +180,7 @@ const TextbookForm = ({ {selectedFile} )} - maxSize={UPLOAD_FILE_MAX_SIZE} + maxSize={getUploadFileMaxSize()} />