Permalink
Browse files

[REST Upload Module] : REST Upload API (#121) :

   - Rename image.py to imaging.py
   - Create ImagesHandler to upload new image with content in body or via multipart/form-data (POST)
   - Create ImageHandler to retrieve, modify and delete existing image (GET, PUT, DELETE)
   - Modify imaging.py to check if the 32 first characters is an image existing in storage
  • Loading branch information...
nhuray authored and Nicolas Huray committed Aug 30, 2012
1 parent 49b2733 commit d8546295c2c202e98abb9925a7afcc4c758f19ab
View
@@ -7,14 +7,16 @@
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com
-
import tornado.web
import tornado.ioloop
-from thumbor.handlers.image import ImageProcessHandler
+from thumbor.handlers.image_process import ImageProcessHandler
from thumbor.handlers.healthcheck import HealthcheckHandler
from thumbor.handlers.upload import UploadHandler
+from thumbor.handlers.images import ImagesHandler
+from thumbor.handlers.image import ImageHandler
from thumbor.url import Url
+from thumbor.handlers.imaging import ImagingHandler
class ThumborServiceApp(tornado.web.Application):
@@ -25,13 +27,25 @@ def __init__(self, context):
(r'/healthcheck', HealthcheckHandler),
]
+ # TODO Old handler to upload images
if context.config.UPLOAD_ENABLED:
handlers.append(
(r'/upload', UploadHandler, { 'context': context })
)
+ # Handler to upload images (POST).
+ handlers.append(
+ (r'/image', ImagesHandler, { 'context': context })
+ )
+
+ # Handler to retrieve or modify existing images (GET, PUT, DELETE)
+ handlers.append(
+ (r'/image/(.*)', ImageHandler, { 'context': context })
+ )
+
+ # Imaging handler (GET)
handlers.append(
- (Url.regex(), ImageProcessHandler, { 'context': context })
+ (Url.regex(), ImagingHandler, { 'context': context })
)
super(ThumborServiceApp, self).__init__(handlers)
View
@@ -54,6 +54,7 @@
Config.define('UPLOAD_PHOTO_STORAGE', 'thumbor.storages.file_storage', 'The type of storage to store uploaded images with', 'Upload')
Config.define('UPLOAD_DELETE_ALLOWED', False, 'Indicates whether image deletion should be allowed', 'Upload')
Config.define('UPLOAD_PUT_ALLOWED', False, 'Indicates whether image overwrite should be allowed', 'Upload')
+Config.define('UPLOAD_DEFAULT_FILENAME', 'image', 'Default filename for image uploaded', 'Upload')
# ALIASES FOR OLD PHOTO UPLOAD OPTIONS
Config.alias('MAX_SIZE', 'UPLOAD_MAX_SIZE')
@@ -9,8 +9,10 @@
# Copyright (c) 2011 globo.com timehome@corp.globo.com
import functools
+import mimetypes
from os.path import splitext
import datetime
+import magic
import tornado.web
@@ -95,13 +97,13 @@ def callback(normalized, buffer=None, engine=None):
actual_width = engine.get_proportional_width(engine.size[1])
new_crops = self.translate_crop_coordinates(
- engine.source_width,
- engine.source_height,
- actual_width,
- actual_height,
- crop_left,
- crop_top,
- crop_right,
+ engine.source_width,
+ engine.source_height,
+ actual_width,
+ actual_height,
+ crop_left,
+ crop_top,
+ crop_right,
crop_bottom
)
req.crop['left'] = new_crops[0]
@@ -115,6 +117,8 @@ def callback(normalized, buffer=None, engine=None):
after_transform_cb = functools.partial(self.after_transform, self.context)
Transformer(self.context).transform(after_transform_cb)
+
+
self._fetch(self.context.request.image_url, self.context.request.extension, callback)
def after_transform(self, context):
@@ -144,6 +148,7 @@ def finish_request(self, context, result=None):
content_type = 'text/javascript' if context.request.meta_callback else 'application/json'
else:
try:
+ # TODO replace by mimetypes.guess_type(context.request.extension, True)
content_type = CONTENT_TYPE[context.request.extension]
except KeyError:
#extension is not present or could not help determine format => force JPEG
@@ -232,3 +237,38 @@ def initialize(self, context):
self.context = Context(context.server, context.config, context.modules.importer)
+##
+# Base handler for Image API operations
+##
+class ImageApiHandler(ContextHandler):
+
+ def get_mimetype(self, body):
+ return magic.from_buffer(body, True)
+
+ def validate(self, body):
+ conf = self.context.config
+ engine = self.context.modules.engine
+
+ # Check if image is valid
+ try:
+ engine.load(body, None)
+ except IOError:
+ self._error(415, 'Unsupported Media Type')
+ return False
+
+ # Check weight constraints
+ if (conf.UPLOAD_MAX_SIZE != 0 and len(self.request.body) > conf.UPLOAD_MAX_SIZE):
+ self._error(412, 'Image exceed max weight (Expected : %s, Actual : %s)' % (conf.UPLOAD_MAX_SIZE, len(self.request.body)))
+ return False
+
+ # Check size constraints
+ size = engine.size
+ if (conf.MIN_WIDTH > size[0] or conf.MIN_HEIGHT > size[1]):
+ self._error(412, 'Image is too small (Expected: %s/%s , Actual : %s/%s) % (conf.MIN_WIDTH, conf.MIN_HEIGHT, size[0], size[1])')
+ return False
+ return True
+
+ def write_file(self, id, body):
+ storage = self.context.modules.upload_photo_storage
+ storage.put(id, body)
+
View
@@ -7,78 +7,54 @@
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com
+import datetime
-from urllib import quote
+from thumbor.handlers import ContextHandler, ImageApiHandler
-import tornado.web
-from thumbor.handlers import ContextHandler
-from thumbor.context import RequestParameters
-from thumbor.crypto import Cryptor, Signer
-from thumbor.utils import logger
+##
+# Handler to retrieve or modify existing images
+# This handler support GET, PUT and DELETE method to manipulate existing images
+##
+class ImageHandler(ImageApiHandler):
-class ImageProcessHandler(ContextHandler):
-
- def encode_url(self, url):
- return quote(url, '/:?%=&()",\'')
-
- @tornado.web.asynchronous
- def get(self, **kw):
- url = self.request.uri
-
- if not self.validate(kw['image']):
- self._error(404, 'No original image was specified in the given URL')
+ def put(self, id):
+ id = id[:32]
+ # Check if image overwriting is allowed
+ if not self.context.config.UPLOAD_PUT_ALLOWED:
+ self._error(405, 'Unable to modify an uploaded image')
return
- self.context.request = RequestParameters(**kw)
-
- self.context.request.unsafe = self.context.request.unsafe == 'unsafe'
-
- if (self.request.query):
- self.context.request.image_url += '?%s' % self.request.query
- self.context.request.image_url = self.encode_url(self.context.request.image_url.encode('utf-8'))
-
- has_none = not self.context.request.unsafe and not self.context.request.hash
- has_both = self.context.request.unsafe and self.context.request.hash
-
- if has_none or has_both:
- self._error(404, 'URL does not have hash or unsafe, or has both: %s' % url)
- return
+ # Check if the image uploaded is valid
+ if self.validate(self.request.body):
+ self.write_file(id, self.request.body)
+ self.set_status(204)
- if self.context.request.unsafe and not self.context.config.ALLOW_UNSAFE_URL:
- self._error(404, 'URL has unsafe but unsafe is not allowed by the config: %s' % url)
+ def delete(self, id):
+ id = id[:32]
+ # Check if image deleting is allowed
+ if not self.context.config.UPLOAD_DELETE_ALLOWED:
+ self._error(405, 'Unable to delete an uploaded image')
return
- url_signature = self.context.request.hash
- if url_signature:
- signer = Signer(self.context.server.security_key)
-
- url_to_validate = self.encode_url(url).replace('/%s/' % self.context.request.hash, '')
- valid = signer.validate(url_signature, url_to_validate)
-
- if not valid and self.context.config.STORES_CRYPTO_KEY_FOR_EACH_IMAGE:
- # Retrieves security key for this image if it has been seen before
- security_key = self.context.modules.storage.get_crypto(self.context.request.image_url)
- if security_key is not None:
- signer = Signer(security_key)
- valid = signer.validate(url_signature, url_to_validate)
-
- if not valid:
- is_valid = True
- if self.context.config.ALLOW_OLD_URLS:
- cr = Cryptor(self.context.server.security_key)
- options = cr.get_options(self.context.request.hash, self.context.request.image_url)
- if options is None:
- is_valid = False
- else:
- self.context.request = RequestParameters(**options)
- logger.warning('OLD FORMAT URL DETECTED!!! This format of URL will be discontinued in upcoming versions. Please start using the new format as soon as possible. More info at https://github.com/globocom/thumbor/wiki/3.0.0-release-changes')
- else:
- is_valid = False
-
- if not is_valid:
- self._error(404, 'Malformed URL: %s' % url)
- return
-
- return self.execute_image_operations()
-
+ # Check if image exists
+ if self.context.modules.storage.exists(id):
+ self.context.modules.storage.remove(id)
+ self.set_status(204)
+ else:
+ self._error(404, 'Image not found at the given URL')
+
+ def get(self, id):
+ id = id[:32]
+ # Check if image exists
+ if self.context.modules.storage.exists(id):
+ body = self.context.modules.storage.get(id)
+ self.set_status(200)
+ self.set_header('Content-Type', self.get_mimetype(body))
+ max_age = self.context.config.MAX_AGE
+ if max_age:
+ self.set_header('Cache-Control', 'max-age=' + str(max_age) + ',public')
+ self.set_header('Expires', datetime.datetime.utcnow() + datetime.timedelta(seconds=max_age))
+ self.write(body)
+ else:
+ self._error(404, 'Image not found at the given URL')
View
@@ -0,0 +1,65 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# thumbor imaging service
+# https://github.com/globocom/thumbor/wiki
+
+# Licensed under the MIT license:
+# http://www.opensource.org/licenses/mit-license
+# Copyright (c) 2011 globo.com timehome@corp.globo.com
+
+import uuid
+import mimetypes
+from thumbor.handlers import ImageApiHandler
+
+
+##
+# Handler to upload images.
+# This handler support only POST method, but images can be uploaded :
+# - through multipart/form-data (designed for forms)
+# - or with the image content in the request body (rest style)
+##
+
+class ImagesHandler(ImageApiHandler):
+
+ def post(self):
+ # Check if the image uploaded is a multipart/form-data
+ if self.multipart_form_data():
+ file_data = self.request.files['media'][0]
+ body = file_data['body']
+
+ # Retrieve filename from 'filename' field
+ filename = file_data['filename']
+ else:
+ body = self.request.body
+
+ # Retrieve filename from 'Slug' header
+ filename = self.request.headers.get('Slug')
+
+ # Check if the image uploaded is valid
+ if self.validate(body):
+
+ # Use the default filename for the uploaded images
+ if not filename:
+ content_type = self.request.headers.get('Content-Type', self.get_mimetype(body))
+ extension = mimetypes.guess_extension(content_type, False)
+ if extension == '.jpe': extension = '.jpg' # Hack because mimetypes return .jpe by default
+ filename = self.context.config.UPLOAD_DEFAULT_FILENAME + extension
+
+ # Build image id based on a random uuid (32 characters)
+ id = str(uuid.uuid4().hex)
+ self.write_file(id, body)
+ self.set_status(201)
+ self.set_header('Location', self.location(id, filename))
+
+ def multipart_form_data(self):
+ if not 'media' in self.request.files or not self.request.files['media']:
+ return False
+ else:
+ return True
+
+ def location(self, id, filename):
+ base_uri = self.request.uri
+ return base_uri + '/' + id + '/' + filename
+
+
Oops, something went wrong.

0 comments on commit d854629

Please sign in to comment.