Skip to content

Make external file change detection explicit in save pipeline#3026

Open
iron-prog wants to merge 7 commits intometabrainz:masterfrom
iron-prog:file-external-change-detection
Open

Make external file change detection explicit in save pipeline#3026
iron-prog wants to merge 7 commits intometabrainz:masterfrom
iron-prog:file-external-change-detection

Conversation

@iron-prog
Copy link
Contributor

@iron-prog iron-prog commented Feb 3, 2026

Summary

  • This is a…
    • Bug fix
    • Feature addition
    • Refactoring
    • Minor / simple change (like a typo)
    • Other
  • Describe this change in 1-2 sentences:
    Make external file change detection explicit in the save pipeline and prevent silent overwrites by aborting the save and marking the file as error when the on-disk file has changed after loading.

Problem

PR #2963 introduced FileIdentity to detect external file changes.
However, when a file was modified or replaced on disk after being loaded, Picard would only log a warning and continue, potentially overwriting external changes silently.
This could lead to unintended data loss without user awareness.

  • JIRA ticket (optional): PICARD-XXX

Solution

This change:
• Reuses FileIdentity to detect external modifications before saving.
• Raises ExternalFileModifiedError if the file was modified or deleted.
• Aborts the save operation.
• Marks the file as ERROR state with a clear message:
“Cannot save file: it was modified externally after being loaded.”
• Ensures no silent overwrite occurs.
• Keeps behavior batch-safe (no blocking dialogs per file).

This keeps the logic deterministic and avoids UI complexity for batch operations.

Limitation

•	No conflict resolution UI is implemented in this PR.
•	When multiple files are modified externally, each file is marked as error individually.
•	No batch policy (“overwrite all”, “reload all”) is included yet.
•	No diff view of tag differences.

These can be explored in a future PR if desired.

Action

Additional actions required:

  • Update Picard documentation (please include a reference to this PR)
  • Other (please specify below)

@iron-prog
Copy link
Contributor Author

This change is limited to detection/logging only.
If this direction looks reasonable, I can follow up with user-visible handling in a separate PR; otherwise I’m happy to adjust or drop this.

@iron-prog iron-prog changed the title File external change detection Make external file change detection explicit in save pipeline Feb 3, 2026
Copy link
Collaborator

@zas zas left a comment

Choose a reason for hiding this comment

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

The main issue is that the PR adds return statements that abort saves when external changes are detected, whereas the original code only logged warnings and continued. This needs to be clearly documented as it's a significant behavioral change. Users will see no UI feedback when saves are silently aborted.

So I think, to be accepted, the user interface should be updated too, and user should be able to choose what he wants when a change was detected.

It seems that's what you suggest in your comment, and I think that's the way to go before we think about merging this.

@iron-prog iron-prog force-pushed the file-external-change-detection branch 2 times, most recently from 81b245e to 5356846 Compare March 2, 2026 02:08
@iron-prog iron-prog requested a review from zas March 2, 2026 04:03
@iron-prog iron-prog requested a review from zas March 2, 2026 16:17
@zas
Copy link
Collaborator

zas commented Mar 3, 2026

@iron-prog ensure all tests pass (ruff)

@iron-prog iron-prog force-pushed the file-external-change-detection branch from 3edacc9 to 9a41236 Compare March 3, 2026 11:13
@iron-prog
Copy link
Contributor Author

@zas Update on implementation:
• Added retry support after external modification
• Added batch summary logging for skipped files
• Simplified error message

picard/file.py Outdated
# Report summary of skipped files (once batch finishes)
if self.tagger._external_change_count > 0 and not self.is_pending():
log.warning(
_("%d files were skipped because they were modified externally.")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor remark, but we don't usually translate log output (log should stay in English to ease parsing and ease reading when shared.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have fixed it ,any other improvement or suggestion

self.state = File.State.ERROR
self.error_append(_("Cannot save file: it was modified externally after loading."))
# Track batch external changes
self.tagger._external_change_count += 1
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where is _external_change_count initialized?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry it's my mistake I have created this in tagger.py in init_tagger_entities which I does not uploaded

picard/file.py Outdated
change = self._external_file_change(old_filename)
if change == ExternalChange.MISSING:
raise FileNotFoundError(f"{old_filename} no longer exists")
elif change:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
elif change:
elif change == ExternalChange.MODIFIED:

Right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for this

if self.state == File.State.ERROR:
change = self._external_file_change(self.filename)
if change == ExternalChange.MODIFIED:
self._loaded_identity = FileIdentity(self.filename)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Between checking for ExternalChange.MODIFIED and creating the new FileIdentity, the file could be modified again. Not sure if that's a real problem though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is indeed a small race window there. Since the retry simply refreshes the file identity before the next save attempt, I think the behavior remains consistent with the current optimistic checks.

picard/file.py Outdated
log.warning(
"%d files were skipped because they were modified externally.", self.tagger._external_change_count
)
self.tagger._external_change_count = 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think there's a logic flaw here:

  • This assumes the last file to finish saving will reset the counter, but with a single-threaded save pool, this might work accidentally
  • If multiple files are saved and some fail with external modifications, the counter might be reset prematurely by a successful save
  • The logic ties the counter to individual file state rather than batch completion

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You’re right — my initial approach incorrectly tied the counter reset to individual file completion instead of batch completion. I see now that this makes the logic fragile even with a single-threaded save pool, because a successful save could reset the counter before all failures are accounted for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@zas if any other changes requred I'm happy to make and if it's good then should I work on a prototype of giving option to user for batch changes (overwrite , skip, reload)?

@iron-prog iron-prog requested a review from zas March 5, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants