aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2025-12-28 22:31:02 +0100
committerDaniel Schadt <kingdread@gmx.de>2025-12-30 19:19:53 +0100
commitc4c76b6a758cd7c3979d6d109da5efcf384a3b40 (patch)
treebd6467ebbc762f61c593fc27e2825ac7c4f13406
parent9bf3d075cdd36d7f5f8b896cb064a6a09f863d75 (diff)
downloadfietsboek-c4c76b6a758cd7c3979d6d109da5efcf384a3b40.tar.gz
fietsboek-c4c76b6a758cd7c3979d6d109da5efcf384a3b40.tar.bz2
fietsboek-c4c76b6a758cd7c3979d6d109da5efcf384a3b40.zip
cache journey preview images
-rw-r--r--fietsboek/data.py83
-rw-r--r--fietsboek/views/journey.py18
2 files changed, 94 insertions, 7 deletions
diff --git a/fietsboek/data.py b/fietsboek/data.py
index d4bbb07..e288a03 100644
--- a/fietsboek/data.py
+++ b/fietsboek/data.py
@@ -42,6 +42,10 @@ def generate_filename(filename: Optional[str]) -> str:
return str(uuid.uuid4())
+def _log_deletion_error(_, path, exc_info):
+ LOGGER.warning("Failed to remove %s", path, exc_info=exc_info)
+
+
class DataManager:
"""Data manager.
@@ -61,6 +65,9 @@ class DataManager:
def _user_data_dir(self, user_id):
return self.data_dir / "users" / str(user_id)
+ def _journey_data_dir(self, journey_id):
+ return self.data_dir / "journeys" / str(journey_id)
+
def maintenance_mode(self) -> Optional[str]:
"""Checks whether the maintenance mode is enabled.
@@ -103,6 +110,17 @@ class DataManager:
path.mkdir(parents=True)
return UserDataDir(user_id, path, txn=self.txn)
+ def initialize_journey(self, journey_id: int) -> "JourneyDataDir":
+ """Creates the data directory for a journey.
+
+ :raises FileExistsError: If the directory already exists.
+ :param journey_id: ID of the journey.
+ :return: The manager that can be used to manage this journey's data.
+ """
+ path = self._journey_data_dir(journey_id)
+ path.mkdir(parents=True)
+ return JourneyDataDir(journey_id, path)
+
def purge(self, track_id: int):
"""Forcefully purges all data from the given track.
@@ -135,6 +153,18 @@ class DataManager:
raise FileNotFoundError(f"The path {path} is not a directory") from None
return UserDataDir(user_id, path, txn=self.txn)
+ def open_journey(self, journey_id: int) -> "JourneyDataDir":
+ """Open a journey's data directory.
+
+ :raises FileNotFoundError: If the journey directory does not exist.
+ :param journey_id: ID of the journey.
+ :return: The manager that can be used to manage this journey's data.
+ """
+ path = self._journey_data_dir(journey_id)
+ if not path.is_dir():
+ raise FileNotFoundError(f"The path {path} is not a directory") from None
+ return JourneyDataDir(journey_id, path)
+
def size(self) -> int:
"""Returns the size of all data.
@@ -162,6 +192,16 @@ class DataManager:
except FileNotFoundError:
return []
+ def list_journeys(self) -> list[int]:
+ """Returns a list of all journeys.
+
+ :return: A list of all journey IDs.
+ """
+ try:
+ return [int(journey.name) for journey in self._journey_data_dir(".").iterdir()]
+ except FileNotFoundError:
+ return []
+
class TrackDataDir:
"""Manager for a single track's data.
@@ -185,10 +225,6 @@ class TrackDataDir:
"""
return FileLock(self.path / "lock")
- @staticmethod
- def _log_deletion_error(_, path, exc_info):
- LOGGER.warning("Failed to remove %s", path, exc_info=exc_info)
-
def purge(self):
"""Purge all data pertaining to the track.
@@ -199,7 +235,7 @@ class TrackDataDir:
self.txn.purge(self.path)
else:
if self.path.is_dir():
- shutil.rmtree(self.path, ignore_errors=False, onerror=self._log_deletion_error)
+ shutil.rmtree(self.path, ignore_errors=False, onerror=_log_deletion_error)
def size(self) -> int:
"""Returns the size of the data that this track entails.
@@ -343,4 +379,39 @@ class UserDataDir:
return self.path / "tilehunt.sqlite"
-__all__ = ["generate_filename", "DataManager", "TrackDataDir", "UserDataDir"]
+class JourneyDataDir:
+ """Manager for a single journey's data."""
+
+ def __init__(self, journey_id: int, path: Path):
+ self.journey_id = journey_id
+ self.path = path
+
+ def purge(self):
+ """Purge all data pertaining to the journey.
+
+ This function logs errors but raises no exception, as such it can
+ always be used to clean up after a track.
+ """
+ if self.path.is_dir():
+ shutil.rmtree(self.path, ignore_errors=False, onerror=_log_deletion_error)
+
+ def preview_path(self) -> Path:
+ """Gets the path to the "preview image".
+
+ :return: The path to the preview image.
+ """
+ return self.path / "preview.png"
+
+ def set_preview(self, data: bytes):
+ """Sets the preview image to the given data.
+
+ :param data: The data of the preview image.
+ """
+ self.preview_path().write_bytes(data)
+
+ def remove_preview(self):
+ """Deletes the preview image."""
+ self.preview_path().unlink()
+
+
+__all__ = ["generate_filename", "DataManager", "TrackDataDir", "UserDataDir", "JourneyDataDir"]
diff --git a/fietsboek/views/journey.py b/fietsboek/views/journey.py
index 60ed433..8621f4a 100644
--- a/fietsboek/views/journey.py
+++ b/fietsboek/views/journey.py
@@ -10,6 +10,7 @@ from sqlalchemy import select
from sqlalchemy.orm import aliased
from .. import trackmap, util
+from ..data import JourneyDataDir
from ..models.journey import Journey, Visibility
from ..models.track import Track, TrackWithMetadata
from ..models import User
@@ -62,7 +63,14 @@ def journey_gpx(request: Request):
@view_config(route_name="journey-map", http_cache=3600, permission="journey.view")
def journey_map(request: Request):
- journey = request.context
+ journey: Journey = request.context
+ journey_data: JourneyDataDir = request.data_manager.open_journey(journey.id)
+ preview_path = journey_data.preview_path()
+
+ if preview_path.exists():
+ response = Response(preview_path.read_bytes(), content_type="image/png")
+ response.md5_etag()
+ return response
loader: ITileRequester = request.registry.getUtility(ITileRequester)
layer = request.config.public_tile_layers()[0]
@@ -73,6 +81,10 @@ def journey_map(request: Request):
track_image.save(imageio, "png")
tile_data = imageio.getvalue()
+ if not preview_path.exists():
+ LOGGER.debug("Setting preview at %s", preview_path)
+ journey_data.set_preview(tile_data)
+
response = Response(tile_data, content_type="image/png")
response.md5_etag()
return response
@@ -108,6 +120,8 @@ def do_journey_new(request: Request):
track_ids = _extract_valid_tracks(request)
journey.set_track_ids(track_ids)
+ request.data_manager.initialize_journey(journey.id)
+
return HTTPFound(request.route_url("journey-details", journey_id=journey.id))
@@ -130,6 +144,7 @@ def journey_edit(request: Request):
)
def do_journey_edit(request: Request):
journey: Journey = request.context
+ request.data_manager.open_journey(journey.id).remove_preview()
journey.title = request.params.get("journeyTitle")
journey.description = request.params.get("journeyDescription")
@@ -182,6 +197,7 @@ def _extract_valid_tracks(request: Request) -> list[int]:
)
def do_journey_delete(request: Request):
journey: Journey = request.context
+ request.data_manager.open_journey(journey.id).purge()
request.dbsession.delete(journey)
request.session.flash(request.localizer.translate(_("journeys.deleted")))
return HTTPFound(request.route_url("journey-list"))