Skip to content

Commit bbbf092

Browse files
committed
Avoid using storage path method
1 parent a4fbd29 commit bbbf092

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):
@@ -65,25 +66,23 @@ def export_channel_to_kolibri_public(
6566
)
6667
unversioned_db_filename = "{id}.sqlite3".format(id=channel_id)
6768

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

7574
if channel_version is None:
76-
db_path = unversioned_db_path
75+
db_storage_path = unversioned_db_storage_path
7776
else:
78-
db_path = versioned_db_path
77+
db_storage_path = versioned_db_storage_path
7978
_possibly_migrate_unversioned_database(
8079
channel_id=channel_id,
8180
channel_version=channel_version,
82-
unversioned_db_path=unversioned_db_path,
83-
versioned_db_path=versioned_db_path,
81+
unversioned_db_storage_path=unversioned_db_storage_path,
82+
versioned_db_storage_path=versioned_db_storage_path,
8483
)
8584

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

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

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

0 commit comments

Comments
 (0)