Skip to content

Commit

Permalink
Use Pillow and the uploaded mime_type. (#4499)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrobbins authored Oct 30, 2024
1 parent 548e640 commit b4c5ced
Show file tree
Hide file tree
Showing 4 changed files with 16 additions and 46 deletions.
4 changes: 4 additions & 0 deletions framework/basehandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,10 @@ def FlaskApplication(import_name, routes, pattern_base='', debug=False):
# Flask apps also have a debug setting that can be used to auto-reload
# template source code. TODO: investigate using the setting.


# Reject any huge POSTs
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB

# Set the CORS HEADERS.
CORS(app, resources={r'/data/*': {'origins': '*'}})

Expand Down
37 changes: 11 additions & 26 deletions internals/attachments.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import io
import logging
from google.appengine.api import images
from PIL import Image
from google.cloud import ndb # type: ignore
from typing import Tuple

Expand Down Expand Up @@ -43,22 +44,7 @@ class AttachmentTooLarge(Exception):
pass


_EXTENSION_TO_CTYPE_TABLE = {
# These are image types that we trust the browser to display.
'gif': 'image/gif',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'webp': 'image/webp',
'txt': 'text/plain',
}


def guess_content_type_from_filename(filename: str) -> str:
"""Guess a file's content type based on the filename extension."""
ext = filename.split('.')[-1] if ('.' in filename) else ''
ext = ext.lower()
return _EXTENSION_TO_CTYPE_TABLE.get(ext, 'application/octet-stream')
SUPPORTED_MIME_TYPES = RESIZABLE_MIME_TYPES + ['text/plain']


def store_attachment(feature_id: int, content: bytes, mime_type: str) -> str:
Expand All @@ -76,18 +62,17 @@ def store_attachment(feature_id: int, content: bytes, mime_type: str) -> str:
# Create and save a thumbnail too.
thumb_content = None
try:
thumb_content = images.resize(content, THUMB_WIDTH, THUMB_HEIGHT)
except images.LargeImageError:
# Don't log the whole exception because we don't need to see
# this on the Cloud Error Reporting page.
logging.info('Got LargeImageError on image with %d bytes', len(content))
im = Image.open(io.BytesIO(content))
im.thumbnail((THUMB_WIDTH, THUMB_HEIGHT))
thumb_buffer = io.BytesIO()
im.save(thumb_buffer, 'PNG')
thumb_content = thumb_buffer.getvalue()
except Exception as e:
# Do not raise exception for incorrectly formed images.
# See https://bugs.chromium.org/p/monorail/issues/detail?id=597 for more
# detail.
logging.exception(e)
if thumb_content:
attachment.thumbnail = thumb_content
logging.info('Thumbnail is %r bytes', len(thumb_content))

attachment.put()
return attachment
Expand All @@ -101,10 +86,10 @@ def check_attachment_size(content: bytes):

def check_attachment_type(mime_type: str):
"""Reject attachments that are of an unsupported type."""
if mime_type not in _EXTENSION_TO_CTYPE_TABLE.values():
if mime_type not in SUPPORTED_MIME_TYPES:
raise UnsupportedMimeType(
'Please upload an image with one of the following mime types:\n%s' %
', '.join(_EXTENSION_TO_CTYPE_TABLE.values()))
', '.join(SUPPORTED_MIME_TYPES))


def get_attachment(feature_id: int, attachment_id: int) -> Attachment|None:
Expand Down
19 changes: 0 additions & 19 deletions internals/attachments_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,6 @@ def tearDown(self):
for attach in attachments.Attachment.query().fetch(None):
attach.key.delete()

def test_guess_content_type_from_filename(self):
"""We can guess mime type based on filename extension."""
guess = attachments.guess_content_type_from_filename
self.assertEqual(guess('screenshot.gif'), 'image/gif')
self.assertEqual(guess('screenshot.jpg'), 'image/jpeg')
self.assertEqual(guess('screenshot.jpeg'), 'image/jpeg')
self.assertEqual(guess('screenshot.png'), 'image/png')
self.assertEqual(guess('screenshot.webp'), 'image/webp')

self.assertEqual(guess('screen.shot.webp'), 'image/webp')
self.assertEqual(guess('.webp'), 'image/webp')
self.assertEqual(guess('screen shot.webp'), 'image/webp')

self.assertEqual(guess('screenshot.pdf'), 'application/octet-stream')
self.assertEqual(guess('screenshot gif'), 'application/octet-stream')
self.assertEqual(guess('screenshotgif'), 'application/octet-stream')
self.assertEqual(guess('gif'), 'application/octet-stream')
self.assertEqual(guess(''), 'application/octet-stream')

def test_store_attachment(self):
"""We can store attachment content."""
actual = attachments.store_attachment(
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ validators==0.20.0
# Work-around for failure to deploy
# https://stackoverflow.com/questions/69936420/google-cloud-platform-app-deploy-failure-due-to-pyparsing
pyparsing==2.4.7

pillow==11.0.0
# OpenAPI files
./gen/py/chromestatus_openapi
types-python-dateutil==2.9.0.20240821

0 comments on commit b4c5ced

Please sign in to comment.