From be751685702500bc8bea470034268bdecb10eb2d Mon Sep 17 00:00:00 2001 From: Andrew Peabody Date: Mon, 31 Aug 2026 04:41:46 -0700 Subject: [PATCH] fix(backup): add utf-8 encoding when writing url list files (#4345) --- .../blueprint/backups/__init__.py | 4 +- changedetectionio/tests/test_backup.py | 44 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/changedetectionio/blueprint/backups/__init__.py b/changedetectionio/blueprint/backups/__init__.py index 0d1626945..d3c97a61a 100644 --- a/changedetectionio/blueprint/backups/__init__.py +++ b/changedetectionio/blueprint/backups/__init__.py @@ -60,13 +60,13 @@ def create_backup(datastore_path, watches: dict, tags: dict = None): # Create a list file with just the URLs, so it's easier to port somewhere else in the future list_file = "url-list.txt" - with open(os.path.join(datastore_path, list_file), "w") as f: + with open(os.path.join(datastore_path, list_file), "w", encoding="utf-8") as f: for uuid in watches: url = watches[uuid]["url"] f.write("{}\r\n".format(url)) list_with_tags_file = "url-list-with-tags.txt" with open( - os.path.join(datastore_path, list_with_tags_file), "w" + os.path.join(datastore_path, list_with_tags_file), "w", encoding="utf-8" ) as f: for uuid in watches: url = watches[uuid].get('url') diff --git a/changedetectionio/tests/test_backup.py b/changedetectionio/tests/test_backup.py index 476d2f1ad..00d5a17d2 100644 --- a/changedetectionio/tests/test_backup.py +++ b/changedetectionio/tests/test_backup.py @@ -258,4 +258,46 @@ def test_backup_restore_zip_bomb_rejected(client, live_server, measure_memory_us include_watches_replace=True, ) finally: - restore_mod._MAX_DECOMPRESSED_BYTES = original_limit \ No newline at end of file + restore_mod._MAX_DECOMPRESSED_BYTES = original_limit + + +def test_backup_with_extended_utf8_urls_and_tags(datastore_path): + """Test create_backup handles extended UTF-8 characters in URLs and tags without encoding errors (issue #1805).""" + import os + import tempfile + from unittest.mock import MagicMock + + from changedetectionio.blueprint.backups import create_backup + + with tempfile.TemporaryDirectory() as tmp_dir: + watch_mock = MagicMock() + watch_mock.data_dir = tmp_dir + watch_mock.__getitem__.side_effect = lambda key: ( + "https://example.com/products/café-öl-日本語-🎉?query=test#section" if key == "url" else None + ) + watch_mock.get.side_effect = lambda key, default=None: ( + "https://example.com/products/café-öl-日本語-🎉?query=test#section" + if key == "url" + else (["Größe", "日本語タグ", "🏷️special"] if key == "tags" else default) + ) + + watches = {"test-uuid-1": watch_mock} + # Should create backup without UnicodeEncodeError + create_backup(datastore_path=tmp_dir, watches=watches, tags={}) + + url_list_path = os.path.join(tmp_dir, "url-list.txt") + url_list_tags_path = os.path.join(tmp_dir, "url-list-with-tags.txt") + + assert os.path.isfile(url_list_path) + assert os.path.isfile(url_list_tags_path) + + with open(url_list_path, encoding="utf-8") as f: + content = f.read() + assert "café-öl-日本語-🎉" in content + + with open(url_list_tags_path, encoding="utf-8") as f: + content = f.read() + assert "café-öl-日本語-🎉" in content + assert "Größe" in content + assert "日本語タグ" in content + assert "🏷️special" in content