├── .gitignore ├── .pep8 ├── CREDITS.txt ├── LICENSE ├── README.md ├── requirements.txt └── server ├── admin_handlers.py ├── api_handlers.py ├── app.py ├── awsdownloader.py ├── awsuploader.py ├── bin ├── delete-image.py ├── fix-featured-value.py ├── list_all_files.py ├── recalculate-bytes-uploaded.py ├── recalculate-comments.py ├── run_dev_server.sh ├── unfeature-image.py └── unset-cdn_domain.py ├── downloader.py ├── emailer.py ├── handlers.py ├── html2text.py ├── optimizer.py ├── resizer.py ├── settings.py ├── static ├── authentication.js ├── bootstrap │ ├── css │ │ ├── bootstrap-customimage.min.css │ │ ├── bootstrap-modal.css │ │ ├── bootstrap-modal.min.css │ │ ├── bootstrap-responsive.css │ │ ├── bootstrap-responsive.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── bootstrap-collapse.js │ │ ├── bootstrap-collapse.min.js │ │ ├── bootstrap-modal.js │ │ ├── bootstrap-modal.min.js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js ├── browserid.js ├── comment.js ├── compiler.jar ├── css │ ├── app.css │ ├── docs.css │ ├── foundation.css │ ├── foundation.min.css │ ├── image.css │ ├── libs │ │ ├── images │ │ │ ├── draw-circle.png │ │ │ ├── draw-marker-icon.png │ │ │ ├── draw-polygon.png │ │ │ ├── draw-polyline.png │ │ │ ├── draw-rectangle.png │ │ │ ├── layers.png │ │ │ ├── marker-icon.png │ │ │ ├── marker-icon@2x.png │ │ │ ├── marker-shadow.png │ │ │ ├── zoom-in.png │ │ │ └── zoom-out.png │ │ ├── leaflet.css │ │ ├── leaflet.draw.css │ │ ├── leaflet.draw.ie.css │ │ ├── leaflet.ie.css │ │ └── persona-buttons.css │ └── polymap.css ├── download.js ├── edit.js ├── embed.js ├── favicon.ico ├── feedback.js ├── images │ ├── ajax-loader.gif │ ├── anonymous_32.png │ ├── broken.png │ ├── control-icons.png │ ├── control-icons │ │ ├── comment.png │ │ ├── edit.png │ │ ├── fullscreen.png │ │ ├── home.png │ │ ├── link.png │ │ └── nofullscreen.png │ ├── draw-polygon.png │ ├── draw-rectangle.png │ ├── favicon.ico │ └── file_broken.png ├── leafletmap.js ├── libs │ ├── bootstrap.min.js │ ├── head.load.min.js │ ├── humanize.js │ ├── images │ │ ├── layers.png │ │ ├── marker-icon.png │ │ ├── marker-icon@2x.png │ │ ├── marker-shadow.png │ │ ├── zoom-in.png │ │ └── zoom-out.png │ ├── jquery-1.8.3.min.js │ ├── leaflet.draw-src.js │ ├── leaflet.draw.min.js │ ├── leaflet.js │ ├── leaflet.min.js │ ├── modernizr-2.5.3.min.js │ └── modernizr.foundation.js └── stuff.js ├── templates ├── _comment_modal.html ├── _delete_modal.html ├── _edit_modal.html ├── _edit_snippets.html ├── _email.css ├── _email.html ├── _email.txt ├── _email_footer.html ├── _google_analytics.html ├── _meta.html ├── _newsletter_email.html ├── _pagination.html ├── _permalink_modal.html ├── _thumbnail_grid.html ├── about.html ├── admin │ ├── admin_base.html │ ├── banned.html │ ├── feedbacks.html │ ├── home.html │ ├── image.html │ ├── newsletter.html │ ├── render-all-thumbnails.html │ ├── tiles.html │ └── tweet.html ├── api.html ├── base.html ├── complete.upload.html ├── dropbox.html ├── email.upload.html ├── embed.html ├── error.upload.html ├── feedback.html ├── gettingstarted.html ├── image.html ├── index.html ├── popularity.html ├── precomplete.upload.html ├── preprogress.upload.html ├── privacy.html ├── progress.giveup.html ├── progress.upload.html ├── terms.upload.html ├── tweets.html ├── unsubscribed.html ├── upload.html └── yourhelp.html ├── tweeter.py ├── ui_modules.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | server/local_settings.py 2 | server/static/tiles/ 3 | server/static/uploads/ 4 | server/static/thumbnails/ 5 | server/.static_name_conversion 6 | server/static/upload.*.txt 7 | server/send_mail_config.py 8 | TODO 9 | -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pep8] 2 | # regarding E125, see https://github.com/jcrocholl/pep8/issues/126 3 | ignore=E125 4 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | http://led24.de/iconset/ for the favicon 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Peter Bengtsson 2 | * All rights reserved. 3 | * Redistribution and use in source and binary forms, with or without 4 | * modification, are permitted provided that the following conditions are met: 5 | * 6 | * * Redistributions of source code must retain the above copyright 7 | * notice, this list of conditions and the following disclaimer. 8 | * * Redistributions in binary form must reproduce the above copyright 9 | * notice, this list of conditions and the following disclaimer in the 10 | * documentation and/or other materials provided with the distribution. 11 | * * Neither the name of the University of California, Berkeley nor the 12 | * names of its contributors may be used to endorse or promote products 13 | * derived from this software without specific prior written permission. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY 16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tiler 2 | ===== 3 | 4 | App for allowing you to host some huge ass photos on the web. 5 | 6 | This is basically the code for the site: [HUGEpic](http://hugepic.io) which I maintain. 7 | 8 | Examples 9 | -------- 10 | 11 | * [The Garden of Earthly Delights - 222Mb](http://hugepic.io/4ab2ef08b) 12 | * [Lord of the Rings mosaic - 11.7Mb](http://hugepic.io/afacfabeb) 13 | 14 | The code 15 | -------- 16 | 17 | The code consists of the following major pieces: 18 | 19 | * all server-side code is [Tornado](http://www.tornadoweb.org/) 20 | * there are two databases: 21 | * [MongoDB](http://mongodb.org) main storage 22 | * [Redis](http://redis.io) primarily for caching 23 | * connecting Tornado and MongoDB is [Motor](http://emptysquare.net/motor/) 24 | * all message queue processes handled by [RQ](http://python-rq.org/) 25 | * the client-side browsing is all thanks to [Leaflet](http://leafletjs.com/) 26 | * with the annotation drawing thanks to [Leaflet.Draw](https://github.com/jacobtoye/Leaflet.draw) 27 | * Amazon S3 and CloudFront keeps all the images except for temporary copies 28 | * vipsthumbnail from [VIPS](http://www.vips.ecs.soton.ac.uk/index.php?title=VIPS) 29 | 30 | More has been [written about the technical here](http://www.peterbe.com/plog/introducing-hugepic.io). 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado 2 | redis 3 | PIL 4 | tornado-utils 5 | pycurl 6 | rq 7 | greenlet 8 | motor==0.1 9 | cssmin 10 | boto 11 | lxml 12 | cssselect 13 | premailer 14 | twython 15 | -------------------------------------------------------------------------------- /server/api_handlers.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import tornado.web 3 | import tornado.gen 4 | import motor 5 | from tornado_utils.routes import route 6 | from handlers import ( 7 | BaseHandler, 8 | PreviewMixin, 9 | DownloadMixin, 10 | TileMakerMixin, 11 | ProgressMixin, 12 | ImageMetadataMixin 13 | ) 14 | 15 | 16 | class APIKeyMixin(object): 17 | 18 | def generate_new_key(self): 19 | return uuid.uuid4().hex 20 | 21 | def set_new_key(self, user): 22 | your_key = self.generate_new_key() 23 | self.redis.hset('api:users', your_key, user) 24 | self.redis.hset('api:keys', user, your_key) 25 | return your_key 26 | 27 | def get_key(self, user): 28 | return self.redis.hget('api:keys', user) 29 | 30 | def get_user(self, key): 31 | return self.redis.hget('api:users', key) 32 | 33 | 34 | @route('/api/$', name='api') 35 | class APIHandler(BaseHandler, APIKeyMixin): 36 | 37 | def get(self): 38 | user = self.get_current_user() 39 | if user: 40 | your_key = self.get_key(user) 41 | if your_key is None: 42 | your_key = self.set_new_key(user) 43 | else: 44 | your_key = None 45 | 46 | base_url = ( 47 | '%s://%s' % 48 | (self.request.protocol, self.request.host) 49 | ) 50 | data = { 51 | 'your_key': your_key, 52 | 'base_url': base_url, 53 | } 54 | self.render('api.html', **data) 55 | 56 | 57 | class APIBaseHandler(BaseHandler, APIKeyMixin): 58 | 59 | def check_xsrf_cookie(self): 60 | pass 61 | 62 | def get_error_html(self, status_code, **kwargs): 63 | exception = kwargs['exception'] 64 | return {'code': status_code, 'error': exception.log_message} 65 | 66 | 67 | @route('/api/upload$', name='api_upload') 68 | class APIUploadHandler(APIBaseHandler, 69 | PreviewMixin, 70 | DownloadMixin, 71 | APIKeyMixin, 72 | TileMakerMixin): 73 | 74 | def get_current_user(self): 75 | return self.user 76 | 77 | @tornado.web.asynchronous 78 | @tornado.gen.engine 79 | def post(self): 80 | url = self.get_argument('url') 81 | key = self.get_argument('key') 82 | self.user = self.get_user(key) 83 | if not self.user: 84 | raise tornado.web.HTTPError(403, "Key not recognized") 85 | 86 | search = {'email': self.user} 87 | banned = yield motor.Op(self.db.banned.find(search).count) 88 | if banned: 89 | yield motor.Op( 90 | self.db.banned.update, 91 | {'email': self.user}, 92 | {'$inc': {'upload_attempts': 1}} 93 | ) 94 | raise tornado.web.HTTPError(412, "Banned") 95 | 96 | # XXX banned? 97 | response = yield tornado.gen.Task(self.run_preview, url) 98 | if 'error' in response: 99 | self.write(response) 100 | else: 101 | fileid = response['fileid'] 102 | second_response = yield tornado.gen.Task( 103 | self.run_download, 104 | fileid, 105 | add_delay=False, 106 | ) 107 | #print "\tSECOND_RESPONSE", second_response 108 | if 'error' in second_response: 109 | self.write(second_response) 110 | else: 111 | self.write(response) 112 | self.finish() 113 | 114 | 115 | @route('/api/upload/(?P\w{9})$', name='api_upload_progress') 116 | class APIUploadProgressHandler(APIBaseHandler, ProgressMixin): 117 | 118 | def get(self, fileid): 119 | content_type = self.redis.get('contenttype:%s' % fileid) 120 | if content_type is None: 121 | raise tornado.web.HTTPError( 122 | 410, 123 | 'Elvis has already left the building' 124 | ) 125 | expected_size = self.redis.get('expectedsize:%s' % fileid) 126 | 127 | data = self.get_progress(fileid, content_type=content_type) 128 | # data will only contain `done` to say how much has been saved, 129 | # let's make it a bit richer 130 | if expected_size is not None: 131 | data['left'] = int(expected_size) - data['done'] 132 | data['percentage'] = ( 133 | round(100.0 * data['done'] / int(expected_size), 1) 134 | ) 135 | 136 | sizeinfo = self.redis.get('sizeinfo:%s' % fileid) 137 | if sizeinfo is not None: 138 | sizeinfo = tornado.escape.json_decode(sizeinfo) 139 | for key in ('width', 'height'): 140 | if key in sizeinfo: 141 | data[key] = sizeinfo[key] 142 | base_url = ( 143 | '%s://%s' % 144 | (self.request.protocol, self.request.host) 145 | ) 146 | data['url'] = base_url + self.reverse_url('image', fileid) 147 | self.write(data) 148 | 149 | 150 | @route('/api/(?P\w{9})$', name='api_image') 151 | class APIImageHandler(APIBaseHandler, ImageMetadataMixin): 152 | 153 | @tornado.web.asynchronous 154 | @tornado.gen.engine 155 | def put(self, fileid): 156 | key = self.get_argument('key') 157 | self.user = self.get_user(key) 158 | if not self.user: 159 | raise tornado.web.HTTPError(403, "Key not recognized") 160 | 161 | data = yield tornado.gen.Task( 162 | self.run_edit, 163 | fileid, 164 | self.user 165 | ) 166 | self.write(data) 167 | self.finish() 168 | -------------------------------------------------------------------------------- /server/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | import logging 6 | from time import sleep 7 | import tornado.httpserver 8 | import tornado.ioloop 9 | from tornado.options import define, options 10 | from tornado_utils.routes import route 11 | import redis.client 12 | from rq import Queue 13 | import motor 14 | import settings 15 | import handlers 16 | import api_handlers 17 | import admin_handlers 18 | 19 | 20 | define("debug", default=False, help="run in debug mode", type=bool) 21 | define("port", default=8000, help="run on the given port", type=int) 22 | define("dont_optimize_static_content", default=False, 23 | help="Don't combine static resources", type=bool) 24 | define("dont_embed_static_url", default=False, 25 | help="Don't put embed the static URL in static_url()", type=bool) 26 | 27 | 28 | class Application(tornado.web.Application): 29 | 30 | _redis = None 31 | 32 | @property 33 | def redis(self): 34 | if not self._redis: 35 | self._redis = redis.client.Redis( 36 | settings.REDIS_HOST, 37 | settings.REDIS_PORT 38 | ) 39 | return self._redis 40 | 41 | _db_connection = None 42 | 43 | @property 44 | def db(self): 45 | if not self._db_connection: 46 | self._db_connection = motor.MotorClient().open_sync() 47 | return self._db_connection[settings.DATABASE_NAME] 48 | 49 | def __init__(self, database_name=None, optimize_static_content=None): 50 | ui_modules_map = {} 51 | for each in ('ui_modules',): 52 | _ui_modules = __import__(each, globals(), locals(), 53 | ['ui_modules'], -1) 54 | for name in [x for x in dir(_ui_modules) 55 | if re.findall('[A-Z]\w+', x)]: 56 | thing = getattr(_ui_modules, name) 57 | try: 58 | if issubclass(thing, tornado.web.UIModule): 59 | ui_modules_map[name] = thing 60 | except TypeError: # pragma: no cover 61 | # most likely a builtin class or something 62 | pass 63 | 64 | if optimize_static_content is None: 65 | optimize_static_content = not options.dont_optimize_static_content 66 | 67 | try: 68 | cdn_prefix = [x.strip() for x in open('cdn_prefix.conf') 69 | if x.strip() and not x.strip().startswith('#')][0] 70 | logging.info("Using %r as static URL prefix" % cdn_prefix) 71 | except (IOError, IndexError): 72 | cdn_prefix = None 73 | 74 | from tornado_utils import tornado_static 75 | ui_modules_map['Static'] = tornado_static.Static 76 | ui_modules_map['StaticURL'] = tornado_static.StaticURL 77 | ui_modules_map['Static64'] = tornado_static.Static64 78 | if not optimize_static_content: 79 | ui_modules_map['Static'] = tornado_static.PlainStatic 80 | ui_modules_map['StaticURL'] = tornado_static.PlainStaticURL 81 | 82 | routed_handlers = route.get_routes() 83 | app_settings = dict( 84 | template_path=os.path.join(os.path.dirname(__file__), "templates"), 85 | static_path=os.path.join(os.path.dirname(__file__), "static"), 86 | cookie_secret=settings.COOKIE_SECRET, 87 | xsrf_cookies=True, 88 | debug=options.debug, 89 | email_backend=options.debug and \ 90 | 'tornado_utils.send_mail.backends.console.EmailBackend' \ 91 | or 'tornado_utils.send_mail.backends.pickle.EmailBackend', 92 | admin_emails=settings.ADMIN_EMAILS, 93 | ui_modules=ui_modules_map, 94 | embed_static_url_timestamp=not options.dont_embed_static_url, 95 | optimize_static_content=optimize_static_content, 96 | cdn_prefix=cdn_prefix, 97 | CLOSURE_LOCATION=os.path.join(os.path.dirname(__file__), 98 | "static", "compiler.jar"), 99 | twitter_consumer_key=settings.TWITTER_CONSUMER_KEY, 100 | twitter_consumer_secret=settings.TWITTER_CONSUMER_SECRET, 101 | 102 | ) 103 | routed_handlers.append( 104 | tornado.web.url('/.*?', 105 | handlers.PageNotFoundHandler, 106 | name='page_not_found') 107 | ) 108 | super(Application, self).__init__(routed_handlers, **app_settings) 109 | 110 | self.db # property gets created 111 | 112 | 113 | def main(): # pragma: no cover 114 | tornado.options.parse_command_line() 115 | 116 | q = Queue(connection=redis.client.Redis( 117 | settings.REDIS_HOST, 118 | settings.REDIS_PORT 119 | )) 120 | 121 | job = q.enqueue(handlers.sample_queue_job) 122 | for i in range(12): 123 | if job.result: 124 | break 125 | sleep(i / 10.0) 126 | if i > 0 and not i % 3: 127 | print "Waiting to see if Queue workers are awake..." 128 | else: 129 | raise SystemError("Queue workers not responding") 130 | 131 | http_server = tornado.httpserver.HTTPServer(Application()) 132 | print "Starting tornado on port", options.port 133 | logging.info("Starting tornado on port :%s" % options.port) 134 | http_server.listen(options.port) 135 | try: 136 | tornado.ioloop.IOLoop.instance().start() 137 | except KeyboardInterrupt: 138 | pass 139 | 140 | 141 | if __name__ == "__main__": # pragma: no cover 142 | main() 143 | -------------------------------------------------------------------------------- /server/awsdownloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from boto.s3.connection import Location, S3Connection 4 | from boto.s3.key import Key 5 | 6 | import settings 7 | 8 | 9 | def download_original(relative_path, static_path, bucket_id): 10 | conn = S3Connection(settings.AWS_ACCESS_KEY, settings.AWS_SECRET_KEY) 11 | bucket = conn.get_bucket(bucket_id) 12 | k = Key(bucket) 13 | k.key = relative_path 14 | destination = os.path.abspath(os.path.join(static_path, relative_path)) 15 | k.get_contents_to_filename(destination) 16 | -------------------------------------------------------------------------------- /server/awsuploader.py: -------------------------------------------------------------------------------- 1 | import stat 2 | import warnings 3 | import datetime 4 | import random 5 | import os 6 | import time 7 | import email 8 | import motor 9 | import pymongo 10 | import logging 11 | from boto.s3.connection import Location, S3Connection 12 | from boto.s3.key import Key 13 | from tornado import gen 14 | from tornado.ioloop import IOLoop 15 | import redis.client 16 | import settings 17 | from utils import find_all_tiles, find_original 18 | 19 | 20 | def upload_original(fileid, extension, static_path, bucket_id): 21 | conn = S3Connection(settings.AWS_ACCESS_KEY, settings.AWS_SECRET_KEY) 22 | bucket = conn.lookup(bucket_id) or conn.create_bucket(bucket_id, location=Location.EU) 23 | 24 | db_connection = motor.MotorClient().open_sync() 25 | db = db_connection[settings.DATABASE_NAME] 26 | 27 | original = find_original(fileid, static_path, extension) 28 | if original: 29 | relative_path = original.replace(static_path, '') 30 | k = Key(bucket) 31 | k.key = relative_path 32 | print "Uploading original", original 33 | s = os.stat(original)[stat.ST_SIZE] 34 | print "%.1fKb" % (s / 1024.) 35 | # reduced because I'm a cheapskate 36 | k.set_contents_from_filename(original, reduced_redundancy=True) 37 | print "Original uploaded" 38 | else: 39 | print "Original can't be found", repr(original) 40 | 41 | 42 | def update_tiles_metadata(tile_paths, years=1, max_updates=10, suffix=None): 43 | conn = S3Connection(settings.AWS_ACCESS_KEY, settings.AWS_SECRET_KEY) 44 | bucket = conn.lookup(settings.TILES_BUCKET_ID) 45 | 46 | for tile_path in tile_paths: 47 | key = bucket.get_key(tile_path) 48 | if key is None: 49 | logging.warning("%r is not found as a key" % tile_path) 50 | return 51 | 52 | aggressive_headers = _get_aggressive_cache_headers(key, years) 53 | key.copy( 54 | settings.TILES_BUCKET_ID, 55 | key, 56 | metadata=aggressive_headers, 57 | preserve_acl=True 58 | ) 59 | print key, "DONE" 60 | 61 | 62 | def _get_aggressive_cache_headers(key, years): 63 | metadata = key.metadata 64 | 65 | metadata['Content-Type'] = key.content_type 66 | 67 | # HTTP/1.0 68 | metadata['Expires'] = '%s GMT' %\ 69 | (email.Utils.formatdate( 70 | time.mktime((datetime.datetime.now() + 71 | datetime.timedelta(days=365 * years)).timetuple()))) 72 | 73 | # HTTP/1.1 74 | metadata['Cache-Control'] = 'max-age=%d, public' % (3600 * 24 * 360 * years) 75 | 76 | return metadata 77 | 78 | 79 | @gen.engine 80 | def upload_all_tiles(fileid, static_path, bucket_id, max_count=0, 81 | only_if_no_cdn_domain=False, 82 | replace=True): 83 | log_file = os.path.join(static_path, 'upload.%s.txt' % fileid) 84 | 85 | conn = S3Connection(settings.AWS_ACCESS_KEY, settings.AWS_SECRET_KEY) 86 | bucket = conn.lookup(bucket_id) or conn.create_bucket(bucket_id, location=Location.EU) 87 | #bucket.set_acl('public-read') 88 | 89 | db_connection = motor.MotorClient().open_sync() 90 | db = db_connection[settings.DATABASE_NAME] 91 | 92 | document = yield motor.Op( 93 | db.images.find_one, 94 | {'fileid': fileid} 95 | ) 96 | if not document: 97 | logging.warning("Image %r does not exist" % fileid) 98 | IOLoop.instance().stop() 99 | return 100 | 101 | if document.get('cdn_domain'): 102 | if only_if_no_cdn_domain: 103 | IOLoop.instance().stop() 104 | return 105 | else: 106 | warnings.warn("%s already has a cdn_domain (%s)" % 107 | (fileid, document['cdn_domain'])) 108 | 109 | try: 110 | count = 0 111 | all_done = True 112 | all_tiles = list(find_all_tiles(fileid, static_path)) 113 | random.shuffle(all_tiles) 114 | #if len(all_tiles) > max_count: 115 | # total = max_count 116 | #else: 117 | # total = len(all_tiles) 118 | total = len(all_tiles) 119 | aggressive_headers = get_aggressive_headers() 120 | for each in all_tiles: 121 | # load which ones we've done every time to prevent 122 | # parallel workers uploading the same file more than once 123 | try: 124 | done = [x.strip() for x in open(log_file) if x.strip()] 125 | except IOError: 126 | done = [] 127 | if each not in done: 128 | done.append(each) 129 | relative_path = each.replace(static_path, '') 130 | k = Key(bucket) 131 | k.key = relative_path 132 | # docs: 133 | # http://boto.cloudhackers.com/en/latest/ref/s3.html#boto.s3.\ 134 | # key.Key.set_contents_from_filename 135 | print "uploading", relative_path, 136 | try: 137 | count_done = set(x.strip() for x in open(log_file)) 138 | except IOError: 139 | count_done = [] 140 | print "(%d of %d)" % (len(count_done), total) 141 | k.set_contents_from_filename( 142 | each, 143 | replace=replace, 144 | reduced_redundancy=True, 145 | headers=aggressive_headers, 146 | ) 147 | k.make_public() 148 | open(log_file, 'a').write(each + '\n') 149 | count += 1 150 | if max_count > 0 and count >= max_count: 151 | print "STOPPING @", count 152 | all_done = False 153 | break 154 | 155 | if all_done: 156 | data = {'cdn_domain': settings.DEFAULT_CDN_TILER_DOMAIN} 157 | print "Updating document finally" 158 | yield motor.Op( 159 | db.images.update, 160 | {'_id': document['_id']}, 161 | {'$set': data} 162 | ) 163 | # invalidate some redis keys 164 | _redis = redis.client.Redis( 165 | settings.REDIS_HOST, 166 | settings.REDIS_PORT 167 | ) 168 | lock_key = 'uploading:%s' % fileid 169 | _redis.delete(lock_key) 170 | metadata_key = 'metadata:%s' % fileid 171 | # make it expire in a minute 172 | data = _redis.get(metadata_key) 173 | if data: 174 | # this gives all workers a chance to finish 175 | # any leftover jobs such as optimizations 176 | _redis.setex(metadata_key, data, 60) 177 | 178 | finally: 179 | print "# done", count 180 | IOLoop.instance().stop() 181 | 182 | 183 | def get_aggressive_headers(years=1): 184 | cache_control = 'max-age=%d, public' % (3600 * 24 * 360 * years) 185 | _delta = datetime.timedelta(days=365 * years) 186 | expires = email.Utils.formatdate( 187 | time.mktime((datetime.datetime.utcnow() + _delta).timetuple()) 188 | ) 189 | 190 | return { 191 | 'Cache-Control': cache_control, 192 | 'Expires': expires, 193 | } 194 | 195 | 196 | def upload_tiles(fileid, static_path, max_count=10, 197 | only_if_no_cdn_domain=False, 198 | replace=True): 199 | upload_all_tiles( 200 | fileid, 201 | static_path, 202 | settings.TILES_BUCKET_ID, 203 | max_count=max_count, 204 | only_if_no_cdn_domain=only_if_no_cdn_domain, 205 | replace=replace 206 | ) 207 | IOLoop.instance().start() 208 | 209 | 210 | def run(*fileids): 211 | static_path = os.path.join(os.path.abspath(os.curdir), 'static') 212 | for fileid in fileids: 213 | upload_original(fileid, 'jpg', static_path, settings.ORIGINALS_BUCKET_ID) 214 | 215 | #upload_all_tiles(fileid, static_path, settings.TILES_BUCKET_ID, 216 | # max_count=3) 217 | #IOLoop.instance().start() 218 | 219 | 220 | if __name__ == '__main__': 221 | import sys 222 | run(*sys.argv[1:]) 223 | -------------------------------------------------------------------------------- /server/bin/delete-image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import motor 4 | import shutil 5 | from tornado import gen 6 | from tornado.ioloop import IOLoop 7 | import redis.client 8 | import sys 9 | ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 10 | sys.path.insert(0, ROOT) 11 | import settings 12 | 13 | 14 | HERE = os.path.dirname(__file__) 15 | 16 | @gen.engine 17 | def run(*fileids): 18 | _redis = redis.client.Redis( 19 | settings.REDIS_HOST, 20 | settings.REDIS_PORT 21 | ) 22 | connection = motor.MotorClient().open_sync() 23 | db = connection.tiler 24 | cursor = db.images.find({'fileid': {'$in': fileids}}) 25 | _ids = [] 26 | for document in (yield motor.Op(cursor.to_list)): 27 | print document 28 | image_split = document['fileid'][:1] + '/' + document['fileid'][1:3] + '/' + document['fileid'][3:] 29 | for each in ('tiles', 'uploads', 'thumbnails'): 30 | d = os.path.join(HERE, '..', 'static', 'tiles', image_split) 31 | d = os.path.normpath(d) 32 | if os.path.isdir(d): 33 | print "DEL", d 34 | shutil.rmtree(d) 35 | 36 | metadata_key = 'metadata:%s' % document['fileid'] 37 | if _redis.get(metadata_key): 38 | print "Invalidated metadata cache" 39 | _redis.delete(metadata_key) 40 | lock_key = 'uploading:%s' % document['fileid'] 41 | _redis.delete(lock_key) 42 | 43 | all_fileids_key = 'allfileids' 44 | _redis.delete(all_fileids_key) 45 | all_fileids_key += ':%s' % document['user'] 46 | _redis.delete(all_fileids_key) 47 | 48 | cache_keys_key = 'thumbnail_grid:keys' 49 | for key in _redis.lrange(cache_keys_key, 0, -1): 50 | _redis.delete(key) 51 | _redis.delete(cache_keys_key) 52 | 53 | yield motor.Op( 54 | db.images.remove, 55 | {'_id': document['_id']} 56 | ) 57 | 58 | IOLoop.instance().stop() 59 | 60 | 61 | if __name__ == '__main__': 62 | import re 63 | import sys 64 | if not sys.argv[1:]: 65 | print "%s fileid1 fileid2 fileidN" % __file__ 66 | exit() 67 | fileids = [] 68 | for each in sys.argv[1:]: 69 | if len(each) == 9: 70 | fileid = each 71 | elif '://' in each: 72 | fileid = re.findall('[a-f0-9]{9}', each)[0] 73 | fileids.append(fileid) 74 | run(*fileids) 75 | IOLoop.instance().start() 76 | -------------------------------------------------------------------------------- /server/bin/fix-featured-value.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import time 4 | import motor 5 | import shutil 6 | from pprint import pprint 7 | from tornado import gen 8 | from tornado.ioloop import IOLoop 9 | import redis.client 10 | import sys 11 | ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 12 | sys.path.insert(0, ROOT) 13 | import settings 14 | 15 | 16 | @gen.engine 17 | def run(*args): 18 | _redis = redis.client.Redis( 19 | settings.REDIS_HOST, 20 | settings.REDIS_PORT 21 | ) 22 | connection = motor.MotorClient().open_sync() 23 | db = connection.tiler 24 | 25 | yield motor.Op( 26 | db.images.remove, 27 | {'width': {'$exists': False}} 28 | ) 29 | 30 | try: 31 | cursor = ( 32 | db.images.find({'featured': {'$exists': False}}) 33 | ) 34 | image = yield motor.Op(cursor.next_object) 35 | _fileids = {} 36 | while image: 37 | yield motor.Op( 38 | db.images.update, 39 | {'_id': image['_id']}, 40 | {'$set': {'featured': True}} 41 | ) 42 | print image['fileid'] 43 | image = yield motor.Op(cursor.next_object) 44 | 45 | finally: 46 | IOLoop.instance().stop() 47 | 48 | 49 | if __name__ == '__main__': 50 | run(*sys.argv[1:]) 51 | IOLoop.instance().start() 52 | -------------------------------------------------------------------------------- /server/bin/list_all_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, sys 4 | sys.path.insert( 5 | 0, 6 | os.path.join(os.path.dirname(__file__), '..') 7 | ) 8 | 9 | from utils import find_all_tiles 10 | 11 | 12 | def run(fileids): 13 | static_path = os.path.join(os.path.abspath(os.curdir), 'static') 14 | for fileid in fileids: 15 | for each in find_all_tiles(fileid, static_path): 16 | print each 17 | assert os.path.isfile(each), each 18 | 19 | 20 | if __name__ == '__main__': 21 | import sys 22 | run(*sys.argv[1:]) 23 | -------------------------------------------------------------------------------- /server/bin/recalculate-bytes-uploaded.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import time 4 | import motor 5 | import shutil 6 | from pprint import pprint 7 | from tornado import gen 8 | from tornado.ioloop import IOLoop 9 | import redis.client 10 | import sys 11 | ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 12 | sys.path.insert(0, ROOT) 13 | import settings 14 | 15 | 16 | 17 | @gen.engine 18 | def run(*args): 19 | _redis = redis.client.Redis( 20 | settings.REDIS_HOST, 21 | settings.REDIS_PORT 22 | ) 23 | connection = motor.MotorClient().open_sync() 24 | db = connection.tiler 25 | 26 | 27 | try: 28 | cursor = ( 29 | db.images.find() 30 | ) 31 | image = yield motor.Op(cursor.next_object) 32 | while image: 33 | _redis.incr('bytes_downloaded', image['size']) 34 | image = yield motor.Op(cursor.next_object) 35 | 36 | finally: 37 | IOLoop.instance().stop() 38 | 39 | 40 | if __name__ == '__main__': 41 | run(*sys.argv[1:]) 42 | IOLoop.instance().start() 43 | -------------------------------------------------------------------------------- /server/bin/recalculate-comments.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import time 4 | import motor 5 | import shutil 6 | from pprint import pprint 7 | from tornado import gen 8 | from tornado.ioloop import IOLoop 9 | import redis.client 10 | import sys 11 | ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 12 | sys.path.insert(0, ROOT) 13 | import settings 14 | 15 | 16 | 17 | @gen.engine 18 | def run(*args): 19 | _redis = redis.client.Redis( 20 | settings.REDIS_HOST, 21 | settings.REDIS_PORT 22 | ) 23 | connection = motor.MotorClient().open_sync() 24 | db = connection.tiler 25 | 26 | try: 27 | cursor = ( 28 | db.comments.find() 29 | ) 30 | comment = yield motor.Op(cursor.next_object) 31 | _fileids = {} 32 | while comment: 33 | if comment['image'] not in _fileids: 34 | image = yield motor.Op( 35 | db.images.find_one, 36 | {'_id': comment['image']} 37 | ) 38 | _fileids[comment['image']] = image['fileid'] 39 | fileid = _fileids[comment['image']] 40 | print fileid 41 | _redis.hincrby('comments', fileid, 1) 42 | print _redis.hget('comments', fileid) 43 | #_redis.incr('bytes_downloaded', image['size']) 44 | comment = yield motor.Op(cursor.next_object) 45 | 46 | finally: 47 | IOLoop.instance().stop() 48 | 49 | 50 | if __name__ == '__main__': 51 | run(*sys.argv[1:]) 52 | IOLoop.instance().start() 53 | -------------------------------------------------------------------------------- /server/bin/run_dev_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./app.py --debug --dont_embed_static_url --dont_optimize_static_content $@ 3 | -------------------------------------------------------------------------------- /server/bin/unfeature-image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import motor 4 | import shutil 5 | from pprint import pprint 6 | from tornado import gen 7 | from tornado.ioloop import IOLoop 8 | import redis.client 9 | import sys 10 | ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 11 | sys.path.insert(0, ROOT) 12 | import settings 13 | 14 | 15 | 16 | @gen.engine 17 | def run(*fileids): 18 | _redis = redis.client.Redis( 19 | settings.REDIS_HOST, 20 | settings.REDIS_PORT 21 | ) 22 | connection = motor.MotorClient().open_sync() 23 | db = connection.tiler 24 | 25 | try: 26 | cursor = db.images.find({'fileid': {'$in': fileids}}) 27 | for document in (yield motor.Op(cursor.to_list)): 28 | pprint(document) 29 | yield motor.Op( 30 | db.images.update, 31 | {'_id': document['_id']}, 32 | {'$set': {'featured': False}} 33 | ) 34 | 35 | cache_keys_key = 'thumbnail_grid:keys' 36 | for key in _redis.lrange(cache_keys_key, 0, -1): 37 | _redis.delete(key) 38 | _redis.delete(cache_keys_key) 39 | 40 | finally: 41 | IOLoop.instance().stop() 42 | 43 | 44 | if __name__ == '__main__': 45 | import sys 46 | if not sys.argv[1:]: 47 | print "%s fileid1 fileid2 fileidN" % __file__ 48 | exit() 49 | run(*sys.argv[1:]) 50 | IOLoop.instance().start() 51 | -------------------------------------------------------------------------------- /server/bin/unset-cdn_domain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import time 4 | import motor 5 | import shutil 6 | from pprint import pprint 7 | from tornado import gen 8 | from tornado.ioloop import IOLoop 9 | import redis.client 10 | import sys 11 | ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 12 | sys.path.insert(0, ROOT) 13 | import settings 14 | 15 | 16 | 17 | @gen.engine 18 | def run(*fileids): 19 | _redis = redis.client.Redis( 20 | settings.REDIS_HOST, 21 | settings.REDIS_PORT 22 | ) 23 | connection = motor.MotorClient().open_sync() 24 | db = connection.tiler 25 | 26 | try: 27 | cursor = db.images.find({'fileid': {'$in': fileids}}) 28 | for document in (yield motor.Op(cursor.to_list)): 29 | pprint(document) 30 | yield motor.Op( 31 | db.images.update, 32 | {'_id': document['_id']}, 33 | {'$unset': {'cdn_domain': 1}} 34 | ) 35 | metadata_key = 'metadata:%s' % document['fileid'] 36 | if _redis.get(metadata_key): 37 | print "Invalidated metadata cache" 38 | _redis.delete(metadata_key) 39 | lock_key = 'uploading:%s' % document['fileid'] 40 | # locking it from aws upload for 1 hour 41 | _redis.setex(lock_key, time.time(), 60 * 60) 42 | 43 | upload_log = os.path.join( 44 | ROOT, 45 | 'static', 46 | 'upload.%s.txt' % document['fileid'] 47 | ) 48 | if os.path.isfile(upload_log): 49 | os.remove(upload_log) 50 | 51 | print "\n" 52 | 53 | finally: 54 | IOLoop.instance().stop() 55 | 56 | 57 | if __name__ == '__main__': 58 | import sys 59 | if not sys.argv[1:]: 60 | print "%s fileid1 fileid2 fileidN" % __file__ 61 | exit() 62 | run(*sys.argv[1:]) 63 | IOLoop.instance().start() 64 | -------------------------------------------------------------------------------- /server/downloader.py: -------------------------------------------------------------------------------- 1 | import re 2 | import cStringIO 3 | import functools 4 | import pycurl 5 | 6 | 7 | def slow_writer(f, buf): 8 | from time import sleep 9 | sleep(0.1) 10 | f.write(buf) 11 | 12 | def download(url, destination, 13 | follow_redirects=False, request_timeout=600): 14 | _error = _effective_url = None 15 | with open(destination, 'wb') as destination_file: 16 | hdr = cStringIO.StringIO() 17 | c = pycurl.Curl() 18 | c.setopt(pycurl.URL, str(url)) 19 | c.setopt(pycurl.FOLLOWLOCATION, follow_redirects) 20 | c.setopt(pycurl.HEADERFUNCTION, hdr.write) 21 | c.setopt(pycurl.WRITEFUNCTION, destination_file.write) 22 | #c.setopt(pycurl.WRITEFUNCTION, functools.partial(slow_writer, destination_file)) 23 | c.setopt(pycurl.TIMEOUT_MS, int(1000 * request_timeout)) 24 | c.perform() 25 | code = c.getinfo(pycurl.HTTP_CODE) 26 | _effective_url = c.getinfo(pycurl.EFFECTIVE_URL) 27 | if _effective_url == url: 28 | _effective_url = None 29 | code = c.getinfo(pycurl.HTTP_CODE) 30 | if code != 200: 31 | status_line = hdr.getvalue().splitlines()[0] 32 | for each in re.findall(r'HTTP\/\S*\s*\d+\s*(.*?)\s*$', status_line): 33 | _error = each 34 | 35 | response = {'code': code} 36 | if _error: 37 | response['body'] = _error 38 | if _effective_url: 39 | response['url'] = _effective_url 40 | return response 41 | 42 | 43 | if __name__ == '__main__': 44 | import sys 45 | import os 46 | url = sys.argv[1] 47 | assert '://' in url 48 | destination = sys.argv[2] 49 | assert os.path.isdir(os.path.dirname(destination)) 50 | from pprint import pprint 51 | pprint(download(url, destination)) 52 | -------------------------------------------------------------------------------- /server/emailer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | from tornado_utils.send_mail import send_multipart_email, send_email 4 | from html2text import html2text 5 | import settings 6 | 7 | 8 | def _get_backend(debug=False): 9 | if debug: 10 | return 'tornado_utils.send_mail.backends.console.EmailBackend' 11 | else: 12 | return 'tornado_utils.send_mail.backends.smtp.EmailBackend' 13 | 14 | 15 | def send_url(url, fileid, recipient, html_body, plain_body=None, debug=False): 16 | backend = _get_backend(debug) 17 | from_ = 'HUGEPic ' 18 | subject = "Your HUGE upload has finished" 19 | if not plain_body: 20 | plain_body = html2text(html_body) 21 | logging.info('Sending email to %s', recipient) 22 | send_multipart_email( 23 | backend, 24 | plain_body, 25 | html_body, 26 | subject, 27 | [recipient], 28 | from_, 29 | bcc=getattr(settings, 'BCC_EMAIL', None) 30 | ) 31 | 32 | 33 | def send_feedback(document, debug=False): 34 | from_ = 'HUGEPic ' 35 | subject = "Feedback on HUGEpic" 36 | body = '' 37 | for key in ('name', 'email', 'type', 'comment', 'current_user'): 38 | body += '%s: %s\n' % (key.capitalize(), document.get(key, '--')) 39 | body += 'Date: %s\n' % datetime.datetime.utcnow() 40 | send_email( 41 | _get_backend(debug), 42 | subject, 43 | body, 44 | from_, 45 | [settings.ADMIN_EMAILS[0]], 46 | ) 47 | 48 | 49 | def send_newsletter(recipient, subject, html_body, plain_body=None, debug=False): 50 | from_ = 'HUGEPic ' 51 | logging.info('Sending email to %s', recipient) 52 | send_multipart_email( 53 | _get_backend(debug), 54 | plain_body, 55 | html_body, 56 | subject, 57 | [recipient], 58 | from_, 59 | # bcc=getattr(settings, 'BCC_EMAIL', None) 60 | ) 61 | -------------------------------------------------------------------------------- /server/optimizer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | from glob import glob 4 | import subprocess 5 | import stat 6 | 7 | 8 | def optimize_images(image, zoom, extension, static_path): 9 | root = os.path.join( 10 | static_path, 11 | 'tiles' 12 | ) 13 | root = os.path.join(root, image, '256', str(zoom)) 14 | total_before = 0 15 | search_path = os.path.join(root, '*.%s' % extension) 16 | files = glob(search_path) 17 | for each in files: 18 | size = os.stat(each)[stat.ST_SIZE] 19 | #print each, "IS", size 20 | total_before += size 21 | t0 = time.time() 22 | out, err = _optimize(files, extension) 23 | t1 = time.time() 24 | 25 | total_after = 0 26 | for each in files: 27 | total_after += os.stat(each)[stat.ST_SIZE] 28 | 29 | def kb(s): 30 | return "%.1fKb" % (s / 1000.0) 31 | 32 | print "Took", round(t1 - t0, 2), "seconds to optimize", len(files), "tiles" 33 | print "From", kb(total_before), "to", kb(total_after), 34 | print "saving", kb(total_before - total_after) 35 | 36 | def optimize_thumbnails(image, extension, static_path): 37 | root = os.path.join( 38 | static_path, 39 | 'thumbnails' 40 | ) 41 | root = os.path.join(root, image) 42 | total_before = 0 43 | search_path = os.path.join(root, '*.%s' % extension) 44 | files = glob(search_path) 45 | for each in files: 46 | size = os.stat(each)[stat.ST_SIZE] 47 | print each, "IS", size 48 | total_before += size 49 | t0 = time.time() 50 | out, err = _optimize(files, extension) 51 | t1 = time.time() 52 | 53 | total_after = 0 54 | for each in files: 55 | total_after += os.stat(each)[stat.ST_SIZE] 56 | 57 | def kb(s): 58 | return "%.1fKb" % (s / 1000.0) 59 | 60 | print "Took", round(t1 - t0, 2), "seconds to optimize", len(files), "thumbnails" 61 | print "From", kb(total_before), "to", kb(total_after), 62 | print "Saving", kb(total_before - total_after) 63 | 64 | 65 | def _optimize(files, extension): 66 | if extension == 'jpg': 67 | cmd = "jpegoptim --strip-all %s" % (' '.join(files)) 68 | elif extension == 'png': 69 | cmd = "optipng %s" % (' '.join(files)) 70 | else: 71 | raise NotImplementedError(extension) 72 | process = subprocess.Popen( 73 | cmd, 74 | shell=True, 75 | stdout=subprocess.PIPE, 76 | stderr=subprocess.PIPE 77 | ) 78 | out, err = process.communicate() 79 | #print out 80 | #print 'ERRROR' 81 | #print err 82 | return (out, err) 83 | 84 | 85 | if __name__ == '__main__': 86 | optimize_images('b/51/3acd3c', 3, 'png', './static') 87 | -------------------------------------------------------------------------------- /server/resizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | import os 4 | import logging 5 | import subprocess 6 | 7 | 8 | def resize_image(path, width, save_path): 9 | # _resize_tool = 'resize' 10 | # cmd = ( 11 | # 'convert %s -%s %d %s' % 12 | # (path, _resize_tool, width, save_path) 13 | # ) 14 | # cmd = 'MAGICK_THREAD_LIMIT=1 ' + cmd 15 | # see https://github.com/jcupitt/libvips/issues/216 16 | save_path = os.path.abspath(save_path) 17 | cmd = ( 18 | 'vipsthumbnail %s -s %d -o %s' % ( 19 | path, width, save_path 20 | ) 21 | ) 22 | print "CMD", repr(cmd) 23 | t0 = time.time() 24 | process = subprocess.Popen( 25 | cmd, 26 | shell=True, 27 | stdout=subprocess.PIPE, 28 | stderr=subprocess.PIPE 29 | ) 30 | out, err = process.communicate() 31 | t1 = time.time() 32 | print "CMD TOOK", t1 - t0 33 | if err: 34 | logging.warning("resizer: %s" % err) 35 | return save_path 36 | 37 | 38 | def make_resizes(path, ranges): 39 | for zoom in ranges: 40 | #print path, zoom 41 | t0 = time.time() 42 | resized = _resize(path, zoom) 43 | t1 = time.time() 44 | print "Created", resized, "in", round(t1 - t0, 3), "seconds" 45 | 46 | 47 | def make_resize(path, zoom): 48 | t0 = time.time() 49 | resized = _resize(path, zoom) 50 | t1 = time.time() 51 | print "Created", resized, "in", round(t1 - t0, 3), "seconds" 52 | return resized 53 | 54 | 55 | def _resize(path, zoom, extra=0): 56 | width = 256 * (2 ** zoom) 57 | 58 | start, ext = os.path.splitext(path) 59 | save_path = path.replace( 60 | ext, 61 | '-%s-%s%s' % (zoom, width, ext) 62 | ) 63 | if os.path.isfile(save_path): 64 | return save_path 65 | return resize_image(path, width, save_path) 66 | -------------------------------------------------------------------------------- /server/settings.py: -------------------------------------------------------------------------------- 1 | COOKIE_SECRET = 'override this with local settings' 2 | 3 | DATABASE_NAME = 'tiler' 4 | 5 | REDIS_HOST = 'localhost' 6 | REDIS_PORT = 6379 7 | 8 | PROJECT_TITLE = 'HUGEpic' 9 | 10 | # complete this in your local_settings.py to get emails sent on errors 11 | ADMIN_EMAILS = ( 12 | ) 13 | 14 | DEFAULT_CDN_TILER_DOMAIN = 'd220r9wol91huc.cloudfront.net' 15 | TILES_BUCKET_ID = 'tiler-tiles' 16 | ORIGINALS_BUCKET_ID = 'tiler-originals' 17 | 18 | from local_settings import * 19 | 20 | assert BROWSERID_DOMAIN 21 | assert AWS_ACCESS_KEY 22 | assert AWS_SECRET_KEY 23 | 24 | assert TWITTER_CONSUMER_SECRET 25 | assert TWITTER_CONSUMER_KEY 26 | assert TWITTER_ACCESS_TOKEN 27 | assert TWITTER_ACCESS_TOKEN_SECRET 28 | -------------------------------------------------------------------------------- /server/static/authentication.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var _xsrf = $('input[name="_xsrf"]').val(); 3 | 4 | $('a.signin').click(function(e) { 5 | e.preventDefault(); 6 | navigator.id.getVerifiedEmail(function(assertion) { 7 | if (assertion) { 8 | $.ajax({ 9 | type: 'POST', 10 | url: '/auth/browserid/', 11 | data: {assertion: assertion, _xsrf: _xsrf}, 12 | success: function(res, status, xhr) { window.location.reload(); }, 13 | error: function(res, status, xhr) { alert("login failure" + res); } 14 | }); 15 | } else { 16 | alert("Failed to log in"); 17 | } 18 | }); 19 | 20 | 21 | }); 22 | 23 | $('a.signout').click(function(e) { 24 | e.preventDefault(); 25 | navigator.id.logout(); 26 | $.ajax({ 27 | type: 'POST', 28 | url: '/auth/signout/', 29 | data: {_xsrf: _xsrf}, 30 | success: function(res, status, xhr) { window.location.reload(); }, 31 | error: function(res, status, xhr) { alert("logout failure" + res); } 32 | }); 33 | 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /server/static/bootstrap/css/bootstrap-modal.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v2.1.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix { 11 | *zoom: 1; 12 | } 13 | .clearfix:before, 14 | .clearfix:after { 15 | display: table; 16 | content: ""; 17 | line-height: 0; 18 | } 19 | .clearfix:after { 20 | clear: both; 21 | } 22 | .hide-text { 23 | font: 0/0 a; 24 | color: transparent; 25 | text-shadow: none; 26 | background-color: transparent; 27 | border: 0; 28 | } 29 | .input-block-level { 30 | display: block; 31 | width: 100%; 32 | min-height: 30px; 33 | -webkit-box-sizing: border-box; 34 | -moz-box-sizing: border-box; 35 | box-sizing: border-box; 36 | } 37 | .modal-open .modal .dropdown-menu { 38 | z-index: 2050; 39 | } 40 | .modal-open .modal .dropdown.open { 41 | *z-index: 2050; 42 | } 43 | .modal-open .modal .popover { 44 | z-index: 2060; 45 | } 46 | .modal-open .modal .tooltip { 47 | z-index: 2080; 48 | } 49 | .modal-backdrop { 50 | position: fixed; 51 | top: 0; 52 | right: 0; 53 | bottom: 0; 54 | left: 0; 55 | z-index: 1040; 56 | background-color: #000000; 57 | } 58 | .modal-backdrop.fade { 59 | opacity: 0; 60 | } 61 | .modal-backdrop, 62 | .modal-backdrop.fade.in { 63 | opacity: 0.8; 64 | filter: alpha(opacity=80); 65 | } 66 | .modal { 67 | position: fixed; 68 | top: 50%; 69 | left: 50%; 70 | z-index: 1050; 71 | overflow: auto; 72 | width: 560px; 73 | margin: -250px 0 0 -280px; 74 | background-color: #ffffff; 75 | border: 1px solid #999; 76 | border: 1px solid rgba(0, 0, 0, 0.3); 77 | *border: 1px solid #999; 78 | /* IE6-7 */ 79 | 80 | -webkit-border-radius: 6px; 81 | -moz-border-radius: 6px; 82 | border-radius: 6px; 83 | -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); 84 | -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); 85 | box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); 86 | -webkit-background-clip: padding-box; 87 | -moz-background-clip: padding-box; 88 | background-clip: padding-box; 89 | } 90 | .modal.fade { 91 | -webkit-transition: opacity .3s linear, top .3s ease-out; 92 | -moz-transition: opacity .3s linear, top .3s ease-out; 93 | -o-transition: opacity .3s linear, top .3s ease-out; 94 | transition: opacity .3s linear, top .3s ease-out; 95 | top: -25%; 96 | } 97 | .modal.fade.in { 98 | top: 50%; 99 | } 100 | .modal-header { 101 | padding: 9px 15px; 102 | border-bottom: 1px solid #eee; 103 | } 104 | .modal-header .close { 105 | margin-top: 2px; 106 | } 107 | .modal-header h3 { 108 | margin: 0; 109 | line-height: 30px; 110 | } 111 | .modal-body { 112 | overflow-y: auto; 113 | max-height: 400px; 114 | padding: 15px; 115 | } 116 | .modal-form { 117 | margin-bottom: 0; 118 | } 119 | .modal-footer { 120 | padding: 14px 15px 15px; 121 | margin-bottom: 0; 122 | text-align: right; 123 | background-color: #f5f5f5; 124 | border-top: 1px solid #ddd; 125 | -webkit-border-radius: 0 0 6px 6px; 126 | -moz-border-radius: 0 0 6px 6px; 127 | border-radius: 0 0 6px 6px; 128 | -webkit-box-shadow: inset 0 1px 0 #ffffff; 129 | -moz-box-shadow: inset 0 1px 0 #ffffff; 130 | box-shadow: inset 0 1px 0 #ffffff; 131 | *zoom: 1; 132 | } 133 | .modal-footer:before, 134 | .modal-footer:after { 135 | display: table; 136 | content: ""; 137 | line-height: 0; 138 | } 139 | .modal-footer:after { 140 | clear: both; 141 | } 142 | .modal-footer .btn + .btn { 143 | margin-left: 5px; 144 | margin-bottom: 0; 145 | } 146 | .modal-footer .btn-group .btn + .btn { 147 | margin-left: -1px; 148 | } 149 | -------------------------------------------------------------------------------- /server/static/bootstrap/css/bootstrap-modal.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v2.1.1 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0;} 11 | .clearfix:after{clear:both;} 12 | .hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;} 13 | .input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} 14 | .modal-open .modal .dropdown-menu{z-index:2050;} 15 | .modal-open .modal .dropdown.open{*z-index:2050;} 16 | .modal-open .modal .popover{z-index:2060;} 17 | .modal-open .modal .tooltip{z-index:2080;} 18 | .modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;} 19 | .modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);} 20 | .modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;} 21 | .modal.fade.in{top:50%;} 22 | .modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;} 23 | .modal-header h3{margin:0;line-height:30px;} 24 | .modal-body{overflow-y:auto;max-height:400px;padding:15px;} 25 | .modal-form{margin-bottom:0;} 26 | .modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";line-height:0;} 27 | .modal-footer:after{clear:both;} 28 | .modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;} 29 | .modal-footer .btn-group .btn+.btn{margin-left:-1px;} 30 | -------------------------------------------------------------------------------- /server/static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tiler/32dfb0ef97bafd55ffe6e5d9395c58c295c9e056/server/static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /server/static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/tiler/32dfb0ef97bafd55ffe6e5d9395c58c295c9e056/server/static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /server/static/bootstrap/js/bootstrap-collapse.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * bootstrap-collapse.js v2.1.1 3 | * http://twitter.github.com/bootstrap/javascript.html#collapse 4 | * ============================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* COLLAPSE PUBLIC CLASS DEFINITION 27 | * ================================ */ 28 | 29 | var Collapse = function (element, options) { 30 | this.$element = $(element) 31 | this.options = $.extend({}, $.fn.collapse.defaults, options) 32 | 33 | if (this.options.parent) { 34 | this.$parent = $(this.options.parent) 35 | } 36 | 37 | this.options.toggle && this.toggle() 38 | } 39 | 40 | Collapse.prototype = { 41 | 42 | constructor: Collapse 43 | 44 | , dimension: function () { 45 | var hasWidth = this.$element.hasClass('width') 46 | return hasWidth ? 'width' : 'height' 47 | } 48 | 49 | , show: function () { 50 | var dimension 51 | , scroll 52 | , actives 53 | , hasData 54 | 55 | if (this.transitioning) return 56 | 57 | dimension = this.dimension() 58 | scroll = $.camelCase(['scroll', dimension].join('-')) 59 | actives = this.$parent && this.$parent.find('> .accordion-group > .in') 60 | 61 | if (actives && actives.length) { 62 | hasData = actives.data('collapse') 63 | if (hasData && hasData.transitioning) return 64 | actives.collapse('hide') 65 | hasData || actives.data('collapse', null) 66 | } 67 | 68 | this.$element[dimension](0) 69 | this.transition('addClass', $.Event('show'), 'shown') 70 | $.support.transition && this.$element[dimension](this.$element[0][scroll]) 71 | } 72 | 73 | , hide: function () { 74 | var dimension 75 | if (this.transitioning) return 76 | dimension = this.dimension() 77 | this.reset(this.$element[dimension]()) 78 | this.transition('removeClass', $.Event('hide'), 'hidden') 79 | this.$element[dimension](0) 80 | } 81 | 82 | , reset: function (size) { 83 | var dimension = this.dimension() 84 | 85 | this.$element 86 | .removeClass('collapse') 87 | [dimension](size || 'auto') 88 | [0].offsetWidth 89 | 90 | this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') 91 | 92 | return this 93 | } 94 | 95 | , transition: function (method, startEvent, completeEvent) { 96 | var that = this 97 | , complete = function () { 98 | if (startEvent.type == 'show') that.reset() 99 | that.transitioning = 0 100 | that.$element.trigger(completeEvent) 101 | } 102 | 103 | this.$element.trigger(startEvent) 104 | 105 | if (startEvent.isDefaultPrevented()) return 106 | 107 | this.transitioning = 1 108 | 109 | this.$element[method]('in') 110 | 111 | $.support.transition && this.$element.hasClass('collapse') ? 112 | this.$element.one($.support.transition.end, complete) : 113 | complete() 114 | } 115 | 116 | , toggle: function () { 117 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 118 | } 119 | 120 | } 121 | 122 | 123 | /* COLLAPSIBLE PLUGIN DEFINITION 124 | * ============================== */ 125 | 126 | $.fn.collapse = function (option) { 127 | return this.each(function () { 128 | var $this = $(this) 129 | , data = $this.data('collapse') 130 | , options = typeof option == 'object' && option 131 | if (!data) $this.data('collapse', (data = new Collapse(this, options))) 132 | if (typeof option == 'string') data[option]() 133 | }) 134 | } 135 | 136 | $.fn.collapse.defaults = { 137 | toggle: true 138 | } 139 | 140 | $.fn.collapse.Constructor = Collapse 141 | 142 | 143 | /* COLLAPSIBLE DATA-API 144 | * ==================== */ 145 | 146 | $(function () { 147 | $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { 148 | var $this = $(this), href 149 | , target = $this.attr('data-target') 150 | || e.preventDefault() 151 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 152 | , option = $(target).data('collapse') ? 'toggle' : $this.data() 153 | $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') 154 | $(target).collapse(option) 155 | }) 156 | }) 157 | 158 | }(window.jQuery); -------------------------------------------------------------------------------- /server/static/bootstrap/js/bootstrap-collapse.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins: bootstrap-collapse.js 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | 7 | !function($){"use strict";var Collapse=function(element,options){this.$element=$(element) 8 | this.options=$.extend({},$.fn.collapse.defaults,options) 9 | if(this.options.parent){this.$parent=$(this.options.parent)} 10 | this.options.toggle&&this.toggle()} 11 | Collapse.prototype={constructor:Collapse,dimension:function(){var hasWidth=this.$element.hasClass('width') 12 | return hasWidth?'width':'height'},show:function(){var dimension,scroll,actives,hasData 13 | if(this.transitioning)return 14 | dimension=this.dimension() 15 | scroll=$.camelCase(['scroll',dimension].join('-')) 16 | actives=this.$parent&&this.$parent.find('> .accordion-group > .in') 17 | if(actives&&actives.length){hasData=actives.data('collapse') 18 | if(hasData&&hasData.transitioning)return 19 | actives.collapse('hide') 20 | hasData||actives.data('collapse',null)} 21 | this.$element[dimension](0) 22 | this.transition('addClass',$.Event('show'),'shown') 23 | $.support.transition&&this.$element[dimension](this.$element[0][scroll])},hide:function(){var dimension 24 | if(this.transitioning)return 25 | dimension=this.dimension() 26 | this.reset(this.$element[dimension]()) 27 | this.transition('removeClass',$.Event('hide'),'hidden') 28 | this.$element[dimension](0)},reset:function(size){var dimension=this.dimension() 29 | this.$element.removeClass('collapse') 30 | [dimension](size||'auto') 31 | [0].offsetWidth 32 | this.$element[size!==null?'addClass':'removeClass']('collapse') 33 | return this},transition:function(method,startEvent,completeEvent){var that=this,complete=function(){if(startEvent.type=='show')that.reset() 34 | that.transitioning=0 35 | that.$element.trigger(completeEvent)} 36 | this.$element.trigger(startEvent) 37 | if(startEvent.isDefaultPrevented())return 38 | this.transitioning=1 39 | this.$element[method]('in') 40 | $.support.transition&&this.$element.hasClass('collapse')?this.$element.one($.support.transition.end,complete):complete()},toggle:function(){this[this.$element.hasClass('in')?'hide':'show']()}} 41 | $.fn.collapse=function(option){return this.each(function(){var $this=$(this),data=$this.data('collapse'),options=typeof option=='object'&&option 42 | if(!data)$this.data('collapse',(data=new Collapse(this,options))) 43 | if(typeof option=='string')data[option]()})} 44 | $.fn.collapse.defaults={toggle:true} 45 | $.fn.collapse.Constructor=Collapse 46 | $(function(){$('body').on('click.collapse.data-api','[data-toggle=collapse]',function(e){var $this=$(this),href,target=$this.attr('data-target')||e.preventDefault()||(href=$this.attr('href'))&&href.replace(/.*(?=#[^\s]+$)/,'') 47 | ,option=$(target).data('collapse')?'toggle':$this.data() 48 | $this[$(target).hasClass('in')?'addClass':'removeClass']('collapsed') 49 | $(target).collapse(option)})})}(window.jQuery); 50 | -------------------------------------------------------------------------------- /server/static/bootstrap/js/bootstrap-modal.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-modal.js v2.1.1 3 | * http://twitter.github.com/bootstrap/javascript.html#modals 4 | * ========================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* MODAL CLASS DEFINITION 27 | * ====================== */ 28 | 29 | var Modal = function (element, options) { 30 | this.options = options 31 | this.$element = $(element) 32 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) 33 | this.options.remote && this.$element.find('.modal-body').load(this.options.remote) 34 | } 35 | 36 | Modal.prototype = { 37 | 38 | constructor: Modal 39 | 40 | , toggle: function () { 41 | return this[!this.isShown ? 'show' : 'hide']() 42 | } 43 | 44 | , show: function () { 45 | var that = this 46 | , e = $.Event('show') 47 | 48 | this.$element.trigger(e) 49 | 50 | if (this.isShown || e.isDefaultPrevented()) return 51 | 52 | $('body').addClass('modal-open') 53 | 54 | this.isShown = true 55 | 56 | this.escape() 57 | 58 | this.backdrop(function () { 59 | var transition = $.support.transition && that.$element.hasClass('fade') 60 | 61 | if (!that.$element.parent().length) { 62 | that.$element.appendTo(document.body) //don't move modals dom position 63 | } 64 | 65 | that.$element 66 | .show() 67 | 68 | if (transition) { 69 | that.$element[0].offsetWidth // force reflow 70 | } 71 | 72 | that.$element 73 | .addClass('in') 74 | .attr('aria-hidden', false) 75 | .focus() 76 | 77 | that.enforceFocus() 78 | 79 | transition ? 80 | that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : 81 | that.$element.trigger('shown') 82 | 83 | }) 84 | } 85 | 86 | , hide: function (e) { 87 | e && e.preventDefault() 88 | 89 | var that = this 90 | 91 | e = $.Event('hide') 92 | 93 | this.$element.trigger(e) 94 | 95 | if (!this.isShown || e.isDefaultPrevented()) return 96 | 97 | this.isShown = false 98 | 99 | $('body').removeClass('modal-open') 100 | 101 | this.escape() 102 | 103 | $(document).off('focusin.modal') 104 | 105 | this.$element 106 | .removeClass('in') 107 | .attr('aria-hidden', true) 108 | 109 | $.support.transition && this.$element.hasClass('fade') ? 110 | this.hideWithTransition() : 111 | this.hideModal() 112 | } 113 | 114 | , enforceFocus: function () { 115 | var that = this 116 | $(document).on('focusin.modal', function (e) { 117 | if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { 118 | that.$element.focus() 119 | } 120 | }) 121 | } 122 | 123 | , escape: function () { 124 | var that = this 125 | if (this.isShown && this.options.keyboard) { 126 | this.$element.on('keyup.dismiss.modal', function ( e ) { 127 | e.which == 27 && that.hide() 128 | }) 129 | } else if (!this.isShown) { 130 | this.$element.off('keyup.dismiss.modal') 131 | } 132 | } 133 | 134 | , hideWithTransition: function () { 135 | var that = this 136 | , timeout = setTimeout(function () { 137 | that.$element.off($.support.transition.end) 138 | that.hideModal() 139 | }, 500) 140 | 141 | this.$element.one($.support.transition.end, function () { 142 | clearTimeout(timeout) 143 | that.hideModal() 144 | }) 145 | } 146 | 147 | , hideModal: function (that) { 148 | this.$element 149 | .hide() 150 | .trigger('hidden') 151 | 152 | this.backdrop() 153 | } 154 | 155 | , removeBackdrop: function () { 156 | this.$backdrop.remove() 157 | this.$backdrop = null 158 | } 159 | 160 | , backdrop: function (callback) { 161 | var that = this 162 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 163 | 164 | if (this.isShown && this.options.backdrop) { 165 | var doAnimate = $.support.transition && animate 166 | 167 | this.$backdrop = $('