import hashlib import os import shutil from datetime import datetime from pathlib import Path from typing import List from pilgrim.models.photo import Photo from pilgrim.models.travel_diary import TravelDiary from pilgrim.utils import DirectoryManager class PhotoService: def __init__(self, session): self.session = session @staticmethod def hash_file(filepath: Path) -> str: """Calculate the hash of a file using SHA3-384.""" hash_func = hashlib.new('sha3_384') with open(filepath, 'rb') as f: while chunk := f.read(8192): hash_func.update(chunk) return hash_func.hexdigest() def _ensure_images_directory(self, travel_diary: TravelDiary) -> Path: """ Ensures the images directory exists for the given diary. Returns the path to the images directory. """ images_dir = DirectoryManager.get_diary_images_directory(travel_diary.directory_name) if not images_dir.exists(): images_dir.mkdir(parents=True) os.chmod(images_dir, 0o700) # Ensure correct permissions return images_dir def _copy_photo_to_diary(self, source_path: Path, travel_diary: TravelDiary) -> Path: """ Copies a photo to the diary's images directory. Returns the path to the copied file. """ images_dir = self._ensure_images_directory(travel_diary) # Get original filename and extension original_name = Path(source_path).name # Create destination path dest_path = images_dir / original_name # If file with same name exists, add a number counter = 1 while dest_path.exists(): name_parts = original_name.rsplit('.', 1) if len(name_parts) > 1: dest_path = images_dir / f"{name_parts[0]}_{counter}.{name_parts[1]}" else: dest_path = images_dir / f"{original_name}_{counter}" counter += 1 # Copy the file shutil.copy2(source_path, dest_path) os.chmod(dest_path, 0o600) # Read/write for owner only return dest_path def check_photo_by_hash(self, photohash:str, traveldiaryid:int): photo = (self.session.query(Photo).filter(Photo.photo_hash == photohash,Photo.fk_travel_diary_id == traveldiaryid) .first()) return photo def create(self, filepath: Path, name: str, travel_diary_id: int, caption=None, addition_date=None) -> Photo | None: travel_diary = self.session.query(TravelDiary).filter(TravelDiary.id == travel_diary_id).first() if not travel_diary: return None photo_hash = self.hash_file(filepath) if self.check_photo_by_hash(photo_hash, travel_diary_id): return None # Copy photo to diary's images directory copied_path = self._copy_photo_to_diary(filepath, travel_diary) # Convert addition_date string to datetime if needed if isinstance(addition_date, str): try: addition_date = datetime.strptime(addition_date, "%Y-%m-%d %H:%M:%S") except ValueError: addition_date = None new_photo = Photo( filepath=str(copied_path), # Store the path to the copied file name=name, caption=caption, fk_travel_diary_id=travel_diary_id, addition_date=addition_date, photo_hash=photo_hash ) self.session.add(new_photo) self.session.commit() self.session.refresh(new_photo) return new_photo def read_by_id(self, photo_id:int) -> Photo: return self.session.query(Photo).get(photo_id) def read_all(self) -> List[Photo]: return self.session.query(Photo).all() def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None: original: Photo = self.read_by_id(photo_src.id) if original: # If filepath changed, need to copy new file if str(photo_dst.filepath) != str(original.filepath): travel_diary = self.session.query(TravelDiary).filter( TravelDiary.id == original.fk_travel_diary_id).first() if travel_diary: # Copy new photo new_path = self._copy_photo_to_diary(Path(photo_dst.filepath), travel_diary) # Delete old photo if it exists in our images directory old_path = Path(original.filepath) if old_path.exists() and str(DirectoryManager.get_diaries_root()) in str(old_path): old_path.unlink() original.filepath = str(new_path) # Update hash based on the new copied file original.photo_hash = self.hash_file(new_path) original.name = photo_dst.name original.addition_date = photo_dst.addition_date original.caption = photo_dst.caption if photo_dst.entries and len(photo_dst.entries) > 0: if original.entries is None: original.entries = [] original.entries = photo_dst.entries # Replace instead of extend self.session.commit() self.session.refresh(original) return original return None def delete(self, photo_src: Photo) -> Photo | None: excluded = self.read_by_id(photo_src.id) if excluded: # Store photo data before deletion deleted_photo = Photo( filepath=excluded.filepath, name=excluded.name, addition_date=excluded.addition_date, caption=excluded.caption, fk_travel_diary_id=excluded.fk_travel_diary_id, id=excluded.id, photo_hash=excluded.photo_hash, entries=excluded.entries, ) # Delete the physical file if it exists in our images directory file_path = Path(excluded.filepath) if file_path.exists() and str(DirectoryManager.get_diaries_root()) in str(file_path): file_path.unlink() self.session.delete(excluded) self.session.commit() return deleted_photo return None