编者注:本文为知乎博主万梓良的原创文章。
原文链接: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#34;) 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 .. "*#34;) 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。我在其基础上做了文件上传目录优化、上传完成自动刷新、本地化第三方组件等功能。