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

WBOY
풀어 주다: 2016-06-07 16:32:23
원래의
1440명이 탐색했습니다.

前期准备 通过 pip 或 easy_install 安装了 pymongo 之后, 就能通过 Python 调教 mongodb 了. 接着安装个 flask 用来当 web 服务器. 当然 mongo 也是得安装的. 对于 Ubuntu 用户, 特别是使用 Server 12.04 的同学, 安装最新版要略费些周折, 具体说是 sudoapt

前期准备

通过 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<br>echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list<br>sudo apt-get update<br>sudo apt-get install mongodb-10gen<br>

如果你跟我一样觉得让通过上传文件名的后缀判别用户上传的什么文件完全是捏着山药当小黄瓜一样欺骗自己, 那么最好还准备个 Pillow 库

pip install Pillow<br>

或 (更适合 Windows 用户)

easy_install Pillow<br>

正片

Flask 文件上传

    Flask 官网上那个例子居然分了两截让人无从吐槽. 这里先弄个最简单的, 无论什么文件都先弄上来

import flask<br><br>app = flask.Flask(__name__)<br>app.debug = True<br><br>@app.route('/upload', methods=['POST'])<br>def upload():<br>    f = flask.request.files['uploaded_file']<br>    print f.read()<br>    return flask.redirect('/')<br><br>@app.route('/')<br>def index():<br>    return '''<br>    nbsp;html><br>    <br>    <br>    <form> <br>         <input><br>         <input><br>    </form> <br>    '''<br><br>if __name__ == '__main__':<br>    app.run(port=7777)<br>

  • 注: 在 upload 函数中, 使用 flask.request.files[KEY] 获取上传文件对象, KEY 为页面 form 中 input 的 name 值
    因为是在后台输出内容, 所以测试最好拿纯文本文件来测.

保存到 mongodb

    如果不那么讲究的话, 最快速基本的存储方案里只需要

import pymongo<br>import bson.binary<br>from cStringIO import StringIO<br><br>app = flask.Flask(__name__)<br>app.debug = True<br><strong class="ntstrong">db = pymongo.MongoClient('localhost', 27017).test</strong><br><br>def save_file(f):<br>    content = StringIO(f.read())<br>    db.files.save(dict(<br>        content=<strong class="ntstrong">bson.binary.Binary(content.getvalue())</strong>,<br>    ))<br><br>@app.route('/upload', methods=['POST'])<br>def upload():<br>    f = flask.request.files['uploaded_file']<br>    <strong class="ntstrong">save_file(f)</strong><br>    return flask.redirect('/')<br>

    把内容塞进一个 bson.binary.Binary 对象, 再把它扔进 mongodb 就可以了.
    现在试试再上传个什么文件, 在 mongo shell 中通过

db.files.find()<br>

    就能看到了. 不过 content 这个域几乎肉眼无法分辨出什么东西, 即使是纯文本文件, mongo 也会显示为 Base64 编码.

提供文件访问

    给定存进数据库的文件的 ID (作为 URI 的一部分), 返回给浏览器其文件内容, 如下

def save_file(f):<br>    content = StringIO(f.read())<br>    <strong class="ntstrong">c = dict(content=bson.binary.Binary(content.getvalue()))</strong><br>    <strong class="ntstrong">db.files.save(c)</strong><br>    <strong class="ntstrong">return c['_id']</strong><br><br>@app.route('/f/<fid>')<br><strong class="ntstrong">def serve_file(fid):</strong><br>    f = db.files.find_one(bson.objectid.ObjectId(fid))<br>    return f['content']<br><br>@app.route('/upload', methods=['POST'])<br>def upload():<br>    f = flask.request.files['uploaded_file']<br>    fid = save_file(f)<br>    return flask.redirect(<strong class="ntstrong">'/f/' + str(fid)</strong>)<br></fid>

    上传文件之后, upload 函数会跳转到对应的文件浏览页. 这样一来, 文本文件内容就可以正常预览了, 如果不是那么挑剔换行符跟连续空格都被浏览器吃掉的话.

当找不到文件时

    有两种情况, 其一, 数据库 ID 格式就不对, 这时 pymongo 会抛异常 bson.errors.InvalidId; 其二, 找不到对象 (!), 这时 pymongo 会返回 None.
    简单起见就这样处理了

@app.route('/f/<fid>')<br>def serve_file(fid):<br>    import bson.errors<br>    try:<br>        f = db.files.find_one(bson.objectid.ObjectId(fid))<br>        if f is None:<br>            raise bson.errors.InvalidId()<br>        return f['content']<br>    except bson.errors.InvalidId:<br>        flask.abort(404)<br></fid>

正确的 MIME

    从现在开始要对上传的文件严格把关了, 文本文件, 狗与剪刀等皆不能上传.
    判断图片文件之前说了我们动真格用 Pillow

from PIL import Image<br><br>allow_formats = set(['jpeg', 'png', 'gif'])<br><br>def save_file(f):<br>    content = StringIO(f.read())<br>    try:<br>        mime = <strong class="ntstrong">Image.open(content).format.lower()</strong><br>        if mime not in allow_formats:<br>            raise IOError()<br>    except IOError:<br>        flask.abort(400)<br>    c = dict(content=bson.binary.Binary(content.getvalue()))<br>    db.files.save(c)<br>    return c['_id']<br>

    然后试试上传文本文件肯定虚, 传图片文件才能正常进行. 不对, 也不正常, 因为传完跳转之后, 服务器并没有给出正确的 mimetype, 所以仍然以预览文本的方式预览了一坨二进制乱码.
    要解决这个问题, 得把 MIME 一并存到数据库里面去; 并且, 在给出文件时也正确地传输 mimetype

def save_file(f):<br>    content = StringIO(f.read())<br>    try:<br>        mime = Image.open(content).format.lower()<br>        if mime not in allow_formats:<br>            raise IOError()<br>    except IOError:<br>        flask.abort(400)<br>    c = dict(content=bson.binary.Binary(content.getvalue()), mime=mime)<br>    db.files.save(c)<br>    return c['_id']<br><br>@app.route('/f/<fid>')<br>def serve_file(fid):<br>    try:<br>        f = db.files.find_one(bson.objectid.ObjectId(fid))<br>        if f is None:<br>            raise bson.errors.InvalidId()<br>        return <strong class="ntstrong">flask.Response(f['content'], mimetype='image/' + f['mime'])</strong><br>    except bson.errors.InvalidId:<br>        flask.abort(404)<br></fid>

    当然这样的话原来存进去的东西可没有 mime 这个属性, 所以最好先去 mongo shell 用 db.files.drop() 清掉原来的数据.

根据上传时间给出 NOT MODIFIED

    利用 HTTP 304 NOT MODIFIED 可以尽可能压榨与利用浏览器缓存和节省带宽. 这需要三个操作
  • 记录文件最后上传的时间
  • 当浏览器请求这个文件时, 向请求头里塞一个时间戳字符串
  • 当浏览器请求文件时, 从请求头中尝试获取这个时间戳, 如果与文件的时间戳一致, 就直接 304
    体现为代码是

import datetime<br><br>def save_file(f):<br>    content = StringIO(f.read())<br>    try:<br>        mime = Image.open(content).format.lower()<br>        if mime not in allow_formats:<br>            raise IOError()<br>    except IOError:<br>        flask.abort(400)<br>    c = dict(<br>        content=bson.binary.Binary(content.getvalue()),<br>        mime=mime,<br>        <strong class="ntstrong">time=datetime.datetime.utcnow()</strong>,<br>    )<br>    db.files.save(c)<br>    return c['_id']<br><br>@app.route('/f/<fid>')<br>def serve_file(fid):<br>    try:<br>        f = db.files.find_one(bson.objectid.ObjectId(fid))<br>        if f is None:<br>            raise bson.errors.InvalidId()<br>        if <strong class="ntstrong">flask.request.headers.get('If-Modified-Since') == f['time'].ctime()</strong>:<br>            return <strong class="ntstrong">flask.Response(status=304)</strong><br>        resp = flask.Response(f['content'], mimetype='image/' + f['mime'])<br>        <strong class="ntstrong">resp.headers['Last-Modified'] = f['time'].ctime()</strong><br>        return resp<br>    except bson.errors.InvalidId:<br>        flask.abort(404)<br></fid>

    然后, 得弄个脚本把数据库里面已经有的图片给加上时间戳.
    顺带吐个槽, 其实 NoSQL DB 在这种环境下根本体现不出任何优势, 用起来跟 RDB 几乎没两样.

利用 SHA-1 排重

    与冰箱里的可乐不同, 大部分情况下你肯定不希望数据库里面出现一大波完全一样的图片. 图片, 连同其 EXIFF 之类的数据信息, 在数据库中应该是惟一的, 这时使用略强一点的散列技术来检测是再合适不过了.
    达到这个目的最简单的就是建立一个 SHA-1 惟一索引, 这样数据库就会阻止相同的东西被放进去.
    在 MongoDB 中表中建立惟一索引, 执行 (Mongo 控制台中)

db.files.ensureIndex({sha1: 1}, {unique: true})<br>

    如果你的库中有多条记录的话, MongoDB 会给报个错. 这看起来很和谐无害的索引操作被告知数据库中有重复的取值 null (实际上目前数据库里已有的条目根本没有这个属性). 与一般的 RDB 不同的是, MongoDB 规定 null, 或不存在的属性值也是一种相同的属性值, 所以这些幽灵属性会导致惟一索引无法建立.
    解决方案有三个
  • 删掉现在所有的数据 (一定是测试数据库才用这种不负责任的方式吧!)
  • 建立一个 sparse 索引, 这个索引不要求幽灵属性惟一, 不过出现多个 null 值还是会判定重复 (不管现有数据的话可以这么搞)
  • 写个脚本跑一次数据库, 把所有已经存入的数据翻出来, 重新计算 SHA-1, 再存进去
    具体做法随意. 假定现在这个问题已经搞定了, 索引也弄好了, 那么剩是 Python 代码的事情了.

import hashlib<br><br>def save_file(f):<br>    content = StringIO(f.read())<br>    try:<br>        mime = Image.open(content).format.lower()<br>        if mime not in allow_formats:<br>            raise IOError()<br>    except IOError:<br>        flask.abort(400)<br><br>    <strong class="ntstrong">sha1 = hashlib.sha1(content.getvalue()).hexdigest()</strong><br>    c = dict(<br>        content=bson.binary.Binary(content.getvalue()),<br>        mime=mime,<br>        time=datetime.datetime.utcnow(),<br>        <strong class="ntstrong">sha1=sha1</strong>,<br>    )<br>    <strong class="ntstrong">try:</strong><br>        db.files.save(c)<br>    <strong class="ntstrong">except pymongo.errors.DuplicateKeyError:</strong><br>        pass<br>    return c['_id']<br>

    在上传文件这一环就没问题了. 不过, 按照上面这个逻辑, 如果上传了一个已经存在的文件, 返回 c['_id'] 将会是一个不存在的数据 ID. 修正这个问题, 最好是返回 sha1, 另外, 在访问文件时, 相应地修改为用文件 SHA-1 访问, 而不是用 ID.
    最后修改的结果及本篇完整源代码如下

import hashlib<br>import datetime<br>import flask<br>import pymongo<br>import bson.binary<br>import bson.objectid<br>import bson.errors<br>from cStringIO import StringIO<br>from PIL import Image<br><br>app = flask.Flask(__name__)<br>app.debug = True<br>db = pymongo.MongoClient('localhost', 27017).test<br>allow_formats = set(['jpeg', 'png', 'gif'])<br><br>def save_file(f):<br>    content = StringIO(f.read())<br>    try:<br>        mime = Image.open(content).format.lower()<br>        if mime not in allow_formats:<br>            raise IOError()<br>    except IOError:<br>        flask.abort(400)<br><br>    sha1 = hashlib.sha1(content.getvalue()).hexdigest()<br>    c = dict(<br>        content=bson.binary.Binary(content.getvalue()),<br>        mime=mime,<br>        time=datetime.datetime.utcnow(),<br>        sha1=sha1,<br>    )<br>    try:<br>        db.files.save(c)<br>    except pymongo.errors.DuplicateKeyError:<br>        pass<br>    return sha1<br><br>@app.route('/f/<sha1>')<br>def serve_file(sha1):<br>    try:<br>        f = db.files.find_one({'sha1': sha1})<br>        if f is None:<br>            raise bson.errors.InvalidId()<br>        if flask.request.headers.get('If-Modified-Since') == f['time'].ctime():<br>            return flask.Response(status=304)<br>        resp = flask.Response(f['content'], mimetype='image/' + f['mime'])<br>        resp.headers['Last-Modified'] = f['time'].ctime()<br>        return resp<br>    except bson.errors.InvalidId:<br>        flask.abort(404)<br><br>@app.route('/upload', methods=['POST'])<br>def upload():<br>    f = flask.request.files['uploaded_file']<br>    sha1 = save_file(f)<br>    return flask.redirect('/f/' + str(sha1))<br><br>@app.route('/')<br>def index():<br>    return '''<br>    nbsp;html><br>    <br>    <br>    <form> <br>         <input><br>         <input><br>    </form> <br>    '''<br><br>if __name__ == '__main__':<br>    app.run(port=7777)<br></sha1>

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!