Flask / MongoDB 搭建簡易圖片服務器

jopen 11年前發布 | 62K 次閱讀 MongoDB NoSQL數據庫

1、前期準備

通過 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、正

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) </pre></div>

  • 注: 在 upload 函數中, 使用 flask.request.files[KEY] 獲取上傳文件對象, KEY 為頁面 form 中 input 的 name 值
  • </ul>     因為是在后臺輸出內容, 所以測試最好拿純文本文件來測.

    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('/') </pre>    把內容塞進一個  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)) </pre></div>     上傳文件之后,  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'] </pre></div>     然后試試上傳文本文件肯定虛, 傳圖片文件才能正常進行. 不對, 也不正常, 因為傳完跳轉之后, 服務器并沒有給出正確的 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) </pre></div>     當然這樣的話原來存進去的東西可沒有 mime 這個屬性, 所以最好先去 mongo shell 用  db.files.drop()  清掉原來的數據.

    2.6 根據上傳時間給出 NOT MODIFIED

        利用 HTTP 304 NOT MODIFIED 可以盡可能壓榨與利用瀏覽器緩存和節省帶寬. 這需要三個操作

    • 記錄文件最后上傳的時間
    • 當瀏覽器請求這個文件時, 向請求頭里塞一個時間戳字符串
    • 當瀏覽器請求文件時, 從請求頭中嘗試獲取這個時間戳, 如果與文件的時間戳一致, 就直接 304
    • </ul>     體現為代碼是

      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) </pre></div>     然后, 得弄個腳本把數據庫里面已經有的圖片給加上時間戳.
          順帶吐個槽, 其實 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, 再存進去
      • </ul>     具體做法隨意. 假定現在這個問題已經搞定了, 索引也弄好了, 那么剩是 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'] </pre></div>
        

            在上傳文件這一環就沒問題了. 不過, 按照上面這個邏輯, 如果上傳了一個已經存在的文件, 返回  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)</pre>


        3、REF

        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

        原文地址:http://blog.bitfoc.us/?p=514

         本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
         轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
         本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!