基於 lua-resty-upload 實現簡單的文件上傳服務

今天瞭解了一下 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 的表單形式來觸發文件上傳,代碼如下:


<!-- myupload.html -->
<!DOCTYPE html>
<html>
<head>
    <title>File upload example</title>
</head>
<body>
    <form action="upfile" method="post" enctype="multipart/form-data">
        <label for="testFileName">select file: </label>
        <input type="file" name="testFileName"/>
        <input type="submit" name="upload" value="Upload" />
    </form>
</body>
</html>


對應的 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;
        }
    }
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章