Flask / MongoDB 搭建简易图片服务器

一、前期准备

经过 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
若是你跟我同样以为让经过上传文件名的后缀判别用户上传的什么文件彻底是捏着山药当小黄瓜同样欺骗本身, 那么最好还准备个 Pillow 库
pip install Pillow
或 (更适合 Windows 用户)
easy_install Pillow

二、正

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

    若是不那么讲究的话, 最快速基本的存储方案里只须要  html

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() 就能看到了.  python

   不过 content  这个域几乎肉眼没法分辨出什么东西, 即便是纯文本文件, mongo 也会显示为 Base64 编码. git

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

    从如今开始要对上传的文件严格把关了, 文本文件, 狗与剪刀等皆不能上传.
    判断图片文件以前说了咱们动真格用 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']
    而后试试上传文本文件确定虚, 传图片文件才能正常进行. 不对, 也不正常, 由于传完跳转以后, 服务器并无给出正确的 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()  清掉原来的数据.

2.6 根据上传时间给出 NOT MODIFIED

    利用 HTTP 304 NOT MODIFIED 能够尽量压榨与利用浏览器缓存和节省带宽. 这须要三个操做
  • 记录文件最后上传的时间
  • 当浏览器请求这个文件时, 向请求头里塞一个时间戳字符串
  • 当浏览器请求文件时, 从请求头中尝试获取这个时间戳, 若是与文件的时间戳一致, 就直接 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)
    而后, 得弄个脚本把数据库里面已经有的图片给加上时间戳.
    顺带吐个槽, 其实 NoSQL DB 在这种环境下根本体现不出任何优点, 用起来跟 RDB 几乎没两样.

2.7 利用 SHA-1 排重

    与冰箱里的可乐不一样, 大部分状况下你确定不但愿数据库里面出现一大波彻底同样的图片. 图片, 连同其 EXIFF 之类的数据信息, 在数据库中应该是唯一的, 这时使用略强一点的散列技术来检测是再合适不过了.
    达到这个目的最简单的就是创建一个  SHA-1  唯一索引, 这样数据库就会阻止相同的东西被放进去.
    在 MongoDB 中表中创建唯一 索引 , 执行 (Mongo 控制台中)
db.files.ensureIndex({sha1: 1}, {unique: true})
    若是你的库中有多条记录的话, MongoDB 会给报个错. 这看起来很和谐无害的索引操做被告知数据库中有重复的取值 null (实际上目前数据库里已有的条目根本没有这个属性). 与通常的 RDB 不一样的是, MongoDB 规定 null, 或不存在的属性值也是一种相同的属性值, 因此这些幽灵属性会致使唯一索引没法创建.
    解决方案有三个:
  • 删掉如今全部的数据 (必定是测试数据库才用这种不负责任的方式吧!)
  • 创建一个 sparse 索引, 这个索引不要求幽灵属性唯一, 不过出现多个 null 值仍是会断定重复 (无论现有数据的话能够这么搞)
  • 写个脚本跑一次数据库, 把全部已经存入的数据翻出来, 从新计算 SHA-1, 再存进去
    具体作法随意. 假定如今这个问题已经搞定了, 索引也弄好了, 那么剩是 Python 代码的事情了.
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()), 
        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 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', '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)


三、REF

[1] Developing RESTful Web APIs with Python, Flask and MongoDB github

http://www.slideshare.net/nicolaiarocci/developing-restful-web-apis-with-python-flask-and-mongodb web

https://github.com/nicolaiarocci/eve mongodb

[2] Flask Web Development —— 模板(上) shell

http://segmentfault.com/blog/young_ipython/1190000000749914 数据库

相关文章
相关标签/搜索