From 3b686d7773a1f5828f615895e41d20a5a40aaba2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 2 Oct 2013 11:59:17 +0200 Subject: [PATCH] [IMP] extract & simplify image serving, deduplicate a bit between image fields and attachments bzr revid: xmo@openerp.com-20131002095917-coux4ikqfthqhlo7 --- addons/website/controllers/main.py | 160 ++++++++++++++++------------- 1 file changed, 87 insertions(+), 73 deletions(-) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 1bdeddb7107..f7854a90794 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- import base64 import cStringIO +import contextlib import hashlib import json import logging import os +import datetime + +from sys import maxint import psycopg2 import werkzeug @@ -230,79 +234,6 @@ class Website(openerp.addons.web.controllers.main.Home): window.parent['%s'](%s, %s); """ % (func, json.dumps(url), json.dumps(message)) - @website.route('/website/attachment/', type='http', auth="admin") - def attachment(self, id): - # TODO: provide actual thumbnails? - # FIXME: can't use Binary.image because auth=user and website attachments need to be public - attachment = request.registry['ir.attachment'].browse( - request.cr, request.uid, id, request.context) - - buf = cStringIO.StringIO(base64.decodestring(attachment.datas)) - - image = Image.open(buf) - mime = PIL_MIME_MAPPING[image.format] - - w, h = image.size - resized = w > MAX_IMAGE_WIDTH or h > MAX_IMAGE_HEIGHT - - # If saving unnecessary, just send the image buffer, don't go through - # Image.save() (especially as it breaks animated gifs) - if not resized: - buf.seek(0) - return werkzeug.wrappers.Response(buf, status=200, mimetype=mime) - - image.thumbnail(IMAGE_LIMITS, Image.ANTIALIAS) - response = werkzeug.wrappers.Response(status=200, mimetype=mime) - image.save(response.stream, image.format) - return response - - @website.route('/website/image', type='http', auth="public") - def image(self, model, id, field, **kw): - last_update = '__last_update' - Model = request.registry[model] - headers = [('Content-Type', 'image/png')] - etag = request.httprequest.headers.get('If-None-Match') - hashed_session = hashlib.md5(request.session_id).hexdigest() - retag = hashed_session - try: - id = int(id) - ids = Model.search(request.cr, request.uid, [('id', '=', id)], context=request.context) - if not ids: - id = Model.search(request.cr, openerp.SUPERUSER_ID, [('id', '=', id), ('website_published', '=', True)], context=request.context)[0] - if etag: - date = Model.read(request.cr, openerp.SUPERUSER_ID, [id], [last_update], request.context)[0].get(last_update) - if hashlib.md5(date).hexdigest() == etag: - return werkzeug.wrappers.Response(status=304) - - res = Model.read(request.cr, openerp.SUPERUSER_ID, [id], [last_update, field], context=request.context)[0] - retag = hashlib.md5(res.get(last_update)).hexdigest() - image_base64 = res.get(field) - - if kw.get('resize'): - resize = kw.get('resize').split(',') - if len(resize) == 2 and int(resize[0]) and int(resize[1]): - width = int(resize[0]) - height = int(resize[1]) - # resize maximum 500*500 - if width > 500: - width = 500 - if height > 500: - height = 500 - image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG') - - image_data = base64.b64decode(image_base64) - except Exception: - image_data = open(os.path.join(http.addons_manifest['web']['addons_path'], 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read() - - headers.append(('ETag', retag)) - headers.append(('Content-Length', len(image_data))) - try: - ncache = int(kw.get('cache')) - headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache))) - except: - pass - return request.make_response(image_data, headers) - @website.route(['/website/publish'], type='json', auth="public") def publish(self, id, object): _id = int(id) @@ -327,4 +258,87 @@ class Website(openerp.addons.web.controllers.main.Home): def sitemap(self): return request.website.render('website.sitemap', {'pages': request.website.list_pages()}) +class Images(http.Controller): + @website.route('/website/image', auth="public") + def image(self, model, id, field): + Model = request.registry[model] + + response = werkzeug.wrappers.Response() + + id = int(id) + + ids = Model.search(request.cr, request.uid, + [('id', '=', id)], context=request.context)\ + or Model.search(request.cr, openerp.SUPERUSER_ID, + [('id', '=', id), ('website_published', '=', True)], context=request.context) + + if not ids: + # file_open may return a StringIO. StringIO can be closed but are + # not context managers in Python 2 though that is fixed in 3 + with contextlib.closing(openerp.tools.misc.file_open( + os.path.join('web', 'static', 'src', 'img', 'placeholder.png'), + mode='rb')) as f: + response.set_data(f.read()) + return response + + concurrency = '__last_update' + [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id], + [concurrency, field], context=request.context) + + if concurrency in record: + server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT + try: + response.last_modified = datetime.datetime.strptime( + record[concurrency], server_format + '.%f') + except ValueError: + # just in case we have a timestamp without microseconds + response.last_modified = datetime.datetime.strptime( + record[concurrency], server_format) + # FIXME: no field in record? + response.set_etag(hashlib.sha1(record[field]).hexdigest()) + response.make_conditional(request.httprequest) + + # conditional request match + if response.status_code == 304: + return response + + return self.set_image_data(response, record[field].decode('base64')) + + # FIXME: auth + # FIXME: delegate to image? + @website.route('/website/attachment/', auth='admin') + def attachment(self, id): + attachment = request.registry['ir.attachment'].browse( + request.cr, request.uid, id, request.context) + + return self.set_image_data( + werkzeug.wrappers.Response(), + attachment.datas.decode('base64'), + fit=IMAGE_LIMITS,) + + def set_image_data(self, response, data, fit=(maxint, maxint)): + """ Sets an inferred mime type on the response object, and puts the + provided image's data in it, possibly after resizing if requested + + Returns the response object after setting its mime and content, so + the result of ``get_final_image`` can be returned directly. + """ + buf = cStringIO.StringIO(data) + + # FIXME: unknown format or not an image + image = Image.open(buf) + response.mimetype = PIL_MIME_MAPPING[image.format] + + w, h = image.size + max_w, max_h = fit + + if w < max_w and h < max_h: + response.set_data(data) + return response + + image.thumbnail(fit, Image.ANTIALIAS) + image.save(response.stream, image.format) + return response + + # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: