Flask/MongoDB搭建简易图片服务器需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。
接着安装个 flask 用来当 web 服务器.
当然 mongo 也是得安装的. 对于 Ubuntu 用户, 特别是使用 Server 12.04 的同学, 安装最新版要略费些周折, 具体说是
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10echo '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
如果你跟我一样觉得让通过上传文件名的后缀判别用户上传的什么文件完全是捏着山药当小黄瓜一样欺骗自己, 那么最好还准备个 Pillow 库pip install Pillow 或 (更适合 Windows 用户) easy_install Pillowimport flaskapp = 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)
如果不那么讲究的话, 最快速基本的存储方案里只需要

import pymongoimport 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('/')
把内容塞进一个 bson.binary.Binary 对象, 再把它扔进 mongodb 就可以了.现在试试再上传个什么文件, 在 mongo shell 中通过db.files.find()就能看到了.
不过content 这个域几乎肉眼无法分辨出什么东西, 即使是纯文本文件, mongo 也会显示为 Base64 编码.
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))
上传文件之后, upload 函数会跳转到对应的文件浏览页. 这样一来, 文本文件内容就可以正常预览了, 如果不是那么挑剔换行符跟连续空格都被浏览器吃掉的话.简单起见就这样处理了
@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)
判断图片文件之前说了我们动真格用 Pillow
from PIL import Imageallow_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']
然后试试上传文本文件肯定虚, 传图片文件才能正常进行. 不对, 也不正常, 因为传完跳转之后, 服务器并没有给出正确的 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)
当然这样的话原来存进去的东西可没有 mime 这个属性, 所以最好先去 mongo shell 用 db.files.drop() 清掉原来的数据.import datetimedef 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)
然后, 得弄个脚本把数据库里面已经有的图片给加上时间戳.顺带吐个槽, 其实 NoSQL DB 在这种环境下根本体现不出任何优势, 用起来跟 RDB 几乎没两样.
达到这个目的最简单的就是建立一个 SHA-1 惟一索引, 这样数据库就会阻止相同的东西被放进去.
在 MongoDB 中表中建立惟一 索引 , 执行 (Mongo 控制台中)
db.files.ensureIndex({sha1: 1}, {unique: true}) 如果你的库中有多条记录的话, MongoDB 会给报个错. 这看起来很和谐无害的索引操作被告知数据库中有重复的取值 null (实际上目前数据库里已有的条目根本没有这个属性). 与一般的 RDB 不同的是, MongoDB 规定 null, 或不存在的属性值也是一种相同的属性值, 所以这些幽灵属性会导致惟一索引无法建立. 解决方案有三个:
import hashlibdef 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)
sha1 = hashlib.sha1(content.getvalue()).hexdigest()
c = dict(
content=bson.binary.Binary(content.getvalue()),
mime=mime,
time=datetime.datetime.utcnow(),
sha1=sha1,
)
try:
db.files.save(c)
except pymongo.errors.DuplicateKeyError:
pass
return c['_id']
在上传文件这一环就没问题了. 不过, 按照上面这个逻辑, 如果上传了一个已经存在的文件, 返回 c['_id'] 将会是一个不存在的数据 ID. 修正这个问题, 最好是返回 sha1 , 另外, 在访问文件时, 相应地修改为用文件 SHA-1 访问, 而不是用 ID.最后修改的结果及本篇完整源代码如下:
import hashlibimport datetime
import flask
import pymongo
import bson.binary
import bson.objectid
import bson.errors
from cStringIO import StringIO
from PIL import Image
app = flask.Flask(__name__)
app.debug = True
db = pymongo.MongoClient('localhost', 27017).test
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)
sha1 = hashlib.sha1(content.getvalue()).hexdigest()
c = dict(
content=bson.binary.Binary(content.getvalue()),
mime=mime,
time=datetime.datetime.utcnow(),
sha1=sha1,
)
try:
db.files.save(c)
except pymongo.errors.DuplicateKeyError:
pass
return sha1
@app.route('/f/<sha1>')
def serve_file(sha1):
try:
f = db.files.find_one({'sha1': sha1})
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)
@app.route('/upload', methods=['POST'])
def upload():
f = flask.request.files['uploaded_file']
sha1 = save_file(f)
return flask.redirect('/f/' + str(sha1))
@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)
[1] Developing RESTful Web APIs with Python, Flask and MongoDB
http://www.slideshare.net/nicolaiarocci/developing-restful-web-apis-with-python-flask-and-mongodb
https://github.com/nicolaiarocci/eve
[2]Flask Web Development —— 模板(上)
http://segmentfault.com/blog/young_ipython/1190000000749914
Tplink企业版路由器WiFi名称的默认设置介绍(Tplink企业版路由器WiFi名称的默认设置是什么)
Tplink路由器灯常亮无法上网的原因分析(如何解决Tplink路由器灯常亮无法上网的问题)
Tplink千兆企业级路由器自动重启的作用和优势介绍(如何设置Tplink千兆企业级路由器自动重启功能)
一根天线的tplink路由器有哪些(一根天线的Tplink路由器的特点和优势介绍)
tplink路由器外网访问不了nas(Tplink路由器外网访问NAS的原因分析)
Tplink无法搜到路由器的原因分析(如何解决Tplink无法搜到路由器的问题)