From d01ad8e8ee9e52273ff7f432c197fad9329e393a Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 14 Jan 2025 18:33:42 +0000 Subject: [PATCH 01/14] gzip and cache track maps --- example.cfg | 3 ++ freezing/web/config.py | 1 + freezing/web/views/api.py | 79 +++++++++++++++++++++++++++++++++++---- 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/example.cfg b/example.cfg index cf560b97..3a646716 100644 --- a/example.cfg +++ b/example.cfg @@ -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:freezing@127.0.0.1/freezing?charset=utf8mb4&binary_prefix=true"" +# A place to cache json responses. +JSON_CACHE_DIR=/path/to/cache/json + # 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 diff --git a/freezing/web/config.py b/freezing/web/config.py index 390e9b2b..e9358e85 100644 --- a/freezing/web/config.py +++ b/freezing/web/config.py @@ -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=None) TIMEZONE: tzinfo = env( "TIMEZONE", default="America/New_York", diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index 8537e4ab..563017be 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -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 @@ -23,6 +27,9 @@ """A limit on the number of tracks to return.""" TRACK_LIMIT_MAX = 2048 +"""For how many minutes to cache track maps.""" +TRACK_CACHE_MINUTES = 30 + def get_limit(request): """Get the limit parameter from the request, if it exists. @@ -406,23 +413,81 @@ 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) + if cache_file.is_file(): + time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) + age = datetime.datetime.now() - time_stamp + if age.total_seconds() < TRACK_CACHE_MINUTES * 60: + return cache_file.read_bytes() + + content = compute() + cache_file.parent.mkdir(parents=True, exist_ok=True) + cache_file.write_bytes(content) + + return content + + +def _make_gzip_json_response(content): + response = make_response(content) + response.headers["Content-Length"] = len(content) + response.headers["Content-Encoding"] = "gzip" + response.headers["Content-Type"] = "application/json" + 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, + ), + ) ) @blueprint.route("/teams//trackmap.json") def track_map_team(team_id): - return jsonify(_track_map(team_id=team_id, limit=get_limit(request))) + 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 + ), + ) + ) From 31a0999daae4388074566cb040618e5034ac2f66 Mon Sep 17 00:00:00 2001 From: Merlin Hughes Date: Tue, 14 Jan 2025 18:45:27 +0000 Subject: [PATCH 02/14] Fix code scanning alert no. 12: Uncontrolled data used in path expression Make CodeQL happy. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- freezing/web/views/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index 563017be..4d0e4bb7 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -418,7 +418,9 @@ def _get_cached(key: str, compute): if not cache_dir: return compute() - cache_file = Path(cache_dir).joinpath(key) + cache_file = Path(cache_dir).joinpath(key).resolve() + if not str(cache_file).startswith(str(Path(cache_dir).resolve())): + raise Exception("Invalid cache file path") if cache_file.is_file(): time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) age = datetime.datetime.now() - time_stamp From f64d4639a219734bea6285576cd221177399295c Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 14 Jan 2025 18:54:02 +0000 Subject: [PATCH 03/14] flask says it is an int already but.. alrighty --- freezing/web/views/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index 4d0e4bb7..7356a7fe 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -482,7 +482,7 @@ def track_map_my(): @blueprint.route("/teams//trackmap.json") -def track_map_team(team_id): +def track_map_team(team_id: int): limit = get_limit(request) return _make_gzip_json_response( From 486ef3f2268522cd56f9de76bc1f796f1bffc482 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 14 Jan 2025 19:00:25 +0000 Subject: [PATCH 04/14] add browser cache header --- freezing/web/views/api.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index 7356a7fe..feabf210 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -28,7 +28,7 @@ TRACK_LIMIT_MAX = 2048 """For how many minutes to cache track maps.""" -TRACK_CACHE_MINUTES = 30 +JSON_CACHE_MINUTES = 30 def get_limit(request): @@ -424,7 +424,7 @@ def _get_cached(key: str, compute): if cache_file.is_file(): time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) age = datetime.datetime.now() - time_stamp - if age.total_seconds() < TRACK_CACHE_MINUTES * 60: + if age.total_seconds() < JSON_CACHE_MINUTES * 60: return cache_file.read_bytes() content = compute() @@ -434,11 +434,14 @@ def _get_cached(key: str, compute): return content -def _make_gzip_json_response(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 @@ -477,7 +480,8 @@ def track_map_my(): ).encode("utf8"), 5, ), - ) + ), + private=True, ) From 5d5515d79e4e0fa16046ad520255ec316b7a8640 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 14 Jan 2025 19:52:15 +0000 Subject: [PATCH 05/14] default cache dir as per sync --- freezing/web/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freezing/web/config.py b/freezing/web/config.py index e9358e85..9f3cac5f 100644 --- a/freezing/web/config.py +++ b/freezing/web/config.py @@ -61,7 +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=None) + JSON_CACHE_DIR = env("JSON_CACHE_DIR", default="/data/cache/json") TIMEZONE: tzinfo = env( "TIMEZONE", default="America/New_York", From b15c1d5c6506b5913052e6464d412d60b07b8b32 Mon Sep 17 00:00:00 2001 From: merlin Date: Wed, 15 Jan 2025 23:33:40 +0000 Subject: [PATCH 06/14] Move constants to config --- example.cfg | 2 +- freezing/web/config.py | 5 ++++- freezing/web/views/api.py | 21 ++++++--------------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/example.cfg b/example.cfg index 3a646716..119620d4 100644 --- a/example.cfg +++ b/example.cfg @@ -31,7 +31,7 @@ SQLALCHEMY_URL="mysql+pymysql://freezing:zeer0mfreezing-db-dev/freezing?charset= #SQLALCHEMY_URL="mysql+pymysql://freezing:freezing@127.0.0.1/freezing?charset=utf8mb4&binary_prefix=true"" # A place to cache json responses. -JSON_CACHE_DIR=/path/to/cache/json +JSON_CACHE_DIR=data/cache/json # 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, diff --git a/freezing/web/config.py b/freezing/web/config.py index 9f3cac5f..2d3cbb7f 100644 --- a/freezing/web/config.py +++ b/freezing/web/config.py @@ -61,7 +61,10 @@ 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") + JSON_CACHE_DIR = env("JSON_CACHE_DIR", default="/cache/json") + JSON_CACHE_MINUTES = env("JSON_CACHE_MINUTES", cast=int, default=30) + TRACK_LIMIT_DEFAULT = env("TRACK_LIMIT_DEFAULT", cast=int, default=1024) + TRACK_LIMIT_MAX = env("TRACK_LIMIT_MAX", cast=int, default=2048) TIMEZONE: tzinfo = env( "TIMEZONE", default="America/New_York", diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index feabf210..5694a897 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -21,15 +21,6 @@ blueprint = Blueprint("api", __name__) -"""Have a default limit for GeoJSON track APIs.""" -TRACK_LIMIT_DEFAULT = 1024 - -"""A limit on the number of tracks to return.""" -TRACK_LIMIT_MAX = 2048 - -"""For how many minutes to cache track maps.""" -JSON_CACHE_MINUTES = 30 - def get_limit(request): """Get the limit parameter from the request, if it exists. @@ -43,11 +34,11 @@ def get_limit(request): """ limit = request.args.get("limit") if limit is None: - return TRACK_LIMIT_DEFAULT + return config.TRACK_LIMIT_DEFAULT limit = int(limit) - if limit > TRACK_LIMIT_MAX: - abort(400, f"limit {limit} exceeds {TRACK_LIMIT_MAX}") - return min(TRACK_LIMIT_MAX, int(limit)) + if limit > config.TRACK_LIMIT_MAX: + abort(400, f"limit {limit} exceeds {config.TRACK_LIMIT_MAX}") + return min(config.TRACK_LIMIT_MAX, int(limit)) @blueprint.route("/stats/general") @@ -424,7 +415,7 @@ def _get_cached(key: str, compute): if cache_file.is_file(): time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) age = datetime.datetime.now() - time_stamp - if age.total_seconds() < JSON_CACHE_MINUTES * 60: + if age.total_seconds() < config.JSON_CACHE_MINUTES * 60: return cache_file.read_bytes() content = compute() @@ -440,7 +431,7 @@ def _make_gzip_json_response(content, private=False): 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'}" + f"max-age={config.JSON_CACHE_MINUTES * 60}, {'private' if private else 'public'}" ) return response From 8c61c487abcf47435464e23891a970dcf7c403d2 Mon Sep 17 00:00:00 2001 From: Richard Bullington-McGuire Date: Thu, 16 Jan 2025 08:13:44 -0500 Subject: [PATCH 07/14] Force addition of .gitkeep --- data/cache/json/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/cache/json/.gitkeep diff --git a/data/cache/json/.gitkeep b/data/cache/json/.gitkeep new file mode 100644 index 00000000..e69de29b From bea4ca075c5bc8582a170dd827b992121f898ff0 Mon Sep 17 00:00:00 2001 From: Richard Bullington-McGuire Date: Thu, 16 Jan 2025 09:00:21 -0500 Subject: [PATCH 08/14] Get docker-compose working, 500 error on failure The docker-compose.yml setting now works for the new cache dir. This emits a 500 Server Error if something goes wrong now. --- docker-compose.yml | 2 +- freezing/web/views/api.py | 31 ++++++++++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 66fd9f2b..4af27322 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: ports: - "${FREEZING_WEB_PORT:-8000}:8000" volumes: - - ./data/cache:/data/cache + - ./data/cache:/cache - ./leaderboards:/data/leaderboards - ./data/sessions:/data/sessions environment: diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index 5694a897..cc409749 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -410,19 +410,24 @@ def _get_cached(key: str, compute): return compute() cache_file = Path(cache_dir).joinpath(key).resolve() - if not str(cache_file).startswith(str(Path(cache_dir).resolve())): - raise Exception("Invalid cache file path") - if cache_file.is_file(): - time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) - age = datetime.datetime.now() - time_stamp - if age.total_seconds() < config.JSON_CACHE_MINUTES * 60: - return cache_file.read_bytes() - - content = compute() - cache_file.parent.mkdir(parents=True, exist_ok=True) - cache_file.write_bytes(content) - - return content + try: + if not str(cache_file).startswith(str(Path(cache_dir).resolve())): + raise Exception("Invalid cache file path") + if cache_file.is_file(): + time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) + age = datetime.datetime.now() - time_stamp + if age.total_seconds() < config.JSON_CACHE_MINUTES * 60: + return cache_file.read_bytes() + + content = compute() + cache_file.parent.mkdir(parents=True, exist_ok=True) + cache_file.write_bytes(content) + + return content + except Exception as e: + err = f"Error retrieving cached item {key}: {e}" + log.exception(err) + abort(500, err) def _make_gzip_json_response(content, private=False): From 9ff104d73513868e0418cf8bc14c292cecd2f068 Mon Sep 17 00:00:00 2001 From: Richard Bullington-McGuire Date: Thu, 16 Jan 2025 10:32:53 -0500 Subject: [PATCH 09/14] Potential fix for code scanning alert no. 17: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- freezing/web/views/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index cc409749..457e04b5 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -409,9 +409,9 @@ def _get_cached(key: str, compute): if not cache_dir: return compute() - cache_file = Path(cache_dir).joinpath(key).resolve() + cache_file = Path(os.path.normpath(Path(cache_dir).joinpath(key))).resolve() try: - if not str(cache_file).startswith(str(Path(cache_dir).resolve())): + if not str(cache_file).startswith(str(Path(cache_dir).resolve()) + os.sep): raise Exception("Invalid cache file path") if cache_file.is_file(): time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) From 493d96c4870e0550cd069a1b616ad50c5e18eb0a Mon Sep 17 00:00:00 2001 From: Richard Bullington-McGuire Date: Thu, 16 Jan 2025 10:37:04 -0500 Subject: [PATCH 10/14] Add missing import --- freezing/web/views/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index 457e04b5..bfc5b741 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -1,6 +1,7 @@ import datetime import gzip import json +import os import re from datetime import timedelta from decimal import Decimal From 2b550f3e66bdb3da6d229efc0a6ee89d2c21b9b7 Mon Sep 17 00:00:00 2001 From: Richard Bullington-McGuire Date: Thu, 16 Jan 2025 11:34:50 -0500 Subject: [PATCH 11/14] Potential fix for code scanning alert no. 22: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- freezing/web/views/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index bfc5b741..2ce51765 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -422,6 +422,8 @@ def _get_cached(key: str, compute): content = compute() cache_file.parent.mkdir(parents=True, exist_ok=True) + if not str(cache_file).startswith(str(Path(cache_dir).resolve()) + os.sep): + raise Exception("Invalid cache file path") cache_file.write_bytes(content) return content From d464649072b46b0f111d3d290380fe5ae282148b Mon Sep 17 00:00:00 2001 From: Richard Bullington-McGuire Date: Thu, 16 Jan 2025 11:40:35 -0500 Subject: [PATCH 12/14] Potential fix for code scanning alert no. 20: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- freezing/web/views/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index 2ce51765..ad9df295 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -412,7 +412,7 @@ def _get_cached(key: str, compute): cache_file = Path(os.path.normpath(Path(cache_dir).joinpath(key))).resolve() try: - if not str(cache_file).startswith(str(Path(cache_dir).resolve()) + os.sep): + if os.path.commonpath([str(cache_file), str(Path(cache_dir).resolve())]) != str(Path(cache_dir).resolve()): raise Exception("Invalid cache file path") if cache_file.is_file(): time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) @@ -422,7 +422,7 @@ def _get_cached(key: str, compute): content = compute() cache_file.parent.mkdir(parents=True, exist_ok=True) - if not str(cache_file).startswith(str(Path(cache_dir).resolve()) + os.sep): + if os.path.commonpath([str(cache_file), str(Path(cache_dir).resolve())]) != str(Path(cache_dir).resolve()): raise Exception("Invalid cache file path") cache_file.write_bytes(content) From f929d14d6517a3f6ff9cccb3b679c4a8846be2bd Mon Sep 17 00:00:00 2001 From: Richard Bullington-McGuire Date: Thu, 16 Jan 2025 11:43:20 -0500 Subject: [PATCH 13/14] Potential fix for code scanning alert no. 25: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- freezing/web/views/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index ad9df295..916b6097 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -10,6 +10,7 @@ import arrow import pytz from flask import Blueprint, abort, jsonify, make_response, request, session +from werkzeug.utils import secure_filename from freezing.model import meta from freezing.model.orm import Athlete, Ride, RidePhoto, RideTrack from sqlalchemy import func, text @@ -410,7 +411,8 @@ def _get_cached(key: str, compute): if not cache_dir: return compute() - cache_file = Path(os.path.normpath(Path(cache_dir).joinpath(key))).resolve() + sanitized_key = secure_filename(key) + cache_file = Path(os.path.normpath(Path(cache_dir).joinpath(sanitized_key))).resolve() try: if os.path.commonpath([str(cache_file), str(Path(cache_dir).resolve())]) != str(Path(cache_dir).resolve()): raise Exception("Invalid cache file path") @@ -428,7 +430,7 @@ def _get_cached(key: str, compute): return content except Exception as e: - err = f"Error retrieving cached item {key}: {e}" + err = f"Error retrieving cached item {sanitized_key}: {e}" log.exception(err) abort(500, err) From 823c1f3d89453e194ea008ef0e1265ffded7ce08 Mon Sep 17 00:00:00 2001 From: Richard Bullington-McGuire Date: Thu, 16 Jan 2025 11:49:20 -0500 Subject: [PATCH 14/14] Format with black, isort --- freezing/web/views/api.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freezing/web/views/api.py b/freezing/web/views/api.py index 916b6097..27788994 100644 --- a/freezing/web/views/api.py +++ b/freezing/web/views/api.py @@ -10,10 +10,10 @@ import arrow import pytz from flask import Blueprint, abort, jsonify, make_response, request, session -from werkzeug.utils import secure_filename from freezing.model import meta from freezing.model.orm import Athlete, Ride, RidePhoto, RideTrack from sqlalchemy import func, text +from werkzeug.utils import secure_filename from freezing.web import config from freezing.web.autolog import log @@ -412,9 +412,13 @@ def _get_cached(key: str, compute): return compute() sanitized_key = secure_filename(key) - cache_file = Path(os.path.normpath(Path(cache_dir).joinpath(sanitized_key))).resolve() + cache_file = Path( + os.path.normpath(Path(cache_dir).joinpath(sanitized_key)) + ).resolve() try: - if os.path.commonpath([str(cache_file), str(Path(cache_dir).resolve())]) != str(Path(cache_dir).resolve()): + if os.path.commonpath([str(cache_file), str(Path(cache_dir).resolve())]) != str( + Path(cache_dir).resolve() + ): raise Exception("Invalid cache file path") if cache_file.is_file(): time_stamp = datetime.datetime.fromtimestamp(cache_file.stat().st_mtime) @@ -424,7 +428,9 @@ def _get_cached(key: str, compute): content = compute() cache_file.parent.mkdir(parents=True, exist_ok=True) - if os.path.commonpath([str(cache_file), str(Path(cache_dir).resolve())]) != str(Path(cache_dir).resolve()): + if os.path.commonpath([str(cache_file), str(Path(cache_dir).resolve())]) != str( + Path(cache_dir).resolve() + ): raise Exception("Invalid cache file path") cache_file.write_bytes(content)