从开始会写程序开始,一直都很讨厌处理上传文件,因为总觉得在自己写的 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_access
为 all: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_field
和 upload_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
从此,虽然用不了七牛,但是文件上传也可以成为一件愉快的事了。