Skip to content

Commit 33db6d5

Browse files
committed
✨(back) add support for storing markdown images on Scaleway S3
Leverage the existing django-storages backend to enable storing markdown images on Scaleway S3, alongside AWS S3.
1 parent a1ac577 commit 33db6d5

File tree

13 files changed

+610
-154
lines changed

13 files changed

+610
-154
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ Versioning](https://semver.org/spec/v2.0.0.html).
1010

1111
### Added
1212

13-
- Add support for storing classroom documents on Scaleway S3
13+
- Add support for storing markdown images on Scaleway S3
1414
- Add support for storing deposited files on Scaleway S3
15+
- Add support for storing classroom documents on Scaleway S3
1516

1617
### Fixed
1718

src/backend/marsha/core/defaults.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,13 @@
182182
VOD_STORAGE_BASE_DIRECTORY = "vod"
183183
CLASSROOM_STORAGE_BASE_DIRECTORY = "classroom"
184184
FILE_DEPOSITORY_STORAGE_BASE_DIRECTORY = "filedepository"
185+
MARKDOWN_DOCUMENT_STORAGE_BASE_DIRECTORY = "markdowndocument"
185186

186187
STORAGE_BASE_DIRECTORY = (
187188
(TMP_STORAGE_BASE_DIRECTORY, _("tmp")),
188189
(VOD_STORAGE_BASE_DIRECTORY, _("VOD")),
189190
(CLASSROOM_STORAGE_BASE_DIRECTORY, _("Classroom")),
190191
(DELETED_STORAGE_BASE_DIRECTORY, _("deleted")),
191192
(FILE_DEPOSITORY_STORAGE_BASE_DIRECTORY, _("File Depository")),
193+
(MARKDOWN_DOCUMENT_STORAGE_BASE_DIRECTORY, _("Markdown Document")),
192194
)

src/backend/marsha/core/storage/filesystem.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from django.urls import reverse
44
from django.utils import timezone
55

6-
from marsha.core.defaults import TMP_STORAGE_BASE_DIRECTORY
6+
from marsha.core.defaults import (
7+
MARKDOWN_DOCUMENT_STORAGE_BASE_DIRECTORY,
8+
TMP_STORAGE_BASE_DIRECTORY,
9+
)
710
from marsha.core.utils.time_utils import to_timestamp
811

912

@@ -55,7 +58,7 @@ def initiate_classroom_document_storage_upload(request, obj, filename, condition
5558
request : Type[django.http.request.HttpRequest]
5659
The request on the API endpoint
5760
pk: string
58-
The primary key of the video
61+
The primary key of the classroom document
5962
6063
Returns
6164
-------
@@ -90,7 +93,7 @@ def initiate_deposited_file_storage_upload(request, obj, filename, conditions):
9093
request : Type[django.http.request.HttpRequest]
9194
The request on the API endpoint
9295
pk: string
93-
The primary key of the video
96+
The primary key of the deposited file
9497
9598
Returns
9699
-------
@@ -112,3 +115,42 @@ def initiate_deposited_file_storage_upload(request, obj, filename, conditions):
112115
)
113116
),
114117
}
118+
119+
120+
# pylint: disable=unused-argument
121+
def initiate_markdown_image_storage_upload(request, obj, conditions):
122+
"""Get an upload policy for a markdown image.
123+
124+
Returns an upload policy for the filesystem backend.
125+
126+
Parameters
127+
----------
128+
request : Type[django.http.request.HttpRequest]
129+
The request on the API endpoint
130+
pk: string
131+
The primary key of the markdown image
132+
133+
Returns
134+
-------
135+
Dictionary
136+
A dictionary with two elements: url and fields. Url is the url to post to. Fields is a
137+
dictionary filled with the form fields and respective values to use when submitting
138+
the post.
139+
140+
"""
141+
now = timezone.now()
142+
stamp = to_timestamp(now)
143+
key = obj.get_storage_key(
144+
stamp=stamp, base_dir=MARKDOWN_DOCUMENT_STORAGE_BASE_DIRECTORY
145+
)
146+
return {
147+
"fields": {
148+
"key": key,
149+
},
150+
"url": request.build_absolute_uri(
151+
reverse(
152+
"local-markdown-image-upload",
153+
args=[obj.pk],
154+
)
155+
),
156+
}

src/backend/marsha/core/storage/s3.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
from storages.backends.s3 import S3Storage
99

10-
from marsha.core.defaults import TMP_STORAGE_BASE_DIRECTORY
10+
from marsha.core.defaults import (
11+
MARKDOWN_DOCUMENT_STORAGE_BASE_DIRECTORY,
12+
TMP_STORAGE_BASE_DIRECTORY,
13+
)
1114
from marsha.core.models import Document
1215
from marsha.core.utils.cloudfront_utils import get_cloudfront_private_key
1316
from marsha.core.utils.s3_utils import create_presigned_post
@@ -164,3 +167,33 @@ def initiate_deposited_file_storage_upload(request, obj, filename, conditions):
164167
S3FileStorage.bucket_name,
165168
"STORAGE_S3",
166169
)
170+
171+
172+
# pylint: disable=unused-argument
173+
def initiate_markdown_image_storage_upload(request, obj, conditions):
174+
"""Get an upload policy for a markdown image.
175+
176+
The object must implement the get_storage_key method.
177+
Returns an upload policy to our storage S3 destination bucket.
178+
179+
Returns
180+
-------
181+
Dictionary
182+
A dictionary with two elements: url and fields. Url is the url to post to. Fields is a
183+
dictionary filled with the form fields and respective values to use when submitting
184+
the post.
185+
186+
"""
187+
now = timezone.now()
188+
stamp = to_timestamp(now)
189+
key = obj.get_storage_key(
190+
stamp=stamp, base_dir=MARKDOWN_DOCUMENT_STORAGE_BASE_DIRECTORY
191+
)
192+
193+
return create_presigned_post(
194+
conditions,
195+
{},
196+
key,
197+
S3FileStorage.bucket_name,
198+
"STORAGE_S3",
199+
)

src/backend/marsha/development/api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from marsha.core.storage.storage_class import file_storage
1616
from marsha.core.utils import time_utils
1717
from marsha.deposit.models import DepositedFile
18+
from marsha.markdown.models import MarkdownImage
1819

1920

2021
logger = logging.getLogger(__name__)
@@ -103,3 +104,19 @@ def local_deposited_file_upload(request: HttpRequest, uuid=None):
103104

104105
file_storage.save(destination, uploaded_deposited_file)
105106
return Response(status=204)
107+
108+
109+
@api_view(["POST"])
110+
def local_markdown_image_upload(request: HttpRequest, uuid=None):
111+
"""Endpoint to mock s3 markdown image upload."""
112+
uploaded_markdown_image = request.FILES["file"]
113+
114+
try:
115+
markdown_image = MarkdownImage.objects.get(id=uuid)
116+
except MarkdownImage.DoesNotExist:
117+
return Response({"success": False}, status=404)
118+
119+
destination = markdown_image.get_storage_key(filename=markdown_image.filename)
120+
121+
file_storage.save(destination, uploaded_markdown_image)
122+
return Response(status=204)

src/backend/marsha/markdown/api.py

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,17 @@
22

33
from django.conf import settings
44
from django.db.models import Q
5-
from django.utils import timezone
65

76
import django_filters
87
from rest_framework import filters, mixins, viewsets
98
from rest_framework.decorators import action
109
from rest_framework.response import Response
1110
from rest_framework.status import HTTP_200_OK, HTTP_400_BAD_REQUEST
1211

13-
from marsha.core import defaults, permissions as core_permissions
12+
from marsha.core import defaults, permissions as core_permissions, storage
1413
from marsha.core.api import APIViewMixin, ObjectPkMixin, ObjectRelatedMixin
1514
from marsha.core.models import ADMINISTRATOR
16-
from marsha.core.utils.s3_utils import create_presigned_post
17-
from marsha.core.utils.time_utils import to_timestamp
15+
from marsha.core.utils.time_utils import to_datetime
1816
from marsha.markdown import permissions as markdown_permissions, serializers
1917
from marsha.markdown.defaults import LTI_ROUTE
2018
from marsha.markdown.forms import MarkdownDocumentForm
@@ -333,7 +331,7 @@ def initiate_upload(self, request, *args, pk=None, **kwargs):
333331
"""Get an upload policy for a Markdown image.
334332
335333
Calling the endpoint resets the upload state to `pending` and returns an upload policy to
336-
our AWS S3 source bucket.
334+
our S3 storage bucket.
337335
338336
Parameters
339337
----------
@@ -345,33 +343,31 @@ def initiate_upload(self, request, *args, pk=None, **kwargs):
345343
Returns
346344
-------
347345
Type[rest_framework.response.Response]
348-
HttpResponse carrying the AWS S3 upload policy as a JSON object.
346+
HttpResponse carrying the S3 storage upload policy as a JSON object.
349347
350348
"""
351349
markdown_image = self.get_object() # check permissions first
352350

353-
serializer = serializers.MarkdownImageUploadSerializer(
351+
serializer = serializers.MarkdownImageInitiateUploadSerializer(
354352
data=request.data,
355353
)
356354

357355
if not serializer.is_valid():
358356
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
359357

360-
now = timezone.now()
361-
stamp = to_timestamp(now)
362-
363-
key = markdown_image.get_source_s3_key(
364-
stamp=stamp,
365-
extension=serializer.validated_data["extension"],
366-
)
367-
368-
presigned_post = create_presigned_post(
369-
[
370-
["starts-with", "$Content-Type", "image/"],
371-
["content-length-range", 0, settings.MARKDOWN_IMAGE_SOURCE_MAX_SIZE],
372-
],
373-
{},
374-
key,
358+
presigned_post = (
359+
storage.get_initiate_backend().initiate_markdown_image_storage_upload(
360+
request,
361+
markdown_image,
362+
[
363+
["starts-with", "$Content-Type", "image/"],
364+
[
365+
"content-length-range",
366+
0,
367+
settings.MARKDOWN_IMAGE_SOURCE_MAX_SIZE,
368+
],
369+
],
370+
)
375371
)
376372

377373
# Reset the upload state of the Markdown image
@@ -381,3 +377,46 @@ def initiate_upload(self, request, *args, pk=None, **kwargs):
381377
)
382378

383379
return Response(presigned_post)
380+
381+
@action(methods=["post"], detail=True, url_path="upload-ended")
382+
# pylint: disable=unused-argument
383+
def upload_ended(
384+
self,
385+
request,
386+
pk=None,
387+
markdown_document_id=None,
388+
):
389+
"""Notify the API that the markdown image upload has ended.
390+
391+
Calling the endpoint will update the upload state of the markdown image.
392+
The request should have a file_key in the body, which is the key of the
393+
uploaded file.
394+
395+
Parameters
396+
----------
397+
request : Type[django.http.request.HttpRequest]
398+
The request on the API endpoint
399+
pk: string
400+
The primary key of the markdown image
401+
402+
Returns
403+
-------
404+
Type[rest_framework.response.Response]
405+
HttpResponse with the serialized markdown image.
406+
"""
407+
markdown_image = self.get_object() # check permissions first
408+
409+
serializer = serializers.MarkdownImageUploadEndedSerializer(
410+
data=request.data, context={"obj": markdown_image}
411+
)
412+
413+
serializer.is_valid(raise_exception=True)
414+
415+
file_key = serializer.validated_data["file_key"]
416+
# The file_key have the "markdown/{markdown_pk}/markdownimage/{markdownimage}/
417+
# {stamp}" format
418+
stamp = file_key.split("/")[-1]
419+
420+
markdown_image.update_upload_state(defaults.READY, to_datetime(stamp))
421+
422+
return Response(serializer.data)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.0.9 on 2025-05-09 07:59
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("markdown", "0006_alter_markdowndocumenttranslation_title"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="markdownimage",
15+
name="storage_location",
16+
field=models.CharField(
17+
choices=[("AWS", "AWS"), ("SCW", "SCW")],
18+
default="SCW",
19+
help_text="Location used to store the Markdown image",
20+
max_length=255,
21+
verbose_name="storage location",
22+
),
23+
),
24+
]

0 commit comments

Comments
 (0)