9

为Open edX集成七牛云存储

 2 years ago
source link: http://wwj718.github.io/post/edx/open-edx-qiniu/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Open edX与视频流

在Open edX的众多组件和服务中,并不包含视频流服务。不可否认的是,在线教育中,视频是要素之一,也许是最重要的要素之一,对一些人而言,甚至没有之一。

视频流一般被视为一个common server,市面上有数不清的商业或是开源解决方案,Open edX没有去重造车轮,而是和youtube做了很多整合。我们与youtube无缘。当然作为通用的组件,Open edX中的视频模块支持一般的视频资源(url),无论是云存储还是自建服务

自建视频流

如果准备自建视频流服务,可以参考@MT的在内部网络为edX配置视频服务。对于局域网内的用户(学校/企业),自建服务是个有诱惑力的方案。

不过这里边存在的坑是,视频流服务搭建不难,搭建一个友好的客户端,上传管理视频却颇为不易。在此推荐使用minio作为管理视频资源的工具,细节可以参考我的这篇文章:构建类s3存储系统(Minio)

使用云存储

视频解决方案有很多,大家可以自行google,看大家的对比评测,再结合自己的需求选型,在此就不多推荐了

我比较偏好七牛云。对开发者友好,api写得很漂亮

在此演示如何使用七牛云为open edX提供视频服务,并将客户端(js)集成其中

思路与设计

首先我们需要考虑一个问题,视频管理入口以什么形态集成到Open edX中合适(如何集成七牛云存储)。换个角度,Open edX有哪些拓展方式呢。毕竟我们可以把集成外部存储系统,看做一次对系统的拓展

Extending edX中,官方给出了集中常见的拓展方式。此外还有两种很典型的拓展:

  • 对django开发者而言还可以直接侵入式拓展open edx,通过添加django app或者修改增强mvt中的任何一个环节
  • 模仿insights的做法,完全构建一个新的服务(网站),之后使用oauth2来打通用户系统

因为我们希望将系统集成到open edx内部,所以决定采用添加django app的做法。 用户上传和管理视频资源需要UI界面,参考Adding a UI Page,发现侵入式地定制open edx很是繁琐,我们决定为此功能写一个独立的页面,绕开繁重的前端架构

为何不是xblock

也许许多Open edX用户会觉得为何放着xlock不用,而采用侵入性更大的django app来拓展呢。原因有二:

  • 视频管理是一个用户视角下,全局性的操作,应该有一个同意的资源管理入口,而不是每次需要先添加一个组件,再在组件里边管理视频,逻辑上,这样也能做出来。我们可以把xblock视为必须实例化(instance)为组件的东西
  • 我们不想放弃既有的视频组件(数据采集等强大功能)

关于七牛云你需要了解的知识和上传管理的逻辑,可以参考我此前的文章:为Open edX构建存储服务

如果你想读懂接下来的源码,你需要了解django和django-restful-framework,如果只是用的话,就无所谓

just do it

我们直接在/edx/app/edxapp/edx-platform/cms/djangoapps添加一个django appqiniu_storage,形如:

├── add_the_app.sh
├── ajax.js
├── __init__.py
├── models.py
├── permissions.py
├── readme.md
├── serializers.py
├── urls.py
├── views.py

我们重点介绍model和view部分,其他不赘述

models.py

#!/usr/bin/env python
# encoding: utf-8

from __future__ import unicode_literals
from django.db import models
#from django.contrib.auth.models import User

class QiniuFiles(models.Model):
    course_id = models.CharField(max_length=100,blank=True)
    username = models.CharField(max_length=50,blank=True) # 上传用户,资源所有者
    file_key = models.CharField(max_length=100)
    file_url = models.CharField(max_length=100,blank=True)
    file_name = models.CharField(max_length=100)
    file_size = models.CharField(max_length=20,default="0")
    #endUser = Column(String(100),nullable=True)
    create_time = models.DateTimeField(u'创建时间',auto_now=True)

    class Meta:
        ordering = ('create_time',)

views.py

只列出关键部分

qiniu_access_key = getattr(settings,  "QINIU_ACCESS_KEY", None)
class QiniuFilesViewSet(viewsets.ModelViewSet):
    authentication_classes = (TokenAuthentication, SessionAuthentication,) 
    serializer_class = QiniuFilesSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,IsOwnerOrReadOnly,)

    def get_queryset(self):
        """
        This view should return a list of all the purchases
        for the currently authenticated user.
        """
        return QiniuFiles.objects.filter(username=self.request.user.username) #用户级别的管理权限,每个用户只能管理自己上传的文件
    # 删除功能暂不演示

