Add image caching #97

This commit is contained in:
Daniel Grams 2021-01-26 13:18:46 +01:00
parent d220867392
commit 5ea91f894d
11 changed files with 101 additions and 32 deletions

View File

@ -77,11 +77,12 @@ Create `.env` file in the root directory or pass as environment variables.
| MAIL_SERVER | " |
| MAIL_USERNAME | " |
### Resolve addresses with Google Maps
### Misc
| Variable | Function |
| --- | --- |
| GOOGLE_MAPS_API_KEY | API Key with Places API enabled |
| CACHE_PATH | Absolute or relative path to root directory for dump and image caching. Default: tmp |
| GOOGLE_MAPS_API_KEY | Resolve addresses with Google Maps: API Key with Places API enabled |
## Development

View File

@ -15,7 +15,6 @@ from flask_restful import Api
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from flask_apispec.extension import FlaskApiSpec
import pathlib
# Create app
app = Flask(__name__)
@ -40,10 +39,13 @@ app.config["SECURITY_PASSWORD_SALT"] = os.environ.get(
"SECURITY_PASSWORD_SALT", "146585145368132386173505678016728509634"
)
# Temporary pathes
temp_path = os.path.join(app.root_path, "tmp")
dump_path = os.path.join(temp_path, "dump")
pathlib.Path(dump_path).mkdir(parents=True, exist_ok=True)
# Cache pathes
cache_env = os.environ.get("CACHE_PATH", "tmp")
cache_path = (
cache_env if os.path.isabs(cache_env) else os.path.join(app.root_path, cache_env)
)
dump_path = os.path.join(cache_path, "dump")
img_path = os.path.join(cache_path, "img")
# i18n
app.config["BABEL_DEFAULT_LOCALE"] = "de"

View File

@ -16,7 +16,13 @@ class ImageBaseSchema(ImageIdSchema):
class ImageSchema(ImageBaseSchema):
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
image_url = marshmallow.URLFor(
"image",
values=dict(id="<id>", s=500),
metadata={
"description": "Append query arguments w for width, h for height or s for size(width and height)."
},
)
class ImageDumpSchema(ImageBaseSchema):
@ -24,4 +30,10 @@ class ImageDumpSchema(ImageBaseSchema):
class ImageRefSchema(ImageIdSchema):
image_url = marshmallow.URLFor("image", values=dict(id="<id>"))
image_url = marshmallow.URLFor(
"image",
values=dict(id="<id>", s=500),
metadata={
"description": "Append query arguments w for width, h for height or s for size(width and height)."
},
)

View File

@ -23,7 +23,7 @@ from project.api.organization.schemas import OrganizationDumpSchema
from project.api.event_reference.schemas import EventReferenceDumpSchema
import os
import shutil
import pathlib
from project.utils import make_dir
dump_cli = AppGroup("dump")
@ -42,12 +42,7 @@ def dump_items(items, schema, file_base_name, dump_path):
def dump_all():
# Setup temp dir
tmp_path = os.path.join(dump_path, "tmp")
try:
original_umask = os.umask(0)
pathlib.Path(tmp_path).mkdir(parents=True, exist_ok=True)
finally:
os.umask(original_umask)
make_dir(tmp_path)
# Events
events = Event.query.options(joinedload(Event.categories)).all()

View File

@ -284,29 +284,29 @@
{% endif %}
{% endmacro %}
{% macro render_img_src(image) %}
<img src="{{ url_for('image', id=image.id) }}" class="{{ kwargs['class'] or 'img-fluid' }}" style="{{ kwargs['style'] or 'max-width:100%;' }}" />
{% macro render_img_src(image, size=500) %}
<img src="{{ url_for('image', id=image.id, s=size) }}" class="{{ kwargs['class'] or 'img-fluid' }}" style="{{ kwargs['style'] or 'max-width:100%;' }}" />
{% endmacro %}
{% macro render_image(image) %}
{% macro render_image(image, size=500) %}
{% if image %}
{% set img_class = kwargs['class'] if 'class' in kwargs else '' %}
{% set img_style = kwargs['style'] if 'style' in kwargs else '' %}
{% if image.copyright_text %}
<figure class="figure">
{% set img_class = img_class + ' figure-img' %}
{{ render_img_src(image, class=img_class, style=img_style) }}
{{ render_img_src(image, size, class=img_class, style=img_style) }}
<figcaption class="figure-caption">&copy; {{ image.copyright_text }}</figcaption>
</figure>
{% else %}
{% set img_class = img_class + ' mb-2' %}
{{ render_img_src(image, class=img_class, style=img_style) }}
{{ render_img_src(image, size, class=img_class, style=img_style) }}
{% endif %}
{% endif %}
{% endmacro %}
{% macro render_logo(image) %}
{{ render_image(image, style="max-width:200px;") }}
{{ render_image(image, 120, style="max-width:120px;") }}
{% endmacro %}
{% macro render_event_review_status(event) %}
@ -392,7 +392,7 @@
{% endif %}
{% if event.photo_id %}
<div class="my-4">{{ render_image(event.photo) }}</div>
<div class="my-4">{{ render_image(event.photo, 700) }}</div>
{% endif %}
<div class="my-4">{{ event.description }}</div>
@ -415,7 +415,7 @@
<h5 class="card-title">{{ event.event_place.name }}</h5>
{% if event.event_place.photo_id %}
<div class="my-4">{{ render_image(event.event_place.photo) }}</div>
<div class="my-4">{{ render_image(event.event_place.photo, 300) }}</div>
{% endif %}
{% if event.event_place.description %}

View File

@ -67,7 +67,7 @@
</div>
<div class="col-sm-4 text-right">
{% if date.event.photo_id %}
<img src="{{ url_for('image', id=date.event.photo_id) }}" style="object-fit: cover; width: 200px;" />
<img src="{{ url_for('image', id=date.event.photo_id, s=200) }}" style="object-fit: cover; width: 200px;" />
{% endif %}
</div>
</div>
@ -82,7 +82,7 @@
<div class="card">
<div>
{% if date.event.photo_id %}
<img src="{{ url_for('image', id=date.event.photo_id) }}" class="card-img-top" style="object-fit: cover; height: 20vh;" />
<img src="{{ url_for('image', id=date.event.photo_id, s=500) }}" class="card-img-top" style="object-fit: cover; height: 20vh;" />
{% endif %}
</div>
<div class="card-body">

View File

@ -116,7 +116,7 @@
<div class="card mb-4">
<div class="card-body">
{% if admin_unit.logo_id %}
<div class="mb-4 text-center"><img src="{{ url_for('image', id=admin_unit.logo_id) }}" class="img-fluid" style="max-height:10vmin;" /></div>
<div class="mb-4 text-center"><img src="{{ url_for('image', id=admin_unit.logo_id, s=100) }}" class="img-fluid" style="max-height:10vmin;" /></div>
{% endif %}
<p class="card-text">
Hier kannst du als Gast eine Veranstaltung vorschlagen, die anschließend durch <strong>{{ admin_unit.name }}</strong> geprüft wird.

View File

@ -1,4 +1,6 @@
from flask_babelex import lazy_gettext
import pathlib
import os
def get_event_category_name(category):
@ -7,3 +9,11 @@ def get_event_category_name(category):
def get_localized_enum_name(enum):
return lazy_gettext(enum.__class__.__name__ + "." + enum.name)
def make_dir(path):
try:
original_umask = os.umask(0)
pathlib.Path(path).mkdir(parents=True, exist_ok=True)
finally:
os.umask(original_umask)

View File

@ -1,8 +1,44 @@
from project import app
from project import app, img_path
from project.models import Image
from sqlalchemy.orm import load_only
import PIL
from io import BytesIO
import os
from flask import send_file, request
from project.utils import make_dir
@app.route("/image/<int:id>")
def image(id):
image = Image.query.get_or_404(id)
return app.response_class(image.data, mimetype=image.encoding_format)
image = Image.query.options(load_only(Image.id, Image.encoding_format)).get_or_404(
id
)
# Dimensions
width = 500
height = 500
if "s" in request.args:
width = int(request.args["s"])
height = width
elif "w" in request.args:
width = int(request.args["w"])
elif "h" in request.args:
height = int(request.args["h"])
# Generate file name
extension = image.encoding_format.split("/")[-1] if image.encoding_format else "png"
file_path = os.path.join(img_path, f"{id}-{width}-{height}.{extension}")
# Load from disk if exists
if os.path.exists(file_path):
return send_file(file_path)
# Save from database to disk
make_dir(img_path)
img = PIL.Image.open(BytesIO(image.data))
img.thumbnail((width, height), PIL.Image.ANTIALIAS)
img.save(file_path)
# Load from disk
return send_file(file_path)

View File

@ -3,6 +3,7 @@ aniso8601==8.1.0
apispec==4.0.0
apispec-webframeworks==0.5.2
appdirs==1.4.4
argh==0.26.2
attrs==20.3.0
Babel==2.9.0
bcrypt==3.2.0
@ -62,6 +63,7 @@ oauthlib==3.1.0
packaging==20.8
passlib==1.7.4
pathspec==0.8.1
pilkit==2.0
Pillow==8.0.1
pluggy==0.13.1
pre-commit==2.9.3

View File

@ -1,6 +1,17 @@
def test_read(client, seeder, utils):
import pytest
import shutil
from project import img_path
@pytest.mark.parametrize("size", [None, 100])
@pytest.mark.parametrize("width", [None, 100])
@pytest.mark.parametrize("height", [None, 100])
def test_read(app, seeder, utils, size, width, height):
user_id, admin_unit_id = seeder.setup_base()
image_id = seeder.upsert_default_image()
url = utils.get_url("image", id=image_id)
shutil.rmtree(img_path, ignore_errors=True)
url = utils.get_url("image", id=image_id, s=size, w=width, h=height)
utils.get_ok(url)
utils.get_ok(url) # cache