Make external file change detection explicit in save pipeline#3026
Make external file change detection explicit in save pipeline#3026iron-prog wants to merge 7 commits intometabrainz:masterfrom
Conversation
|
This change is limited to detection/logging only. |
zas
left a comment
There was a problem hiding this comment.
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.
81b245e to
5356846
Compare
|
@iron-prog ensure all tests pass (ruff) |
3edacc9 to
9a41236
Compare
|
@zas Update on implementation: |
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.") |
There was a problem hiding this comment.
Minor remark, but we don't usually translate log output (log should stay in English to ease parsing and ease reading when shared.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Where is _external_change_count initialized?
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
| elif change: | |
| elif change == ExternalChange.MODIFIED: |
Right?
| if self.state == File.State.ERROR: | ||
| change = self._external_file_change(self.filename) | ||
| if change == ExternalChange.MODIFIED: | ||
| self._loaded_identity = FileIdentity(self.filename) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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)?
Summary
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.
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
These can be explored in a future PR if desired.
Action
Additional actions required: