Python中使用Flask、MongoDB搭建简易图片服务器
1、前期准备 通过 pip 或 easy_install 安装了 pymongo 之后,就能通过 Python 调教 mongodb 了. 当然 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 如果你跟我一样觉得让通过上传文件名的后缀判别用户上传的什么文件完全是捏着山药当小黄瓜一样欺骗自己,那么最好还准备个 Pillow 库 复制代码 代码如下: pip install Pillow 或 (更适合 Windows 用户) 复制代码 代码如下: easy_install Pillow 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) 注: 在 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('/') 把内容塞进一个 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)) 上传文件之后, 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) 2.5 正确的 MIME 从现在开始要对上传的文件严格把关了,文本文件,狗与剪刀等皆不能上传. 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'] 然后试试上传文本文件肯定虚,传图片文件才能正常进行. 不对,也不正常,因为传完跳转之后,服务器并没有给出正确的 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() 清掉原来的数据. 2.6 根据上传时间给出 NOT MODIFIED 1)、记录文件最后上传的时间 体现为代码是 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) 然后,得弄个脚本把数据库里面已经有的图片给加上时间戳. 2.7 利用 SHA-1 排重 与冰箱里的可乐不同,大部分情况下你肯定不希望数据库里面出现一大波完全一样的图片. 图片,连同其 EXIFF 之类的数据信息,在数据库中应该是惟一的,这时使用略强一点的散列技术来检测是再合适不过了. 达到这个目的最简单的就是建立一个 SHA-1 惟一索引,这样数据库就会阻止相同的东西被放进去. 在 MongoDB 中表中建立惟一 索引,执行 (Mongo 控制台中) 复制代码 代码如下: db.files.ensureIndex({sha1: 1},{unique: true}) 如果你的库中有多条记录的话,MongoDB 会给报个错. 这看起来很和谐无害的索引操作被告知数据库中有重复的取值 null (实际上目前数据库里已有的条目根本没有这个属性). 与一般的 RDB 不同的是,MongoDB 规定 null,或不存在的属性值也是一种相同的属性值,所以这些幽灵属性会导致惟一索引无法建立. 解决方案有三个: 1)删掉现在所有的数据 (一定是测试数据库才用这种不负责任的方式吧!) import hashlib 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()),sha1=sha1,) try: db.files.save(c) except pymongo.errors.DuplicateKeyError: pass return c['_id'] 在上传文件这一环就没问题了. 不过,按照上面这个逻辑,如果上传了一个已经存在的文件,返回 c['_id'] 将会是一个不存在的数据 ID. 修正这个问题,最好是返回 sha1,另外,在访问文件时,相应地修改为用文件 SHA-1 访问,而不是用 ID. import hashlib import 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','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()),) 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)
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 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |