使用OpenResty搭建DataEase视频服务器

发布于 2022年04月19日

编者注:本文为知乎博主万梓良的原创文章。

原文链接:

https://zhuanlan.zhihu.com/p/490108585。

DataEase是飞致云公司旗下的一款开源数据可视化分析工具,初代产品是在2021年6月份正式发布的。我是在从这款产品v1.4版本的时候入坑的,被它的功能所深深吸引,从此每月都紧跟产品的每月更新迭代。

之后DataEase v1.5版本更新了支持视频组件的功能,能够支持MP4格式和WebM格式的视频,刚好我公司想在制作的仪表板上播放公司的宣传视频,而DataEase的视频组件是通过直接嵌入视频链接的形式。具体如下图所示:

因此,公司内部想使用视频组件就需要搭建自己的视频文件服务器了。传统的文件服务器有很多,比如 Apache、Nginx等,不过这种方式搭建的文件服务器存在一个共性的问题,就是用户想要上传文件只能连接到服务器上,将文件上传到指定的目录。

这就需要将文件服务器的用户名/密码提供给所有需要使用到该文件服务器的人。这样操作一方面使用起来不是很便捷,特别是对业务人员来说,还需要去学习Linux的相关知识;另一方面,服务器用户名/密码的大量披露也会带来相应的安全隐患。

除此之外,也可以单独开发一套文件上传系统,但是带来的人力成本比较高,领导觉得性价比不高不予同意。

基于以上种种原因,公司希望能够搭建一个无需过多额外开发,且可以通过浏览器上传文件的文件服务器。带着这样的问题,我在网上一通搜索,发现了OpenResty。

OpenResty是一个基于Nginx与Lua的高性能Web平台,其内部集成了大量精良的Lua库、第三方模块以及大多数的依赖项。OpenResty可用于方便地搭建能够处理超高并发、扩展性极高的动态Web应用、Web服务和动态网关。

OpenResty通过汇聚各种设计精良的Nginx模块,从而将Nginx有效地变成一个强大的通用Web应用平台。这样,Web开发人员和系统工程师可以使用Lua脚本语言调动Nginx支持的各种C以及Lua模块,快速构造出足以胜任10K乃至1000K以上单机并发连接的高性能Web应用系统。

安装步骤

接下来我来演示一下使用OpenResty搭建DataEase视频服务器的具体步骤。

■ 安装OpenResty

官方提供了OpenResty的Docker镜像,执行以下命令获取最新的Docker镜像:

docker pull openresty/openresty

镜像拉取完成后,执行以下命令启动容器:

docker run -itd --name openresty -p 80:80   openresty/openresty:latest

容器启动完成后,访问IP,若出现如下页面,则说明启动成功:

■ 设置OpenResty

接着来调整OpenResty的基础配置,使其先成为一个普通的文件服务器。由于OpenResty镜像未内置vi/vim等编辑器,所以我们需要先在外部进行配置文件的编写,再将配置文件拷贝至容器内部。

配置文件如下所示:

autoindex on;# 显示目录
autoindex_exact_size on;# 显示文件大小
autoindex_localtime on;# 显示文件时间

server {
    listen  80 ;
    charset utf-8;
    root         /data/;

    location / {
    }
}

将配置文件保存为default.conf ,执行以下命令拷贝到容器内部:

docker cp default.conf openresty:/etc/nginx/conf.d

接着重启容器:

docker restart openresty

重启完成后进入容器,创建我们配置文件中写的 /data 目录:

docker exec -it openresty bashmkdir /data

接着打开浏览器,访问 http://IP ,看下效果:

接着我们拷贝任意文件至容器内的 /data 目录,看下效果:

docker cp default.conf openresty:/data

可以看到,文件名称、上传时间、大小等已经在我们的浏览器中展示出来了。至此,基础的文件服务器就搭建完成了。

■ 设置页面文件上传

接下来我们来编写lua脚本,实现页面文件上传,并且展示上传进度的功能。

首先是页面嵌入脚本:

vi inject.lua
-- ignore *.html files
if ngx.var.uri:match(".html$") then
    return "</body>"
end

local inject_div = [[
<div id="upload-inject">
<link rel="stylesheet" href="/code/iview.css">
<script src="/code/vue.min.js"></script>
<script src="/code/iview.min.js"></script>
<style>a {color: -webkit-link}</style>
<div id="app-upload">
<upload
    multiple
    type="drag"
    :action="uploadUrl"
    :on-success="success"
    :before-upload="beforeUpload">
    <div style="padding: 20px 0">
        <icon type="ios-cloud-upload" size="52" style="color: #3399ff"></icon>
        <p>点击或拖拽文件上传</p>
    </div>
</upload>
</div>
<script>
new Vue({
    el: '#app-upload',
    data: {
        uploadUrl: ''
    },
    mounted() {
        this.uploadApi = '/_upload'
    },
    methods: {
        beforeUpload: function() {
            this.uploadUrl = this.uploadApi + window.location.pathname
            let promise = new Promise((resolve) => {
                this.$nextTick(function () {
                    resolve(true);
                })
            })
            return promise
        },
       success: function(){
         console.log("success");
         location.reload();

       }
    }
})
</script>
<div>
</body>
]]

return inject_div

然后是上传文件脚本:

vi upload.lua
local upload = require "resty.upload"
local cjson = require "cjson"


local chunk_size = 4096
local home = "/data"


local form, err = upload:new(chunk_size)
if not form then
    ngx.log(ngx.ERR, "failed to new upload: ", err)
    ngx.exit(500)
end

form:set_timeout(1000) -- 1 sec

local function getsubdir(uri)
    return uri:gsub("^/_upload", "")
end

local function split(s, delimiter)
    result = {};
    for match in (s..delimiter):gmatch("(.-)"..delimiter) do
        table.insert(result, match);
    end
    return result;
end

local function strip(s)
        char = "%s"
        return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s
end

local function startswith(line, s)
    return line:find("^" .. s) ~= nil
end

local function getfilename(line)
    items = split(line, ";")
    for i, item in ipairs(items) do
        item = strip(item)
        if startswith(item, "filename") then
            name = split(item, "=")
            return name[2]:sub(2, -2)
        end
    end
    return ""
end

local filename = ""
local subdir = getsubdir(ngx.var.uri)
local file

while true do
    local typ, res, err = form:read()
    if not typ then
        ngx.say("failed to read: ", err)
        ngx.exit(500)
    end        
    if typ == "header" then
        if res[1] == "Content-Disposition" then
            filename = getfilename(res[2])
            if filename == "" then
                ngx.say("filename not found")
                ngx.exit(400)
            end
            
            local path = home .. subdir .. "/" .. filename
            file = assert(io.open(path, "w+"))
            if not file then
                ngx.say("open " .. path .. " failed")
                ngx.exit(500)
            end
        end
    elseif typ == "body" then
        if file then
            file:write(res)
        end
    elseif typ == "part_end" then
        if file then
            file:close()
            file = nil
        end
    elseif typ == "eof" then
        break
    end
end

ngx.header.content_type = "text/html"
ngx.say("<p>upload success</p>")
ngx.flush(true)

除此之外,我还引用了一些外部的CSS、JavaScript、Fonts文件,文件的分享地址如下。有需要可以自行下载:

链接:

https://pan.baidu.com/share/init?surl=FWt3HeY8Xy0OuZ6VJbeGiw

密码:up1j

我将这些文件拷贝至OpenResty容器内部:

docker cp code/ openresty:/

接着调整我们的default.conf文件,同样拷贝至容器内部:

vi default.conf

autoindex on;# 显示目录
autoindex_exact_size on;# 显示文件大小
autoindex_localtime on;# 显示文件时间

server {
    listen  80 ;
    charset utf-8;
    root         /data/;


    location /code/ {
        alias /code/;
    }

    location / {
        sub_filter_once on;
        set_by_lua_file $inject_div_before_body /code/inject.lua;
        sub_filter '</body>' $inject_div_before_body;
    }


    location ~ ^/_upload {
        client_max_body_size 4000m;
        content_by_lua_file /code/upload.lua;
    }

}


docker cp default.conf openresty:/etc/nginx/conf.d

再次访问 http://IP :

页面拖拽文件上传:

至此,可以通过浏览器上传文件的文件服务器便制作完成了。接下来,我们就可以复制视频链接,并且在仪表板上播放视频了。

最终效果

现在,一个简单、页面拖拽即可完成视频上传的文件服务器就此完成了。搭配上人人可用的开源数据可视化分析平台DataEase,完美解决了公司的需求!

注:文中的lua脚本参考了GitHub上大佬的代码,仓库地址如下:/http://github.com/yangbinnnn/ngx-upload-web。我在其基础上做了文件上传目录优化、上传完成自动刷新、本地化第三方组件等功能。