diff --git a/changedetectionio/model/__init__.py b/changedetectionio/model/__init__.py index b2bdac08e..70b4a58b3 100644 --- a/changedetectionio/model/__init__.py +++ b/changedetectionio/model/__init__.py @@ -510,7 +510,12 @@ class watch_base(dict): # Save to disk via subclass implementation try: + # Determine entity type from module name (Watch.py -> watch, Tag.py -> tag) + from changedetectionio.model.persistence import _determine_entity_type + entity_type = _determine_entity_type(self.__class__) + filename = f"{entity_type}.json" + self._save_to_disk(data_dict, uuid) - logger.debug(f"Committed {self.__class__.__name__.lower()} {uuid}") + logger.debug(f"Committed {entity_type} {uuid} to {uuid}/{filename}") except Exception as e: logger.error(f"Failed to commit {uuid}: {e}") \ No newline at end of file diff --git a/changedetectionio/store/__init__.py b/changedetectionio/store/__init__.py index cd4798b13..1158b22e9 100644 --- a/changedetectionio/store/__init__.py +++ b/changedetectionio/store/__init__.py @@ -123,10 +123,17 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): self.__data['settings']['application'].update(settings_data['settings']['application']) def _rehydrate_tags(self): - """Rehydrate tag entities from stored data.""" + """Rehydrate tag entities from stored data into Tag objects with restock_diff processor.""" + from ..model import Tag + for uuid, tag in self.__data['settings']['application']['tags'].items(): - self.__data['settings']['application']['tags'][uuid] = self.rehydrate_entity( - uuid, tag, processor_override='restock_diff' + # Force processor to restock_diff for override functionality (technical debt) + tag['processor'] = 'restock_diff' + + self.__data['settings']['application']['tags'][uuid] = Tag.model( + datastore_path=self.datastore_path, + __datastore=self.__data, + default=tag ) logger.info(f"Tag: {uuid} {tag['title']}") @@ -402,9 +409,22 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore): File backend implementation: reads individual tag.json files. Tags loaded from files override any tags in settings (migration path). """ + from ..model import Tag + + def rehydrate_tag(uuid, entity_dict): + """Rehydrate tag as Tag object with forced restock_diff processor.""" + entity_dict['uuid'] = uuid + entity_dict['processor'] = 'restock_diff' # Force processor for override functionality + + return Tag.model( + datastore_path=self.datastore_path, + __datastore=self.__data, + default=entity_dict + ) + tags = load_all_tags( self.datastore_path, - self.rehydrate_entity + rehydrate_tag ) # Override settings tags with loaded tags diff --git a/changedetectionio/store/updates.py b/changedetectionio/store/updates.py index a6f11efbd..895abfe7a 100644 --- a/changedetectionio/store/updates.py +++ b/changedetectionio/store/updates.py @@ -154,10 +154,10 @@ class DatastoreUpdatesMixin: 2. For each update > current schema version: - Create backup of datastore - Run update method - - Update schema version - - Mark settings and watches dirty + - Update schema version and commit settings + - Commit all watches and tags 3. If any update fails, stop processing - 4. Save all changes immediately + 4. All changes saved via individual .commit() calls """ updates_available = self.get_updates_available() @@ -230,15 +230,11 @@ class DatastoreUpdatesMixin: # Tag doesn't have commit() method yet (pre-update_27) pass - # Save changes immediately after each update (more resilient than batching) - logger.critical(f"Saving all changes after update_{update_n}") - try: - self._save_dirty_items() - logger.success(f"Update {update_n} changes saved successfully") - except Exception as e: - logger.error(f"Failed to save update_{update_n} changes: {e}") - # Don't raise - update already ran, but changes might not be persisted - # The update will try to run again on next startup + # All changes already saved via individual .commit() calls: + # - Settings saved via self.commit() above + # - All watches saved via watch.commit() above + # - All tags saved via tag.commit() above + logger.success(f"Update {update_n} completed - all changes persisted") # Track which updates ran updates_ran.append(update_n) @@ -678,7 +674,7 @@ class DatastoreUpdatesMixin: logger.success(f"All {len(self.data['watching'])} watches have valid hashes after migration") # Set schema version to latest available update - # This prevents re-running updates and re-marking all watches as dirty + # This prevents re-running updates on next startup updates_available = self.get_updates_available() latest_schema = updates_available[-1] if updates_available else 26 self.data['settings']['application']['schema_version'] = latest_schema @@ -735,17 +731,34 @@ class DatastoreUpdatesMixin: saved_count = 0 failed_count = 0 - for uuid, tag in tags.items(): + for uuid, tag_data in tags.items(): try: - # Save tag to its own file - tag.commit() + # Force save as tag.json (not watch.json) even if object is corrupted + from changedetectionio.store.file_saving_datastore import save_entity_atomic + import os + + tag_dir = os.path.join(self.datastore_path, uuid) + os.makedirs(tag_dir, exist_ok=True) + + # Convert to dict if it's an object + tag_dict = dict(tag_data) if hasattr(tag_data, '__iter__') else tag_data + + # Save explicitly as tag.json + save_entity_atomic( + tag_dir, + uuid, + tag_dict, + filename='tag.json', + entity_type='tag', + max_size_mb=1 + ) saved_count += 1 if saved_count % 10 == 0: logger.info(f" Progress: {saved_count}/{tag_count} tags migrated...") except Exception as e: - logger.error(f"Failed to save tag {uuid} ({tag.get('title', 'unknown')}): {e}") + logger.error(f"Failed to save tag {uuid} ({tag_data.get('title', 'unknown')}): {e}") failed_count += 1 if failed_count > 0: