From eac2757a22a033880149a8edb34b93266bc7a6d7 Mon Sep 17 00:00:00 2001 From: nauman4386 Date: Sat, 13 May 2023 22:59:12 +0500 Subject: [PATCH] Custom storage for SCORM xblock --- README.rst | 20 ++++++++++-- openedxscorm/scormxblock.py | 31 +++++++++++++++++- openedxscorm/storage.py | 63 +++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 openedxscorm/storage.py diff --git a/README.rst b/README.rst index 5f201a8..2c2b0ea 100644 --- a/README.rst +++ b/README.rst @@ -82,12 +82,28 @@ By default, static assets are stored in the default Django storage backend. To o "STORAGE_FUNC": scorm_storage, } -This should be added both to the LMS and the CMS settings. Instead of a function, a string that points to an importable module may be passed:: +## Configuration for SCORM XBlock for AWS S3 storage use case +In order to configure the SCORM XBlock for AWS S3 storage use case, you'll need to modify the `XBLOCK_SETTINGS` in both the `lms/envs/private.py` and `cms/envs/private.py` files. + +Add the following lines to these files:: + + # XBlock settings for ScormXBlock XBLOCK_SETTINGS["ScormXBlock"] = { - "STORAGE_FUNC": "my.custom.storage.module.get_scorm_storage_function", + "STORAGE_FUNC": "openedxscorm.storage.s3", + "SCORM_S3_BUCKET_NAME": "", + "SCORM_S3_QUERY_AUTH": True, + "SCORM_S3_EXPIRES_IN": 604800 } +This configuration is specifically for when using an S3 bucket to store SCORM assets. + +The STORAGE_FUNC is set to the s3 storage module in the SCORM XBlock. +SCORM_S3_BUCKET_NAME should be replaced with your specific S3 bucket name. If you do not set SCORM_S3_BUCKET_NAME, you should have a bucket named ScormS3Bucket which you have to newly-created in your AWS account. +SCORM_S3_QUERY_AUTH is a boolean flag that indicates whether or not to use query string authentication for your S3 URLs. If your bucket is public, you should set this value to False. If it is private, no need to set it. +SCORM_S3_EXPIRES_IN sets the time duration (in seconds) for the presigned URLs to stay valid. The default value here is 604800 which corresponds to one week. If this is not set, the default value will be used. +Once you've made these changes, save both files and restart your LMS and Studio instances for the changes to take effect. + Development ----------- diff --git a/openedxscorm/scormxblock.py b/openedxscorm/scormxblock.py index ea9cc77..65b8615 100644 --- a/openedxscorm/scormxblock.py +++ b/openedxscorm/scormxblock.py @@ -5,6 +5,8 @@ import re import xml.etree.ElementTree as ET import zipfile +import requests +import mimetypes from django.core.files.base import ContentFile from django.core.files.storage import default_storage @@ -12,6 +14,7 @@ from django.template import Context, Template from django.utils import timezone from django.utils.module_loading import import_string +from urllib.parse import urlparse from webob import Response import pkg_resources from six import string_types @@ -61,7 +64,6 @@ class ScormXBlock(XBlock, CompletableXBlockMixin): Note that neither the folder the folder nor the package file are deleted when the xblock is removed. - By default, static assets are stored in the default Django storage backend. To override this behaviour, you should define a custom storage function. This function must take the xblock instance as its first and only argument. For instance, @@ -202,6 +204,33 @@ def student_view(self, context=None): ) return frag + @XBlock.handler + def assets_proxy(self, request, _suffix): + """ + Proxy view for serving assets. It receives a request with the path to the asset to serve, generates a pre-signed + URL to access the content in the AWS S3 bucket, and returns a redirect response to the pre-signed URL. + + Parameters: + ---------- + request : django.http.request.HttpRequest + HTTP request object containing the path to the asset to serve. + _suffix : str + The part of the URL after 'assets_proxy/', i.e., the path to the asset to serve. + + Returns: + ------- + Response object containing the content of the requested file with the appropriate content type. + """ + path = urlparse(_suffix).path + file_name = os.path.basename(path) + signed_url = self.storage.url(path) + file_type, _ = mimetypes.guess_type(file_name) + file_content = requests.get(signed_url).content + + return Response( + file_content, content_type=file_type + ) + def studio_view(self, context=None): # Note that we cannot use xblockutils's StudioEditableXBlockMixin because we # need to support package file uploads. diff --git a/openedxscorm/storage.py b/openedxscorm/storage.py new file mode 100644 index 0000000..65cdb70 --- /dev/null +++ b/openedxscorm/storage.py @@ -0,0 +1,63 @@ +""" +Storage backend for scorm metadata export. +""" +import os + +from django.core.files.storage import get_storage_class +from storages.backends.s3boto3 import S3Boto3Storage + + +class S3ScormStorage(S3Boto3Storage): + """ + S3 backend for scorm metadata export + """ + def __init__(self, xblock, bucket, querystring_auth, querystring_expire): + self.xblock = xblock + super().__init__(bucket=bucket, querystring_auth=querystring_auth, + querystring_expire=querystring_expire) + + def url(self, name): + """ + Override url method of S3Boto3Storage + """ + if not self.querystring_auth: + return super().url(name) + + if name.startswith(self.xblock.extract_folder_path): + handler_url = self.xblock.runtime.handler_url(self.xblock, 'assets_proxy') + + # remove trailing '?' if it's present + if handler_url.endswith('?'): + handler_url = handler_url[:-1] + # add '/' if not present at the end + elif not handler_url.endswith('/'): + handler_url += '/' + + # construct the URL for proxy function + return f'{handler_url}{self.xblock.index_page_path}' + + return super().url(os.path.join(self.xblock.extract_folder_path, name)) + + +def s3(xblock): + """ + Creates and returns an instance of the S3ScormStorage class. + + This function takes an xblock instance as its argument and returns an instance + of the S3ScormStorage class. The S3ScormStorage class is defined in the + 'openedxscorm.storage' module and provides storage functionality specific to + SCORM XBlock. + + Args: + xblock (XBlock): An instance of the SCORM XBlock. + + Returns: + S3ScormStorage: An instance of the S3ScormStorage class. + """ + bucket = xblock.xblock_settings.get('SCORM_S3_BUCKET_NAME', "ScormS3Bucket") + querystring_auth = xblock.xblock_settings.get('SCORM_S3_QUERY_AUTH', True) + querystring_expire = xblock.xblock_settings.get('SCORM_S3_EXPIRES_IN', 604800) + storage_class = get_storage_class('openedxscorm.storage.S3ScormStorage') + return storage_class(xblock=xblock, bucket=bucket, + querystring_auth=querystring_auth, + querystring_expire=querystring_expire) diff --git a/setup.py b/setup.py index 987531b..6edab3d 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ def package_data(pkg, roots): setup( name="openedx-scorm-xblock", - version="15.0.0", + version="15.0.1", description="Scorm XBlock for Open edX", long_description=readme, long_description_content_type="text/x-rst",