介绍
本文对OpenResty实现文件上传进行简单介绍,主要用到resty.upload中的API,下面我们来看下
lua-resty-upload中的上传示例
先看一个lua-resty-upload中的上传示例:
lua_package_path "/path/to/lua-resty-upload/lib/?.lua;;";
server {
location /test {
content_by_lua '
local upload = require "resty.upload"
local cjson = require "cjson"
local chunk_size = 5 -- should be set to 4096 or 8192
-- for real-world settings
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
while true do
local typ, res, err = form:read()
if not typ then
ngx.say("failed to read: ", err)
return
end
ngx.say("read: ", cjson.encode({typ, res}))
if typ == "eof" then
break
end
end
local typ, res, err = form:read()
ngx.say("read: ", cjson.encode({typ, res}))
';
}
}
复制代码
lua-resty-upload库支持 multipart/form-data MIME 类型。这个库的API通过循环调用form:read()返回token数据,token数据为数组,以header, body, part end,eof为开头,直到返回nil的token类型。如下所示:
read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]]
read: ["header",["Content-Type","text\/plain","Content-Type: text\/plain"]]
read: ["body","Hello"]
read: ["body",", wor"]
read: ["body","ld"]
read: ["part_end"]
read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]]
read: ["body","value"]
read: ["body","\r\n"]
read: ["part_end"]
read: ["eof"]
read: ["eof"]
复制代码
这就是流式阅读的工作原理。 即使是千兆字节的文件数据输入,只要用户自己不积累输入数据块,lua 域中使用的内存也可以很小且恒定。
此Lua 库利用了 ngx_lua 的 cosocket API,它确保了 100% 的非阻塞行为。
upload的简单封装
下面再来看一个使用的简单封装:
local _M = {
_VERSION = '0.01'
}
local upload = require "resty.upload"
local chunk_size = 4096
local cjson = require 'cjson.safe'
local os_exec = os.execute
local os_date = os.date
local md5 = ngx.md5
local io_open = io.open
local tonumber = tonumber
local gsub = string.gsub
local ngx_var = ngx.var
local ngx_req = ngx.req
local os_time = os.time
local json_encode = cjson.encode
local function get_ext(res)
local kvfile = string.split(res, "=")
local filename = string.sub(kvfile[2], 2, -2)
if filename then
return filename:match(".+%.(%w+)$")
end
return ''
end
local function file_exists(path)
local file = io.open(path, "rb")
if file then file:close() end
return file ~= nil
end
local function json_return(code, message, data)
ngx.say(json_encode({code = code, msg = message, data = data}))
end
local function in_array(b,list)
if not list then
return false
end
if list then
for k, v in pairs(list) do
if v == b then
return true
end
end
return false
end
end
-- 字符串 split 分割
string.split = function(s, p)
local rt= {}
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 function uploadfile()
local file
local file_name
local form = upload:new(chunk_size)
local conf = {max_size = 1000000, allow_exts = {'jpg', 'png', 'gif', 'jpeg'} }
local root_path = ngx_var.document_root
local file_info = {extension = '', filesize = 0, url = '', mime = '' }
local content_len = ngx_req.get_headers()['Content-length']
local body_size = content_len and tonumber(content_len) or 0
if not form then
return nil, '没有上传的文件'
end
if body_size > 0 and body_size > conf.max_size then
return nil, '文件过大'
end
file_info.filesize = body_size
while true do
local typ, res, err = form:read()
if typ == "header" then
if res[1] == "Content-Type" then
file_info.mime = res[2]
elseif res[1] == "Content-Disposition" then
-- 如果是文件参数:Content-Disposition: form-data; name="data"; filename="1.jpeg"
local kvlist = string.split(res[2], ';')
for _, kv in ipairs(kvlist) do
local seg = string.trim(kv)
-- 带filename标识则为文件参数名
if seg:find("filename") then
local file_id = md5('upload'..os_time())
local extension = get_ext(seg)
file_info.extension = extension
if not extension then
return nil, '未获取文件后缀'
end
if not in_array(extension, conf.allow_exts) then
return nil, '不支持该文件格式'
end
local dir = root_path..'/uploads/images/'..os_date('%Y')..'/'..os_date('%m')..'/'..os_date('%d')..'/'
if file_exists(dir) ~= true then
local status = os_exec('mkdir -p '..dir)
if status ~= true then
return nil, '创建目录失败'
end
end
file_name = dir..file_id.."."..extension
if file_name then
file = io_open(file_name, "w+")
if not file then
return nil, '打开文件失败'
end
end
end
end
end
elseif typ == "body" then
-- 读取body内容
if file then
file:write(res)
end
elseif typ == "part_end" then
-- 写结束,关闭文件
if file then
file:close()
file = nil
end
elseif typ == "eof" then
-- 读取结束返回
file_name = gsub(file_name, root_path, '')
file_info.url = file_name
return file_info
else
end
end
end
local file_info, err = uploadfile()
if file_info then
json_return(200, '上传成功', {imgurl = file_info.url})
else
json_return(5003, err)
end
复制代码
上面封装是使用openresty-practices进行相应修改后的上传代码.文件名称:md5('upload'..os_time()),如果想使用封装的uuid生成文件名称,可以使用lua-resty-uuid库,复制到OpenResty安装目录lualib.resty下代码中引用require 'resty.uuid'即可使用。
下面我们来对此进行测试,我们先配置nginx.conf:
worker_processes 1;
events {
worker_connections 1024;
}
http {
lua_package_path "$prefix/lua/?.lua;$prefix/libs/?.lua;;";
server {
server_name localhost;
listen 8080;
charset utf-8;
set $LESSON_ROOT lua/;
error_log logs/error.log;
access_log logs/access.log;
location /upload{
default_type text/html;
content_by_lua_file $LESSON_ROOT/upload.lua;
}
}
}
复制代码
测试前我们已经在Mac上安装了OpenResty,具体安装步骤可以查看OpenResty在Mac上的安装.接下来我们就来测试下上面的上传代码.
我们来看下当前的目录结构:
wukongdeMacBook-Pro:upload wukong$ tree
.
|-- conf
| `-- nginx.conf
|-- logs
| |-- access.log
| `-- error.log
|-- lua
| `-- upload.lua
复制代码
我们进到该目录执行如下命令启动上传应用:
wukongdeMacBook-Pro:upload wukong$ openresty -p $PWD/ -c conf/nginx.conf
复制代码
接下来我们通过IDEA的HTTP Request来设置图片上传的post请求:
### Send a form with the text and file fields
POST http://127.0.0.1:8080/upload
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="element-name"
Content-Type: text/plain
123
--WebAppBoundary
Content-Disposition: form-data; name="files"; filename="1.jpeg"
< /Users/wukong/Desktop/1.jpeg
--WebAppBoundary--
复制代码
图片路径为/Users/wukong/Desktop/1.jpeg,然后我们执行上面的请求,会看到如下返回:
{"msg":"上传成功","data":{"imgurl":"\/Users\/wukong\/Desktop\/tool\/openresty-test\/upload\/html\/uploads\/images\/2022\/03\/04\/87671af7f050839a463fafff52d5156e.jpeg"},"code":200}
复制代码
上传成功。