Skip to content

Commit 936dc23

Browse files
authored
Merge pull request learningequality#6041 from rtibblesbot/issue-5987-e45197
feat: export 64-bit file_size_bigint at publish
2 parents da92f80 + f5f87d0 commit 936dc23

5 files changed

Lines changed: 102 additions & 2 deletions

File tree

contentcuration/contentcuration/tests/test_exportchannel.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from kolibri_content.router import cleanup_content_database_connection
1717
from kolibri_content.router import get_active_content_database
1818
from kolibri_content.router import set_active_content_database
19+
from le_utils.constants import content_kinds
1920
from le_utils.constants import exercises
2021
from le_utils.constants import format_presets
2122
from le_utils.constants import modalities
@@ -57,6 +58,10 @@
5758
pytestmark = pytest.mark.django_db
5859

5960

61+
# Larger than the signed 32-bit maximum (2_147_483_647); ~3 GB.
62+
LARGE_FILE_SIZE = 3 * 1024 ** 3
63+
64+
6065
def description():
6166
return "".join(random.sample(string.printable, 20))
6267

@@ -387,6 +392,36 @@ def setUp(self):
387392
lesson_topic.extra_fields = {"options": {"modality": modalities.LESSON}}
388393
lesson_topic.save()
389394

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+
390425
set_channel_icon_encoding(self.content_channel)
391426
self.tempdb = create_content_database(
392427
self.content_channel, True, self.admin_user.id, True
@@ -506,6 +541,22 @@ def test_contentnode_file_size_data(self):
506541
for file in files.prefetch_related("local_file"):
507542
self.assertEqual(file.file_size, file.local_file.file_size)
508543

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(pk=self.large_file_checksum)
557+
self.assertEqual(local_file.file_size_bigint, LARGE_FILE_SIZE)
558+
self.assertIsNone(local_file.file_size)
559+
509560
def test_file_included_presets_renderable(self):
510561
# Every non-supplementary (renderable) exported file carries its own preset bit.
511562
files = kolibri_models.File.objects.filter(supplementary=False)

contentcuration/contentcuration/utils/publish.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
PERSEUS_IMG_DIR = exercises.IMG_PLACEHOLDER + "/images"
6666
THUMBNAIL_DIMENSION = 128
6767
MIN_SCHEMA_VERSION = "1"
68+
# Largest value the legacy 32-bit LocalFile.file_size / File.file_size columns hold.
69+
INT_32BIT_MAX = 2 ** 31 - 1
6870
PUBLISHING_UPDATE_THRESHOLD = 3600
6971

7072

@@ -659,11 +661,23 @@ def create_associated_file_objects(kolibrinode, ccnode):
659661
create_associated_thumbnail(ccnode, ccfilemodel) or ccfilemodel
660662
)
661663

664+
# The true size lives in the studio#5974 file_size_bigint shadow (the
665+
# legacy 32-bit file_size cannot hold >2.1 GB); fall back to file_size
666+
# for rows the shadow has not been backfilled onto yet.
667+
real_size = ccfilemodel.file_size_bigint
668+
if real_size is None:
669+
real_size = ccfilemodel.file_size
670+
if real_size is not None and real_size > INT_32BIT_MAX:
671+
legacy_size = None
672+
else:
673+
legacy_size = real_size
674+
662675
kolibrilocalfilemodel, new = kolibrimodels.LocalFile.objects.get_or_create(
663676
pk=ccfilemodel.checksum,
664677
defaults={
665678
"extension": fformat.extension,
666-
"file_size": ccfilemodel.file_size,
679+
"file_size": legacy_size,
680+
"file_size_bigint": real_size,
667681
},
668682
)
669683

@@ -687,7 +701,7 @@ def create_associated_file_objects(kolibrinode, ccnode):
687701
checksum=ccfilemodel.checksum,
688702
extension=fformat.extension,
689703
available=True, # TODO: Set this to False, once we have availability stamping implemented in Kolibri
690-
file_size=ccfilemodel.file_size,
704+
file_size=legacy_size,
691705
contentnode=kolibrinode,
692706
preset=preset.pk,
693707
supplementary=preset.supplementary,

contentcuration/kolibri_content/base_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ class LocalFile(models.Model):
152152
)
153153
available = models.BooleanField(default=False)
154154
file_size = models.IntegerField(blank=True, null=True)
155+
file_size_bigint = models.BigIntegerField(blank=True, null=True)
155156

156157
class Meta:
157158
abstract = True
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.db import migrations
2+
from django.db import models
3+
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
("content", "0024_file_included_presets"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="localfile",
14+
name="file_size_bigint",
15+
field=models.BigIntegerField(blank=True, null=True),
16+
),
17+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.db import migrations
2+
from django.db import models
3+
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
("kolibri_public", "0009_file_included_presets"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="localfile",
14+
name="file_size_bigint",
15+
field=models.BigIntegerField(blank=True, null=True),
16+
),
17+
]

0 commit comments

Comments
 (0)