Skip to content

Commit 2f6e2af

Browse files
committed
Fix ensure_versioned_database_exists to not open a file for writing
1 parent 794cafe commit 2f6e2af

File tree

5 files changed

+85
-60
lines changed

5 files changed

+85
-60
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from django.core.files.storage import FileSystemStorage
2+
3+
4+
class RestrictedFileSystemStorage:
5+
"""
6+
A wrapper around FileSystemStorage that is restricted to more closely match
7+
the behavior of S3Storage which is used in production. In particuler,
8+
it does not expose the `path` method, and opening files for writing
9+
is not allowed.
10+
This cannot be solved by just mocking the `path` method, because
11+
it is used by the `FileSystemStorage` class internally.
12+
"""
13+
14+
def __init__(self, *args, **kwargs):
15+
self._inner = FileSystemStorage(*args, **kwargs)
16+
17+
def __getattr__(self, name):
18+
if name == "path":
19+
raise NotImplementedError(
20+
"The 'path' method is intentionally not available."
21+
)
22+
return getattr(self._inner, name)
23+
24+
def open(self, name, mode="rb"):
25+
if "w" in mode:
26+
raise ValueError(
27+
"Opening files for writing will not be available in production."
28+
)
29+
return self._inner.open(name, mode)
30+
31+
def __dir__(self):
32+
return [x for x in dir(self._inner) if x != "path"]

contentcuration/contentcuration/tests/utils/test_publish.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from unittest import mock
44

55
from django.conf import settings
6-
from django.core.files.storage import FileSystemStorage
76

87
from contentcuration.tests import testdata
98
from contentcuration.tests.base import StudioTestCase
9+
from contentcuration.tests.utils.restricted_filesystemstorage import (
10+
RestrictedFileSystemStorage,
11+
)
1012
from contentcuration.utils.publish import ensure_versioned_database_exists
1113

1214

@@ -17,7 +19,7 @@ def setUp(self):
1719
self._temp_directory_ctx = tempfile.TemporaryDirectory()
1820
self.test_db_root_dir = self._temp_directory_ctx.__enter__()
1921

20-
storage = FileSystemStorage(location=self.test_db_root_dir)
22+
storage = RestrictedFileSystemStorage(location=self.test_db_root_dir)
2123

2224
self._storage_patch_ctx = mock.patch(
2325
"contentcuration.utils.publish.storage",
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import tempfile
2+
3+
from django.core.files.base import ContentFile
4+
from django.test import TestCase
5+
6+
from contentcuration.tests.utils.restricted_filesystemstorage import (
7+
RestrictedFileSystemStorage,
8+
)
9+
10+
11+
class RestrictedFileSystemStorageTestCase(TestCase):
12+
# Sanity-checks that the RestrictedFileSystemStorage wrapper used in tests
13+
# works as expected, not actually testing application code
14+
15+
def setUp(self):
16+
super().setUp()
17+
18+
self._temp_directory_ctx = tempfile.TemporaryDirectory()
19+
self.temp_dir = self._temp_directory_ctx.__enter__()
20+
21+
self.storage = RestrictedFileSystemStorage(location=self.temp_dir)
22+
23+
def tearDown(self):
24+
self._temp_directory_ctx.__exit__(None, None, None)
25+
super().tearDown()
26+
27+
def test_opening_for_read_works(self):
28+
test_content = "test content"
29+
30+
self.storage.save("filename", ContentFile(test_content))
31+
with self.storage.open("filename", "r") as f:
32+
content = f.read()
33+
self.assertEqual(content, test_content)
34+
35+
def test_opening_for_write_does_not_work(self):
36+
test_content = "test content"
37+
38+
with self.assertRaises(ValueError):
39+
with self.storage.open("filename", "w") as f:
40+
f.write(test_content)
41+
42+
def test_path_does_not_work(self):
43+
with self.assertRaises(NotImplementedError):
44+
self.storage.path("filename")

contentcuration/contentcuration/utils/publish.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import json
33
import logging as logmodule
44
import os
5-
import shutil
65
import tempfile
76
import time
87
import uuid
@@ -1119,8 +1118,7 @@ def ensure_versioned_database_exists(channel):
11191118
)
11201119

11211120
with storage.open(unversioned_db_storage_path, "rb") as unversioned_db_file:
1122-
with storage.open(versioned_db_storage_path, "wb") as versioned_db_file:
1123-
shutil.copyfileobj(unversioned_db_file, versioned_db_file)
1121+
storage.save(versioned_db_storage_path, unversioned_db_file)
11241122

11251123
logging.info(
11261124
f"Versioned database for channel {channel.id} did not exist, copied the unversioned database to {versioned_db_storage_path}."

contentcuration/kolibri_public/tests/test_export_channels_to_kolibri_public.py

Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from unittest import mock
66

77
from django.conf import settings
8-
from django.core.files.storage import FileSystemStorage
98
from django.core.management import call_command
109
from django.test import TestCase
1110
from kolibri_content.apps import KolibriContentConfig
@@ -18,59 +17,9 @@
1817
)
1918

2019
from contentcuration.models import Country
21-
22-
23-
class FileSystemStorageWithoutPath:
24-
"""
25-
A wrapper around FileSystemStorage that does not expose the `path` method,
26-
as this will not be available in production where S3Storage is used.
27-
This cannot be solved by just mocking the `path` method, because
28-
it is used by the `FileSystemStorage` class internally.
29-
"""
30-
31-
def __init__(self, *args, **kwargs):
32-
self._inner = FileSystemStorage(*args, **kwargs)
33-
34-
def __getattr__(self, name):
35-
if name == "path":
36-
raise NotImplementedError(
37-
"The 'path' method is intentionally not available."
38-
)
39-
return getattr(self._inner, name)
40-
41-
def __dir__(self):
42-
return [x for x in dir(self._inner) if x != "path"]
43-
44-
45-
class FileSystemStorageWithoutPathTestCase(TestCase):
46-
# Sanity-checks that the wrapper above works as expected,
47-
# not actually testing application code
48-
49-
def setUp(self):
50-
super().setUp()
51-
52-
self._temp_directory_ctx = tempfile.TemporaryDirectory()
53-
self.temp_dir = self._temp_directory_ctx.__enter__()
54-
55-
self.storage = FileSystemStorageWithoutPath(location=self.temp_dir)
56-
57-
def tearDown(self):
58-
self._temp_directory_ctx.__exit__(None, None, None)
59-
super().tearDown()
60-
61-
def test_open_works(self):
62-
test_content = "test content"
63-
64-
with self.storage.open("filename", "w") as f:
65-
f.write(test_content)
66-
67-
with open(os.path.join(self.temp_dir, "filename"), "r") as f:
68-
content = f.read()
69-
self.assertEqual(content, test_content)
70-
71-
def test_path_does_not_work(self):
72-
with self.assertRaises(NotImplementedError):
73-
self.storage.path("filename")
20+
from contentcuration.tests.utils.restricted_filesystemstorage import (
21+
RestrictedFileSystemStorage,
22+
)
7423

7524

7625
class ExportTestCase(TestCase):
@@ -80,7 +29,7 @@ def setUp(self):
8029
self._temp_directory_ctx = tempfile.TemporaryDirectory()
8130
test_db_root_dir = self._temp_directory_ctx.__enter__()
8231

83-
self.storage = FileSystemStorageWithoutPath(location=test_db_root_dir)
32+
self.storage = RestrictedFileSystemStorage(location=test_db_root_dir)
8433

8534
self._storage_patch_ctx = mock.patch(
8635
"kolibri_public.utils.export_channel_to_kolibri_public.storage",

0 commit comments

Comments
 (0)