今天瞭解了一下 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; } } }