Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gzip and cache track maps #407

Merged
merged 15 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions example.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ SQLALCHEMY_URL="mysql+pymysql://freezing:zeer0mfreezing-db-dev/freezing?charset=
# If you keep your MySQL database somewhere else, fix this up to match.
#SQLALCHEMY_URL="mysql+pymysql://freezing:[email protected]/freezing?charset=utf8mb4&binary_prefix=true""

# A place to cache json responses.
JSON_CACHE_DIR=/path/to/cache/json
obscurerichard marked this conversation as resolved.
Show resolved Hide resolved

# Configuration for the Strava client. These settings come from your App setup.
# Setting this is only required if you are testing parts of this application that exercise the Strava API,
# such as user registration. That is an advanced topic and not required to make most changes to
Expand Down
1 change: 1 addition & 0 deletions freezing/web/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Config:
)
STRAVA_CLIENT_ID = env("STRAVA_CLIENT_ID")
STRAVA_CLIENT_SECRET = env("STRAVA_CLIENT_SECRET")
JSON_CACHE_DIR = env("JSON_CACHE_DIR", default="/data/cache/json")
TIMEZONE: tzinfo = env(
"TIMEZONE",
default="America/New_York",
Expand Down
85 changes: 78 additions & 7 deletions freezing/web/views/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import datetime
import gzip
import json
import re
from datetime import timedelta
from decimal import Decimal
from pathlib import Path

import arrow
import pytz
from flask import Blueprint, abort, jsonify, request, session
from flask import Blueprint, abort, jsonify, make_response, request, session
from freezing.model import meta
from freezing.model.orm import Athlete, Ride, RidePhoto, RideTrack
from sqlalchemy import func, text
Expand All @@ -23,6 +27,9 @@
"""A limit on the number of tracks to return."""
TRACK_LIMIT_MAX = 2048
merlinorg marked this conversation as resolved.
Show resolved Hide resolved

"""For how many minutes to cache track maps."""
JSON_CACHE_MINUTES = 30
merlinorg marked this conversation as resolved.
Show resolved Hide resolved


def get_limit(request):
"""Get the limit parameter from the request, if it exists.
Expand Down Expand Up @@ -406,23 +413,87 @@ def _track_map(
return {"tracks": tracks}


def _get_cached(key: str, compute):
cache_dir = config.JSON_CACHE_DIR
if not cache_dir:
return compute()

cache_file = Path(cache_dir).joinpath(key).resolve()
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
if not str(cache_file).startswith(str(Path(cache_dir).resolve())):
raise Exception("Invalid cache file path")
if cache_file.is_file():
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime)
Fixed Show fixed Hide fixed
age = datetime.datetime.now() - time_stamp
if age.total_seconds() < JSON_CACHE_MINUTES * 60:
return cache_file.read_bytes()
Fixed Show fixed Hide fixed

content = compute()
cache_file.parent.mkdir(parents=True, exist_ok=True)
Fixed Show fixed Hide fixed
cache_file.write_bytes(content)
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed

return content


def _make_gzip_json_response(content, private=False):
response = make_response(content)
response.headers["Content-Length"] = len(content)
response.headers["Content-Encoding"] = "gzip"
response.headers["Content-Type"] = "application/json"
response.headers["Cache-Control"] = (
f"max-age={JSON_CACHE_MINUTES * 60}, {'private' if private else 'public'}"
)
return response


@blueprint.route("/all/trackmap.json")
def track_map_all():
hash_tag = request.args.get("hashtag")
return jsonify(_track_map(hash_tag=hash_tag, limit=get_limit(request)))
limit = get_limit(request)

hash_clean = re.sub(r"\W+", "", hash_tag) if hash_tag else None
return _make_gzip_json_response(
_get_cached(
(
f"track_map/all/{hash_clean}-{limit}.json.gz"
if hash_clean
else f"track_map/all/{limit}.json.gz"
),
lambda: gzip.compress(
json.dumps(_track_map(hash_tag=hash_tag, limit=limit)).encode("utf8"), 5
),
)
)


@blueprint.route("/my/trackmap.json")
@auth.requires_auth
def track_map_my():
athlete_id = session.get("athlete_id")
return jsonify(
_track_map(
athlete_id=athlete_id, include_private=True, limit=get_limit(request)
limit = get_limit(request)

return _make_gzip_json_response(
_get_cached(
f"track_map/athlete/{athlete_id}-{limit}.json.gz",
lambda: gzip.compress(
json.dumps(
_track_map(athlete_id=athlete_id, include_private=True, limit=limit)
).encode("utf8"),
5,
),
),
private=True,
)


@blueprint.route("/teams/<int:team_id>/trackmap.json")
def track_map_team(team_id):
return jsonify(_track_map(team_id=team_id, limit=get_limit(request)))
def track_map_team(team_id: int):
limit = get_limit(request)

return _make_gzip_json_response(
_get_cached(
f"track_map/team/{team_id}-{limit}.json.gz",
lambda: gzip.compress(
json.dumps(_track_map(team_id=team_id, limit=limit)).encode("utf8"), 5
),
)
)
Loading