Skip to content

Commit b3c3f6f

Browse files
committed
Avoid using storage path method
1 parent cfcebee commit b3c3f6f

2 files changed

Lines changed: 78 additions & 21 deletions

File tree

contentcuration/kolibri_public/tests/test_export_channels_to_kolibri_public.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,71 @@ def postprocess(self, target):
4646
return target
4747

4848

49+
class FileSystemStorageWithoutPath:
50+
"""
51+
A wrapper around FileSystemStorage that does not expose the `path` method,
52+
as this will not be available in production where S3Storage is used.
53+
This cannot be solved by just mocking the `path` method, because
54+
it is used by the `FileSystemStorage` class internally.
55+
"""
56+
57+
def __init__(self, *args, **kwargs):
58+
self._inner = FileSystemStorage(*args, **kwargs)
59+
60+
def __getattr__(self, name):
61+
if name == "path":
62+
raise NotImplementedError(
63+
"The 'path' method is intentionally not available."
64+
)
65+
return getattr(self._inner, name)
66+
67+
def __dir__(self):
68+
return [x for x in dir(self._inner) if x != "path"]
69+
70+
71+
class FileSystemStorageWithoutPathTestCase(TestCase):
72+
# Sanity-checks that the wrapper above works as expected,
73+
# not actually testing application code
74+
75+
def setUp(self):
76+
super().setUp()
77+
78+
self._temp_directory_ctx = tempfile.TemporaryDirectory()
79+
self.temp_dir = self._temp_directory_ctx.__enter__()
80+
81+
self.storage = FileSystemStorageWithoutPath(location=self.temp_dir)
82+
83+
def tearDown(self):
84+
self._temp_directory_ctx.__exit__(None, None, None)
85+
super().tearDown()
86+
87+
def test_open_works(self):
88+
test_content = "test content"
89+
90+
with self.storage.open("filename", "w") as f:
91+
f.write(test_content)
92+
93+
with open(os.path.join(self.temp_dir, "filename"), "r") as f:
94+
content = f.read()
95+
self.assertEqual(content, test_content)
96+
97+
def test_path_does_not_work(self):
98+
with self.assertRaises(NotImplementedError):
99+
self.storage.path("filename")
100+
101+
49102
class ExportTestCase(TestCase):
50103
def setUp(self):
51104
super().setUp()
52105

53106
self._temp_directory_ctx = tempfile.TemporaryDirectory()
54107
test_db_root_dir = self._temp_directory_ctx.__enter__()
55108

56-
storage = FileSystemStorage(location=test_db_root_dir)
109+
self.storage = FileSystemStorageWithoutPath(location=test_db_root_dir)
57110

58111
self._storage_patch_ctx = mock.patch(
59112
"kolibri_public.utils.export_channel_to_kolibri_public.storage",
60-
new=storage,
113+
new=self.storage,
61114
)
62115
self._storage_patch_ctx.__enter__()
63116

contentcuration/kolibri_public/utils/export_channel_to_kolibri_public.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@ class using_temp_migrated_database:
2424
database and then using this temporary database.
2525
"""
2626

27-
def __init__(self, database_path):
28-
self.database_path = database_path
27+
def __init__(self, database_storage_path):
28+
self.database_path = database_storage_path
2929
self._inner_mgr = None
3030

3131
def __enter__(self):
3232
self._named_temporary_file_mgr = tempfile.NamedTemporaryFile(suffix=".sqlite3")
3333
self.temp_database_file = self._named_temporary_file_mgr.__enter__()
3434

35-
shutil.copy(self.database_path, self.temp_database_file.name)
35+
with storage.open(self.database_path, "rb") as db_file:
36+
shutil.copyfileobj(db_file, self.temp_database_file)
3637
self.temp_database_file.seek(0)
3738

3839
with using_content_database(self.temp_database_file.name):
@@ -66,25 +67,23 @@ def export_channel_to_kolibri_public(
6667
)
6768
unversioned_db_filename = "{id}.sqlite3".format(id=channel_id)
6869

69-
versioned_db_path = storage.path(
70-
os.path.join(settings.DB_ROOT, versioned_db_filename)
71-
)
72-
unversioned_db_path = storage.path(
73-
os.path.join(settings.DB_ROOT, unversioned_db_filename)
70+
versioned_db_storage_path = os.path.join(settings.DB_ROOT, versioned_db_filename)
71+
unversioned_db_storage_path = os.path.join(
72+
settings.DB_ROOT, unversioned_db_filename
7473
)
7574

7675
if channel_version is None:
77-
db_path = unversioned_db_path
76+
db_storage_path = unversioned_db_storage_path
7877
else:
79-
db_path = versioned_db_path
78+
db_storage_path = versioned_db_storage_path
8079
_possibly_migrate_unversioned_database(
8180
channel_id=channel_id,
8281
channel_version=channel_version,
83-
unversioned_db_path=unversioned_db_path,
84-
versioned_db_path=versioned_db_path,
82+
unversioned_db_storage_path=unversioned_db_storage_path,
83+
versioned_db_storage_path=versioned_db_storage_path,
8584
)
8685

87-
with using_temp_migrated_database(db_path):
86+
with using_temp_migrated_database(db_storage_path):
8887
channel = ExportedChannelMetadata.objects.get(id=channel_id)
8988
logger.info(
9089
"Found channel {} for id: {} mapping now".format(channel.name, channel_id)
@@ -102,25 +101,30 @@ def export_channel_to_kolibri_public(
102101
def _possibly_migrate_unversioned_database(
103102
channel_id,
104103
channel_version,
105-
unversioned_db_path,
106-
versioned_db_path,
104+
unversioned_db_storage_path,
105+
versioned_db_storage_path,
107106
):
108107
"""
109108
Older channels may only have a single database file and not
110109
versioned databases. If this is the case and the requested channel version
111110
is present in the single database file, this function copies the database to a file
112111
containing the version in the filename.
113112
"""
114-
if os.path.exists(versioned_db_path) or not os.path.exists(unversioned_db_path):
113+
if storage.exists(versioned_db_storage_path) or not storage.exists(
114+
unversioned_db_storage_path
115+
):
115116
return
116117

117-
with using_temp_migrated_database(unversioned_db_path):
118+
with using_temp_migrated_database(unversioned_db_storage_path):
118119
contains_requested_version = ExportedChannelMetadata.objects.filter(
119120
id=channel_id, version=channel_version
120121
).exists()
121122

122123
if contains_requested_version:
123124
logger.info(
124-
f"Migrating unversioned database {unversioned_db_path} to versioned database {versioned_db_path}"
125+
f"Migrating unversioned database {unversioned_db_storage_path} to versioned database {versioned_db_storage_path}"
125126
)
126-
shutil.copy(unversioned_db_path, versioned_db_path)
127+
128+
with storage.open(unversioned_db_storage_path, "rb") as unversioned_db_file:
129+
with storage.open(versioned_db_storage_path, "wb") as versioned_db_file:
130+
shutil.copyfileobj(unversioned_db_file, versioned_db_file)

0 commit comments

Comments
 (0)