Skip to content
19 changes: 19 additions & 0 deletions backend/app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import logging
import os
import sys
import secrets
import tempfile

from platformdirs import user_data_dir

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -41,6 +44,22 @@
THUMBNAIL_IMAGES_PATH = os.path.join(user_data_dir("PictoPy"), "thumbnails")
IMAGES_PATH = "./images"

# Generate session token for authenticated shutdown.
SHUTDOWN_TOKEN: str = secrets.token_hex(32)
SHUTDOWN_TOKEN_FILE: str = os.path.join(tempfile.gettempdir(), "pictopy_shutdown.token")

# Write token with owner-only permissions (0o600).
try:
_fd = os.open(SHUTDOWN_TOKEN_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(_fd, "w") as _f:
_f.write(SHUTDOWN_TOKEN)
# Enforce permissions.
os.chmod(SHUTDOWN_TOKEN_FILE, 0o600)
except OSError as e:
logger.fatal(f"Failed to write shutdown token to {SHUTDOWN_TOKEN_FILE}: {e}")
logger.fatal("Cannot start backend securely. Exiting.")
sys.exit(1)


def _get_env_float(
name: str,
Expand Down
12 changes: 4 additions & 8 deletions backend/app/database/albums.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,15 @@ def db_create_albums_table() -> None:
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS albums (
album_id TEXT PRIMARY KEY,
album_name TEXT UNIQUE,
description TEXT,
is_hidden BOOLEAN DEFAULT 0,
password_hash TEXT
)
"""
)
""")
conn.commit()
finally:
if conn is not None:
Expand All @@ -31,17 +29,15 @@ def db_create_album_images_table() -> None:
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS album_images (
album_id TEXT,
image_id TEXT,
PRIMARY KEY (album_id, image_id),
FOREIGN KEY (album_id) REFERENCES albums(album_id) ON DELETE CASCADE,
FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE
)
"""
)
""")
conn.commit()
finally:
if conn is not None:
Expand Down
12 changes: 4 additions & 8 deletions backend/app/database/face_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@ def db_create_clusters_table() -> None:
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS face_clusters (
cluster_id TEXT PRIMARY KEY,
cluster_name TEXT,
face_image_base64 TEXT
)
"""
)
""")
conn.commit()
finally:
if conn is not None:
Expand Down Expand Up @@ -245,8 +243,7 @@ def db_get_all_clusters_with_face_counts() -> (
cursor = conn.cursor()

try:
cursor.execute(
"""
cursor.execute("""
SELECT
fc.cluster_id,
fc.cluster_name,
Expand All @@ -256,8 +253,7 @@ def db_get_all_clusters_with_face_counts() -> (
LEFT JOIN faces f ON fc.cluster_id = f.cluster_id
GROUP BY fc.cluster_id, fc.cluster_name, fc.face_image_base64
ORDER BY fc.cluster_id
"""
)
""")

rows = cursor.fetchall()

Expand Down
24 changes: 8 additions & 16 deletions backend/app/database/faces.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ def db_create_faces_table() -> None:
conn = sqlite3.connect(DATABASE_PATH)
conn.execute("PRAGMA foreign_keys = ON")
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS faces (
face_id INTEGER PRIMARY KEY AUTOINCREMENT,
image_id TEXT,
Expand All @@ -44,8 +43,7 @@ def db_create_faces_table() -> None:
FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE,
FOREIGN KEY (cluster_id) REFERENCES face_clusters(cluster_id) ON DELETE SET NULL
)
"""
)
""")
conn.commit()
finally:
if conn is not None:
Expand Down Expand Up @@ -146,8 +144,7 @@ def get_all_face_embeddings():
cursor = conn.cursor()

try:
cursor.execute(
"""
cursor.execute("""
SELECT
f.embeddings,
f.bbox,
Expand All @@ -162,8 +159,7 @@ def get_all_face_embeddings():
JOIN images i ON f.image_id=i.id
LEFT JOIN image_classes ic ON i.id = ic.image_id
LEFT JOIN mappings m ON ic.class_id = m.class_id
"""
)
""")
results = cursor.fetchall()

from app.utils.images import image_util_parse_metadata
Expand Down Expand Up @@ -256,14 +252,12 @@ def db_get_all_faces_with_cluster_names() -> (
cursor = conn.cursor()

try:
cursor.execute(
"""
cursor.execute("""
SELECT f.face_id, f.embeddings, fc.cluster_name
FROM faces f
LEFT JOIN face_clusters fc ON f.cluster_id = fc.cluster_id
ORDER BY f.face_id
"""
)
""")

rows = cursor.fetchall()

Expand Down Expand Up @@ -353,14 +347,12 @@ def db_get_cluster_mean_embeddings() -> List[Dict[str, Union[str, FaceEmbedding]
cursor = conn.cursor()

try:
cursor.execute(
"""
cursor.execute("""
SELECT f.cluster_id, f.embeddings
FROM faces f
WHERE f.cluster_id IS NOT NULL
ORDER BY f.cluster_id
"""
)
""")

rows = cursor.fetchall()

Expand Down
12 changes: 4 additions & 8 deletions backend/app/database/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ def db_create_folders_table() -> None:
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS folders (
folder_id TEXT PRIMARY KEY,
parent_folder_id TEXT,
Expand All @@ -28,8 +27,7 @@ def db_create_folders_table() -> None:
taggingCompleted BOOLEAN,
FOREIGN KEY (parent_folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE
)
"""
)
""")
conn.commit()
finally:
if conn is not None:
Expand Down Expand Up @@ -406,8 +404,7 @@ def db_get_all_folder_details() -> (
cursor = conn.cursor()

try:
cursor.execute(
"""
cursor.execute("""
SELECT
f.folder_id,
f.folder_path,
Expand All @@ -420,8 +417,7 @@ def db_get_all_folder_details() -> (
LEFT JOIN images i ON f.folder_id = i.folder_id
GROUP BY f.folder_id
ORDER BY f.folder_path
"""
)
""")
return cursor.fetchall()
finally:
conn.close()
Expand Down
30 changes: 10 additions & 20 deletions backend/app/database/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ def db_create_images_table() -> None:
cursor = conn.cursor()

# Create new images table with merged fields including Memories feature columns
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS images (
id TEXT PRIMARY KEY,
path VARCHAR UNIQUE,
Expand All @@ -77,8 +76,7 @@ def db_create_images_table() -> None:
captured_at DATETIME,
FOREIGN KEY (folder_id) REFERENCES folders(folder_id) ON DELETE CASCADE
)
"""
)
""")

# Create indexes for Memories feature queries
cursor.execute("CREATE INDEX IF NOT EXISTS ix_images_latitude ON images(latitude)")
Expand All @@ -93,17 +91,15 @@ def db_create_images_table() -> None:
)

# Create new image_classes junction table
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS image_classes (
image_id TEXT,
class_id INTEGER,
PRIMARY KEY (image_id, class_id),
FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE,
FOREIGN KEY (class_id) REFERENCES mappings(class_id) ON DELETE CASCADE
)
"""
)
""")

conn.commit()
conn.close()
Expand Down Expand Up @@ -265,15 +261,13 @@ def db_get_untagged_images() -> List[UntaggedImageRecord]:
cursor = conn.cursor()

try:
cursor.execute(
"""
cursor.execute("""
SELECT i.id, i.path, i.folder_id, i.thumbnailPath, i.metadata
FROM images i
JOIN folders f ON i.folder_id = f.folder_id
WHERE f.AI_Tagging = TRUE
AND i.isTagged = FALSE
"""
)
""")

results = cursor.fetchall()

Expand Down Expand Up @@ -754,8 +748,7 @@ def db_get_images_with_location() -> List[dict]:
cursor = conn.cursor()

try:
cursor.execute(
"""
cursor.execute("""
SELECT
i.id,
i.path,
Expand All @@ -775,8 +768,7 @@ def db_get_images_with_location() -> List[dict]:
AND i.longitude IS NOT NULL
GROUP BY i.id
ORDER BY i.captured_at DESC
"""
)
""")

results = cursor.fetchall()

Expand Down Expand Up @@ -821,8 +813,7 @@ def db_get_all_images_for_memories() -> List[dict]:
cursor = conn.cursor()

try:
cursor.execute(
"""
cursor.execute("""
SELECT
i.id,
i.path,
Expand All @@ -840,8 +831,7 @@ def db_get_all_images_for_memories() -> List[dict]:
LEFT JOIN mappings m ON ic.class_id = m.class_id
GROUP BY i.id
ORDER BY i.captured_at DESC
"""
)
""")

results = cursor.fetchall()

Expand Down
6 changes: 2 additions & 4 deletions backend/app/database/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ def db_create_metadata_table() -> None:
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS metadata (
metadata TEXT
)
"""
)
""")

# Insert initial row if table is empty
cursor.execute("SELECT COUNT(*) FROM metadata")
Expand Down
6 changes: 2 additions & 4 deletions backend/app/database/yolo_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ def db_create_YOLO_classes_table():
try:
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
cursor.execute(
"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS mappings (
class_id INTEGER PRIMARY KEY,
name VARCHAR NOT NULL
)
"""
)
""")
for class_id, name in enumerate(class_names):
cursor.execute(
"INSERT OR REPLACE INTO mappings (class_id, name) VALUES (?, ?)",
Expand Down
1 change: 0 additions & 1 deletion backend/app/routes/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from app.database.images import db_toggle_image_favourite_status, db_get_image_by_id
from app.logging.setup_logging import get_logger


# Initialize logger
logger = get_logger(__name__)
router = APIRouter()
Expand Down
Loading
Loading