1

I'm working with OpenEdX, it has a plugin system, called XBlocks, that in this case allows importing content created by third party "studio apps." This content can be uploaded as a zip file. it is then processed by the following code:

@XBlock.handler

    def studio_submit(self, request, _suffix):

        self.display_name = request.params["display_name"]

        self.width = request.params["width"]

        self.height = request.params["height"]

        self.has_score = request.params["has_score"]

        self.weight = request.params["weight"]

        self.icon_class = "problem" if self.has_score == "True" else "video"



        response = {"result": "success", "errors": []}

        if not hasattr(request.params["file"], "file"):

            # File not uploaded

            return self.json_response(response)



        package_file = request.params["file"].file

        self.update_package_meta(package_file)



        # First, save scorm file in the storage for mobile clients

        if default_storage.exists(self.package_path):

            logger.info('Removing previously uploaded "%s"', self.package_path)

            default_storage.delete(self.package_path)

        default_storage.save(self.package_path, File(package_file))

        logger.info('Scorm "%s" file stored at "%s"', package_file, self.package_path)



        # Then, extract zip file

        if default_storage.exists(self.extract_folder_base_path):

            logger.info(

                'Removing previously unzipped "%s"', self.extract_folder_base_path

            )

            recursive_delete(self.extract_folder_base_path)

        with zipfile.ZipFile(package_file, "r") as scorm_zipfile:

            for zipinfo in scorm_zipfile.infolist():

                default_storage.save(

                    os.path.join(self.extract_folder_path, zipinfo.filename),

                    scorm_zipfile.open(zipinfo.filename),

                )



        try:

            self.update_package_fields()

        except ScormError as e:

            response["errors"].append(e.args[0])



        return self.json_response(response)

where the code

                default_storage.save(

                    os.path.join(self.extract_folder_path, zipinfo.filename),

                    scorm_zipfile.open(zipinfo.filename),

                )

is the origin of the following (Django) error trace:

cms_1             |   File "/openedx/venv/lib/python3.5/site-packages/openedxscorm/scormxblock.py", line 193, in studio_submit
cms_1             |     scorm_zipfile.open(zipinfo.filename),
cms_1             |   File "/openedx/venv/lib/python3.5/site-packages/django/core/files/storage.py", line 52, in save
cms_1             |     return self._save(name, content)
cms_1             |   File "/openedx/venv/lib/python3.5/site-packages/django/core/files/storage.py", line 249, in _save
cms_1             |     raise IOError("%s exists and is not a directory." % directory)
cms_1             | OSError: /openedx/media/scorm/c154229b568d45128e1098b530267a35/a346b1db27aaa89b89b31e1c3e2a1af04482abad/assets exists and is not a directory.

I posted the issue on github too

exception FileExistsError Raised when trying to create a file or directory which already exists. Corresponds to errno EEXIST.

I don't really understand what is going on. It's based on a hairball of javascript in layered docker containers, so I can't readily hack&print for extra info.

The only thing I found was that some of the folders in the zip file are written to the docker volume as files instead of directories at the moment the error is thrown. This may however be expected and these files might be rewritten as or changed to directories later (?) on Linux (?).

The error lists the assets folder

root@93f0d2b9667f:/openedx/media/scorm/5e085cbc04e24b3b911802f7cba44296/92b12100be7651c812a1d29a041153db5ba89239# ls -la
total 84
drwxr-xr-x 2 root root  4096 Aug  2 22:17 .
drwxr-xr-x 3 root root  4096 Aug  2 22:17 ..
-rw-r--r-- 1 root root  4398 Aug  2 22:17 adlcp_rootv1p2.xsd
-rw-r--r-- 1 root root     0 Aug  2 22:17 assets
-rw-r--r-- 1 root root     0 Aug  2 22:17 course
-rw-r--r-- 1 root root 14560 Aug  2 22:17 imscp_rootv1p1p2.xsd
-rw-r--r-- 1 root root  1847 Aug  2 22:17 imsmanifest.xml
-rw-r--r-- 1 root root 22196 Aug  2 22:17 imsmd_rootv1p2p1.xsd
-rw-r--r-- 1 root root  1213 Aug  2 22:17 ims_xml.xsd
-rw-r--r-- 1 root root  1662 Aug  2 22:17 index.html
-rw-r--r-- 1 root root     0 Aug  2 22:17 libraries
-rw-r--r-- 1 root root  1127 Aug  2 22:17 log_output.html
-rw-r--r-- 1 root root   481 Aug  2 22:17 main.html
-rw-r--r-- 1 root root   759 Aug  2 22:17 offline_API_wrapper.js
-rw-r--r-- 1 root root     0 Aug  2 22:17 player
-rw-r--r-- 1 root root  1032 Aug  2 22:17 popup.html
root@93f0d2b9667f:/openedx/media/scorm/5e085cbc04e24b3b911802f7cba44296/92b12100be7651c812a1d29a041153db5ba89239# cd assets
bash: cd: assets: Not a directory
Julius Baer
  • 61
  • 10
  • Deleting the `/assets` folder from the zipped folder changes the error message to the next folder; `/course` so it indeed seems that assets and course are created as files and that this causes the error. There are however zip files with a different structure, where the error is also thrown but the upload succeeds. That would make the error not essential and something else would be going on. That seems rare of course, but there is a small chance. – Julius Baer Aug 03 '20 at 07:23
  • pre-creating all the subdirectories allows the upload to succeed... so it seems to be an error in these bits of Python code and since Django has been well tested, it must be in the XBlock's code – Julius Baer Aug 03 '20 at 08:09

0 Answers0