mirror of https://github.com/gmbrax/Pilgrim.git
Merge branch 'development' into fix/binding-not-working
This commit is contained in:
commit
42c5471ba7
|
|
@ -1,47 +1,18 @@
|
|||
from pilgrim.database import Database
|
||||
from pilgrim.service.servicemanager import ServiceManager
|
||||
from pilgrim.ui.ui import UIApp
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
from pilgrim.utils import DirectoryManager
|
||||
|
||||
|
||||
class Application:
|
||||
def __init__(self):
|
||||
self.config_dir = self._ensure_config_directory()
|
||||
self.config_dir = DirectoryManager.get_config_directory()
|
||||
self.database = Database()
|
||||
session = self.database.session()
|
||||
session_manager = ServiceManager()
|
||||
session_manager.set_session(session)
|
||||
self.ui = UIApp(session_manager)
|
||||
|
||||
def _ensure_config_directory(self) -> Path:
|
||||
"""
|
||||
Ensures the ~/.pilgrim directory exists and has the correct permissions.
|
||||
Creates it if it doesn't exist.
|
||||
Returns the Path object for the config directory.
|
||||
"""
|
||||
home = Path.home()
|
||||
config_dir = home / ".pilgrim"
|
||||
|
||||
try:
|
||||
# Create directory if it doesn't exist
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Ensure correct permissions (rwx for user only)
|
||||
os.chmod(config_dir, 0o700)
|
||||
|
||||
# Create an empty .gitignore if it doesn't exist
|
||||
gitignore = config_dir / ".gitignore"
|
||||
if not gitignore.exists():
|
||||
gitignore.write_text("*\n")
|
||||
|
||||
return config_dir
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error setting up Pilgrim configuration directory: {str(e)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def run(self):
|
||||
self.database.create()
|
||||
self.ui.run()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from typing import Any
|
||||
|
||||
from pilgrim.models.photo_in_entry import photo_entry_association
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pilgrim.models.photo_in_entry import photo_entry_association
|
||||
from ..database import Base
|
||||
|
||||
|
||||
|
|
@ -18,6 +18,8 @@ class Entry(Base):
|
|||
secondary=photo_entry_association,
|
||||
back_populates="entries")
|
||||
fk_travel_diary_id = Column(Integer, ForeignKey("travel_diaries.id"), nullable=False)
|
||||
travel_diary = relationship("TravelDiary", back_populates="entries")
|
||||
|
||||
def __init__(self, title: str, text: str, date: str, travel_diary_id: int, **kw: Any):
|
||||
super().__init__(**kw)
|
||||
self.title = title
|
||||
|
|
|
|||
|
|
@ -1,14 +1,26 @@
|
|||
from typing import Any
|
||||
|
||||
from sqlalchemy import Column, String, Integer
|
||||
from sqlalchemy import Column, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from ..database import Base
|
||||
from .. import database
|
||||
|
||||
class TravelDiary(Base):
|
||||
|
||||
class TravelDiary(database.Base):
|
||||
__tablename__ = "travel_diaries"
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
name = Column(String, nullable=False)
|
||||
directory_name = Column(String, nullable=False, unique=True)
|
||||
entries = relationship("Entry", back_populates="travel_diary", cascade="all, delete-orphan")
|
||||
|
||||
def __init__(self, name: str, **kw: Any):
|
||||
__table_args__ = (
|
||||
UniqueConstraint('directory_name', name='uq_travel_diary_directory_name'),
|
||||
)
|
||||
|
||||
def __init__(self, name: str, directory_name: str = None, **kw: Any):
|
||||
super().__init__(**kw)
|
||||
self.name = name
|
||||
self.directory_name = directory_name # Será definido pelo service
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TravelDiary(id={self.id}, name='{self.name}', directory_name='{self.directory_name}')>"
|
||||
|
|
|
|||
|
|
@ -1,27 +1,77 @@
|
|||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
|
||||
|
||||
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
|
||||
def _hash_file(self,filepath):
|
||||
|
||||
def _hash_file(self, filepath: Path) -> str:
|
||||
"""Calculate 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 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
|
||||
|
||||
# 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:
|
||||
|
|
@ -29,19 +79,23 @@ class PhotoService:
|
|||
except ValueError:
|
||||
addition_date = None
|
||||
|
||||
# Calculate hash from the copied file
|
||||
photo_hash = self._hash_file(copied_path)
|
||||
|
||||
new_photo = Photo(
|
||||
filepath=filepath,
|
||||
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=self._hash_file(filepath)
|
||||
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)
|
||||
|
||||
|
|
@ -51,15 +105,30 @@ class PhotoService:
|
|||
def update(self, photo_src: Photo, photo_dst: Photo) -> Photo | None:
|
||||
original: Photo = self.read_by_id(photo_src.id)
|
||||
if original:
|
||||
original.filepath = photo_dst.filepath
|
||||
# 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
|
||||
original.photo_hash = original.photo_hash
|
||||
|
||||
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
|
||||
|
|
@ -78,9 +147,13 @@ class PhotoService:
|
|||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,132 @@
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from pilgrim.utils import DirectoryManager
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from ..models.travel_diary import TravelDiary
|
||||
import asyncio
|
||||
|
||||
|
||||
class TravelDiaryService:
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
def _sanitize_directory_name(self, name: str) -> str:
|
||||
"""
|
||||
Sanitizes a diary name for use as a directory name.
|
||||
- Removes special characters
|
||||
- Replaces spaces with underscores
|
||||
- Ensures name is unique by adding a suffix if needed
|
||||
"""
|
||||
# Remove special characters and replace spaces
|
||||
safe_name = re.sub(r'[^\w\s-]', '', name)
|
||||
safe_name = safe_name.strip().replace(' ', '_').lower()
|
||||
|
||||
# Ensure we have a valid name
|
||||
if not safe_name:
|
||||
safe_name = "unnamed_diary"
|
||||
|
||||
# Check if name is already used in database
|
||||
base_name = safe_name
|
||||
counter = 1
|
||||
while self.session.query(TravelDiary).filter_by(directory_name=safe_name).first() is not None:
|
||||
safe_name = f"{base_name}_{counter}"
|
||||
counter += 1
|
||||
|
||||
return safe_name
|
||||
|
||||
def _get_diary_directory(self, diary: TravelDiary) -> Path:
|
||||
"""Returns the directory path for a diary."""
|
||||
return DirectoryManager.get_diary_directory(diary.directory_name)
|
||||
|
||||
def _get_diary_data_directory(self, diary: TravelDiary) -> Path:
|
||||
"""Returns the data directory path for a diary."""
|
||||
return DirectoryManager.get_diary_data_directory(diary.directory_name)
|
||||
|
||||
def _ensure_diary_directory(self, diary: TravelDiary) -> Path:
|
||||
"""
|
||||
Creates and returns the directory structure for a diary:
|
||||
~/.pilgrim/diaries/{directory_name}/data/
|
||||
"""
|
||||
# Create diary directory
|
||||
diary_dir = self._get_diary_directory(diary)
|
||||
diary_dir.mkdir(exist_ok=True)
|
||||
os.chmod(diary_dir, 0o700)
|
||||
|
||||
# Create data subdirectory
|
||||
data_dir = self._get_diary_data_directory(diary)
|
||||
data_dir.mkdir(exist_ok=True)
|
||||
os.chmod(data_dir, 0o700)
|
||||
|
||||
return data_dir
|
||||
|
||||
def _cleanup_diary_directory(self, diary: TravelDiary):
|
||||
"""Removes the diary directory and all its contents."""
|
||||
diary_dir = self._get_diary_directory(diary)
|
||||
if diary_dir.exists():
|
||||
shutil.rmtree(diary_dir)
|
||||
|
||||
async def async_create(self, name: str):
|
||||
new_travel_diary = TravelDiary(name)
|
||||
# Generate safe directory name
|
||||
directory_name = self._sanitize_directory_name(name)
|
||||
|
||||
# Create diary with directory name
|
||||
new_travel_diary = TravelDiary(name=name, directory_name=directory_name)
|
||||
|
||||
try:
|
||||
self.session.add(new_travel_diary)
|
||||
self.session.commit()
|
||||
self.session.refresh(new_travel_diary)
|
||||
|
||||
# Create directory structure for the new diary
|
||||
self._ensure_diary_directory(new_travel_diary)
|
||||
|
||||
return new_travel_diary
|
||||
except IntegrityError:
|
||||
self.session.rollback()
|
||||
raise ValueError(f"Could not create diary: directory name '{directory_name}' already exists")
|
||||
|
||||
def read_by_id(self, travel_id: int):
|
||||
return self.session.query(TravelDiary).get(travel_id)
|
||||
diary = self.session.query(TravelDiary).get(travel_id)
|
||||
if diary:
|
||||
# Ensure directory exists when reading
|
||||
self._ensure_diary_directory(diary)
|
||||
return diary
|
||||
|
||||
def read_all(self):
|
||||
return self.session.query(TravelDiary).all()
|
||||
diaries = self.session.query(TravelDiary).all()
|
||||
# Ensure directories exist for all diaries
|
||||
for diary in diaries:
|
||||
self._ensure_diary_directory(diary)
|
||||
return diaries
|
||||
|
||||
def update(self, travel_diary_id: int, name: str):
|
||||
original = self.read_by_id(travel_diary_id)
|
||||
if original is not None:
|
||||
try:
|
||||
# Generate new directory name
|
||||
new_directory_name = self._sanitize_directory_name(name)
|
||||
old_directory = self._get_diary_directory(original)
|
||||
|
||||
# Update diary
|
||||
original.name = name
|
||||
original.directory_name = new_directory_name
|
||||
self.session.commit()
|
||||
self.session.refresh(original)
|
||||
|
||||
# Rename directory if it exists
|
||||
new_directory = self._get_diary_directory(original)
|
||||
if old_directory.exists() and old_directory != new_directory:
|
||||
old_directory.rename(new_directory)
|
||||
|
||||
return original
|
||||
except IntegrityError:
|
||||
self.session.rollback()
|
||||
raise ValueError(f"Could not update diary: directory name '{new_directory_name}' already exists")
|
||||
|
||||
return None
|
||||
|
||||
async def async_update(self, travel_diary_id: int, name: str):
|
||||
return self.update(travel_diary_id, name)
|
||||
|
|
@ -32,7 +134,14 @@ class TravelDiaryService:
|
|||
def delete(self, travel_diary_id: TravelDiary):
|
||||
excluded = self.read_by_id(travel_diary_id.id)
|
||||
if excluded is not None:
|
||||
try:
|
||||
# First delete the directory
|
||||
self._cleanup_diary_directory(excluded)
|
||||
# Then delete from database
|
||||
self.session.delete(travel_diary_id)
|
||||
self.session.commit()
|
||||
return excluded
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
raise ValueError(f"Could not delete diary: {str(e)}")
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -329,9 +329,11 @@ class EditEntryScreen(Screen):
|
|||
all_photos = photo_service.read_all()
|
||||
self.cached_photos = [photo for photo in all_photos if photo.fk_travel_diary_id == diary_id]
|
||||
self.cached_photos.sort(key=lambda x: x.id)
|
||||
return self.cached_photos
|
||||
|
||||
except Exception as e:
|
||||
self.notify(f"Error loading photos: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def action_toggle_sidebar(self):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from .directory_manager import DirectoryManager
|
||||
|
||||
__all__ = ['DirectoryManager']
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class DirectoryManager:
|
||||
@staticmethod
|
||||
def get_config_directory() -> Path:
|
||||
"""
|
||||
Get the ~/.pilgrim directory path.
|
||||
Creates it if it doesn't exist.
|
||||
"""
|
||||
home = Path.home()
|
||||
config_dir = home / ".pilgrim"
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
os.chmod(config_dir, 0o700)
|
||||
return config_dir
|
||||
|
||||
@staticmethod
|
||||
def get_diaries_root() -> Path:
|
||||
"""Returns the path to the diaries directory."""
|
||||
diaries_dir = DirectoryManager.get_config_directory() / "diaries"
|
||||
diaries_dir.mkdir(exist_ok=True)
|
||||
os.chmod(diaries_dir, 0o700)
|
||||
return diaries_dir
|
||||
|
||||
@staticmethod
|
||||
def get_diary_directory(directory_name: str) -> Path:
|
||||
"""Returns the directory path for a specific diary."""
|
||||
return DirectoryManager.get_diaries_root() / directory_name
|
||||
|
||||
@staticmethod
|
||||
def get_diary_data_directory(directory_name: str) -> Path:
|
||||
"""Returns the data directory path for a specific diary."""
|
||||
return DirectoryManager.get_diary_directory(directory_name) / "data"
|
||||
|
||||
@staticmethod
|
||||
def get_diary_images_directory(directory_name: str) -> Path:
|
||||
"""Returns the images directory path for a specific diary."""
|
||||
return DirectoryManager.get_diary_data_directory(directory_name) / "images"
|
||||
Loading…
Reference in New Issue