nginx+lua实现文件上传功能

需要用到nginx实现文件上传,刚好手里面的版本支持lua,下面是完整实现:

首先是nginx的配置如下:注意$home_path设置的是上传文件的保存目录

location /uploadFile 
{
    set $home_path "/root/up2";
     content_by_lua_file conf/upfile.lua;
}

接着在web根目录放入Html文件,命名成myupload.html

<html>
<head>
<title>File Upload to nginx</title>
</head>

<body>
<form action="uploadFile" 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>

接着在conf目录放入下面的两个文件:

 upload.lua

-- Copyright (C) Yichun Zhang (agentzh)


-- local sub = string.sub
local req_socket = ngx.req.socket
local match = string.match
local setmetatable = setmetatable
local type = type
local ngx_var = ngx.var
local ngx_init_body = ngx.req.init_body
local ngx_finish_body = ngx.req.finish_body
local ngx_append_body = ngx.req.append_body
-- local print = print


local _M = { _VERSION = '0.11' }


local CHUNK_SIZE = 4096
local MAX_LINE_SIZE = 512

local STATE_BEGIN = 1
local STATE_READING_HEADER = 2
local STATE_READING_BODY = 3
local STATE_EOF = 4

local mt = { __index = _M }

local state_handlers

local function wrapped_receiveuntil(self, until_str)
    local iter, err_outer = self:old_receiveuntil(until_str)
    if iter == nil then
        ngx_finish_body()
    end

    local function wrapped(size)
        local ret, err = iter(size)
        if ret then
            ngx_append_body(ret)
        end

        -- non-nil ret for call with no size or successful size call and nil ret
        if (not size and ret) or (size and not ret and not err) then
            ngx_append_body(until_str)
        end
        return ret, err
    end

    return wrapped, err_outer
end


local function wrapped_receive(self, arg)
    local ret, err, partial = self:old_receive(arg)
    if ret then
        ngx_append_body(ret)

    elseif partial then
        ngx_append_body(partial)
    end

    if ret == nil then
        ngx_finish_body()
    end

    return ret, err
end


local function req_socket_body_collector(sock)
    sock.old_receiveuntil = sock.receiveuntil
    sock.old_receive = sock.receive
    sock.receiveuntil = wrapped_receiveuntil
    sock.receive = wrapped_receive
end


local function get_boundary()
    local header = ngx_var.content_type
    if not header then
        return nil
    end

    if type(header) == "table" then
        header = header[1]
    end

    local m = match(header, ";%s*boundary=\"([^\"]+)\"")
    if m then
        return m
    end

    return match(header, ";%s*boundary=([^\",;]+)")
end


function _M.new(self, chunk_size, max_line_size, preserve_body)
    local boundary = get_boundary()

    -- print("boundary: ", boundary)

    if not boundary then
        return nil, "no boundary defined in Content-Type"
    end

    -- print('boundary: "', boundary, '"')

    local sock, err = req_socket()
    if not sock then
        return nil, err
    end

    if preserve_body then
        ngx_init_body(chunk_size)
        req_socket_body_collector(sock)
    end

    local read2boundary, err = sock:receiveuntil("--" .. boundary)
    if not read2boundary then
        return nil, err
    end

    local read_line, err = sock:receiveuntil("\r\n")
    if not read_line then
        return nil, err
    end

    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,
        preserve_body = preserve_body
    }, mt)
end


function _M.set_timeout(self, timeout)
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    return sock:settimeout(timeout)
end


local function discard_line(self)
    local read_line = self.read_line

    local line, err = read_line(self.line_size)
    if not line then
        return nil, err
    end

    local dummy, err = read_line(1)
    if dummy then
        if self.preserve_body then
            ngx_finish_body()
        end

        return nil, "line too long: " .. line .. dummy .. "..."
    end

    if err then
        return nil, err
    end

    return 1
end


local function discard_rest(self)
    local sock = self.sock
    local size = self.size

    while true do
        local dummy, err = sock:receive(size)
        if err and err ~= 'closed' then
            return nil, err
        end

        if not dummy then
            return 1
        end
    end
end


local function read_body_part(self)
    local read2boundary = self.read2boundary

    local chunk, err = read2boundary(self.size)
    if err then
        return nil, nil, err
    end

    if not chunk then
        local sock = self.sock

        local data = sock:receive(2)
        if data == "--" then
            local ok, err = discard_rest(self)
            if not ok then
                return nil, nil, err
            end

            self.state = STATE_EOF
            return "part_end"
        end

        if data ~= "\r\n" then
            local ok, err = discard_line(self)
            if not ok then
                return nil, nil, err
            end
        end

        self.state = STATE_READING_HEADER
        return "part_end"
    end

    return "body", chunk
end


local function read_header(self)
    local read_line = self.read_line

    local line, err = read_line(self.line_size)
    if err then
        return nil, nil, err
    end

    local dummy, err = read_line(1)
    if dummy then
        if self.preserve_body then
            ngx_finish_body()
        end
 
        return nil, nil, "line too long: " .. line .. dummy .. "..."
    end

    if err then
        return nil, nil, err
    end

    -- print("read line: ", line)

    if line == "" then
        -- after the last header
        self.state = STATE_READING_BODY
        return read_body_part(self)
    end

    local key, value = match(line, "([^: \t]+)%s*:%s*(.+)")
    if not key then
        return 'header', line
    end

    return 'header', {key, value, line}
end


local function eof()
    return "eof", nil
end


function _M.read(self)
    -- local size = self.size

    local handler = state_handlers[self.state]
    if handler then
        return handler(self)
    end

    return nil, nil, "bad state: " .. self.state
end


local function read_preamble(self)
    local sock = self.sock
    if not sock then
        return nil, nil, "not initialized"
    end

    local size = self.size
    local read2boundary = self.read2boundary

    while true do
        local preamble = read2boundary(size)
        if not preamble then
            break
        end

        -- discard the preamble data chunk
        -- print("read preamble: ", preamble)
    end

    local ok, err = discard_line(self)
    if not ok then
        return nil, nil, err
    end

    local read2boundary, err = sock:receiveuntil("\r\n--" .. self.boundary)
    if not read2boundary then
        return nil, nil, err
    end

    self.read2boundary = read2boundary

    self.state = STATE_READING_HEADER
    return read_header(self)
end


state_handlers = {
    read_preamble,
    read_header,
    read_body_part,
    eof
}


return _M

接着upfile.lua

local upload = require("conf.upload")

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)
string.split = function(s, p)
    local rt = {}
    string.gsub(s, "[^" .. p .. "]+", function(w)
        table.insert(rt, w)
    end)
    return rt
end

string.trim = function(s)
    return (s:gsub("^%s*(.-)%s*$", "%1"))
end

function ensure_dir_exists(dirname)  
    local file = io.open(dirname, "r")  
    
    if file then  
        file:close()  
        return true   
    else  
        local ok, err = os.execute("mkdir " .. dirname)  
        if not ok then  
            ngx.log(ngx.ERR, "Failed to create directory: " .. err)  
            return false
        end  
        return true
    end  
end

-- FILE LOCATION
local saveRootPath = "/tmp/"
local fileToSave
local ret_save = false
if ngx.var.home_path~=nil then saveRootPath=ngx.var.home_path.."/" end
ensure_dir_exists(saveRootPath)

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
        -- read fileName from 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
        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

现在就可以用了 

可以用网页上传,也可以用curl命令上传

curl -X POST -F "file=@21.flv" http://127.0.0.1/uploadFile

 

posted on 2024-10-18 11:10  弘道者  阅读(144)  评论(0编辑  收藏  举报