fix(scan): only resurrect discards when file changed; add Saved toast

The scan_folder resurrect path was unflagging every discarded photo on
every backend boot. start_initial_scan fires scan_all_source_roots on
container start, which fans out scan_folder for every source root,
which walked every file and silently set is_discarded=False on rows
whose file was still on disk -- so every deploy wiped the user's
discard decisions. Today's series of resurrect log lines for
admin/Photos came from that path, not from any actual user re-upload.

Gate the resurrect on os.path.getmtime(file) > discarded_at so the
WebDAV-DELETE-then-re-upload and trashbin-restore-via-PUT-overwrite
flows still trigger (those rewrite the file and bump mtime), but
routine sweeps respect the user's intent. Rows with discarded_at NULL
(legacy) fall through to skipped -- preserve intent over cleanup.

While there: add a Saved toast to the single-photo updateMutation.
The previous patch made cache writes synchronous, which removed the
visible save delay but also removed any signal that the change was
actually persisted. Toast picks a per-field label from the patched
keys (Title updated / Date updated / etc.) and falls back to a count
for multi-field saves.
This commit is contained in:
Claudio
2026-05-11 22:29:25 +02:00
parent abe5c1ec6b
commit 09c12ea35b
2 changed files with 75 additions and 14 deletions

View File

@@ -206,21 +206,50 @@ async def _scan_folder_async(folder_path: str, source_root_id: Optional[str], ta
)
existing_photo = existing.scalar_one_or_none()
if existing_photo is not None:
# Resurrect a previously-discarded row if the
# file is back on disk. WebDAV DELETE +
# re-upload, trashbin restore via PUT-overwrite,
# and any "I removed it then put it back" flow
# all land here. Re-queue extract_metadata in
# case the bytes changed (different EXIF, new
# nextcloud_fileid).
# Resurrect a previously-discarded row only
# when the file's mtime is newer than
# discarded_at. Bare existence on disk isn't
# proof the user changed their mind: every
# backend boot fires scan_all_source_roots,
# which used to walk every file and silently
# un-discard the lot. The mtime check still
# covers the legitimate flows (WebDAV DELETE
# + re-upload, trashbin restore via PUT-
# overwrite, any "I removed it then put it
# back") because those rewrite the file and
# bump mtime past the discard time. Rows
# with discarded_at IS NULL (legacy) are
# left alone — preserve user intent over
# best-effort cleanup.
if existing_photo.is_discarded:
existing_photo.is_discarded = False
existing_photo.discarded_at = None
await session.commit()
logger.info(
f"Resurrected discarded photo on rescan: {filepath}"
discarded_at = existing_photo.discarded_at
try:
mtime = os.path.getmtime(filepath)
except OSError:
mtime = 0.0
file_modified_after_discard = (
discarded_at is not None
and mtime
> discarded_at.replace(
tzinfo=timezone.utc
).timestamp()
)
extract_metadata.delay(existing_photo.id)
if file_modified_after_discard:
existing_photo.is_discarded = False
existing_photo.discarded_at = None
await session.commit()
logger.info(
f"Resurrected discarded photo "
f"(file modified after discard): "
f"{filepath}"
)
extract_metadata.delay(existing_photo.id)
else:
logger.debug(
f"Skipping discarded photo "
f"(file unchanged since discard): "
f"{filepath}"
)
else:
logger.debug(f"File already indexed: {filepath}")
processed_files += 1