今天了解了一下 lua-resty-upload 模块,并基于 lua-resty-upload 模块简单实现了一个基本的表单文件上传服务。
lua-resty-upload 在 github 上的项目地址为: https://github.com/openresty/lua-resty-upload
从实现可以看到,其实 upload 服务的实现还是比较简单的,就一个源文件 lualib/resty/upload.lua,总的代码行数也只有 300 行不到。
下面我整理了一下搭建文件上传服务的过程:
1,前端页面很简单,就是使用 input file 的表单形式来触发文件上传,代码如下:
File upload example
对应的 myupload.html 文件部署于 openresty/nginx/html/ 下。
2,实现接收文件上传表单信息,并保存至本地路径的 lua 代码,代码如下:
-- myupload.lua
--==========================================
-- 文件上传
--==========================================
local upload = require "resty.upload"
local cjson = require "cjson"
local chunk_size = 4096
local form, err = upload:new(chunk_size)
if not form then
ngx.log(ngx.ERR, "failed to new upload: ", err)
ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end
form:set_timeout(1000)
-- 字符串 split 分割
string.split = function(s, p)
local rt= {}
string.gsub(s, '[^'..p..']+', function(w) table.insert(rt, w) end )
return rt
end
-- 支持字符串前后 trim
string.trim = function(s)
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
-- 文件保存的根路径
local saveRootPath = "/home/steven/openresty/nginx/upload/"
-- 保存的文件对象
local fileToSave
--文件是否成功保存
local ret_save = false
while true do
local typ, res, err = form:read()
if not typ then
ngx.say("failed to read: ", err)
return
end
if typ == "header" then
-- 开始读取 http header
-- 解析出本次上传的文件名
local key = res[1]
local value = res[2]
if key == "Content-Disposition" then
-- 解析出本次上传的文件名
-- form-data; name="testFileName"; filename="testfile.txt"
local kvlist = string.split(value, ';')
for _, kv in ipairs(kvlist) do
local seg = string.trim(kv)
if seg:find("filename") then
local kvfile = string.split(seg, "=")
local filename = string.sub(kvfile[2], 2, -2)
if filename then
fileToSave = io.open(saveRootPath .. filename, "w+")
if not fileToSave then
ngx.say("failed to open file ", filename)
return
end
break
end
end
end
end
elseif typ == "body" then
-- 开始读取 http body
if fileToSave then
fileToSave:write(res)
end
elseif typ == "part_end" then
-- 文件写结束,关闭文件
if fileToSave then
fileToSave:close()
fileToSave = nil
end
ret_save = true
elseif typ == "eof" then
-- 文件读取结束
break
else
ngx.log(ngx.INFO, "do other things")
end
end
if ret_save then
ngx.say("save file ok")
end
通过阅读 lualib/resty/upload.lua 源码,该模块在解析文件上传请求的过程中,主要采用了简单的类似有限状态机的算法来实现的,在不同的状态由相应的 handler 进行处理,支持的状态包括如下状态:
STATE_BEGIN(1),初始状态,是在 upload:new 实例化的时候初始化的,如下源码(只保留了主干):
function _M.new(self, chunk_size, max_line_size)
local boundary = get_boundary()
local sock, err = req_socket()
local read2boundary, err = sock:receiveuntil("--" .. boundary)
local read_line, err = sock:receiveuntil("\r\n")
return setmetatable({
sock = sock,
size = chunk_size or CHUNK_SIZE,
line_size = max_line_size or MAX_LINE_SIZE,
read2boundary = read2boundary,
read_line = read_line,
boundary = boundary,
state = STATE_BEGIN
}, mt)
end
STATE_READING_HEADER(2),开始解析 HTTP 头部消息,一般在这个阶段主要用于解析出其中的文件名, boundary 等信息;相应的 handler 为 read_header;
STATE_READING_BODY(3),开始解析 HTTP 包体,这个阶段就是读取文件内容;
STATE_EOF(4),如果文件全部都解析和读取完后,则进入该状态,一般这个阶段表示文件都已经读取完;
这 4 个状态分别的 handler 为:
state_handlers = {
read_preamble,
read_header,
read_body_part,
eof
}
- 这里要注意的是不同阶段/状态下 read 返回的结构不同,如在 STATE_READING_HEADER 下返回的结构是 "header",{ key, value, line}
- 上传的文件会被保存在本地的路径 /home/steven/openresty/nginx/upload/ 下
3,配置 nginx.conf,添加 location /upfile 用于接收文件上传的 action,并通过 myupload.lua 来解析文件上传内容后保存至本地文件系统,如下:
http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 19080; server_name localhost; location / { root html; index index.html index.htm; } location /upfile { content_by_lua_file lua/myupload.lua; } # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }