|
16 | 16 | from kolibri_content.router import cleanup_content_database_connection |
17 | 17 | from kolibri_content.router import get_active_content_database |
18 | 18 | from kolibri_content.router import set_active_content_database |
| 19 | +from le_utils.constants import content_kinds |
19 | 20 | from le_utils.constants import exercises |
20 | 21 | from le_utils.constants import format_presets |
21 | 22 | from le_utils.constants import modalities |
|
57 | 58 | pytestmark = pytest.mark.django_db |
58 | 59 |
|
59 | 60 |
|
| 61 | +# Larger than the signed 32-bit maximum (2_147_483_647); ~3 GB. |
| 62 | +LARGE_FILE_SIZE = 3 * 1024 ** 3 |
| 63 | + |
| 64 | + |
60 | 65 | def description(): |
61 | 66 | return "".join(random.sample(string.printable, 20)) |
62 | 67 |
|
@@ -387,6 +392,36 @@ def setUp(self): |
387 | 392 | lesson_topic.extra_fields = {"options": {"modality": modalities.LESSON}} |
388 | 393 | lesson_topic.save() |
389 | 394 |
|
| 395 | + document_kind, _ = cc.ContentKind.objects.get_or_create( |
| 396 | + kind=content_kinds.DOCUMENT |
| 397 | + ) |
| 398 | + large_file_node = cc.ContentNode( |
| 399 | + kind=document_kind, |
| 400 | + parent=self.content_channel.main_tree, |
| 401 | + title="Large file node", |
| 402 | + node_id=uuid.uuid4(), |
| 403 | + content_id=uuid.uuid4(), |
| 404 | + sort_order=1, |
| 405 | + complete=True, |
| 406 | + ) |
| 407 | + large_file_node.save() |
| 408 | + |
| 409 | + large_db_file = create_studio_file( |
| 410 | + b"large file body", preset="document", ext="pdf" |
| 411 | + )["db_file"] |
| 412 | + # A >2.1 GB file cannot fit the legacy 32-bit File.file_size column; its |
| 413 | + # true size lives in the studio#5974 file_size_bigint shadow, with the |
| 414 | + # legacy file_size left NULL. |
| 415 | + large_db_file.file_size = None |
| 416 | + large_db_file.contentnode = large_file_node |
| 417 | + large_db_file.save() |
| 418 | + # Set the shadow directly; the mirror trigger leaves it alone because |
| 419 | + # file_size is unchanged (NULL). |
| 420 | + cc.File.objects.filter(pk=large_db_file.pk).update( |
| 421 | + file_size_bigint=LARGE_FILE_SIZE |
| 422 | + ) |
| 423 | + self.large_file_checksum = large_db_file.checksum |
| 424 | + |
390 | 425 | set_channel_icon_encoding(self.content_channel) |
391 | 426 | self.tempdb = create_content_database( |
392 | 427 | self.content_channel, True, self.admin_user.id, True |
@@ -506,6 +541,24 @@ def test_contentnode_file_size_data(self): |
506 | 541 | for file in files.prefetch_related("local_file"): |
507 | 542 | self.assertEqual(file.file_size, file.local_file.file_size) |
508 | 543 |
|
| 544 | + def test_localfile_file_size_bigint_matches_small_files(self): |
| 545 | + # Files that fit in 32 bits write the same value to both columns. |
| 546 | + local_files = kolibri_models.LocalFile.objects.exclude( |
| 547 | + pk=self.large_file_checksum |
| 548 | + ) |
| 549 | + assert local_files.count() > 0 |
| 550 | + for local_file in local_files: |
| 551 | + self.assertEqual(local_file.file_size_bigint, local_file.file_size) |
| 552 | + |
| 553 | + def test_localfile_large_file_size_bigint(self): |
| 554 | + # A >2.1 GB file keeps its real size in file_size_bigint and NULLs the |
| 555 | + # legacy 32-bit file_size. |
| 556 | + local_file = kolibri_models.LocalFile.objects.get( |
| 557 | + pk=self.large_file_checksum |
| 558 | + ) |
| 559 | + self.assertEqual(local_file.file_size_bigint, LARGE_FILE_SIZE) |
| 560 | + self.assertIsNone(local_file.file_size) |
| 561 | + |
509 | 562 | def test_file_included_presets_renderable(self): |
510 | 563 | # Every non-supplementary (renderable) exported file carries its own preset bit. |
511 | 564 | files = kolibri_models.File.objects.filter(supplementary=False) |
|
0 commit comments