OpenResty文件上传

语言: CN / TW / HK

介绍

本文对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](https://github.com/openresty/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} ``` 上传成功。

image.png