从开始会写程序开始,一直都很讨厌处理上传文件,因为总觉得在自己写的 controller 里面从 HTTP Body 里读文件,然后再写到本地磁盘的过程是一件非常不(易)靠(出)谱(错)的事,总怕文件没有正确写入,而且常用的框架对于上传文件,都是直接将整个文件加载到内存中,当处理大文件上传时,内存岂不是要爆炸了。

后来知道了七牛和又拍云之类的云服务,顿时感觉对于文件上传的过程的瞬间简化了。类似与七牛之类的文件储存云服务一般都会提供文件直传的接口,直接在前端就把文件上传到了七牛的服务器,而不经过业务服务器,然后当文件上传成功后,可以选择让七牛通知(回调)到业务服务器。

但是一般只有『互联网』企业才会选择七牛之类的云服务,可惜我就没怎么待过『互联网』企业,所以一直都没有正式的使用过文件直传这种方式,甚是伤心。如果你没有用过七牛,强烈推荐你试用一下,这里有我的邀请链接,你注册成功,我将收到 40G 的免费流量 https://portal.qiniu.com/signup?code=3lpwnduxidob5 ,然后这里是没有邀请码的链接 https://portal.qiniu.com/signup

然后最近做的项目有部分需要文件上传,于是就研究了下 Nginx Upload Module 的使用方法,目测七牛也是用的类似的方法。

Nginx Upload Module

Nginx Upload Module 是 Nginx 的一个模块,一般 Linux 发行版里的 Nginx 包都没有预装这个模块,所以需要自己手动编译 Nginx,并且加入这个模块。

安装

手动编译 Nginx 需要一些依赖包,本着极简主义的原则,只安装必须的依赖包

  • openssl Nginx Upload Module 会用到
  • pcre Nginx Rewrite 会用到
  • zlib gzip 压缩会用到
wget https://www.openssl.org/source/openssl-1.0.2j.tar.gz
wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.39.tar.gz
wget http://zlib.net/zlib-1.2.8.tar.gz

然后是下载最重要的 Nginx 了,这里我选择的 Nginx 是 1.10.2,目前的最新稳定版,然后 Nginx Upload Module,看 git 记录,已经至少有 6 年没更新过了,从 Nginx Upload Module 的 Github 直接下载的最新版,在新版本(1.8.x)以上没法编译成功,需要用2.2这个分之的代码

wget http://nginx.org/download/nginx-1.10.2.tar.gz
git clone -b 2.2 https://github.com/vkholodkov/nginx-upload-module

然后就是安装了

tar zxf openssl-1.0.2j.tar.gz
tar zxf pcre-8.39.tar.gz
tar zxf zlib-1.2.8.tar.gz
tar zxf nginx-1.10.2.tar.gz
cd nginx-1.10.2

./configure --add-module=../nginx-upload-module --with-openssl=../openssl-OpenSSL_1_0_2j --with-http_gzip_static_module --with-http_ssl_module --with-pcre=../pcre-8.39/ --with-zlib=../zlib-1.2.8
make && sudo make install

如果一切顺利的话,Nginx 已经被默认安装到了 /usr/local/nginx/ 目录下了。

配置

上述完成后,通过 sudo /usr/local/nginx/sbin/nginx 就可以启动 Nginx 了。接下来是配置 Nginx Upload Module 了,具体的配置项,在官方文档 已经说得很清楚了

server {
    client_max_body_size 100m;
    listen       80;

    # Upload form should be submitted to this location
    location /upload {
        # Pass altered request body to this location
        upload_pass   @test;

        # Store files to this directory
        # The directory is hashed, subdirectories 0 1 2 3 4 5 6 7 8 9 should exist
        upload_store /tmp 1;

        # Allow uploaded files to be read only by user
        upload_store_access user:r;

        # Set specified fields in request body
        upload_set_form_field $upload_field_name.name "$upload_file_name";
        upload_set_form_field $upload_field_name.content_type "$upload_content_type";
        upload_set_form_field $upload_field_name.path "$upload_tmp_path";

        # Inform backend about hash and size of a file
        upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
        upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";

        upload_pass_form_field "^submit$|^description$";

        upload_cleanup 400 404 499 500-505;
    }

    # Pass altered request body to a backend
    location @test {
        proxy_pass   http://localhost:8080;
    }
}

Nginx 安装好后,默认的用户为 nobody,如果不修改,Nginx Upload Module 上传的文件所属是 nobody,其他用户没权限动它,要么是将 Nginx 的 user 配置修改,要么修改 Nginx Upload Module 的 upload_store_accessall:rw,不然没法读写。
接下来是在 upload_store 设置的目录下创建对应的是个文件夹,从 0 到 9,注意确保 Nginx 有权限操作这 10 个目录

mkdir -p /tmp/0 /tmp/1 /tmp/2 /tmp/3 /tmp/4 /tmp/5 /tmp/6 /tmp/7 /tmp/8 /tmp/9

其中 upload_store 后面跟随的数字表示 Nginx Upload Module 存的文件的目录的级数,如果为 2 则表示上述的 10 个目录下再来 10 个目录,同样要自己创建。

然后重新加载 Nginx 的配置文件

sudo /usr/local/nginx/sbin/nginx -s reload

如果没意外 Nginx Upload Module 就正式安装好了。

上传参数

前端的上传参数如下

<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
      <input type="file" name="file"><br>
      <input type="file" name="file"><br>
      <input type="submit" name="submit" value="Upload">
    </form>

业务程序处理文件

Nginx Upload Module 只做一件事,那就是上传文件存到对应的地方,文件直接存起来是没啥用的,还需要跟真正的业务系统结合起来,所以 Nginx Upload Module 提供了一个 directive upload_pass 来将上传成功后的信息发送给业务程序,并且可以通过 upload_set_form_fieldupload_aggregate_form_field 将前端的一些参数一起传递给后端去,这样真是太方便了。

使用 Tornado 来处理文件

class UploadHandler(tornado.web.RequestHandler):

    @tornado.gen.coroutine
    def post(self):
        keys = self.request.arguments.keys()
        if "file.path" not in keys:
            self.set_status(status_code=400, reason="file field not exist.")
            self.write("400")
            return
        if filter(lambda x: not x.startswith("file."), keys):
            self.set_status(status_code=400, reason="only allow file field upload")
            self.write("400")
            return
        files = list()
        file_path = self.request.arguments['file.path']
        for index in xrange(len(file_path)):
            file = {}
            file['name'] = self.request.arguments['file.name'][index]
            file['content_type'] = self.request.arguments['file.content_type'][index]
            file['path'] = self.request.arguments['file.path'][index]
            file['md5'] = self.request.arguments['file.md5'][index]
            file['size'] = self.request.arguments['file.size'][index]
            files.append(file)
        # mv tmp file to save store storage
        for file in files:
            # debug
            src_file = file['path']
            mime = magic.from_file(src_file, mime=True)
            ext = mimetypes.guess_extension(mime, False)
            dest_file = os.path.join(
                options.storage_path,
                file['md5'] + ext
            )
            if not os.path.exists(dest_file):
                yield LinuxUtils.mv(
                    src_file,
                    dest_file
                )
            else:
                yield LinuxUtils.rm(
                    src_file
                )
            file['path'] = file['md5'] + ext
        self.write({"data": files})

上面是处理文件的示例,主要是做两件事

  • 配合 Nginx Upload Module 的 upload_cleanup directive 过滤掉不正确的文件上传参数
  • 将文件从临时目录移到其他目录,并且使用 libmagic 来判断文件的类型,从而重命名文件

完整的代码在这里 https://gist.github.com/cloverstd/deef6e9a4db76dab5c4d33ee68f63ec5

从此,虽然用不了七牛,但是文件上传也可以成为一件愉快的事了。

参考