Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion backend/app/database/images.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Standard library imports
import sqlite3
from typing import Any, List, Mapping, Tuple, TypedDict, Union
from typing import Any, List, Mapping, Optional, Tuple, TypedDict, Union

# App-specific imports
from app.config.settings import (
Expand Down Expand Up @@ -82,6 +82,16 @@ def db_create_images_table() -> None:
"""
)

# Create deleted_images table (tombstone)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS deleted_images (
path TEXT PRIMARY KEY,
deleted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)

conn.commit()
conn.close()

Expand Down Expand Up @@ -419,3 +429,75 @@ def db_toggle_image_favourite_status(image_id: str) -> bool:
return False
finally:
conn.close()


def db_get_image_by_id(image_id: str) -> Optional[dict]:
"""
Fetch a single image record by its ID.
"""
conn = _connect()
cursor = conn.cursor()
try:
cursor.execute(
"SELECT id, path, folder_id, thumbnailPath, metadata, isTagged, isFavourite FROM images WHERE id = ?",
(image_id,),
)
row = cursor.fetchone()
if not row:
return None

from app.utils.images import image_util_parse_metadata

(
img_id,
path,
folder_id,
thumbnail_path,
metadata,
is_tagged,
is_favourite,
) = row
return {
"id": img_id,
"path": path,
"folder_id": str(folder_id),
"thumbnailPath": thumbnail_path,
"metadata": image_util_parse_metadata(metadata),
"isTagged": bool(is_tagged),
"isFavourite": bool(is_favourite),
}
finally:
conn.close()
Comment on lines +434 to +470
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle potential None value for folder_id.

If folder_id is None in the database, str(folder_id) will return the string "None" rather than None, which may cause downstream issues or type inconsistencies with other functions like db_get_all_images (line 262) that handle this case explicitly.

Proposed fix
         return {
             "id": img_id,
             "path": path,
-            "folder_id": str(folder_id),
+            "folder_id": str(folder_id) if folder_id is not None else None,
             "thumbnailPath": thumbnail_path,
             "metadata": image_util_parse_metadata(metadata),
             "isTagged": bool(is_tagged),
             "isFavourite": bool(is_favourite),
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def db_get_image_by_id(image_id: str) -> Optional[dict]:
"""
Fetch a single image record by its ID.
"""
conn = _connect()
cursor = conn.cursor()
try:
cursor.execute(
"SELECT id, path, folder_id, thumbnailPath, metadata, isTagged, isFavourite FROM images WHERE id = ?",
(image_id,),
)
row = cursor.fetchone()
if not row:
return None
from app.utils.images import image_util_parse_metadata
(
img_id,
path,
folder_id,
thumbnail_path,
metadata,
is_tagged,
is_favourite,
) = row
return {
"id": img_id,
"path": path,
"folder_id": str(folder_id),
"thumbnailPath": thumbnail_path,
"metadata": image_util_parse_metadata(metadata),
"isTagged": bool(is_tagged),
"isFavourite": bool(is_favourite),
}
finally:
conn.close()
def db_get_image_by_id(image_id: str) -> Optional[dict]:
"""
Fetch a single image record by its ID.
"""
conn = _connect()
cursor = conn.cursor()
try:
cursor.execute(
"SELECT id, path, folder_id, thumbnailPath, metadata, isTagged, isFavourite FROM images WHERE id = ?",
(image_id,),
)
row = cursor.fetchone()
if not row:
return None
from app.utils.images import image_util_parse_metadata
(
img_id,
path,
folder_id,
thumbnail_path,
metadata,
is_tagged,
is_favourite,
) = row
return {
"id": img_id,
"path": path,
"folder_id": str(folder_id) if folder_id is not None else None,
"thumbnailPath": thumbnail_path,
"metadata": image_util_parse_metadata(metadata),
"isTagged": bool(is_tagged),
"isFavourite": bool(is_favourite),
}
finally:
conn.close()
🤖 Prompt for AI Agents
In `@backend/app/database/images.py` around lines 434 - 470, db_get_image_by_id
currently converts folder_id with str(folder_id) which yields the string "None"
when folder_id is NULL; update db_get_image_by_id to mirror db_get_all_images by
returning None for folder_id when the DB value is NULL (e.g., set "folder_id":
str(folder_id) if folder_id is not None else None) so downstream consumers get a
true None instead of the string "None".



def db_add_to_deleted_images(path: str) -> bool:
"""
Record a path in the deleted_images tombstone table.
"""
conn = _connect()
cursor = conn.cursor()
try:
cursor.execute(
"INSERT OR IGNORE INTO deleted_images (path) VALUES (?)",
(path,),
)
conn.commit()
return True
except Exception as e:
logger.error(f"Error adding to deleted_images: {e}")
return False
finally:
conn.close()


def db_is_image_deleted(path: str) -> bool:
"""
Check if a path exists in the deleted_images tombstone table.
"""
conn = _connect()
cursor = conn.cursor()
try:
cursor.execute("SELECT 1 FROM deleted_images WHERE path = ?", (path,))
return cursor.fetchone() is not None
finally:
conn.close()
13 changes: 3 additions & 10 deletions backend/app/logging/setup_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,16 +243,9 @@ def emit(self, record: logging.LogRecord) -> None:
# Create a message that includes the original module in the format
msg = record.getMessage()

record.msg = f"[{module_name}] {msg}"
record.args = ()
# Clear exception / stack info to avoid duplicate traces
record.exc_info = None
record.stack_info = None

root_logger = logging.getLogger()
for handler in root_logger.handlers:
if handler is not self:
handler.handle(record)
# Use the root logger to avoid recursion if the module-specific logger
# is the one we are currently intercepting.
logging.getLogger().log(record.levelno, f"[{module_name}] {msg}")


def configure_uvicorn_logging(component_name: str) -> None:
Expand Down
72 changes: 68 additions & 4 deletions backend/app/routes/images.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from fastapi import APIRouter, HTTPException, Query, status
from typing import List, Optional
from app.database.images import db_get_all_images
from app.schemas.images import ErrorResponse
from app.utils.images import image_util_parse_metadata
from app.database.images import (
db_get_all_images,
db_toggle_image_favourite_status,
db_get_image_by_id,
db_delete_images_by_ids,
db_add_to_deleted_images,
)
from app.schemas.images import ErrorResponse, DeleteImageResponse
from app.utils.images import image_util_parse_metadata, image_util_delete_image_files
from pydantic import BaseModel
from app.database.images import db_toggle_image_favourite_status
from app.logging.setup_logging import get_logger

# Initialize logger
Expand Down Expand Up @@ -128,3 +133,62 @@ class ImageInfoResponse(BaseModel):
isTagged: bool
isFavourite: bool
tags: Optional[List[str]] = None


@router.delete(
"/{image_id}",
response_model=DeleteImageResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
)
def delete_image(image_id: str):
"""
Explicitly delete an image from the library.
Steps:
1. Fetch image record from DB.
2. Add to 'deleted_images' tombstone (prevents re-sync).
3. Delete physical files (original + thumbnail).
4. Delete DB record (triggers cascaded delete for tags/metadata).
"""
try:
logger.info(f"Image deletion requested for ID: {image_id}")

# 1. Fetch image record
image = db_get_image_by_id(image_id)
if not image:
logger.warning(f"Image deletion failed: ID {image_id} not found")
raise HTTPException(status_code=404, detail="Image not found")

original_path = image.get("path")

# 2. Add to tombstone
db_add_to_deleted_images(original_path)

# 3. Delete files
# We do this before DB deletion to have the paths available,
# but if this fails, we still proceed with DB cleanup to keep it consistent.
image_util_delete_image_files(image)

# 4. Delete DB record
# This will trigger ON DELETE CASCADE for faces, album_images, image_classes
db_delete_images_by_ids([image_id])
Comment on lines +161 to +173
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate original_path before tombstoning.

If image.get("path") returns None, passing it to db_add_to_deleted_images will insert a NULL path into the tombstone table, which could cause issues with future lookups or constraint violations.

Proposed fix
         original_path = image.get("path")
+        if not original_path:
+            logger.error(f"Image {image_id} has no path, cannot delete")
+            raise HTTPException(status_code=500, detail="Image has no path")

         # 2. Add to tombstone
         db_add_to_deleted_images(original_path)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
original_path = image.get("path")
# 2. Add to tombstone
db_add_to_deleted_images(original_path)
# 3. Delete files
# We do this before DB deletion to have the paths available,
# but if this fails, we still proceed with DB cleanup to keep it consistent.
image_util_delete_image_files(image)
# 4. Delete DB record
# This will trigger ON DELETE CASCADE for faces, album_images, image_classes
db_delete_images_by_ids([image_id])
original_path = image.get("path")
if not original_path:
logger.error(f"Image {image_id} has no path, cannot delete")
raise HTTPException(status_code=500, detail="Image has no path")
# 2. Add to tombstone
db_add_to_deleted_images(original_path)
# 3. Delete files
# We do this before DB deletion to have the paths available,
# but if this fails, we still proceed with DB cleanup to keep it consistent.
image_util_delete_image_files(image)
# 4. Delete DB record
# This will trigger ON DELETE CASCADE for faces, album_images, image_classes
db_delete_images_by_ids([image_id])
🤖 Prompt for AI Agents
In `@backend/app/routes/images.py` around lines 161 - 173, The code currently
reads original_path = image.get("path") and passes it to
db_add_to_deleted_images without validation; update the block to validate
original_path (non-None and non-empty) before calling db_add_to_deleted_images
so you don't insert NULL/empty paths into the tombstone table—if original_path
is missing, skip the tombstone insert and optionally log or record a warning;
still proceed with image_util_delete_image_files(image) and
db_delete_images_by_ids([image_id]) as before.


logger.info(f"Successfully deleted image {image_id} and path {original_path}")

return DeleteImageResponse(
success=True,
message="Image deleted successfully",
data=image_id,
)

except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting image {image_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ErrorResponse(
success=False,
error="Internal server error",
message=f"Failed to delete image: {str(e)}",
).model_dump(),
)
39 changes: 39 additions & 0 deletions backend/app/utils/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
db_insert_image_classes_batch,
db_get_images_by_folder_ids,
db_delete_images_by_ids,
db_is_image_deleted,
)
from app.models.FaceDetector import FaceDetector
from app.models.ObjectClassifier import ObjectClassifier
Expand Down Expand Up @@ -151,6 +152,11 @@ def image_util_prepare_image_records(
"""
image_records = []
for image_path in image_files:
# Check if image was explicitly deleted before (tombstone)
if db_is_image_deleted(image_path):
logger.debug(f"Skipping tombstoned image: {image_path}")
continue

folder_id = image_util_find_folder_id_for_image(image_path, folder_path_to_id)

if not folder_id:
Expand Down Expand Up @@ -511,3 +517,36 @@ def image_util_parse_metadata(db_metadata: Any) -> Mapping[str, Any]:
return db_metadata

return {}


def image_util_delete_image_files(image: dict) -> bool:
"""
Safely delete original image and its thumbnail from disk.

Args:
image: Image record dictionary containing 'path' and 'thumbnailPath'
"""
try:
original_path = image.get("path")
thumbnail_path = image.get("thumbnailPath")

# Delete original file
if original_path and os.path.exists(original_path):
try:
os.remove(original_path)
logger.info(f"Deleted original image: {original_path}")
except OSError as e:
logger.error(f"Error deleting original image {original_path}: {e}")

# Delete thumbnail
if thumbnail_path and os.path.exists(thumbnail_path):
try:
os.remove(thumbnail_path)
logger.info(f"Deleted thumbnail: {thumbnail_path}")
except OSError as e:
logger.error(f"Error deleting thumbnail {thumbnail_path}: {e}")

return True
except Exception as e:
logger.error(f"Unexpected error in image_util_delete_image_files: {e}")
return False
Comment on lines +522 to +552
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Function always returns True even when file deletion fails.

The function logs errors when os.remove() fails but continues and returns True. This could mask partial failures where the original image is deleted but the thumbnail deletion fails (or vice versa). The docstring also has an incomplete "Returns" section.

🐛 Proposed fix to track deletion success
 def image_util_delete_image_files(image: dict) -> bool:
     """
     Safely delete original image and its thumbnail from disk.
 
     Args:
         image: Image record dictionary containing 'path' and 'thumbnailPath'
+
+    Returns:
+        bool: True if all deletions succeeded, False on any error
     """
     try:
         original_path = image.get("path")
         thumbnail_path = image.get("thumbnailPath")
+        success = True
 
         # Delete original file
         if original_path and os.path.exists(original_path):
             try:
                 os.remove(original_path)
                 logger.info(f"Deleted original image: {original_path}")
             except OSError as e:
                 logger.error(f"Error deleting original image {original_path}: {e}")
+                success = False
 
         # Delete thumbnail
         if thumbnail_path and os.path.exists(thumbnail_path):
             try:
                 os.remove(thumbnail_path)
                 logger.info(f"Deleted thumbnail: {thumbnail_path}")
             except OSError as e:
                 logger.error(f"Error deleting thumbnail {thumbnail_path}: {e}")
+                success = False
 
-        return True
+        return success
     except Exception as e:
         logger.error(f"Unexpected error in image_util_delete_image_files: {e}")
         return False

121 changes: 121 additions & 0 deletions backend/tests/test_image_deletion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import os
import pytest
from fastapi.testclient import TestClient
import sys
from pathlib import Path

# Add backend to path to allow imports and find main.py
sys.path.append(str(Path(__file__).parent.parent))

from main import app
from app.database.images import db_get_image_by_id, db_is_image_deleted, db_bulk_insert_images, db_create_images_table
from app.config.settings import DATABASE_PATH
import sqlite3
import uuid
import json

client = TestClient(app)

@pytest.fixture(autouse=True)
def setup_db():
# Make sure images table (and deleted_images) is created
db_create_images_table()

# Use config-defined DATABASE_PATH
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
# Ensure folders table exists (might already be created by conftest)
cursor.execute("""
CREATE TABLE IF NOT EXISTS folders (
folder_id TEXT PRIMARY KEY,
folder_path TEXT UNIQUE
)
""")
# Insert a fake folder for FK constraints
cursor.execute("INSERT OR IGNORE INTO folders (folder_id, folder_path) VALUES ('1', '/fake/path')")
conn.commit()
conn.close()

@pytest.fixture
def mock_image(tmp_path):
# Setup: Create a fake image file and a fake thumbnail
img_dir = tmp_path / "images"
img_dir.mkdir()
img_path = img_dir / "test_image.jpg"
img_path.write_bytes(b"fake image data")

thumb_dir = tmp_path / "thumbnails"
thumb_dir.mkdir()
thumb_path = thumb_dir / "test_thumb.jpg"
thumb_path.write_bytes(b"fake thumb data")

Comment on lines +44 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fake image data may cause test failures.

The test writes b"fake image data" which is not valid image data. The image_util_prepare_image_records function calls image_util_generate_thumbnail, which uses PIL.Image.open(). This will fail for non-image files. The test_tombstone_prevents_indexing test may incorrectly pass because thumbnail generation fails, not because of the tombstone check.

🐛 Proposed fix using a minimal valid JPEG
+# Minimal valid 1x1 JPEG (red pixel)
+MINIMAL_JPEG = bytes([
+    0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
+    0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43,
+    0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09,
+    0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12,
+    0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
+    0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29,
+    0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32,
+    0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01,
+    0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00,
+    0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+    0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03,
+    0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
+    0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
+    0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08,
+    0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72,
+    0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
+    0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45,
+    0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
+    0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
+    0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
+    0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3,
+    0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6,
+    0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9,
+    0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
+    0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4,
+    0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
+    0x00, 0x00, 0x3F, 0x00, 0xFB, 0xD5, 0xDB, 0x20, 0xA8, 0xA0, 0x02, 0x80,
+    0x0A, 0x00, 0xFF, 0xD9
+])
+
 `@pytest.fixture`
 def mock_image(tmp_path):
     # Setup: Create a fake image file and a fake thumbnail
     img_dir = tmp_path / "images"
     img_dir.mkdir()
     img_path = img_dir / "test_image.jpg"
-    img_path.write_bytes(b"fake image data")
+    img_path.write_bytes(MINIMAL_JPEG)
     
     thumb_dir = tmp_path / "thumbnails"
     thumb_dir.mkdir()
     thumb_path = thumb_dir / "test_thumb.jpg"
-    thumb_path.write_bytes(b"fake thumb data")
+    thumb_path.write_bytes(MINIMAL_JPEG)

Alternatively, use PIL to generate a valid image:

from PIL import Image as PILImage
import io

# In fixture:
img = PILImage.new('RGB', (1, 1), color='red')
buffer = io.BytesIO()
img.save(buffer, format='JPEG')
img_path.write_bytes(buffer.getvalue())
🤖 Prompt for AI Agents
In `@backend/tests/test_image_deletion.py` around lines 44 - 51, The test writes
non-image bytes which causes PIL.Image.open() in image_util_generate_thumbnail
(called by image_util_prepare_image_records) to fail and masks the real
tombstone behavior; replace the raw b"fake image data" writes in the fixture
used by test_tombstone_prevents_indexing with a minimal valid JPEG/PNG byte
payload (create a 1x1 image via PIL.Image.new and save it into an in-memory
buffer, then write buffer.getvalue() to both img_path and thumb_path) and add
the necessary PIL import to the test so thumbnail generation succeeds and the
tombstone logic is actually exercised.

image_id = str(uuid.uuid4())
image_record = {
"id": image_id,
"path": str(img_path),
"folder_id": 1,
"thumbnailPath": str(thumb_path),
"metadata": json.dumps({
"name": "test_image.jpg",
"width": 100,
"height": 100,
"file_size": 100,
"file_location": str(img_path),
"item_type": "image/jpeg"
}),
"isTagged": False
}

# Insert into DB
db_bulk_insert_images([image_record])

return image_record

def test_delete_image_success(mock_image):
image_id = mock_image["id"]
img_path = mock_image["path"]
thumb_path = mock_image["thumbnailPath"]

# 1. Verify existence before deletion
assert os.path.exists(img_path)
assert os.path.exists(thumb_path)
assert db_get_image_by_id(image_id) is not None

# 2. Call DELETE API
response = client.delete(f"/images/{image_id}")
assert response.status_code == 200
data = response.json()
assert data["success"] is True

# 3. Verify deletion
assert not os.path.exists(img_path)
assert not os.path.exists(thumb_path)
assert db_get_image_by_id(image_id) is None
assert db_is_image_deleted(img_path) is True

def test_delete_image_not_found():
response = client.delete("/images/non-existent-id")
assert response.status_code == 404

def test_tombstone_prevents_indexing(mock_image):
image_id = mock_image["id"]
img_path = mock_image["path"]

# Delete the image first
client.delete(f"/images/{image_id}")
assert db_is_image_deleted(img_path) is True

# Re-create the file manually
os.makedirs(os.path.dirname(img_path), exist_ok=True)
with open(img_path, "wb") as f:
f.write(b"recreated file")

# Try to re-index it via utilities
from app.utils.images import image_util_prepare_image_records

# Mocking folder_path_to_id
folder_path = str(Path(img_path).parent)
records = image_util_prepare_image_records([str(img_path)], {folder_path: 1})

# It should skip it because of the tombstone
assert len(records) == 0
Loading