Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Experience with eventlet

  • Loading branch information...
commit a6e815b28447e30479223e1576bb100c2b5e6905 1 parent 7988431
@heynemann heynemann authored
View
168 thumbor/cli.py
@@ -8,7 +8,173 @@
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com
-class Cli(object):
+from os.path import splitext
+import tempfile
+
+from thumbor.transformer import Transformer
+from thumbor.engines.json_engine import JSONEngine
+from thumbor.utils import logger, real_import
+from thumbor.config import conf
+
+CONTENT_TYPE = {
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.gif': 'image/gif',
+ '.png': 'image/png'
+}
+class Cli(object):
def health_check(self):
return 'working'
+
+ def unsafe(self, options):
+ loader = real_import(conf.LOADER)
+ storage = real_import(conf.STORAGE).Storage
+ engine = real_import(conf.ENGINE).Engine
+
+ options['loader'] = loader
+ options['storage'] = storage()
+ options['engine'] = engine()
+
+ if not self.validate(options):
+ self._error(404)
+ return
+
+ return self.execute_image_operations(options)
+
+ def _error(self, status, msg=None):
+ self.set_status(status)
+ if msg is not None:
+ logger.error(msg)
+ self.finish()
+
+ def execute_image_operations(self, opt):
+
+ should_crop = opt['crop']['left'] > 0 or \
+ opt['crop']['top'] > 0 or \
+ opt['crop']['right'] > 0 or \
+ opt['crop']['bottom'] > 0
+
+ crop_left = crop_top = crop_right = crop_bottom = None
+ if should_crop:
+ crop_left = opt['crop']['left']
+ crop_top = opt['crop']['top']
+ crop_right = opt['crop']['right']
+ crop_bottom = opt['crop']['bottom']
+
+ width = opt['width']
+ height = opt['height']
+
+ if conf.MAX_WIDTH and width > conf.MAX_WIDTH:
+ width = conf.MAX_WIDTH
+ if conf.MAX_HEIGHT and height > conf.MAX_HEIGHT:
+ height = conf.MAX_HEIGHT
+
+ halign = opt['halign']
+ valign = opt['valign']
+
+ extension = splitext(opt['image'])[-1].lower()
+
+ return self.get_image(opt, opt['meta'], should_crop, crop_left,
+ crop_top, crop_right, crop_bottom,
+ opt['fit_in'],
+ opt['horizontal_flip'], width, opt['vertical_flip'],
+ height, halign, valign, extension,
+ opt['smart'], opt['image'])
+
+ def get_image(self,
+ options,
+ meta,
+ should_crop,
+ crop_left,
+ crop_top,
+ crop_right,
+ crop_bottom,
+ fit_in,
+ horizontal_flip,
+ width,
+ vertical_flip,
+ height,
+ halign,
+ valign,
+ extension,
+ should_be_smart,
+ image
+ ):
+
+ buffer = self._fetch(options, image, extension)
+
+ if buffer is None:
+ self._error(404)
+ return
+
+ context = dict(
+ loader=options['loader'],
+ engine=options['engine'],
+ storage=options['storage'],
+ buffer=buffer,
+ should_crop=should_crop,
+ crop_left=crop_left,
+ crop_top=crop_top,
+ crop_right=crop_right,
+ crop_bottom=crop_bottom,
+ fit_in=fit_in,
+ should_flip_horizontal=horizontal_flip,
+ width=width,
+ should_flip_vertical=vertical_flip,
+ height=height,
+ halign=halign,
+ valign=valign,
+ extension=extension,
+ focal_points=[]
+ )
+
+ context['engine'].load(buffer, extension)
+
+ if meta:
+ context['engine'] = JSONEngine(self.engine, image)
+
+ if 'detectors' in options and should_be_smart:
+ #work this out to be async
+ #with tempfile.NamedTemporaryFile(suffix='.jpg') as temp_file:
+ #jpg_buffer = buffer if extension in ('.jpg', '.jpeg') else self.engine.read('.jpg')
+ #temp_file.write(jpg_buffer)
+ #temp_file.seek(0)
+ #context['file'] = temp_file.name
+
+ #self.detectors[0](index=0, detectors=self.detectors).detect(context)
+ pass
+
+ Transformer(context).transform()
+
+ content_type = "application/json" if meta else CONTENT_TYPE[context['extension']]
+
+ results = context['engine'].read(context['extension'])
+
+ return (content_type, results)
+
+ def validate(self, options):
+ if not hasattr(options['loader'], 'validate'):
+ return True
+
+ is_valid = options['loader'].validate(options['image'])
+
+ if not is_valid:
+ logger.error('Request denied because the specified path "%s" was not identified by the loader as a valid path' % path)
+
+ return is_valid
+
+ def _fetch(self, options, url, extension):
+ buffer = options['storage'].get(url)
+
+ if buffer is not None:
+ return buffer
+ else:
+ buffer = options['loader'].load(url)
+ options['engine'].load(buffer, extension)
+ options['engine'].normalize()
+ buffer = options['engine'].read()
+ options['storage'].put(url, buffer)
+
+ return buffer
+
View
10 thumbor/config.py
@@ -11,7 +11,15 @@
from os.path import join
import tempfile
-from tornado.options import options, define
+from thumbor.options import options, define
+
+
+define('VERBOSE', type=bool, default=False)
+define('HOST', type=str, default='0.0.0.0')
+define('PORT', type=int, default=8888)
+define('PROCESSES', type=int, default=4)
+define('THREADS', type=int, default=4)
+define('AUTO_RELOAD', type=bool, default=False)
define('MAX_WIDTH', type=int, default=0)
define('MAX_HEIGHT', type=int, default=0)
View
22 thumbor/engines/__init__.py
@@ -8,9 +8,7 @@
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com
-import math
-
-from tornado.options import options
+from thumbor.config import conf
class BaseEngine(object):
@@ -29,15 +27,15 @@ def size(self):
def normalize(self):
width, height = self.size
- if width > options.MAX_WIDTH or height > options.MAX_HEIGHT:
- width_diff = width - options.MAX_WIDTH
- height_diff = height - options.MAX_HEIGHT
- if options.MAX_WIDTH and width_diff > height_diff:
- height = self.get_proportional_height(options.MAX_WIDTH)
- self.resize(options.MAX_WIDTH, height)
- elif options.MAX_HEIGHT and height_diff > width_diff:
- width = self.get_proportional_width(options.MAX_HEIGHT)
- self.resize(width, options.MAX_HEIGHT)
+ if width > conf.MAX_WIDTH or height > conf.MAX_HEIGHT:
+ width_diff = width - conf.MAX_WIDTH
+ height_diff = height - conf.MAX_HEIGHT
+ if conf.MAX_WIDTH and width_diff > height_diff:
+ height = self.get_proportional_height(conf.MAX_WIDTH)
+ self.resize(conf.MAX_WIDTH, height)
+ elif conf.MAX_HEIGHT and height_diff > width_diff:
+ width = self.get_proportional_width(conf.MAX_HEIGHT)
+ self.resize(width, conf.MAX_HEIGHT)
def get_proportional_width(self, new_height):
width, height = self.size
View
4 thumbor/engines/pil.py
@@ -11,9 +11,9 @@
from cStringIO import StringIO
from PIL import Image
-from tornado.options import options
from thumbor.engines import BaseEngine
+from thumbor.config import conf
FORMATS = {
'.jpg': 'JPEG',
@@ -44,7 +44,7 @@ def flip_vertically(self):
def flip_horizontally(self):
self.image = self.image.transpose(Image.FLIP_LEFT_RIGHT)
- def read(self, extension=None, quality=options.QUALITY):
+ def read(self, extension=None, quality=conf.QUALITY):
#returns image buffer in byte format.
img_buffer = StringIO()
View
53 thumbor/handlers/eventlet/app.py
@@ -9,40 +9,39 @@
# Copyright (c) 2011 globo.com timehome@corp.globo.com
import sys
-import os
-import eventlet
-eventlet.monkey_patch()
-
-from spawning.spawning_controller import start_controller
+import spawning.spawning_controller as controller
+from eventlet.greenthread import spawn
from thumbor.handlers.eventlet.urls import URLS
+from thumbor.config import conf
+from thumbor.options import parse_config_file
def dispatcher(environ, start_response):
- for url in URLS:
- if url[0].match(environ['PATH_INFO']):
- return url[1]().process_request(environ, start_response)
-
- start_response('404', [('content-type', 'text/html')])
- return ''
-
-def run(options):
- sock = None
-
- os.setpgrp()
-
+ def get_response(environ, start_response):
+ for url in URLS:
+ match = url[0].match(environ['PATH_INFO'])
+ if match:
+ return url[1]().process_request(environ, start_response, **match.groupdict())
+
+ start_response('404', [('content-type', 'text/html')])
+ return ''
+ func = spawn(get_response, environ, start_response)
+ result = func.wait()
+ return result
+
+def run(conf_path):
+ parse_config_file(conf_path)
factory = 'spawning.wsgi_factory.config_factory'
factory_args = {
- 'verbose': options['verbose'],
- 'host': options['host'],
- 'port': options['port'],
- #'num_processes': options['processes'],
- #'threadpool_workers': options['threads'],
- 'num_processes': 1,
- 'threadpool_workers': 4,
+ 'verbose': conf.VERBOSE,
+ 'host': conf.HOST,
+ 'port': conf.PORT,
+ 'num_processes': conf.PROCESSES,
+ 'threadpool_workers': conf.THREADS,
'watch': None,
- 'reload': options['reload'],
+ 'reload': conf.AUTO_RELOAD,
'deadman_timeout': 10,
'access_log_file': None,
'pidfile': None,
@@ -53,7 +52,7 @@ def run(options):
'argv_str': " ".join(sys.argv[1:]),
'args': ['thumbor.handlers.eventlet.app.dispatcher'],
'status_port': None,
- 'status_host': options['host']
+ 'status_host': conf.HOST
}
- start_controller(sock, factory, factory_args)
+ controller.start_controller(None, factory, factory_args)
View
4 thumbor/handlers/eventlet/base.py
@@ -18,8 +18,8 @@ def __init__(self):
def get(self):
return ''
- def process_request(self, environ, start_response):
- func = spawn(self.get)
+ def process_request(self, environ, start_response, *args, **kw):
+ func = spawn(self.get, *args, **kw)
result = func.wait()
start_response(str(self.status_code), [('content-type', self.content_type)])
View
50 thumbor/handlers/eventlet/handlers.py
@@ -0,0 +1,50 @@
+#!/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
+
+from thumbor.handlers.eventlet.base import Handler
+from thumbor.cli import Cli
+
+class HealthCheckHandler(Handler):
+
+ def get(self):
+ cli = Cli()
+ return cli.health_check()
+
+class UnsafeHandler(Handler):
+
+ def get(self, *args, **kw):
+ cli = Cli()
+
+ int_or_0 = lambda value: 0 if value is None else int(value)
+
+ opt = {
+ 'meta': kw['meta'] == 'meta',
+ 'crop': {
+ 'left': int_or_0(kw['crop_left']),
+ 'top': int_or_0(kw['crop_top']),
+ 'right': int_or_0(kw['crop_right']),
+ 'bottom': int_or_0(kw['crop_bottom'])
+ },
+ 'fit_in': kw['fit_in'],
+ 'width': int_or_0(kw['width']),
+ 'height': int_or_0(kw['height']),
+ 'horizontal_flip': kw['horizontal_flip'] == '-',
+ 'vertical_flip': kw['vertical_flip'] == '-',
+ 'halign': kw['halign'] or 'center',
+ 'valign': kw['valign'] or 'middle',
+ 'smart': kw['smart'] == 'smart',
+ 'image': kw['image']
+ }
+
+ content_type, result = cli.unsafe(opt)
+
+ self.content_type = content_type
+
+ return result
View
19 thumbor/handlers/eventlet/healthcheck.py
@@ -1,19 +0,0 @@
-#!/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
-
-from thumbor.handlers.eventlet.base import Handler
-from thumbor.cli import Cli
-
-class HealthCheckHandler(Handler):
-
- def get(self):
- cli = Cli()
- return cli.health_check()
-
View
4 thumbor/handlers/eventlet/urls.py
@@ -10,10 +10,12 @@
import re
-from thumbor.handlers.eventlet.healthcheck import HealthCheckHandler
+from thumbor.url import Url
+from thumbor.handlers.eventlet.handlers import HealthCheckHandler, UnsafeHandler
URLS = (
[r'/healthcheck', HealthCheckHandler],
+ [Url.regex(with_unsafe=True, include_image=True), UnsafeHandler]
)
for url in URLS:
View
0  thumbor/handlers/healthcheck.py → thumbor/handlers/handlers.py
File renamed without changes
View
22 thumbor/loaders/http_loader.py
@@ -10,16 +10,10 @@
import re
from urlparse import urlparse
-
-import tornado.httpclient
+import urllib2
from thumbor.config import conf
-http = tornado.httpclient.AsyncHTTPClient()
-
-def fetch(url, callback):
- http.fetch(url, callback=callback)
-
def _normalize_url(url):
return url if url.startswith('http') else 'http://%s' % url
@@ -41,14 +35,10 @@ def validate(url):
#actual = 0
#return (float(actual) / 1024) <= max_size
-def load(url, callback):
- #if conf.MAX_SOURCE_SIZE and not verify_size(url, conf.MAX_SOURCE_SIZE):
- #return None
-
- def return_contents(response):
- if response.error: callback(None)
- callback(response.body)
-
+def load(url):
url = _normalize_url(url)
- fetch(url, callback=return_contents)
+ usock = urllib2.urlopen(url)
+ actual = usock.read()
+ import ipdb;ipdb.set_trace()
+ return actual
View
404 thumbor/options.py
@@ -0,0 +1,404 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""A command line parsing module that lets modules define their own options.
+
+Each module defines its own options, e.g.,
+
+ from tornado.options import define, options
+
+ define("mysql_host", default="127.0.0.1:3306", help="Main user DB")
+ define("memcache_hosts", default="127.0.0.1:11011", multiple=True,
+ help="Main user memcache servers")
+
+ def connect():
+ db = database.Connection(options.mysql_host)
+ ...
+
+The main() method of your application does not need to be aware of all of
+the options used throughout your program; they are all automatically loaded
+when the modules are loaded. Your main() method can parse the command line
+or parse a config file with:
+
+ import tornado.options
+ tornado.options.parse_config_file("/etc/server.conf")
+ tornado.options.parse_command_line()
+
+Command line formats are what you would expect ("--myoption=myvalue").
+Config files are just Python files. Global names become options, e.g.,
+
+ myoption = "myvalue"
+ myotheroption = "myothervalue"
+
+We support datetimes, timedeltas, ints, and floats (just pass a 'type'
+kwarg to define). We also accept multi-value options. See the documentation
+for define() below.
+"""
+
+import datetime
+import logging
+import logging.handlers
+import re
+import sys
+import time
+
+# For pretty log messages, if available
+try:
+ import curses
+except:
+ curses = None
+
+
+def define(name, default=None, type=None, help=None, metavar=None,
+ multiple=False):
+ """Defines a new command line option.
+
+ If type is given (one of str, float, int, datetime, or timedelta)
+ or can be inferred from the default, we parse the command line
+ arguments based on the given type. If multiple is True, we accept
+ comma-separated values, and the option value is always a list.
+
+ For multi-value integers, we also accept the syntax x:y, which
+ turns into range(x, y) - very useful for long integer ranges.
+
+ help and metavar are used to construct the automatically generated
+ command line help string. The help message is formatted like:
+
+ --name=METAVAR help string
+
+ Command line option names must be unique globally. They can be parsed
+ from the command line with parse_command_line() or parsed from a
+ config file with parse_config_file.
+ """
+ if name in options:
+ raise Error("Option %r already defined in %s", name,
+ options[name].file_name)
+ frame = sys._getframe(0)
+ options_file = frame.f_code.co_filename
+ file_name = frame.f_back.f_code.co_filename
+ if file_name == options_file: file_name = ""
+ if type is None:
+ if not multiple and default is not None:
+ type = default.__class__
+ else:
+ type = str
+ options[name] = _Option(name, file_name=file_name, default=default,
+ type=type, help=help, metavar=metavar,
+ multiple=multiple)
+
+
+def parse_command_line(args=None):
+ """Parses all options given on the command line.
+
+ We return all command line arguments that are not options as a list.
+ """
+ if args is None: args = sys.argv
+ remaining = []
+ for i in xrange(1, len(args)):
+ # All things after the last option are command line arguments
+ if not args[i].startswith("-"):
+ remaining = args[i:]
+ break
+ if args[i] == "--":
+ remaining = args[i+1:]
+ break
+ arg = args[i].lstrip("-")
+ name, equals, value = arg.partition("=")
+ name = name.replace('-', '_')
+ if not name in options:
+ print_help()
+ raise Error('Unrecognized command line option: %r' % name)
+ option = options[name]
+ if not equals:
+ if option.type == bool:
+ value = "true"
+ else:
+ raise Error('Option %r requires a value' % name)
+ option.parse(value)
+ if options.help:
+ print_help()
+ sys.exit(0)
+
+ # Set up log level and pretty console logging by default
+ if options.logging != 'none':
+ logging.getLogger().setLevel(getattr(logging, options.logging.upper()))
+ enable_pretty_logging()
+
+ return remaining
+
+
+def parse_config_file(path):
+ """Parses and loads the Python config file at the given path."""
+ config = {}
+ execfile(path, config, config)
+ for name in config:
+ if name in options:
+ options[name].set(config[name])
+
+
+def print_help(file=sys.stdout):
+ """Prints all the command line options to stdout."""
+ print >> file, "Usage: %s [OPTIONS]" % sys.argv[0]
+ print >> file, ""
+ print >> file, "Options:"
+ by_file = {}
+ for option in options.itervalues():
+ by_file.setdefault(option.file_name, []).append(option)
+
+ for filename, o in sorted(by_file.items()):
+ if filename: print >> file, filename
+ o.sort(key=lambda option: option.name)
+ for option in o:
+ prefix = option.name
+ if option.metavar:
+ prefix += "=" + option.metavar
+ print >> file, " --%-30s %s" % (prefix, option.help or "")
+ print >> file
+
+
+class _Options(dict):
+ """Our global program options, an dictionary with object-like access."""
+ @classmethod
+ def instance(cls):
+ if not hasattr(cls, "_instance"):
+ cls._instance = cls()
+ return cls._instance
+
+ def __getattr__(self, name):
+ if isinstance(self.get(name), _Option):
+ return self[name].value()
+ raise AttributeError("Unrecognized option %r" % name)
+
+
+class _Option(object):
+ def __init__(self, name, default=None, type=str, help=None, metavar=None,
+ multiple=False, file_name=None):
+ if default is None and multiple:
+ default = []
+ self.name = name
+ self.type = type
+ self.help = help
+ self.metavar = metavar
+ self.multiple = multiple
+ self.file_name = file_name
+ self.default = default
+ self._value = None
+
+ def value(self):
+ return self.default if self._value is None else self._value
+
+ def parse(self, value):
+ _parse = {
+ datetime.datetime: self._parse_datetime,
+ datetime.timedelta: self._parse_timedelta,
+ bool: self._parse_bool,
+ str: self._parse_string,
+ }.get(self.type, self.type)
+ if self.multiple:
+ if self._value is None:
+ self._value = []
+ for part in value.split(","):
+ if self.type in (int, long):
+ # allow ranges of the form X:Y (inclusive at both ends)
+ lo, _, hi = part.partition(":")
+ lo = _parse(lo)
+ hi = _parse(hi) if hi else lo
+ self._value.extend(range(lo, hi+1))
+ else:
+ self._value.append(_parse(part))
+ else:
+ self._value = _parse(value)
+ return self.value()
+
+ def set(self, value):
+ if self.multiple:
+ if not isinstance(value, list):
+ raise Error("Option %r is required to be a list of %s" %
+ (self.name, self.type.__name__))
+ for item in value:
+ if item != None and not isinstance(item, self.type):
+ raise Error("Option %r is required to be a list of %s" %
+ (self.name, self.type.__name__))
+ else:
+ if value != None and not isinstance(value, self.type):
+ raise Error("Option %r is required to be a %s" %
+ (self.name, self.type.__name__))
+ self._value = value
+
+ # Supported date/time formats in our options
+ _DATETIME_FORMATS = [
+ "%a %b %d %H:%M:%S %Y",
+ "%Y-%m-%d %H:%M:%S",
+ "%Y-%m-%d %H:%M",
+ "%Y-%m-%dT%H:%M",
+ "%Y%m%d %H:%M:%S",
+ "%Y%m%d %H:%M",
+ "%Y-%m-%d",
+ "%Y%m%d",
+ "%H:%M:%S",
+ "%H:%M",
+ ]
+
+ def _parse_datetime(self, value):
+ for format in self._DATETIME_FORMATS:
+ try:
+ return datetime.datetime.strptime(value, format)
+ except ValueError:
+ pass
+ raise Error('Unrecognized date/time format: %r' % value)
+
+ _TIMEDELTA_ABBREVS = [
+ ('hours', ['h']),
+ ('minutes', ['m', 'min']),
+ ('seconds', ['s', 'sec']),
+ ('milliseconds', ['ms']),
+ ('microseconds', ['us']),
+ ('days', ['d']),
+ ('weeks', ['w']),
+ ]
+
+ _TIMEDELTA_ABBREV_DICT = dict(
+ (abbrev, full) for full, abbrevs in _TIMEDELTA_ABBREVS
+ for abbrev in abbrevs)
+
+ _FLOAT_PATTERN = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?'
+
+ _TIMEDELTA_PATTERN = re.compile(
+ r'\s*(%s)\s*(\w*)\s*' % _FLOAT_PATTERN, re.IGNORECASE)
+
+ def _parse_timedelta(self, value):
+ try:
+ sum = datetime.timedelta()
+ start = 0
+ while start < len(value):
+ m = self._TIMEDELTA_PATTERN.match(value, start)
+ if not m:
+ raise Exception()
+ num = float(m.group(1))
+ units = m.group(2) or 'seconds'
+ units = self._TIMEDELTA_ABBREV_DICT.get(units, units)
+ sum += datetime.timedelta(**{units: num})
+ start = m.end()
+ return sum
+ except:
+ raise
+
+ def _parse_bool(self, value):
+ return value.lower() not in ("false", "0", "f")
+
+ def _parse_string(self, value):
+ return value.decode("utf-8")
+
+
+class Error(Exception):
+ pass
+
+
+def enable_pretty_logging():
+ """Turns on formatted logging output as configured."""
+ root_logger = logging.getLogger()
+ if options.log_file_prefix:
+ channel = logging.handlers.RotatingFileHandler(
+ filename=options.log_file_prefix,
+ maxBytes=options.log_file_max_size,
+ backupCount=options.log_file_num_backups)
+ channel.setFormatter(_LogFormatter(color=False))
+ root_logger.addHandler(channel)
+
+ if (options.log_to_stderr or
+ (options.log_to_stderr is None and not root_logger.handlers)):
+ # Set up color if we are in a tty and curses is installed
+ color = False
+ if curses and sys.stderr.isatty():
+ try:
+ curses.setupterm()
+ if curses.tigetnum("colors") > 0:
+ color = True
+ except:
+ pass
+ channel = logging.StreamHandler()
+ channel.setFormatter(_LogFormatter(color=color))
+ root_logger.addHandler(channel)
+
+
+
+class _LogFormatter(logging.Formatter):
+ def __init__(self, color, *args, **kwargs):
+ logging.Formatter.__init__(self, *args, **kwargs)
+ self._color = color
+ if color:
+ # The curses module has some str/bytes confusion in python3.
+ # Most methods return bytes, but only accept strings.
+ # The explict calls to unicode() below are harmless in python2,
+ # but will do the right conversion in python3.
+ fg_color = unicode(curses.tigetstr("setaf") or
+ curses.tigetstr("setf") or "", "ascii")
+ self._colors = {
+ logging.DEBUG: unicode(curses.tparm(fg_color, 4), # Blue
+ "ascii"),
+ logging.INFO: unicode(curses.tparm(fg_color, 2), # Green
+ "ascii"),
+ logging.WARNING: unicode(curses.tparm(fg_color, 3), # Yellow
+ "ascii"),
+ logging.ERROR: unicode(curses.tparm(fg_color, 1), # Red
+ "ascii"),
+ }
+ self._normal = unicode(curses.tigetstr("sgr0"), "ascii")
+
+ def format(self, record):
+ try:
+ record.message = record.getMessage()
+ except Exception, e:
+ record.message = "Bad message (%r): %r" % (e, record.__dict__)
+ record.asctime = time.strftime(
+ "%y%m%d %H:%M:%S", self.converter(record.created))
+ prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \
+ record.__dict__
+ if self._color:
+ prefix = (self._colors.get(record.levelno, self._normal) +
+ prefix + self._normal)
+ formatted = prefix + " " + record.message
+ if record.exc_info:
+ if not record.exc_text:
+ record.exc_text = self.formatException(record.exc_info)
+ if record.exc_text:
+ formatted = formatted.rstrip() + "\n" + record.exc_text
+ return formatted.replace("\n", "\n ")
+
+
+options = _Options.instance()
+
+
+# Default options
+define("help", type=bool, help="show this help information")
+define("logging", default="info",
+ help=("Set the Python log level. If 'none', tornado won't touch the "
+ "logging configuration."),
+ metavar="info|warning|error|none")
+define("log_to_stderr", type=bool, default=None,
+ help=("Send log output to stderr (colorized if possible). "
+ "By default use stderr if --log_file_prefix is not set and "
+ "no other logging is configured."))
+define("log_file_prefix", type=str, default=None, metavar="PATH",
+ help=("Path prefix for log files. "
+ "Note that if you are running multiple tornado processes, "
+ "log_file_prefix must be different for each of them (e.g. "
+ "include the port number)"))
+define("log_file_max_size", type=int, default=100 * 1000 * 1000,
+ help="max size of log files before rollover")
+define("log_file_num_backups", type=int, default=10,
+ help="number of log files to keep")
+
View
13 thumbor/server.py
@@ -8,6 +8,7 @@
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com
+from os.path import abspath, join, dirname
import signal
import optparse
import logging
@@ -93,16 +94,8 @@ def run_app(ip, port, conf, log_level, app):
except Exception, err:
raise RuntimeError('Could not import your custom application "%s" because of error: %s' % (app, str(err)))
- options = {
- 'verbose': False,
- 'host': ip,
- 'port': port,
- 'processes': 1,
- 'threads': 30,
- 'reload': False
- }
-
- application.run(options)
+ conf_path = conf or join(dirname(__file__), 'thumbor_conf.py')
+ application.run(abspath(conf_path))
if __name__ == "__main__":
main()
View
12 thumbor/storages/file_storage.py
@@ -12,10 +12,10 @@
from datetime import datetime
from os.path import splitext
-from tornado.options import options
from os.path import exists, dirname, join, getmtime
from thumbor.storages import BaseStorage
+from thumbor.config import conf
class Storage(BaseStorage):
@@ -29,14 +29,14 @@ def put(self, path, bytes):
with open(file_abspath, 'w') as _file:
_file.write(bytes)
- if options.STORES_CRYPTO_KEY_FOR_EACH_IMAGE:
+ if conf.STORES_CRYPTO_KEY_FOR_EACH_IMAGE:
- if not options.SECURITY_KEY:
+ if not conf.SECURITY_KEY:
raise RuntimeError("STORES_CRYPTO_KEY_FOR_EACH_IMAGE can't be True if no SECURITY_KEY specified")
crypto_path = '%s.txt' % splitext(file_abspath)[0]
with open(crypto_path, 'w') as _file:
- _file.write(options.SECURITY_KEY)
+ _file.write(conf.SECURITY_KEY)
def get_crypto(self, path):
file_abspath = self.__normalize_path(path)
@@ -57,8 +57,8 @@ def __normalize_path(self, path):
if path.startswith('/'):
path = path[1:]
- return join(options.FILE_STORAGE_ROOT_PATH, path)
+ return join(conf.FILE_STORAGE_ROOT_PATH, path)
def __is_expired(self, path):
timediff = datetime.now() - datetime.fromtimestamp(getmtime(path))
- return timediff.seconds > options.STORAGE_EXPIRATION_SECONDS
+ return timediff.seconds > conf.STORAGE_EXPIRATION_SECONDS
View
12 thumbor/thumbor.conf → thumbor/thumbor_conf.py
@@ -8,6 +8,18 @@
# http://www.opensource.org/licenses/mit-license
# Copyright (c) 2011 globo.com timehome@corp.globo.com
+VERBOSE = False
+
+HOST = '0.0.0.0'
+
+PORT = 8888
+
+PROCESSES = 4
+
+THREADS = 20
+
+AUTO_RELOAD = True
+
# the domains that can have their images resized
# use an empty list for allow all sources
#ALLOWED_SOURCES = ['mydomain.com']
Please sign in to comment.
Something went wrong with that request. Please try again.