Nginx与Lua: 使用Nginx的upload模块打造文件上传服务

2015-10-13 Li Shuai 更多博文 » 博客 » GitHub »

Nginx Lua 项目

原文链接 https://cyrusin.github.io/2015/10/13/lua-20151013/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


客户端提出搞一个日志上传的接口,方便向服务端提交日志,研究了一下,决定用Nginx嵌入Lua的方式搞。

Lua是一个可以嵌入到Nginx配置文件中的动态脚本语言,从而可以在Nginx请求处理的任何阶段执行各种Lua代码。业余时间使用过,感觉小巧轻快,代码风格和Python一样简洁优雅。在Nginx中内嵌Lua,需要你重新编译安装Nginx,加上ngx_lua模块,同时需要安装LuaJIT,可以看看这篇文章"Nginx与Lua"。

安装好相关的模块后,Nginx配置如下:

location ~ /upload { # 调用的路由

    # 转到后台处理URL 
    upload_pass /uploadHandle;
    # 临时保存路径
    upload_store /opt/upload/tmp;

    #上传文件的权限,rw表示读写,r只读
    upload_store_access user:rw group:rw all:rw;

    # 是否允许nginx上传参数到后台
    upload_pass_args on;

    # 这里写入http报头,pass到后台页面后能获取这里set的报头字段
    upload_set_form_field "file_name" $upload_file_name;
    upload_set_form_field "file_content_type" $upload_content_type;
    upload_set_form_field "file_tmp_path" $upload_tmp_path;

    # Upload模块自动生成的一些信息,如文件大小与文件md5值
    upload_aggregate_form_field "file_md5" $upload_file_md5;
    upload_aggregate_form_field "file_size" $upload_file_size;

    # 允许的字段,允许全部可以 "^.*$"
    upload_pass_form_field "^submit$|^description$";
    # 每秒字节速度控制,0表示不受控制,默认0 
    upload_limit_rate 0;

    # 如果pass页面是以下状态码,就删除此次上传的临时文件
    upload_cleanup 400 404 499 500-505;
}

# 后台处理路由,使用Lua
location ~  /uploadHandle {
    lua_need_request_body on; 
    content_by_lua_file /opt/nginx/luas/onupload.lua; # Lua脚本的位置
    return 302 'http://***********'; # 处理完重定向到展示页面
}   

客户端直接post提交过来就行,下面是用于处理提交过来的文件的Lua脚本:

function onupload()
    ngx.req.read_body();
    local post_args=ngx.req.get_post_args();
    local table_params=getFormParams(post_args);

    ret_val=processFile(table_params);
    if (ret_val==0) then
        ngx.say("Boy, upload success!")
    else
        ngx.say("Something wrong with nginx!!")
    end
end

function processFile(params)
    local root_dir="/opt/upload/"; -- 文件目录
    local filename=params["file_name"];
    local mv_command="mv "..trim(params["file_tmp_path"]).." "..root_dir..filename;

    if (os.execute(mv_command)~=0)then
        ngx.exec("/50x.html");
        return 1;
    else
        return 0;
    end
end

function trim(str)
    if (str~=nil)then
        return string.gsub(str, "%s+", "");
    else
        return nil;
    end
end

-- [[提交过来的表单数据是一个超长的字符串,
    需要从这个字符串中解析出字典结构的数据,
    这样可以利用key来访问字典中对应的值
]]
function getFormParams(post_args)
    local table_params={};
    for key, val in pairs(post_args) do
        str_params = key ..val
    end

    local str_start = " name";  
    local str_start_len = string.len(str_start);  
    local str_end = "%-%-";  
    local str_sign = "\"";  
    local idx,idx_end = string.find(str_params,str_start);  
    local i = 0;  

    -- 如果字符串内仍有开始标记,则说明还有内容需要分离。继续分离到没内容为止。  
    while idx do  
        str_params = string.sub(str_params,idx_end); -- 截取开始标记后所有字符待用  
        i = string.find(str_params,str_sign); -- 查找字段名开始处的引号索引  
        str_params = string.sub(str_params,i+1); -- 去掉开始处的引号  
        i = string.find(str_params,str_sign); -- 查找字段名结束位置的索引  
        f_name = string.sub(str_params,0,i-1); -- 截取到字段名称                                          
        str_params = string.sub(str_params,i+1); -- 去掉名称字段以及结束时的引号  
        i,i2 = string.find(str_params,str_end); -- 查找字段值结尾标识的索引  
        f_value = string.sub(str_params,1,i-1); -- 截取到字段值  
        real_value=trim(f_value)
        table_params[f_name] = real_value;  
        idx = string.find(str_params,str_start,0); -- 查找判断下一个字段是否存在的  
    end
    local root_dir="/opt/upload/";
    table_params["file_path"]=root_dir..table_params["file_name"]
    return table_params
end

onupload();