mirror of
https://github.com/lucaspalomodevelop/eventcally.git
synced 2026-03-13 00:07:22 +00:00
Add image caching #97
This commit is contained in:
parent
d220867392
commit
5ea91f894d
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)."
|
||||
},
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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">© {{ 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 %}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user