-
Notifications
You must be signed in to change notification settings - Fork 580
Right-Click Context Menu Actions for Images *(Favorite, Copy, View Details)* #1043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
577e63e
85e0aa5
5311ecb
f8e00a6
2b495e8
ebc7229
6d88afe
fdec570
7e4d2eb
4315f5b
f4fa12e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate If 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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: | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Function always returns The function logs errors when 🐛 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 |
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fake image data may cause test failures. The test writes 🐛 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 |
||
| 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle potential
Nonevalue forfolder_id.If
folder_idisNonein the database,str(folder_id)will return the string"None"rather thanNone, which may cause downstream issues or type inconsistencies with other functions likedb_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
🤖 Prompt for AI Agents