From 199ba44a8fcd2126e56113567b90a7609ecbd216 Mon Sep 17 00:00:00 2001 From: Adam Schubert Date: Sun, 19 Sep 2021 16:29:33 +0200 Subject: [PATCH 1/4] Replace subprocess.call with os.rename --- flask_images/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flask_images/core.py b/flask_images/core.py index d299902..a4ef08a 100755 --- a/flask_images/core.py +++ b/flask_images/core.py @@ -1,7 +1,6 @@ from __future__ import division from io import BytesIO as StringIO -from subprocess import call import base64 import cgi import datetime @@ -343,7 +342,7 @@ def handle_request(self, path): fh = open(tmp_path, 'wb') fh.write(remote_file) fh.close() - call(['mv', tmp_path, path]) + os.rename(tmp_path, path) else: path = self.find_img(path) if not path: From a0b610447a83522f25753050c38068632d5193bd Mon Sep 17 00:00:00 2001 From: Adam Schubert Date: Mon, 12 Feb 2024 15:11:46 +0100 Subject: [PATCH 2/4] feat(Deps): Drop Python2 support, drop isdangerous, use hmac --- .travis.yml | 81 --------------------------- README.md | 7 +-- flask_images/core.py | 129 ++++++++++++++++++++++--------------------- requirements.txt | 3 - setup.py | 5 +- 5 files changed, 68 insertions(+), 157 deletions(-) delete mode 100644 .travis.yml delete mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3a96ed5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,81 +0,0 @@ -language: python - -git: - # Testing doesn't need Flask themes. - submodules: false - -matrix: - - include: - - # 3 - - - python: "3.7" - env: FLASK_VERSION="" - os: linux - - - python: "3.7" - env: FLASK_VERSION="==1.0" - os: linux - - - python: "3.7" - env: FLASK_VERSION="==0.10" - os: linux - - # 2 - - - python: "2.7" - env: FLASK_VERSION="" - os: linux - - - python: "2.7" - env: FLASK_VERSION="==1.0" - os: linux - - - python: "2.7" - env: FLASK_VERSION="==0.10" - os: linux - - - python: "2.7" - env: FLASK_VERSION="==0.9" - os: linux - - # PyPy. - - - python: "pypy" - env: FLASK_VERSION="" - os: linux - - - python: "pypy" - env: FLASK_VERSION="==0.10" - os: linux - - # Older 3s. - - - python: "3.6" - env: FLASK_VERSION="" - os: linux - - - python: "3.5" - env: FLASK_VERSION="" - os: linux - - - python: "3.4" - env: FLASK_VERSION="" - os: linux - - -before_install: - - sudo apt-get install libjpeg8 libjpeg8-dev libfreetype6 libfreetype6-dev zlib1g-dev tree - - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libjpeg.so.8 ~/virtualenv/python2.7/lib/ - - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libfreetype.so ~/virtualenv/python2.7/lib/ - - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libz.so ~/virtualenv/python2.7/lib/ - - pip install nose - - -install: - - pip install Flask$FLASK_VERSION - - pip install -e . - - -script: nosetests diff --git a/README.md b/README.md index 5c98645..06c042b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@ Flask-Images ============ -[![Travis Build Status][travis-badge]][travis] Flask-Images is a Flask extension that provides dynamic image resizing for your application. -[Read the docs][docs], [try the demo][demo], have fun, and good luck! +[Read the docs][docs], have fun, and good luck! -[travis-badge]: https://img.shields.io/travis/mikeboers/Flask-Images/develop.svg?logo=travis&label=travis -[travis]: https://travis-ci.org/mikeboers/Flask-Images - [docs]: https://mikeboers.github.io/Flask-Images/ -[demo]: https://flask-images.herokuapp.com/ diff --git a/flask_images/core.py b/flask_images/core.py index a4ef08a..d9aabf0 100755 --- a/flask_images/core.py +++ b/flask_images/core.py @@ -7,31 +7,20 @@ import errno import hashlib import logging -import math import os import re import struct -import sys - -from six import iteritems, PY3, string_types, text_type -if PY3: - from urllib.parse import urlparse, urlencode, quote as urlquote - from urllib.request import urlopen - from urllib.error import HTTPError -else: - from urlparse import urlparse - from urllib import urlencode, quote as urlquote - from urllib2 import urlopen, HTTPError +import hmac + +from six import iteritems, string_types, text_type + +from urllib.parse import urlparse, urlencode, quote as urlquote +from urllib.request import urlopen +from urllib.error import HTTPError from PIL import Image, ImageFilter from flask import request, current_app, send_file, abort -try: - from itsdangerous import Signer, constant_time_compare -except ImportError: - from itsdangerous import Signer - from itsdangerous._compat import constant_time_compare - from . import modes from .size import ImageSize from .transform import Transform @@ -40,12 +29,12 @@ log = logging.getLogger(__name__) - def encode_str(value): if isinstance(value, text_type): return value.encode('utf-8') return value + def encode_int(value): return base64.urlsafe_b64encode(struct.pack('>I', int(value))).decode('utf-8').rstrip('=').lstrip('A') @@ -60,7 +49,7 @@ def makedirs(path): # We must whitelist schemes which are permitted, otherwise craziness (such as # allowing access to the filesystem) may ensue. -ALLOWED_SCHEMES = set(('http', 'https', 'ftp')) +ALLOWED_SCHEMES = {'http', 'https', 'ftp'} # The options which we immediately recognize and shorten. LONG_TO_SHORT = dict( @@ -81,11 +70,8 @@ def makedirs(path): SHORT_TO_LONG = dict((v, k) for k, v in iteritems(LONG_TO_SHORT)) +class Images: - -class Images(object): - - def __init__(self, app=None): if app is not None: self.init_app(app) @@ -118,7 +104,6 @@ def init_app(self, app): } app.context_processor(lambda: ctx) - def build_error_handler(self, error, endpoint, values): # See if we were asked for "images" or "images.". @@ -127,7 +112,7 @@ def build_error_handler(self, error, endpoint, values): '|'.join(re.escape(mode) for mode in modes.ALL) ), endpoint) if m: - + filename = values.pop('filename') mode = m.group(1) @@ -164,7 +149,7 @@ def build_url(self, local_path, **kwargs): raise ValueError('images have no _anchor') if kwargs.get('_method'): raise ValueError('images have no _method') - + # Remote URLs are encoded into the query. parsed = urlparse(local_path) if parsed.scheme or parsed.netloc: @@ -178,7 +163,7 @@ def build_url(self, local_path, **kwargs): abs_path = self.find_img(local_path) if abs_path: kwargs['version'] = encode_int(int(os.path.getmtime(abs_path))) - + # Prep the cache flag, which defaults to True. cache = kwargs.pop('cache', True) if not cache: @@ -207,14 +192,17 @@ def build_url(self, local_path, **kwargs): if v is not None and not k.startswith('_') } query = urlencode(sorted(iteritems(public_kwargs)), True) - signer = Signer(current_app.secret_key) - sig = signer.get_signature('%s?%s' % (local_path, query)) + sig_auth = hmac.new( + current_app.secret_key.encode('utf-8'), + f'{local_path}?{query}'.encode('utf-8'), + digestmod='sha256' + ) url = '%s/%s?%s&s=%s' % ( current_app.config['IMAGES_URL'], urlquote(local_path, "/$-_.+!*'(),"), query, - sig.decode('utf-8'), + sig_auth.hexdigest(), ) if external: @@ -226,14 +214,14 @@ def build_url(self, local_path, **kwargs): ) return url - + def find_img(self, local_path): local_path = os.path.normpath(local_path.lstrip('/')) for path_base in current_app.config['IMAGES_PATH']: path = os.path.join(current_app.root_path, path_base, local_path) if os.path.exists(path): return path - + def calculate_size(self, path, **kw): path = self.find_img(path) if not path: @@ -241,7 +229,7 @@ def calculate_size(self, path, **kw): return ImageSize(path=path, **kw) def resize(self, image, background=None, **kw): - + size = ImageSize(image=image, **kw) # Get into the right colour space. @@ -251,17 +239,20 @@ def resize(self, image, background=None, **kw): # Apply any requested transform. if size.transform: image = Transform(size.transform, image.size).apply(image) - + + resample = Image.ANTIALIAS if hasattr(Image, 'ANTIALIAS') else Image.LANCZOS + # Handle the easy cases. if size.mode in (modes.RESHAPE, None) or size.req_width is None or size.req_height is None: - return image.resize((size.width, size.height), Image.ANTIALIAS) + + return image.resize((size.width, size.height), resample) if size.mode not in (modes.FIT, modes.PAD, modes.CROP): raise ValueError('unknown mode %r' % size.mode) if image.size != (size.op_width, size.op_height): - image = image.resize((size.op_width, size.op_height), Image.ANTIALIAS) - + image = image.resize((size.op_width, size.op_height), resample) + if size.mode == modes.FIT: return image @@ -269,22 +260,22 @@ def resize(self, image, background=None, **kw): pad_color = str(background or 'black') padded = Image.new('RGBA', (size.width, size.height), pad_color) padded.paste(image, ( - (size.width - size.op_width ) // 2, + (size.width - size.op_width ) // 2, (size.height - size.op_height) // 2 )) return padded - + elif size.mode == modes.CROP: - dx = (size.op_width - size.width ) // 2 + dx = (size.op_width - size.width ) // 2 dy = (size.op_height - size.height) // 2 return image.crop( (dx, dy, dx + size.width, dy + size.height) ) - + else: raise RuntimeError('unhandled mode %r' % size.mode) - + def post_process(self, image, sharpen=None): if sharpen: @@ -301,15 +292,20 @@ def handle_request(self, path): # Verify the signature. query = dict(iteritems(request.args)) - old_sig = str(query.pop('s', None)) + old_sig = bytes(query.pop('s', None), 'utf-8') if not old_sig: abort(404) - signer = Signer(current_app.secret_key) - new_sig = signer.get_signature('%s?%s' % (path, urlencode(sorted(iteritems(query)), True))) - if not constant_time_compare(str(old_sig), str(new_sig.decode('utf-8'))): - log.warning("Signature mismatch: url's {} != expected {}".format(old_sig, new_sig)) + query_info = urlencode(sorted(iteritems(query)), True) + msg_auth = hmac.new( + current_app.secret_key.encode('utf-8'), + f'{path}?{query_info}'.encode('utf-8'), + digestmod='sha256' + ) + new_sig = bytes(msg_auth.hexdigest(), 'utf-8') + if not hmac.compare_digest(old_sig, new_sig): + log.warning(f"Signature mismatch: url's {old_sig} != expected {new_sig}") abort(404) - + # Expand kwargs. query = dict((SHORT_TO_LONG.get(k, k), v) for k, v in iteritems(query)) @@ -354,7 +350,7 @@ def handle_request(self, path): # log.debug('if_modified_since: %r' % request.if_modified_since) if request.if_modified_since and request.if_modified_since >= mtime: return '', 304 - + mode = query.get('mode') transform = query.get('transform') @@ -376,6 +372,9 @@ def handle_request(self, path): sharpen = query.get('sharpen') sharpen = re.split(r'[+:;,_/ ]', sharpen) if sharpen else None + cache_mtime = None + cache_dir = None + cache_path = None if use_cache: # The parts in this initial list were parameters cached in version 1. @@ -389,28 +388,30 @@ def handle_request(self, path): if enlarge: cache_key_parts.append(('enlarge', enlarge)) - cache_key = hashlib.md5(repr(tuple(cache_key_parts)).encode('utf-8')).hexdigest() cache_dir = os.path.join(current_app.config['IMAGES_CACHE'], cache_key[:2]) cache_path = os.path.join(cache_dir, cache_key + '.' + format) cache_mtime = os.path.getmtime(cache_path) if os.path.exists(cache_path) else None - + mimetype = 'image/%s' % format cache_timeout = 31536000 if has_version else current_app.config['IMAGES_MAX_AGE'] if not use_cache or not cache_mtime or cache_mtime < raw_mtime: - + log.info('resizing %r for %s' % (path, query)) image = Image.open(path) - image = self.resize(image, - background=background, + image = self.resize( + image, + BACKGROUND=background, enlarge=enlarge, height=height, mode=mode, transform=transform, width=width, ) - image = self.post_process(image, + + image = self.post_process( + image, sharpen=sharpen, ) @@ -421,22 +422,24 @@ def handle_request(self, path): ('Content-Type', mimetype), ('Cache-Control', str(cache_timeout)), ] - + makedirs(cache_dir) cache_file = open(cache_path, 'wb') image.save(cache_file, format, quality=quality) cache_file.close() - - return send_file(cache_path, mimetype=mimetype, cache_timeout=cache_timeout) - + try: + return send_file(cache_path, mimetype=mimetype, max_age=cache_timeout) + except TypeError: + return send_file(cache_path, mimetype=mimetype, cache_timeout=cache_timeout) def resized_img_size(path, **kw): self = current_app.extensions['images'] return self.calculate_size(path, **kw) + def resized_img_attrs(path, hidpi=None, width=None, height=None, enlarge=False, **kw): - + self = current_app.extensions['images'] page = image = self.calculate_size( @@ -469,7 +472,7 @@ def resized_img_attrs(path, hidpi=None, width=None, height=None, enlarge=False, kw[k[6:]] = v kw.setdefault('quality', 60) - + else: hidpi = False @@ -488,7 +491,7 @@ def resized_img_attrs(path, hidpi=None, width=None, height=None, enlarge=False, enlarge=enlarge, **kw ), - + } diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4b3fab5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Demo requirements. -gunicorn --e . diff --git a/setup.py b/setup.py index 453a02f..2115291 100644 --- a/setup.py +++ b/setup.py @@ -14,10 +14,7 @@ packages=['flask_images'], install_requires=[ - 'Flask>=0.9', - 'itsdangerous', # For Flask v0.9 - # We need either PIL, or the newer Pillow. Since this may induce some # dependency madness, I have created a module that should flatten that # out. See: https://github.com/mikeboers/Flask-Images/pull/10 for more. @@ -39,7 +36,7 @@ ], tests_require=[ - 'nose>=1.0', + 'nose-py3>=1.6.0', ], test_suite='nose.collector', From ee0a81bc6b5dd6eca965017013d10faa01eba647 Mon Sep 17 00:00:00 2001 From: Adam Schubert Date: Mon, 12 Feb 2024 15:17:31 +0100 Subject: [PATCH 3/4] feat(Version): Added version config --- .version.yml | 13 +++++++++++++ flask_images/__init__.py | 2 ++ setup.py | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .version.yml diff --git a/.version.yml b/.version.yml new file mode 100644 index 0000000..710a604 --- /dev/null +++ b/.version.yml @@ -0,0 +1,13 @@ +GIT: + AUTO_COMMIT: true + AUTO_TAG: true + AUTO_PUSH: true # false=disabled, true=enabled, 'remote_name'=enabled and push to remote_name + COMMIT_MESSAGE: 'New version {version}' + +REGEXPS: + 'python': __version__\s*=\s*\'(?P\d+)\.(?P\d+)\.(?P\d+)\' + 'setup.py': version\s*=\s*\'(?P\d+)\.(?P\d+)\.(?P\d+)\' + +VERSION_FILES: + 'flask_images/__init__.py': 'python' + 'setup.py': 'setup.py' diff --git a/flask_images/__init__.py b/flask_images/__init__.py index 9bd539d..673bfa9 100644 --- a/flask_images/__init__.py +++ b/flask_images/__init__.py @@ -1,2 +1,4 @@ +__version__ = '4.0.0' + from .core import Images, resized_img_src, resized_img_size, resized_img_attrs, resized_img_tag from .size import ImageSize diff --git a/setup.py b/setup.py index 2115291..b08a05a 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='Flask-Images', - version='3.0.2', + version='4.0.0', description='Dynamic image resizing for Flask.', url='http://github.com/mikeboers/Flask-Images', From 694f86baf1877ade3891c3ffa6eea04aa09ae9d6 Mon Sep 17 00:00:00 2001 From: Adam Schubert Date: Mon, 12 Feb 2024 15:17:39 +0100 Subject: [PATCH 4/4] New version 4.0.1 --- flask_images/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_images/__init__.py b/flask_images/__init__.py index 673bfa9..c5f31f8 100644 --- a/flask_images/__init__.py +++ b/flask_images/__init__.py @@ -1,4 +1,4 @@ -__version__ = '4.0.0' +__version__ = '4.0.1' from .core import Images, resized_img_src, resized_img_size, resized_img_attrs, resized_img_tag from .size import ImageSize diff --git a/setup.py b/setup.py index b08a05a..937e985 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='Flask-Images', - version='4.0.0', + version='4.0.1', description='Dynamic image resizing for Flask.', url='http://github.com/mikeboers/Flask-Images',