Просмотр исходного кода

fix(config): create parent directories for log file automatically (#600)

Copilot 1 месяц назад
Родитель
Сommit
21539a6cb4

+ 3 - 1
.github/instructions/python.instructions.md

@@ -244,7 +244,9 @@ if response is None:
 
 ### Code Validation
 
-- **Linting**: Use editor's Python extensions instead of command-line tools
+- **Install Ruff**: Before making changes, install ruff linter/formatter: `pip3 install ruff`
+- **Linting**: Run `ruff check --fix --unsafe-fixes .` before each commit to fix linting issues
+- **Formatting**: Run `ruff format .` before each commit to format code consistently
 - **Type Checking**: Ensure Pylance compatibility
 - **Testing**: Run tests before committing changes
 - **Documentation**: Update relevant documentation when making changes

+ 7 - 0
ddns/config/__init__.py

@@ -63,6 +63,13 @@ def _setup_logging(cli_config, env_config, all_json_configs):
         log_format = "%(asctime)s %(levelname)s: %(message)s"
     else:
         log_format = "%(asctime)s %(levelname)s [%(name)s]: %(message)s"
+
+    # Create parent directory for log file if it doesn't exist
+    if global_conf.log_file:
+        log_dir = os.path.dirname(global_conf.log_file)
+        if log_dir and not os.path.exists(log_dir):
+            os.makedirs(log_dir)
+
     logging.basicConfig(
         level=global_conf.log_level, format=log_format, datefmt=global_conf.log_datefmt, filename=global_conf.log_file
     )

+ 148 - 0
tests/test_config_log_file_dir.py

@@ -0,0 +1,148 @@
+# coding=utf-8
+# type: ignore[index,operator,assignment]
+"""
+Unit tests for log file directory creation issue
+Reproduces the issue where log_file path has non-existent parent directories
+@author: GitHub Copilot
+"""
+
+from __init__ import unittest
+import tempfile
+import json
+import os
+import sys
+import shutil
+from ddns.config import load_configs
+
+
+class TestLogFileDirectory(unittest.TestCase):
+    """测试日志文件目录创建问题"""
+
+    def setUp(self):
+        self.temp_dir = tempfile.mkdtemp()
+        self.original_argv = sys.argv[:]
+
+    def tearDown(self):
+        sys.argv = self.original_argv
+        # Clean up logging handlers
+        import logging
+        for handler in logging.root.handlers[:]:
+            logging.root.removeHandler(handler)
+        # Clean up temp directory
+        shutil.rmtree(self.temp_dir, ignore_errors=True)
+
+    def test_log_file_with_nonexistent_directory_single_config(self):
+        """测试单个配置时,log文件所在目录不存在的情况"""
+        # 创建一个不存在的目录路径
+        log_dir = os.path.join(self.temp_dir, "nonexistent", "subdir")
+        log_file = os.path.join(log_dir, "ddns.log")
+
+        # 确保目录不存在
+        self.assertFalse(os.path.exists(log_dir))
+
+        config_data = {
+            "dns": "debug",
+            "id": "[email protected]",
+            "token": "secret123",
+            "ipv4": ["test.example.com"],
+            "log": {"file": log_file, "level": "INFO"},
+        }
+
+        config_path = os.path.join(self.temp_dir, "config.json")
+        with open(config_path, "w") as f:
+            json.dump(config_data, f)
+
+        sys.argv = ["ddns", "-c", config_path]
+
+        # 加载配置,应该自动创建目录
+        configs = load_configs("test", "1.0", "2023-01-01")
+
+        # 验证目录被创建
+        self.assertTrue(os.path.exists(log_dir), "Log directory should be created")
+        self.assertEqual(len(configs), 1)
+        self.assertEqual(configs[0].log_file, log_file)
+
+    def test_log_file_with_nonexistent_directory_multi_provider(self):
+        """测试多个provider配置时,log文件所在目录不存在的情况(复现issue中的问题)"""
+        # 创建一个不存在的目录路径
+        log_dir = os.path.join(self.temp_dir, "ddns")
+        log_file = os.path.join(log_dir, "ddns.log")
+
+        # 确保目录不存在
+        self.assertFalse(os.path.exists(log_dir))
+
+        # 模拟issue中的配置:多个provider,共享同一个log文件
+        config_data = {
+            "$schema": "https://ddns.newfuture.cc/schema/v4.1.json",
+            "ssl": "auto",
+            "cache": os.path.join(self.temp_dir, "cache"),
+            "log": {"level": "INFO", "file": log_file},
+            "index4": "default",
+            "index6": "default",
+            "providers": [
+                {"provider": "debug", "token": "token1", "ipv6": ["test1.xyz"]},
+                {"provider": "debug", "token": "token2", "ipv6": ["test2.org"]},
+            ],
+        }
+
+        config_path = os.path.join(self.temp_dir, "multi_config.json")
+        with open(config_path, "w") as f:
+            json.dump(config_data, f)
+
+        sys.argv = ["ddns", "-c", config_path]
+
+        # 加载配置,应该自动创建目录
+        configs = load_configs("test", "1.0", "2023-01-01")
+
+        # 验证目录被创建
+        self.assertTrue(os.path.exists(log_dir), "Log directory should be created")
+        self.assertEqual(len(configs), 2)
+
+        # 验证两个配置都使用同一个log文件
+        for config in configs:
+            self.assertEqual(config.log_file, log_file)
+
+    def test_log_file_with_nested_nonexistent_directory(self):
+        """测试log文件路径有多级不存在的目录"""
+        # 创建多级不存在的目录路径
+        log_file = os.path.join(self.temp_dir, "a", "b", "c", "d", "ddns.log")
+
+        config_data = {"dns": "debug", "id": "[email protected]", "token": "secret123", "log": {"file": log_file}}
+
+        config_path = os.path.join(self.temp_dir, "config.json")
+        with open(config_path, "w") as f:
+            json.dump(config_data, f)
+
+        sys.argv = ["ddns", "-c", config_path]
+
+        # 加载配置,应该自动创建所有父目录
+        configs = load_configs("test", "1.0", "2023-01-01")
+
+        # 验证所有父目录被创建
+        self.assertTrue(os.path.exists(os.path.dirname(log_file)), "All parent directories should be created")
+        self.assertEqual(configs[0].log_file, log_file)
+
+    def test_log_file_with_existing_directory(self):
+        """测试log文件所在目录已存在的情况(不应该出错)"""
+        # 创建目录
+        log_dir = os.path.join(self.temp_dir, "existing_dir")
+        os.makedirs(log_dir)
+        log_file = os.path.join(log_dir, "ddns.log")
+
+        config_data = {"dns": "debug", "id": "[email protected]", "token": "secret123", "log": {"file": log_file}}
+
+        config_path = os.path.join(self.temp_dir, "config.json")
+        with open(config_path, "w") as f:
+            json.dump(config_data, f)
+
+        sys.argv = ["ddns", "-c", config_path]
+
+        # 加载配置,应该正常工作
+        configs = load_configs("test", "1.0", "2023-01-01")
+
+        self.assertTrue(os.path.exists(log_dir))
+        self.assertEqual(configs[0].log_file, log_file)
+
+
+if __name__ == "__main__":
+    unittest.main()