Start to cache actor icon

This commit is contained in:
Thomas Sileo 2018-07-05 01:02:51 +02:00
parent 8556cd29ef
commit 13c63e473a
5 changed files with 99 additions and 1 deletions

44
app.py
View file

@ -10,6 +10,7 @@ from datetime import timezone
from functools import wraps from functools import wraps
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import Tuple
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.parse import urlparse from urllib.parse import urlparse
@ -45,6 +46,7 @@ from config import BASE_URL
from config import DB from config import DB
from config import DEBUG_MODE from config import DEBUG_MODE
from config import DOMAIN from config import DOMAIN
from config import GRIDFS
from config import HEADERS from config import HEADERS
from config import ICON_URL from config import ICON_URL
from config import ID from config import ID
@ -69,9 +71,12 @@ from little_boxes.httpsig import HTTPSigAuth
from little_boxes.httpsig import verify_request from little_boxes.httpsig import verify_request
from little_boxes.webfinger import get_actor_url from little_boxes.webfinger import get_actor_url
from little_boxes.webfinger import get_remote_follow_template from little_boxes.webfinger import get_remote_follow_template
from utils.img import ImageCache
from utils.key import get_secret_key from utils.key import get_secret_key
from utils.object_service import ObjectService from utils.object_service import ObjectService
IMAGE_CACHE = ImageCache(GRIDFS)
OBJECT_SERVICE = ACTOR_SERVICE = ObjectService() OBJECT_SERVICE = ACTOR_SERVICE = ObjectService()
back = activitypub.MicroblogPubBackend() back = activitypub.MicroblogPubBackend()
@ -180,6 +185,30 @@ def clean_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS) return bleach.clean(html, tags=ALLOWED_TAGS)
_GRIDFS_CACHE: Dict[Tuple[str, int], str] = {}
def _get_actor_icon_url(url, size):
k = (url, size)
cached = _GRIDFS_CACHE.get(k)
if cached:
return cached
doc = IMAGE_CACHE.fs.find_one({"url": url, "size": size})
if doc:
u = f"/img/{str(doc._id)}"
_GRIDFS_CACHE[k] = u
return u
IMAGE_CACHE.cache_actor_icon(url)
return _get_actor_icon_url(url, size)
@app.template_filter()
def get_actor_icon_url(url, size):
return _get_actor_icon_url(url, size)
@app.template_filter() @app.template_filter()
def permalink_id(val): def permalink_id(val):
return str(hash(val)) return str(hash(val))
@ -357,6 +386,21 @@ def handle_activitypub_error(error):
# App routes # App routes
@app.route("/img/<img_id>")
def serve_img(img_id):
f = IMAGE_CACHE.fs.get(ObjectId(img_id))
resp = app.response_class(f, direct_passthrough=True, mimetype=f.content_type)
resp.headers.set("Content-Length", f.length)
resp.headers.set("ETag", f.md5)
resp.headers.set(
"Last-Modified", f.uploadDate.strftime("%a, %d %b %Y %H:%M:%S GMT")
)
resp.headers.set("Cache-Control", "public,max-age=31536000,immutable")
resp.headers.set("Content-Encoding", "gzip")
return resp
####### #######
# Login # Login

View file

@ -97,6 +97,7 @@ mongo_client = MongoClient(
DB_NAME = "{}_{}".format(USERNAME, DOMAIN.replace(".", "_")) DB_NAME = "{}_{}".format(USERNAME, DOMAIN.replace(".", "_"))
DB = mongo_client[DB_NAME] DB = mongo_client[DB_NAME]
GRIDFS = mongo_client[f"{DB_NAME}_gridfs"]
def _drop_db(): def _drop_db():

View file

@ -19,3 +19,4 @@ passlib
git+https://github.com/erikriver/opengraph.git git+https://github.com/erikriver/opengraph.git
git+https://github.com/tsileo/little-boxes.git git+https://github.com/tsileo/little-boxes.git
pyyaml pyyaml
pillow

View file

@ -20,7 +20,7 @@
<div class="note h-entry" id="activity-{{ obj.id | permalink_id }}"> <div class="note h-entry" id="activity-{{ obj.id | permalink_id }}">
<div class="h-card p-author"> <div class="h-card p-author">
<a class="u-url u-uid no-hover" href="{{ actor.url | get_url }}"><img class="u-photo" src="{% if not actor.icon %}/static/nopic.png{% else %}{{ actor.icon.url }}{% endif %}"> <a class="u-url u-uid no-hover" href="{{ actor.url | get_url }}"><img class="u-photo" src="{% if not actor.icon %}/static/nopic.png{% else %}{{ actor.icon.url | get_actor_icon_url(50) }}{% endif %}">
</a> </a>
<data class="p-name" value="{{ actor.name or actor.preferredUsername }}"></data> <data class="p-name" value="{{ actor.name or actor.preferredUsername }}"></data>
</div> </div>

52
utils/img.py Normal file
View file

@ -0,0 +1,52 @@
import base64
from gzip import GzipFile
from io import BytesIO
from typing import Any
import gridfs
import requests
from PIL import Image
def load(url):
"""Initializes a `PIL.Image` from the URL."""
# TODO(tsileo): user agent
resp = requests.get(url, stream=True)
resp.raise_for_status()
try:
image = Image.open(BytesIO(resp.raw.read()))
finally:
resp.close()
return image
def to_data_uri(img):
out = BytesIO()
img.save(out, format=img.format)
out.seek(0)
data = base64.b64encode(out.read()).decode("utf-8")
return f"data:{img.get_format_mimetype()};base64,{data}"
class ImageCache(object):
def __init__(self, gridfs_db: str) -> None:
self.fs = gridfs.GridFS(gridfs_db)
def cache_actor_icon(self, url: str):
if self.fs.find_one({"url": url}):
return
i = load(url)
for size in [50, 80]:
t1 = i.copy()
t1.thumbnail((size, size))
with BytesIO() as buf:
f1 = GzipFile(mode='wb', fileobj=buf)
t1.save(f1, format=i.format)
f1.close()
buf.seek(0)
self.fs.put(
buf, url=url, size=size, content_type=i.get_format_mimetype()
)
def get_file(self, url: str, size: int) -> Any:
return self.fs.find_one({"url": url, "size": size})