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 數據庫

相關文章
相關標籤/搜索