From e4abad69c18964a4e87560ceecd097daa07f00e9 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 14 Dec 2022 22:03:59 +0100 Subject: rework DataManager Since we plan on moving the GPX data (and the original copy) into the data directory, it makes more sense to have a per-track "handle" instead of having all methods of DataManager take a track_id parameter. --- fietsboek/data.py | 81 ++++++++++++++++++++++++++++++++--------------- fietsboek/views/detail.py | 11 +++++-- fietsboek/views/edit.py | 17 +++++++--- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/fietsboek/data.py b/fietsboek/data.py index 94c6c11..83cabed 100644 --- a/fietsboek/data.py +++ b/fietsboek/data.py @@ -50,58 +50,90 @@ class DataManager: def _track_data_dir(self, track_id): return self.data_dir / "tracks" / str(track_id) - def images(self, track_id: int) -> List[str]: - """Returns a list of images that belong to a track. + def initialize(self, track_id: int) -> "TrackDataDir": + """Creates the data directory for a track. - :param track_id: Numerical ID of the track. - :return: A list of image IDs. + :raises FileExistsError: If the directory already exists. + :param track_id: ID of the track. + :return: The manager that can be used to manage this track's data. """ - image_dir = self._track_data_dir(track_id) / "images" - if not image_dir.exists(): - return [] - images = [] - for image in image_dir.iterdir(): - images.append(image.name) - return images + path = self._track_data_dir(track_id) + path.mkdir(parents=True) + return TrackDataDir(track_id, path) def purge(self, track_id: int): - """Purge all data pertaining to the given track. + """Forcefully purges all data from the given track. This function logs errors but raises no exception, as such it can always be used to clean up after a track. + """ + TrackDataDir(track_id, self._track_data_dir(track_id)).purge() - :param track_id: The ID of the track. + def open(self, track_id: int) -> "TrackDataDir": + """Opens a track's data directory. + + :raises FileNotFoundError: If the track directory does not exist. + :param track_id: ID of the track. + :return: The manager that can be used to manage this track's data. + """ + path = self._track_data_dir(track_id) + if not path.is_dir(): + raise FileNotFoundError(f"The path {path} is not a directory") from None + return TrackDataDir(track_id, path) + + +class TrackDataDir: + """Manager for a single track's data.""" + + def __init__(self, track_id: int, path: Path): + self.track_id: int = track_id + self.path: Path = path + + def purge(self): + """Purge all data pertaining to the track. + + This function logs errors but raises no exception, as such it can + always be used to clean up after a track. """ def log_error(_, path, exc_info): LOGGER.warning("Failed to remove %s", path, exc_info=exc_info) - path = self._track_data_dir(track_id) - if path.is_dir(): - shutil.rmtree(path, ignore_errors=False, onerror=log_error) + if self.path.is_dir(): + shutil.rmtree(self.path, ignore_errors=False, onerror=log_error) - def image_path(self, track_id: int, image_id: str) -> Path: + def images(self) -> List[str]: + """Returns a list of images that belong to the track. + + :param track_id: Numerical ID of the track. + :return: A list of image IDs. + """ + image_dir = self.path / "images" + if not image_dir.exists(): + return [] + images = [image.name for image in image_dir.iterdir()] + return images + + def image_path(self, image_id: str) -> Path: """Returns a path to a saved image. :raises FileNotFoundError: If the given image could not be found. - :param track_id: ID of the track. :param image_id: ID of the image. :return: A path pointing to the requested image. """ - image = self._track_data_dir(track_id) / "images" / secure_filename(image_id) + image = self.path / "images" / secure_filename(image_id) if not image.exists(): raise FileNotFoundError("The requested image does not exist") return image - def add_image(self, track_id: int, image: BinaryIO, filename: Optional[str] = None) -> str: + def add_image(self, image: BinaryIO, filename: Optional[str] = None) -> str: """Saves an image to a track. - :param track_id: ID of the track. :param image: The image, as a file-like object to read from. :param filename: The image's original filename. :return: The ID of the saved image. """ - image_dir = self._track_data_dir(track_id) / "images" + image_dir = self.path / "images" image_dir.mkdir(parents=True, exist_ok=True) filename = generate_filename(filename) @@ -111,16 +143,15 @@ class DataManager: return filename - def delete_image(self, track_id: int, image_id: str): + def delete_image(self, image_id: str): """Deletes an image from a track. :raises FileNotFoundError: If the given image could not be found. - :param track_id: ID of the track. :param image_id: ID of the image. """ # Be sure to not delete anything else than the image file image_id = secure_filename(image_id) if "/" in image_id or "\\" in image_id: return - path = self.image_path(track_id, image_id) + path = self.image_path(image_id) path.unlink() diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index 485bad3..e602780 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -26,8 +26,15 @@ def details(request): description = util.safe_markdown(track.description) show_edit_link = track.owner == request.identity + on_disk_images = [] + try: + manager = request.data_manager.open(track.id) + on_disk_images.extend(manager.images()) + except FileNotFoundError: + pass + images = [] - for image_name in request.data_manager.images(track.id): + for image_name in on_disk_images: query = [] if "secret" in request.GET: query.append(("secret", request.GET["secret"])) @@ -138,7 +145,7 @@ def image(request): """ track = request.context try: - image_path = request.data_manager.image_path(track.id, request.matchdict["image_name"]) + image_path = request.data_manager.open(track.id).image_path(request.matchdict["image_name"]) except FileNotFoundError: return HTTPNotFound() else: diff --git a/fietsboek/views/edit.py b/fietsboek/views/edit.py index 003f7c7..a26452d 100644 --- a/fietsboek/views/edit.py +++ b/fietsboek/views/edit.py @@ -36,8 +36,13 @@ def edit(request): badges = request.dbsession.execute(select(models.Badge)).scalars() badges = [(badge in track.badges, badge) for badge in badges] + on_disk_images = [] + try: + on_disk_images = request.data_manager.open(track.id).images() + except FileNotFoundError: + pass images = [] - for image in request.data_manager.images(track.id): + for image in on_disk_images: metadata = request.dbsession.execute( select(models.ImageMetadata).filter_by(track=track, image_name=image) ).scalar_one_or_none() @@ -106,9 +111,13 @@ def edit_images(request, track): :type track: fietsboek.models.track.Track """ + try: + manager = request.data_manager.open(track.id) + except FileNotFoundError: + manager = request.data_manager.initialize(track.id) # Delete requested images for image in request.params.getall("delete-image[]"): - request.data_manager.delete_image(track.id, image) + manager.delete_image(image) image_meta = request.dbsession.execute( select(models.ImageMetadata).filter_by(track_id=track.id, image_name=image) ).scalar_one_or_none() @@ -127,14 +136,14 @@ def edit_images(request, track): continue upload_id = match.group(1) - image_name = request.data_manager.add_image(track.id, image.file, image.filename) + image_name = manager.add_image(image.file, image.filename) image_meta = models.ImageMetadata(track=track, image_name=image_name) image_meta.description = request.params.get(f"image-description[{upload_id}]", "") request.dbsession.add(image_meta) LOGGER.debug("Uploaded image %s %s", track.id, image_name) set_descriptions.add(upload_id) - images = request.data_manager.images(track.id) + images = manager.images() # Set image descriptions for param_name, description in request.params.items(): match = re.match("image-description\\[(.+)\\]$", param_name) -- cgit v1.2.3