Merge pull request #15661 from gavindsouza/thumbnail-for-images
fix: Thumbnail for external images (from URL)
This commit is contained in:
commit
5c17f9ad79
7 changed files with 67 additions and 19 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
"""
|
||||
|
|
@ -7,7 +7,6 @@ record of files
|
|||
naming for same name files: file.gif, file-1.gif, file-2.gif etc
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import imghdr
|
||||
import io
|
||||
|
|
@ -17,9 +16,10 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
from requests.exceptions import HTTPError, SSLError
|
||||
from PIL import Image, ImageFile, ImageOps
|
||||
from io import BytesIO
|
||||
from urllib.parse import quote, unquote
|
||||
|
|
@ -31,6 +31,11 @@ from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, g
|
|||
from frappe.utils.image import strip_exif_data, optimize_image
|
||||
from frappe.utils.file_manager import safe_b64decode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL.ImageFile import ImageFile
|
||||
from requests.models import Response
|
||||
|
||||
|
||||
class MaxFileSizeReachedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
|
@ -276,7 +281,7 @@ class File(Document):
|
|||
image, filename, extn = get_local_image(self.file_url)
|
||||
else:
|
||||
image, filename, extn = get_web_image(self.file_url)
|
||||
except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
|
||||
except (HTTPError, SSLError, IOError, TypeError):
|
||||
return
|
||||
|
||||
size = width, height
|
||||
|
|
@ -648,9 +653,17 @@ def setup_folder_path(filename, new_parent):
|
|||
from frappe.model.rename_doc import rename_doc
|
||||
rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True)
|
||||
|
||||
def get_extension(filename, extn, content):
|
||||
def get_extension(filename, extn, content: bytes = None, response: "Response" = None) -> str:
|
||||
mimetype = None
|
||||
|
||||
if response:
|
||||
content_type = response.headers.get("Content-Type")
|
||||
|
||||
if content_type:
|
||||
_extn = mimetypes.guess_extension(content_type)
|
||||
if _extn:
|
||||
return _extn[1:]
|
||||
|
||||
if extn:
|
||||
# remove '?' char and parameters from extn if present
|
||||
if '?' in extn:
|
||||
|
|
@ -693,14 +706,14 @@ def get_local_image(file_url):
|
|||
|
||||
return image, filename, extn
|
||||
|
||||
def get_web_image(file_url):
|
||||
def get_web_image(file_url: str) -> Tuple["ImageFile", str, str]:
|
||||
# download
|
||||
file_url = frappe.utils.get_url(file_url)
|
||||
r = requests.get(file_url, stream=True)
|
||||
try:
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if "404" in e.args[0]:
|
||||
except HTTPError:
|
||||
if r.status_code == 404:
|
||||
frappe.msgprint(_("File '{0}' not found").format(file_url))
|
||||
else:
|
||||
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
|
||||
|
|
@ -719,7 +732,10 @@ def get_web_image(file_url):
|
|||
filename = get_random_filename()
|
||||
extn = None
|
||||
|
||||
extn = get_extension(filename, extn, r.content)
|
||||
extn = get_extension(filename, extn, response=r)
|
||||
if extn == "bin":
|
||||
extn = get_extension(filename, extn, content=r.content) or "png"
|
||||
|
||||
filename = "/files/" + strip(unquote(filename))
|
||||
|
||||
return image, filename, extn
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import base64
|
||||
import json
|
||||
import frappe
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from frappe import _
|
||||
from frappe.core.doctype.file.file import File, get_attached_images, move_file, get_files_in_folder, unzip_file
|
||||
from frappe.utils import get_files_path
|
||||
|
|
@ -384,6 +384,16 @@ class TestFile(unittest.TestCase):
|
|||
test_file.make_thumbnail()
|
||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
|
||||
|
||||
# test web image without extension
|
||||
test_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": 'logo',
|
||||
"file_url": frappe.utils.get_url('/_test/assets/image'),
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
test_file.make_thumbnail()
|
||||
self.assertTrue(test_file.thumbnail_url.endswith("_small.jpeg"))
|
||||
|
||||
# test local image
|
||||
test_file.db_set('thumbnail_url', None)
|
||||
test_file.reload()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.utils import set_request
|
||||
from frappe.website.page_renderers.static_page import StaticPage
|
||||
from frappe.website.serve import get_response, get_response_content
|
||||
from frappe.website.utils import (build_response, clear_website_cache, get_home_page)
|
||||
|
||||
|
|
@ -97,6 +99,19 @@ class TestWebsite(unittest.TestCase):
|
|||
response = get_response()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
set_request(method="GET", path="/_test/assets/image.jpg")
|
||||
response = get_response()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
set_request(method="GET", path="/_test/assets/image")
|
||||
response = get_response()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
with patch.object(StaticPage, "render") as static_render:
|
||||
set_request(method="GET", path="/_test/assets/image")
|
||||
response = get_response()
|
||||
static_render.assert_called()
|
||||
|
||||
def test_error_page(self):
|
||||
set_request(method='GET', path='/_test/problematic_page')
|
||||
response = get_response()
|
||||
|
|
@ -127,7 +142,6 @@ class TestWebsite(unittest.TestCase):
|
|||
response = get_response()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
|
||||
def test_redirect(self):
|
||||
import frappe.hooks
|
||||
frappe.set_user('Administrator')
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from werkzeug.wsgi import wrap_file
|
|||
|
||||
import frappe
|
||||
from frappe.website.page_renderers.base_renderer import BaseRenderer
|
||||
from frappe.website.utils import is_binary_file
|
||||
|
||||
UNSUPPORTED_STATIC_PAGE_TYPES = ('html', 'md', 'js', 'xml', 'css', 'txt', 'py', 'json')
|
||||
|
||||
|
|
@ -20,21 +21,20 @@ class StaticPage(BaseRenderer):
|
|||
return
|
||||
for app in frappe.get_installed_apps():
|
||||
file_path = frappe.get_app_path(app, 'www') + '/' + self.path
|
||||
if os.path.isfile(file_path):
|
||||
if os.path.isfile(file_path) and is_binary_file(file_path):
|
||||
self.file_path = file_path
|
||||
|
||||
def can_render(self):
|
||||
return self.is_valid_file_path() and self.file_path
|
||||
|
||||
def is_valid_file_path(self):
|
||||
if ('.' not in self.path):
|
||||
return False
|
||||
extension = self.path.rsplit('.', 1)[-1]
|
||||
if extension in UNSUPPORTED_STATIC_PAGE_TYPES:
|
||||
return False
|
||||
return True
|
||||
|
||||
def render(self):
|
||||
# file descriptor to be left open, closed by middleware
|
||||
f = open(self.file_path, 'rb')
|
||||
response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True)
|
||||
response.mimetype = mimetypes.guess_type(self.file_path)[0] or 'application/octet-stream'
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from frappe.website.router import get_page_info
|
|||
from frappe.website.page_renderers.base_template_page import BaseTemplatePage
|
||||
from frappe.website.router import get_base_template
|
||||
from frappe.website.utils import (extract_comment_tag, extract_title, get_next_link,
|
||||
get_toc, get_frontmatter, cache_html, get_sidebar_items)
|
||||
get_toc, get_frontmatter, is_binary_file, cache_html, get_sidebar_items)
|
||||
|
||||
WEBPAGE_PY_MODULE_PROPERTIES = ("base_template_path", "template", "no_cache", "sitemap", "condition_field")
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ class TemplatePage(BaseTemplatePage):
|
|||
for dirname in folders:
|
||||
search_path = os.path.join(app_path, dirname, self.path)
|
||||
for file_path in self.get_index_path_options(search_path):
|
||||
if os.path.isfile(file_path):
|
||||
if os.path.isfile(file_path) and not is_binary_file(file_path):
|
||||
self.app = app
|
||||
self.app_path = app_path
|
||||
self.file_dir = dirname
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
from functools import wraps
|
||||
from functools import cache, wraps
|
||||
from typing import Dict, Optional
|
||||
|
||||
import yaml
|
||||
|
|
@ -511,3 +511,11 @@ def add_preload_headers(response):
|
|||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
@cache
|
||||
def is_binary_file(path):
|
||||
# ref: https://stackoverflow.com/a/7392391/10309266
|
||||
textchars = bytearray({7,8,9,10,12,13,27} | set(range(0x20, 0x100)) - {0x7f})
|
||||
with open(path, 'rb') as f:
|
||||
content = f.read(1024)
|
||||
return bool(content.translate(None, textchars))
|
||||
|
|
|
|||
BIN
frappe/www/_test/assets/image
Normal file
BIN
frappe/www/_test/assets/image
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
Loading…
Add table
Reference in a new issue