Skip to content

Commit 8817db4

Browse files
committed
🚚(back) serve legacy videos from Scaleway S3
Videos are now served from the aws/ directory in Scaleway S3. They are served using the django-storage already in place. As the newer videos are already stored in Scaleway, we do not rename these files and allow them to be served without requiring signed URLs.
1 parent 59c5c59 commit 8817db4

File tree

9 files changed

+216
-289
lines changed

9 files changed

+216
-289
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
1111
### Changed
1212

1313
- Serve legacy classroom documents from Scaleway S3 after AWS migration
14+
- Serve legacy videos and related files from Scaleway S3 after AWS migration
1415

1516
## [5.9.1] - 2025-06-25
1617

src/backend/marsha/core/models/video.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -293,28 +293,6 @@ class Meta:
293293
),
294294
]
295295

296-
def get_source_s3_key(self, stamp=None):
297-
"""Compute the S3 key in the source bucket (ID of the video + version stamp).
298-
299-
Parameters
300-
----------
301-
stamp: Type[string]
302-
Passing a value for this argument will return the source S3 key for the video assuming
303-
its active stamp is set to this value. This is useful to create an upload policy for
304-
this prospective version of the video, so that the client can upload the file to S3
305-
and the confirmation lambda can set the `uploaded_on` field to this value only after
306-
the video transcoding job is successful.
307-
308-
Returns
309-
-------
310-
string
311-
The S3 key for the video in the source bucket, where uploaded videos are stored before
312-
they are converted to the destination bucket.
313-
314-
"""
315-
stamp = stamp or self.uploaded_on_stamp()
316-
return f"{self.pk}/video/{self.pk}/{stamp}"
317-
318296
def get_storage_prefix(
319297
self,
320298
stamp=None,
@@ -341,6 +319,10 @@ def get_storage_prefix(
341319
"""
342320
stamp = stamp or self.uploaded_on_stamp()
343321
base = base_dir
322+
323+
if base == AWS_STORAGE_BASE_DIRECTORY:
324+
return f"{base}/{self.pk}"
325+
344326
if base == DELETED_STORAGE_BASE_DIRECTORY:
345327
base = f"{base}/{VOD_STORAGE_BASE_DIRECTORY}"
346328

@@ -693,8 +675,12 @@ def get_storage_prefix(
693675
"""
694676
stamp = stamp or self.uploaded_on_stamp()
695677
base = base_dir
678+
679+
if base_dir == AWS_STORAGE_BASE_DIRECTORY:
680+
return f"{base}/{self.video.pk}/timedtext"
681+
696682
if base_dir == DELETED_STORAGE_BASE_DIRECTORY:
697-
base = f"{DELETED_STORAGE_BASE_DIRECTORY}/{VOD_STORAGE_BASE_DIRECTORY}"
683+
base = f"{base}/{VOD_STORAGE_BASE_DIRECTORY}"
698684

699685
return f"{base}/{self.video.pk}/timedtext/{self.pk}/{stamp}"
700686

src/backend/marsha/core/serializers/video.py

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@
44
from datetime import timedelta
55
from hashlib import sha256
66
from inspect import signature
7-
from urllib.parse import quote_plus
87

98
from django.conf import settings
109
from django.urls import reverse
1110
from django.utils import timezone
12-
from django.utils.text import slugify
1311

1412
from rest_framework import serializers
1513
from sentry_sdk import capture_message
1614

1715
from marsha.core.defaults import (
1816
AWS_PIPELINE,
17+
AWS_STORAGE_BASE_DIRECTORY,
1918
ENDED,
2019
HARVESTED,
2120
IDLE,
@@ -28,7 +27,7 @@
2827
TMP_STORAGE_BASE_DIRECTORY,
2928
)
3029
from marsha.core.models import TimedTextTrack, Video
31-
from marsha.core.serializers.base import TimestampField, get_video_cloudfront_url_params
30+
from marsha.core.serializers.base import TimestampField
3231
from marsha.core.serializers.playlist import PlaylistLiteSerializer
3332
from marsha.core.serializers.shared_live_media import (
3433
SharedLiveMediaId3TagsSerializer,
@@ -37,7 +36,7 @@
3736
from marsha.core.serializers.thumbnail import ThumbnailSerializer
3837
from marsha.core.serializers.timed_text_track import TimedTextTrackSerializer
3938
from marsha.core.storage.storage_class import file_storage
40-
from marsha.core.utils import cloudfront_utils, jitsi_utils, time_utils, xmpp_utils
39+
from marsha.core.utils import jitsi_utils, time_utils, xmpp_utils
4140
from marsha.core.utils.time_utils import to_datetime
4241

4342

@@ -220,40 +219,30 @@ def get_vod_urls(self, obj):
220219
)
221220

222221
if obj.transcode_pipeline == AWS_PIPELINE:
223-
base = f"{settings.AWS_S3_URL_PROTOCOL}://{settings.CLOUDFRONT_DOMAIN}/{obj.pk}"
224-
if settings.CLOUDFRONT_SIGNED_URLS_ACTIVE:
225-
params = get_video_cloudfront_url_params(obj.pk)
226-
227-
filename = f"{slugify(obj.playlist.title)}_{stamp}.mp4"
228-
content_disposition = quote_plus(f"attachment; filename={filename}")
222+
base = obj.get_storage_prefix(base_dir=AWS_STORAGE_BASE_DIRECTORY)
229223

230224
for resolution in obj.resolutions:
231225
# MP4
232-
mp4_url = (
233-
f"{base}/mp4/{stamp}_{resolution}.mp4"
234-
f"?response-content-disposition={content_disposition}"
235-
)
226+
mp4_url = file_storage.url(f"{base}/mp4/{stamp}_{resolution}.mp4")
236227

237228
# Thumbnails
238229
urls["thumbnails"][resolution] = thumbnail_urls.get(
239230
resolution,
240-
f"{base}/thumbnails/{stamp}_{resolution}.0000000.jpg",
231+
file_storage.url(
232+
f"{base}/thumbnails/{stamp}_{resolution}.0000000.jpg"
233+
),
241234
)
242235

243-
# Sign the urls of mp4 videos only if the functionality is activated
244-
if settings.CLOUDFRONT_SIGNED_URLS_ACTIVE:
245-
mp4_url = cloudfront_utils.build_signed_url(mp4_url, params)
246-
247236
urls["mp4"][resolution] = mp4_url
248237

249238
if obj.live_state != HARVESTED:
250239
# Adaptive Bit Rate manifests
251240
urls["manifests"] = {
252-
"hls": f"{base}/cmaf/{stamp}.m3u8",
241+
"hls": file_storage.url(f"{base}/cmaf/{stamp}.m3u8"),
253242
}
254243

255244
# Previews
256-
urls["previews"] = f"{base}/previews/{stamp}_100.jpg"
245+
urls["previews"] = file_storage.url(f"{base}/previews/{stamp}_100.jpg")
257246

258247
elif obj.transcode_pipeline == PEERTUBE_PIPELINE:
259248
base = obj.get_storage_prefix(stamp=stamp)

src/backend/marsha/core/tests/api/video/test_live_to_vod.py

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@ def assert_user_can_call_live_to_vod(self, user, video):
4646
"""Assert the user can convert the live."""
4747
self.assertNotEqual(video.upload_state, READY)
4848

49-
with mock.patch(
50-
"marsha.websocket.utils.channel_layers_utils.dispatch_video_to_groups"
51-
) as mock_dispatch_video_to_groups, mock.patch.object(
52-
api.video, "reopen_room_for_vod"
53-
) as mock_reopen_room, mock.patch(
54-
"marsha.core.serializers.xmpp_utils.generate_jwt"
55-
) as mock_jwt_encode:
49+
with (
50+
mock.patch(
51+
"marsha.websocket.utils.channel_layers_utils.dispatch_video_to_groups"
52+
) as mock_dispatch_video_to_groups,
53+
mock.patch.object(api.video, "reopen_room_for_vod") as mock_reopen_room,
54+
mock.patch(
55+
"marsha.core.serializers.xmpp_utils.generate_jwt"
56+
) as mock_jwt_encode,
57+
):
5658
mock_jwt_encode.return_value = "xmpp_jwt"
5759
jwt_token = UserAccessTokenFactory(user=user)
5860
response = self.client.post(
@@ -155,6 +157,7 @@ def test_live_to_vod_by_playlist_admin(self):
155157
@override_settings(XMPP_CONVERSE_PERSISTENT_STORE="localStorage")
156158
@override_settings(XMPP_DOMAIN="conference.xmpp-server.com")
157159
@override_settings(XMPP_JWT_SHARED_SECRET="xmpp_shared_secret")
160+
@override_settings(MEDIA_URL="https://abc.svc.edge.scw.cloud/")
158161
def test_api_video_instructor_harvested_live_to_vod(self):
159162
"""An instructor can transform a harvested live to a vod."""
160163
video = factories.VideoFactory(
@@ -169,13 +172,15 @@ def test_api_video_instructor_harvested_live_to_vod(self):
169172
)
170173
jwt_token = InstructorOrAdminLtiTokenFactory(playlist=video.playlist)
171174

172-
with mock.patch(
173-
"marsha.websocket.utils.channel_layers_utils.dispatch_video_to_groups"
174-
) as mock_dispatch_video_to_groups, mock.patch.object(
175-
api.video, "reopen_room_for_vod"
176-
) as mock_reopen_room, mock.patch(
177-
"marsha.core.serializers.xmpp_utils.generate_jwt"
178-
) as mock_jwt_encode:
175+
with (
176+
mock.patch(
177+
"marsha.websocket.utils.channel_layers_utils.dispatch_video_to_groups"
178+
) as mock_dispatch_video_to_groups,
179+
mock.patch.object(api.video, "reopen_room_for_vod") as mock_reopen_room,
180+
mock.patch(
181+
"marsha.core.serializers.xmpp_utils.generate_jwt"
182+
) as mock_jwt_encode,
183+
):
179184
mock_jwt_encode.return_value = "xmpp_jwt"
180185
response = self.client.post(
181186
f"/api/videos/{video.id}/live-to-vod/",
@@ -212,29 +217,26 @@ def test_api_video_instructor_harvested_live_to_vod(self):
212217
"title": video.title,
213218
"urls": {
214219
"mp4": {
215-
"240": f"https://abc.cloudfront.net/{video.id}/"
216-
"mp4/1569309880_240.mp4?response-content-disposition=attachment%3B+"
217-
"filename%3Dplaylist-002_1569309880.mp4",
218-
"480": f"https://abc.cloudfront.net/{video.id}/"
219-
"mp4/1569309880_480.mp4?response-content-disposition=attachment%3B+"
220-
"filename%3Dplaylist-002_1569309880.mp4",
221-
"720": f"https://abc.cloudfront.net/{video.id}/"
222-
"mp4/1569309880_720.mp4?response-content-disposition=attachment%3B+"
223-
"filename%3Dplaylist-002_1569309880.mp4",
220+
"240": f"https://abc.svc.edge.scw.cloud/aws/{video.id}/"
221+
"mp4/1569309880_240.mp4",
222+
"480": f"https://abc.svc.edge.scw.cloud/aws/{video.id}/"
223+
"mp4/1569309880_480.mp4",
224+
"720": f"https://abc.svc.edge.scw.cloud/aws/{video.id}/"
225+
"mp4/1569309880_720.mp4",
224226
},
225227
"thumbnails": {
226-
"240": f"https://abc.cloudfront.net/{video.id}/"
228+
"240": f"https://abc.svc.edge.scw.cloud/aws/{video.id}/"
227229
"thumbnails/1569309880_240.0000000.jpg",
228-
"480": f"https://abc.cloudfront.net/{video.id}/"
230+
"480": f"https://abc.svc.edge.scw.cloud/aws/{video.id}/"
229231
"thumbnails/1569309880_480.0000000.jpg",
230-
"720": f"https://abc.cloudfront.net/{video.id}/"
232+
"720": f"https://abc.svc.edge.scw.cloud/aws/{video.id}/"
231233
"thumbnails/1569309880_720.0000000.jpg",
232234
},
233235
"manifests": {
234-
"hls": f"https://abc.cloudfront.net/{video.id}/"
236+
"hls": f"https://abc.svc.edge.scw.cloud/aws/{video.id}/"
235237
"cmaf/1569309880.m3u8"
236238
},
237-
"previews": f"https://abc.cloudfront.net/{video.id}/"
239+
"previews": f"https://abc.svc.edge.scw.cloud/aws/{video.id}/"
238240
"previews/1569309880_100.jpg",
239241
},
240242
"should_use_subtitle_as_transcript": False,

src/backend/marsha/core/tests/api/video/test_retrieve.py

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,7 @@ def test_api_video_read_detail_token_user(self):
197197
for resolution in resolutions
198198
}
199199

200-
mp4_template = (
201-
"https://abc.svc.edge.scw.cloud/aws/{!s}/mp4/1533686400_{!s}.mp4"
202-
"?response-content-disposition=attachment%3B+filename%3Dfoo-bar_1533686400.mp4"
203-
)
200+
mp4_template = "https://abc.svc.edge.scw.cloud/aws/{!s}/mp4/1533686400_{!s}.mp4"
204201
mp4_dict = {
205202
# pylint: disable=consider-using-f-string
206203
str(resolution): mp4_template.format(video.pk, resolution)
@@ -243,8 +240,7 @@ def test_api_video_read_detail_token_user(self):
243240
"upload_state": "ready",
244241
"source_url": (
245242
"https://abc.svc.edge.scw.cloud/aws/a2f27fde-973a-4e89-8dca-cc59e01d255c/"
246-
"timedtext/source/1533686400_fr_cc?response-content-disposition=a"
247-
"ttachment%3B+filename%3Dfoo-bar_1533686400.srt"
243+
"timedtext/source/1533686400_fr_cc"
248244
),
249245
"url": (
250246
"https://abc.svc.edge.scw.cloud/aws/a2f27fde-973a-4e89-8dca-cc59e01d255c/"
@@ -344,11 +340,11 @@ def test_api_video_read_detail_token_user(self):
344340
)
345341

346342
@override_settings(MEDIA_URL="https://abc.svc.edge.scw.cloud/")
347-
def test_api_video_read_detail_token_user_nested_shared_live_media_urls_signed(
343+
def test_api_video_read_detail_token_user_nested_shared_live_media(
348344
self,
349345
):
350346
"""
351-
An instructor reading video details with nested shared live media and cloudfront signed
347+
An instructor reading video details with nested shared live media
352348
urls activated should have the urls.media available
353349
"""
354350
resolutions = [144, 240, 480, 720, 1080]
@@ -541,17 +537,13 @@ def test_api_video_read_detail_token_user_nested_shared_live_media_urls_signed(
541537
content, {"detail": "You do not have permission to perform this action."}
542538
)
543539

544-
@override_settings(
545-
CLOUDFRONT_SIGNED_URLS_ACTIVE=True,
546-
CLOUDFRONT_SIGNED_PUBLIC_KEY_ID="cloudfront-access-key-id",
547-
MEDIA_URL="https://abc.svc.edge.scw.cloud/",
548-
)
540+
@override_settings(MEDIA_URL="https://abc.svc.edge.scw.cloud")
549541
def test_api_video_read_detail_token_student_user_nested_shared_live_media_urls_signed(
550542
self,
551543
):
552544
"""
553-
A student reading video details with nested shared live media and cloudfront signed
554-
urls activated should not have the urls.media available
545+
A student reading video details with nested shared live media should not have
546+
the urls.media available
555547
"""
556548
resolutions = [144, 240, 480, 720, 1080]
557549
video = factories.VideoFactory(
@@ -605,19 +597,6 @@ def test_api_video_read_detail_token_student_user_nested_shared_live_media_urls_
605597
for resolution in resolutions
606598
}
607599

608-
expected_cloudfront_signature = (
609-
"Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNl"
610-
"IjoiaHR0cHM6Ly9hYmMuY2xvdWRmcm9udC5uZXQvZDlkNzA0OWMtNWEzZi00MDcwLWE0"
611-
"OTQtZTZiZjBiZDhiOWZiLyoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFX"
612-
"UzpFcG9jaFRpbWUiOjE2MzgyMzc2MDB9fX1dfQ__&Signature=IVWMFfS7WQVTKLZl~"
613-
"gKgGES~BS~wVLBIOncSE6yVgg9zIrEI1Epq3AVkOsI7z10dyjgInNbPviArnxmlV~DQe"
614-
"N-ykgEWmGy7aT4lRCx61oXuHFtNkq8Qx-we~UY87mZ4~UTqmM~JVuuLduMiRQB-I3XKa"
615-
"RQGRlsok5yGu0RhvLcZntVFp6QgYui3WtGvxSs2LjW0IakR1qepSDl9LXI-F2bgl9Vd1"
616-
"U9eapPBhhoD0okebXm7NGg9gUMLXlmUo-RvsrAzzEteKctPp0Xzkydk~tcnMkJs4jfbQ"
617-
"xKrpyF~N9OuCRYCs68ONhHvypOYU3K-wQEoAFlERBLiaOzDZUzlyA__&Key-Pair-Id="
618-
"cloudfront-access-key-id"
619-
)
620-
621600
self.assertEqual(
622601
content,
623602
{
@@ -648,40 +627,35 @@ def test_api_video_read_detail_token_student_user_nested_shared_live_media_urls_
648627
"urls": {
649628
"mp4": {
650629
"144": (
651-
"https://abc.cloudfront.net/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
652-
"1533686400_144.mp4?response-content-disposition=attachment%3B+filen"
653-
f"ame%3Dfoo-bar_1533686400.mp4&{expected_cloudfront_signature}"
630+
"https://abc.svc.edge.scw.cloud/aws/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
631+
"1533686400_144.mp4"
654632
),
655633
"240": (
656-
"https://abc.cloudfront.net/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
657-
"1533686400_240.mp4?response-content-disposition=attachment%3B+filen"
658-
f"ame%3Dfoo-bar_1533686400.mp4&{expected_cloudfront_signature}"
634+
"https://abc.svc.edge.scw.cloud/aws/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
635+
"1533686400_240.mp4"
659636
),
660637
"480": (
661-
"https://abc.cloudfront.net/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
662-
"1533686400_480.mp4?response-content-disposition=attachment%3B+filen"
663-
f"ame%3Dfoo-bar_1533686400.mp4&{expected_cloudfront_signature}"
638+
"https://abc.svc.edge.scw.cloud/aws/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
639+
"1533686400_480.mp4"
664640
),
665641
"720": (
666-
"https://abc.cloudfront.net/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
667-
"1533686400_720.mp4?response-content-disposition=attachment%3B+filen"
668-
f"ame%3Dfoo-bar_1533686400.mp4&{expected_cloudfront_signature}"
642+
"https://abc.svc.edge.scw.cloud/aws/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
643+
"1533686400_720.mp4"
669644
),
670645
"1080": (
671-
"https://abc.cloudfront.net/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
672-
"1533686400_1080.mp4?response-content-disposition=attachment%3B+filen"
673-
f"ame%3Dfoo-bar_1533686400.mp4&{expected_cloudfront_signature}"
646+
"https://abc.svc.edge.scw.cloud/aws/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/mp4/"
647+
"1533686400_1080.mp4"
674648
),
675649
},
676650
"thumbnails": thumbnails_dict,
677651
"manifests": {
678652
"hls": (
679-
"https://abc.cloudfront.net/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/"
653+
"https://abc.svc.edge.scw.cloud/aws/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/"
680654
"cmaf/1533686400.m3u8"
681655
),
682656
},
683657
"previews": (
684-
"https://abc.cloudfront.net/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/"
658+
"https://abc.svc.edge.scw.cloud/aws/d9d7049c-5a3f-4070-a494-e6bf0bd8b9fb/"
685659
"previews/1533686400_100.jpg"
686660
),
687661
},
@@ -846,7 +820,6 @@ def test_api_video_read_detail_as_instructor_in_read_only(self):
846820
)
847821
self.assertEqual(response.status_code, 200)
848822

849-
@override_settings(CLOUDFRONT_SIGNED_URLS_ACTIVE=False)
850823
def test_api_video_read_detail_token_user_no_active_stamp(self):
851824
"""A video with no active stamp should not fail and its "urls" should be set to `None`."""
852825
video = factories.VideoFactory(
@@ -913,7 +886,6 @@ def test_api_video_read_detail_token_user_no_active_stamp(self):
913886
},
914887
)
915888

916-
@override_settings(CLOUDFRONT_SIGNED_URLS_ACTIVE=False)
917889
def test_api_video_read_detail_token_user_not_sucessfully_uploaded(self):
918890
"""A video that has never been uploaded successfully should have no url."""
919891
state = random.choice(["pending", "error", "ready"])

0 commit comments

Comments
 (0)