file.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. # -*- coding:utf-8 -*-
  2. """
  3. Configuration file loader for DDNS. supports both JSON and AST parsing.
  4. @author: NewFuture
  5. """
  6. from ast import literal_eval
  7. from io import open
  8. from json import loads as json_decode, dumps as json_encode
  9. from sys import stderr, stdout
  10. from ..util.comment import remove_comment
  11. def _process_multi_providers(config):
  12. # type: (dict) -> list[dict]
  13. """Process v4.1 providers format and return list of configs."""
  14. result = []
  15. # 提取全局配置(除providers之外的所有配置)
  16. global_config = _flatten_single_config(config, exclude_keys=["providers"])
  17. # 检查providers和dns字段不能同时使用
  18. if global_config.get("dns"):
  19. stderr.write("Error: 'providers' and 'dns' fields cannot be used simultaneously in config file!\n")
  20. raise ValueError("providers and dns fields conflict")
  21. # 为每个provider创建独立配置
  22. for provider_config in config["providers"]:
  23. # 验证provider必须有provider字段
  24. if not provider_config.get("provider"):
  25. stderr.write("Error: Each provider must have a 'provider' field!\n")
  26. raise ValueError("provider missing provider field")
  27. flat_config = global_config.copy() # 从全局配置开始
  28. provider_flat = _flatten_single_config(provider_config, exclude_keys=["provider"])
  29. flat_config["dns"] = provider_config.get("provider")
  30. flat_config.update(provider_flat)
  31. result.append(flat_config)
  32. return result
  33. def _flatten_single_config(config, exclude_keys=None):
  34. # type: (dict, list[str]|None) -> dict
  35. """Flatten a single config object with optional key exclusion."""
  36. if exclude_keys is None:
  37. exclude_keys = []
  38. flat_config = {}
  39. for k, v in config.items():
  40. if k in exclude_keys:
  41. continue
  42. if isinstance(v, dict):
  43. for subk, subv in v.items():
  44. flat_config["{}_{}".format(k, subk)] = subv
  45. else:
  46. flat_config[k] = v
  47. return flat_config
  48. def load_config(config_path):
  49. # type: (str) -> dict|list[dict]
  50. """
  51. 加载配置文件并返回配置字典或配置字典数组。
  52. 对于单个对象返回dict,对于数组返回list[dict]。
  53. 优先尝试JSON解析,失败后尝试AST解析。
  54. Args:
  55. config_path (str): 配置文件路径
  56. Returns:
  57. dict|list[dict]: 配置字典或配置字典数组
  58. Raises:
  59. Exception: 当配置文件加载失败时抛出异常
  60. """
  61. try:
  62. with open(config_path, "r", encoding="utf-8") as f:
  63. content = f.read()
  64. # 移除注释后尝试JSON解析
  65. content_without_comments = remove_comment(content)
  66. try:
  67. config = json_decode(content_without_comments)
  68. except (ValueError, SyntaxError) as json_error:
  69. # JSON解析失败,尝试AST解析
  70. try:
  71. config = literal_eval(content)
  72. stdout.write("Successfully loaded config file with AST parser: %s\n" % config_path)
  73. except (ValueError, SyntaxError) as ast_error:
  74. if config_path.endswith(".json"):
  75. stderr.write("JSON parsing failed for %s\n" % (config_path))
  76. raise json_error
  77. stderr.write(
  78. "Both JSON and AST parsing failed for %s\nJSON Error: %s\nAST Error: %s\n"
  79. % (config_path, json_error, ast_error)
  80. )
  81. raise ast_error
  82. except Exception as e:
  83. stderr.write("Failed to load config file `%s`: %s\n" % (config_path, e))
  84. raise
  85. # 处理配置格式:v4.1 providers格式或单个对象
  86. if "providers" in config and isinstance(config["providers"], list):
  87. return _process_multi_providers(config)
  88. else:
  89. return _flatten_single_config(config)
  90. def save_config(config_path, config):
  91. # type: (str, dict) -> bool
  92. """
  93. 保存配置到文件。
  94. Args:
  95. config_path (str): 配置文件路径
  96. config (dict): 配置字典
  97. Returns:
  98. bool: 保存成功返回True
  99. Raises:
  100. Exception: 保存失败时抛出异常
  101. """
  102. # 补全默认配置
  103. config = {
  104. "$schema": "https://ddns.newfuture.cc/schema/v4.1.json",
  105. "dns": config.get("dns", "debug"),
  106. "id": config.get("id", "YOUR ID or EMAIL for DNS Provider"),
  107. "token": config.get("token", "YOUR TOKEN or KEY for DNS Provider"),
  108. "ipv4": config.get("ipv4", ["ddns.newfuture.cc"]),
  109. "index4": config.get("index4", ["default"]),
  110. "ipv6": config.get("ipv6", []),
  111. "index6": config.get("index6", []),
  112. "ttl": config.get("ttl", 600),
  113. "line": config.get("line"),
  114. "proxy": config.get("proxy", []),
  115. "cache": config.get("cache", True),
  116. "ssl": config.get("ssl", "auto"),
  117. "log": {
  118. "file": config.get("log_file"),
  119. "level": config.get("log_level", "INFO"),
  120. "format": config.get("log_format"),
  121. "datefmt": config.get("log_datefmt"),
  122. },
  123. }
  124. try:
  125. with open(config_path, "w", encoding="utf-8") as f:
  126. content = json_encode(config, indent=2, ensure_ascii=False)
  127. # Python 2 兼容性:检查是否需要解码
  128. if hasattr(content, "decode"):
  129. content = content.decode("utf-8") # type: ignore
  130. f.write(content)
  131. return True
  132. except Exception:
  133. stderr.write("Cannot open config file to write: `%s`!\n" % config_path)
  134. raise