From 3df8add3b623ecc3e29cd490cec4d9710de98324 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 9 Apr 2026 10:14:40 +0200 Subject: [PATCH] fix: stop scan crashing on duplicate file hashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scan.py used scalar_one_or_none() to test whether any other photo shared the same file_hash, but that helper raises MultipleResultsFound the moment 2+ rows match — i.e. exactly the duplicate case it was trying to flag. Every file beyond the second copy bombed out with "Multiple rows were found when one or none was required" and was left in the failed bucket. Replace with a COUNT(*) > 0 check. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/tasks/scan.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/backend/app/tasks/scan.py b/backend/app/tasks/scan.py index 71c1760..97efa54 100644 --- a/backend/app/tasks/scan.py +++ b/backend/app/tasks/scan.py @@ -11,7 +11,7 @@ import json from typing import List, Dict, Optional from celery import shared_task -from sqlalchemy import select +from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession import aiofiles import redis @@ -172,10 +172,21 @@ async def _scan_folder_async(folder_path: str, source_root_id: Optional[str], ta # Calculate file hash for duplicate detection file_hash = await calculate_file_hash(filepath) - # Check for duplicate by hash - duplicate = await session.execute( - select(Photo).where(Photo.file_hash == file_hash) - ) if file_hash else None + # Check for duplicate by hash. We only care + # whether *any* other photo shares this hash, so + # use a count rather than scalar_one_or_none() + # which raises "Multiple rows were found" the + # moment the library has 2+ copies of the same + # file (i.e. exactly the case we're trying to + # flag). + is_dup = False + if file_hash: + dup_count = (await session.execute( + select(func.count(Photo.id)).where( + Photo.file_hash == file_hash + ) + )).scalar() or 0 + is_dup = dup_count > 0 # Create photo entry photo = Photo( @@ -188,7 +199,7 @@ async def _scan_folder_async(folder_path: str, source_root_id: Optional[str], ta file_size=stat.st_size, taken_at=datetime.fromtimestamp(stat.st_mtime), taken_at_source='filesystem', - is_duplicate=bool(duplicate.scalar_one_or_none() if duplicate else False), + is_duplicate=is_dup, processing_status='pending' )