feat: add api for multiple video downloads (#33882)

This commit is contained in:
Kristin Aoki
2023-12-12 09:37:48 -05:00
committed by GitHub
parent d08e93d42c
commit ebf91fc9ad
6 changed files with 120 additions and 4 deletions

View File

@@ -17,5 +17,6 @@ from .videos import (
CourseVideosSerializer,
VideoUploadSerializer,
VideoImageSerializer,
VideoUsageSerializer
VideoUsageSerializer,
VideoDownloadSerializer
)

View File

@@ -115,6 +115,15 @@ class VideoUsageSerializer(serializers.Serializer):
)
class VideoDownloadSerializer(serializers.Serializer):
"""Serializer for video downloads"""
files = serializers.ListField(
child=serializers.DictField(
child=serializers.CharField()
)
)
class VideoUploadSerializer(StrictSerializer):
"""
Strict Serializer for video upload urls.

View File

@@ -15,7 +15,8 @@ from .views import (
ProctoredExamSettingsView,
ProctoringErrorsView,
HelpUrlsView,
VideoUsageView
VideoUsageView,
VideoDownloadView
)
app_name = 'v1'
@@ -38,6 +39,11 @@ urlpatterns = [
VideoUsageView.as_view(),
name="video_usage"
),
re_path(
fr'^videos/{COURSE_ID_PATTERN}/download$',
VideoDownloadView.as_view(),
name="video_usage"
),
re_path(
fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$',
ProctoredExamSettingsView.as_view(),

View File

@@ -11,5 +11,6 @@ from .settings import CourseSettingsView
from .videos import (
CourseVideosView,
VideoUsageView,
VideoDownloadView
)
from .help_urls import HelpUrlsView

View File

@@ -14,11 +14,13 @@ from common.djangoapps.student.auth import has_studio_read_access
from ....utils import get_course_videos_context
from cms.djangoapps.contentstore.video_storage_handlers import (
get_video_usage_path
get_video_usage_path,
create_video_zip,
)
from cms.djangoapps.contentstore.rest_api.v1.serializers import (
CourseVideosSerializer,
VideoUsageSerializer,
VideoDownloadSerializer
)
import cms.djangoapps.contentstore.toggles as contentstore_toggles
@@ -180,3 +182,43 @@ class VideoUsageView(DeveloperErrorViewMixin, APIView):
usage_locations = get_video_usage_path(course_key, edx_video_id)
serializer = VideoUsageSerializer(usage_locations)
return Response(serializer.data)
@view_auth_classes(is_authenticated=True)
class VideoDownloadView(DeveloperErrorViewMixin, APIView):
"""
View for course video downloads.
"""
@apidocs.schema(
body=VideoDownloadSerializer,
parameters=[
apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"),
],
responses={
200: "In case of success, a 200.",
401: "The requester is not authenticated",
403: "The requester cannot access the specified course",
404: "The requested course does not exist",
},
)
@verify_course_exists()
def put(self, request: Request, course_id: str) -> Response:
"""
Get an object containing course videos.
**Example Request**
PUT /api/contentstore/v1/videos/{course_id}/download, {
"files": [
{"url": 'someUrl.com', "name": 'test.mp4'}
]
}
**Response Values**
If the request is successful, an HTTP 200 "OK" response is returned.
The HTTP 200 response contains a zip file attachment containing all the
requested videos. The returned file's name will be {course_id}_videos_{random_id}.zip.
"""
course_key = CourseKey.from_string(course_id)
if not has_studio_read_access(request.user, course_key):
self.permission_denied(request)
files = request.data['files']
return create_video_zip(course_id, files)

View File

@@ -8,6 +8,12 @@ import csv
import io
import json
import logging
import os
import requests
import shutil
import pathlib
import zipfile
from contextlib import closing
from datetime import datetime, timedelta
from uuid import uuid4
@@ -15,7 +21,7 @@ from boto.s3.connection import S3Connection
from boto import s3
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage
from django.http import FileResponse, HttpResponseNotFound
from django.http import FileResponse, HttpResponseNotFound, StreamingHttpResponse
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from django.utils.translation import gettext_noop
@@ -35,10 +41,14 @@ from edxval.api import (
update_video_image,
update_video_status
)
from fs.osfs import OSFS
from opaque_keys.edx.keys import CourseKey
from path import Path as path
from pytz import UTC
from rest_framework import status as rest_status
from rest_framework.response import Response
from tempfile import NamedTemporaryFile, mkdtemp
from wsgiref.util import FileWrapper
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.util.json_request import JsonResponse
@@ -221,6 +231,53 @@ def handle_videos(request, course_key_string, edx_video_id=None):
return JsonResponse(data, status=status)
def send_zip(zip_file, size=None):
"""
Generates a streaming http response for the zip file
"""
wrapper = FileWrapper(zip_file, settings.COURSE_EXPORT_DOWNLOAD_CHUNK_SIZE)
response = StreamingHttpResponse(wrapper, content_type='application/zip')
response['Content-Dispositon'] = 'attachment; filename=%s' % os.path.basename(zip_file.name)
response['Content-Length'] = size
return response
def create_video_zip(course_key_string, files):
"""
Generates the video zip, or returns None if there was an error.
Updates the context with any error information if applicable.
"""
name = course_key_string + '_videos'
video_folder_zip = NamedTemporaryFile(prefix=name + '_',
suffix=".zip") # lint-amnesty, pylint: disable=consider-using-with
root_dir = path(mkdtemp())
video_dir = root_dir + '/' + name
zip_folder = None
try:
for file in files:
url = file['url']
file_name = file['name']
response = requests.get(url, allow_redirects=True)
file_type = '.' + response.headers['Content-Type'][6:]
if file_type not in file_name:
file_name = file['name'] + file_type
if not os.path.isdir(video_dir):
os.makedirs(video_dir)
with OSFS(video_dir).open(file_name, mode="wb") as f:
f.write(response.content)
directory = pathlib.Path(video_dir)
with zipfile.ZipFile(video_folder_zip, mode="w") as archive:
for file_path in directory.iterdir():
archive.write(file_path, arcname=file_path.name)
zip_folder = open(video_folder_zip.name, '+rb')
return send_zip(zip_folder, video_folder_zip.tell())
finally:
if os.path.exists(root_dir / name):
shutil.rmtree(root_dir / name)
def get_video_usage_path(course_key, edx_video_id):
"""
API for fetching the locations a specific video is used in a course.