其中的IsOwnerOrReadOnly值得关注,校验用户与资源的关系

from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Object-level permission to only allow owners of an object to edit it.
    Assumes the model instance has an `owner` attribute.
    """

    def has_object_permission(self, request, view, obj):

        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.username == request.user.username

前端部分主要参考七牛的js-sdk,使用了clipboard.js用于点击事件,使用了noty用于消息提醒

代码形如:

$(function() {
    var uploader = Qiniu.uploader({
        runtimes: 'html5,flash,html4',
        browse_button: 'pickfiles',//是哪个可上传的元素
        container: 'container',
        drop_element: 'container',//是否可拖动,且是哪个元素
        max_file_size: '1000mb',
       // flash_swf_url: 'bower_components/plupload/js/Moxie.swf',
        dragdrop: true,
        chunk_size: '4mb',
        //uptoken: 'xxx' //测试用
        uptoken_url: '/qiniu/uptoken',  //key由后端生成,定制化的规则包含在载荷中
        domain: 'xxx',//这是域名的绑定地址
        get_new_uptoken: false,
        unique_names: true,
        auto_start: true,
        log_level: 5,
        ...

上传流程涉及的代码

在七牛的上传原理中,上传需要凭证,我们来看看凭证的生成规则

@api_view(['GET'])
def make_uptoken(request, format=None):
            test_uptoken = QiniuTool().get_test_uptoken(request)
            #跨域的问题 Access-Control-Allow-Origin
            response = Response({"uptoken": test_uptoken})
            return response

其中Qiniu类为

class QiniuTool(object):
        '''
        #处理七牛凭证相关的工具,生成uptoken
        存储相关的部分被抽象为rest服务
        函数只接受get和post
        '''
        callback_url = 'http://studio.xxx.com/qiniu/post_from_qiniu'
        #http://developer.qiniu.com/article/kodo/kodo-developer/up/vars.html 所有的魔法变量
        #callback_body = 'filename=$(fname)&filesize=$(fsize)&key=$(key)&mimeType=$(mimeType)&endUser=$(endUser)&etag=$(etag)'
        access_key = getattr(settings,  "QINIU_ACCESS_KEY", None)
        secret_key = getattr(settings,  "QINIU_SECRET_KEY", None)
        q = Auth(access_key, secret_key) # access_key和secret_key来自settings里
        bucket_name = "easy-edx"
        def get_test_uptoken(self,request):
            callback_body = 'file_name=$(fname)&file_size=$(fsize)&file_key=$(key)&mimeType=$(mimeType)&endUser=$(endUser)&etag=$(etag)&username={}'.format(request.user.username)
            # 上传策略有许多可选的参数,方便服务于业务逻辑:参考[python-sdk](http://developer.qiniu.com/docs/v6/sdk/python-sdk.html)
            #上传文件到七牛后, 七牛将文件名和文件大小回调给业务服务器。
            policy={
                    'scope':self.bucket_name,
                    'callbackUrl':self.callback_url, #回调 请求方式为POST
                    'callbackBody':callback_body
            }
            #token = q.upload_token(bucket_name,3600,policy)
            token = self.q.upload_token(self.bucket_name,policy=policy)
            return token

视频上传好之后,七牛会可以发送一个消息给服务器,我们在此存下文件信息即可

from qiniu_files.serializers import QiniuFilesSerializer

@api_view(['POST'])
def post_from_qiniu(request, format=None):
            origin_authorization = request.META.get('HTTP_AUTHORIZATION', None)
            access_key = re.split(r'\W',origin_authorization)[1]
            request.data["file_url"] = "http://media.xxx.com/"+ request.data["file_key"]
            request.data["file_size"] = request.data["file_size"]
            serializer =  QiniuFilesSerializer(data=request.data)
            if access_key == Qiniu().access_key and serializer.is_valid():
                serializer.save() #把信息存储到qiniu_storage模型里
                instance = serializer.save()
                data = file_info_format(request.data)
                #使用序列化就能存入本地
                data["id"]=instance.pk
                return Response(data)
            return Response({"success":False,"message":u"请求不合格"})

还有许多细节可以改进,诸如校验用户是否有教师权限

上边实际给出了open edx集成外部存储的方式,思路是通用的,不限于七牛。诸如你也可以将你自建的视频存储集成到open edx中,区别仅在抽象的存储接口(我们可以用minio构建)


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK