1、前期准备
通过 pip 或 easy_install 安装了 pymongo 之后, 就能通过 Python 调教 mongodb 了.
接着安装个 flask 用来当 web 服务器.
当然 mongo 也是得安装的. 对于 Ubuntu 用户, 特别是使用 Server 12.04 的同学, 安装最新版要略费些周折, 具体说是
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list sudo apt-get update sudo apt-get install mongodb-10gen</div>
如果你跟我一样觉得让通过上传文件名的后缀判别用户上传的什么文件完全是捏着山药当小黄瓜一样欺骗自己, 那么最好还准备个 Pillow 库
pip install Pillow
</div>
或 (更适合 Windows 用户)
easy_install Pillow
</div>
2、正片
2.1 Flask 文件上传
Flask 官网上那个例子居然分了两截让人无从吐槽. 这里先弄个最简单的, 无论什么文件都先弄上来
import flask
app = flask.Flask(__name__)
app.debug = True
@app.route('/upload', methods=['POST'])
def upload():
f = flask.request.files['uploaded_file']
print f.read()
return flask.redirect('/')
@app.route('/')
def index():
return '''
<!doctype html>
<html>
<body>
<form action='/upload' method='post' enctype='multipart/form-data'>
<input type='file' name='uploaded_file'>
<input type='submit' value='Upload'>
</form>
'''
if __name__ == '__main__':
app.run(port=7777)
</div>
注: 在 upload 函数中, 使用 flask.request.files[KEY] 获取上传文件对象, KEY 为页面 form 中 input 的 name 值
因为是在后台输出内容, 所以测试最好拿纯文本文件来测.
2.2 保存到 mongodb
如果不那么讲究的话, 最快速基本的存储方案里只需要
import pymongo
import bson.binary
from cStringIO import StringIO
app = flask.Flask(__name__)
app.debug = True
db = pymongo.MongoClient('localhost', 27017).test
def save_file(f):
content = StringIO(f.read())
db.files.save(dict(
content= bson.binary.Binary(content.getvalue()),
))
@app.route('/upload', methods=['POST'])
def upload():
f = flask.request.files['uploaded_file']
save_file(f)
return flask.redirect('/')
</div>
把内容塞进一个 bson.binary.Binary 对象, 再把它扔进 mongodb 就可以了.
现在试试再上传个什么文件, 在 mongo shell 中通过 db.files.find() 就能看到了.
不过 content 这个域几乎肉眼无法分辨出什么东西, 即使是纯文本文件, mongo 也会显示为 Base64 编码.
2.3 提供文件访问
给定存进数据库的文件的 ID (作为 URI 的一部分), 返回给浏览器其文件内容, 如下
def save_file(f):
content = StringIO(f.read())
c = dict(content=bson.binary.Binary(content.getvalue()))
db.files.save(c)
return c['_id']
@app.route('/f/<fid>')
def serve_file(fid):
f = db.files.find_one(bson.objectid.ObjectId(fid))
return f['content']
@app.route('/upload', methods=['POST'])
def upload():
f = flask.request.files['uploaded_file']
fid = save_file(f)
return flask.redirect( '/f/' + str(fid))
</div>
上传文件之后, upload 函数会跳转到对应的文件浏览页. 这样一来, 文本文件内容就可以正常预览了, 如果不是那么挑剔换行符跟连续空格都被浏览器吃掉的话.
2.4 当找不到文件时
有两种情况, 其一, 数据库 ID 格式就不对, 这时 pymongo 会抛异常 bson.errors.InvalidId ; 其二, 找不到对象 (!), 这时 pymongo 会返回 None .
简单起见就这样处理了
@app.route('/f/<fid>')
def serve_file(fid):
import bson.errors
try:
f = db.files.find_one(bson.objectid.ObjectId(fid))
if f is None:
raise bson.errors.InvalidId()
return f['content']
except bson.errors.InvalidId:
flask.abort(404)
</div>
2.5 正确的 MIME
从现在开始要对上传的文件严格把关了, 文本文件, 狗与剪刀等皆不能上传.
判断图片文件之前说了我们动真格用 Pillow
from PIL import Image
allow_formats = set(['jpeg', 'png', 'gif'])
def save_file(f):
content = StringIO(f.read())
try:
mime = Image.open(content).format.lower()
if mime not in allow_formats:
raise IOError()
except IOError:
flask.abort(400)
c = dict(content=bson.binary.Binary(content.getvalue()))
db.files.save(c)
return c['_id']
</div>
然后试试上传文本文件肯定虚, 传图片文件才能正常进行. 不对, 也不正常, 因为传完跳转之后, 服务器并没有给出正确的 mimetype, 所以仍然以预览文本的方式预览了一坨二进制乱码.
要解决这个问题, 得把 MIME 一并存到数据库里面去; 并且, 在给出文件时也正确地传输 mimetype
def save_file(f):
content = StringIO(f.read())
try:
mime = Image.open(content).format.lower()
if mime not in allow_formats:
raise IOError()
except IOError:
flask.abort(400)
c = dict(content=bson.binary.Binary(content.getvalue()), mime=mime)
db.files.save(c)
return c['_id']
@app.route('/f/<fid>')
def serve_file(fid):
try:
f = db.files.find_one(bson.objectid.ObjectId(fid))
if f is None:
raise bson.errors.InvalidId()
return flask.Response(f['content'], mimetype='image/' + f['mime'])
except bson.errors.InvalidId:
flask.abort(404)
</div>
当然这样的话原来存进去的东西可没有 mime 这个属性, 所以最好先去 mongo shell 用 db.files.drop() 清掉原来的数据.
2.6 根据上传时间给出 NOT MODIFIED
利用 HTTP 304 NOT MODIFIED 可以尽可能压榨与利用浏览器缓存和节省带宽. 这需要三个操作
1)、记录文件最后上传的时间
2)、当浏览器请求这个文件时, 向请求头里塞一个时间戳字符串
3)、当浏览器请求文件时, 从请求头中尝试获取这个时间戳, 如果与文件的时间戳一致, 就直接 304
体现为代码是
import datetime
def save_file(f):
content = StringIO(f.read())
try:
mime = Image.open(content).format.lower()
if mime not in allow_formats:
raise IOError()
except IOError:
flask.abort(400)
c = dict(
content=bson.binary.Binary(content.getvalue()),
mime=mime,
time=datetime.datetime.utcnow(),
)
db.files.save(c)
return c['_id']
@app.route('/f/<fid>')
def serve_file(fid):
try:
f = db.files.find_one(bson.objectid.ObjectId(fid))
if f is None:
raise bson.errors.InvalidId()
if flask.request.headers.get('If-Modified-Since') == f['time'].ctime():
return flask.Response(status=304)
resp = flask.Response(f['content'], mimetype='image/' + f['mime'])
resp.headers['Last-Modified'] = f['time'].ctime()
return resp
except bson.errors.InvalidId:
flask.abort(404)
</div>
然后, 得弄个脚本把数据库里面已经有的图片给加上时间戳.
顺带吐个槽, 其实 NoSQL DB 在这种环境下根本体现不出任何优势, 用起来跟 RDB 几乎没两样.
2.7 利用 SHA-1 排重
与冰箱里的可乐不同, 大部分情况下你肯定不希望数据库里面出现一大波完全一样的图片. 图片, 连同其 EXIFF 之类的数据信息, 在数据库中应该是惟一的, 这时使用略强一点的散列技术来检测是再合适不过了.

