1
0
Эх сурвалжийг харах

feat(config): 支持环境变量传递数组参数 Support array in environment variables and improve logic (#476)

* feat(config): 环境变量支持数组
* 简单参数支持‘,’分割
* fix typo and update doc
* 优化逻辑
New Future 5 сар өмнө
parent
commit
46918ef98d
5 өөрчлөгдсөн 427 нэмэгдсэн , 15 устгасан
  1. 7 1
      README.md
  2. 361 0
      doc/env.md
  3. 12 1
      run.py
  4. 6 6
      schema/v4.0.json
  5. 41 7
      util/config.py

+ 7 - 1
README.md

@@ -16,8 +16,12 @@
 
 
 - 兼容和跨平台:
 - 兼容和跨平台:
   - [Docker (@NN708)](https://hub.docker.com/r/newfuture/ddns) [![Docker Image Size](https://img.shields.io/docker/image-size/newfuture/ddns/latest?logo=docker&style=social)](https://hub.docker.com/r/newfuture/ddns)[![Docker Platforms](https://img.shields.io/badge/arch-amd64%20%7C%20arm64%20%7C%20arm%2Fv7%20%7C%20arm%2Fv6%20%7C%20ppc64le%20%7C%20s390x%20%7C%20386%20%7C%20mips64le-blue?style=social)](https://hub.docker.com/r/newfuture/ddns)
   - [Docker (@NN708)](https://hub.docker.com/r/newfuture/ddns) [![Docker Image Size](https://img.shields.io/docker/image-size/newfuture/ddns/latest?logo=docker&style=social)](https://hub.docker.com/r/newfuture/ddns)[![Docker Platforms](https://img.shields.io/badge/arch-amd64%20%7C%20arm64%20%7C%20arm%2Fv7%20%7C%20arm%2Fv6%20%7C%20ppc64le%20%7C%20s390x%20%7C%20386%20%7C%20mips64le-blue?style=social)](https://hub.docker.com/r/newfuture/ddns)
-  - [PIP 安装 (兼容Python2)](https://pypi.org/project/ddns/) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/ddns.svg?logo=pypi&style=social) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ddns.svg?style=social)
   - [二进制文件](https://github.com/NewFuture/DDNS/releases/latest) ![cross platform](https://img.shields.io/badge/system-windows_%7C%20linux_%7C%20mac-success.svg?style=social)
   - [二进制文件](https://github.com/NewFuture/DDNS/releases/latest) ![cross platform](https://img.shields.io/badge/system-windows_%7C%20linux_%7C%20mac-success.svg?style=social)
+  
+- 配置方式:
+  - [命令行参数](#详细配置)
+  - [JSON 配置文件](#详细配置)
+  - [环境变量配置](doc/env.md) 📖
 
 
 - 域名支持:
 - 域名支持:
   - 多个域名支持
   - 多个域名支持
@@ -116,6 +120,8 @@
 2. JSON 配置文件(值为 null 认为是有效值,会覆盖环境变量的设置,如果没有对应的 key 则会尝试使用环境变量)
 2. JSON 配置文件(值为 null 认为是有效值,会覆盖环境变量的设置,如果没有对应的 key 则会尝试使用环境变量)
 3. 环境变量 DDNS_ 前缀加上 key 全大写或者全小写,点转下划线(`${ddns_id}` 或 `${DDNS_ID}`,`${DDNS_LOG_LEVEL}`)
 3. 环境变量 DDNS_ 前缀加上 key 全大写或者全小写,点转下划线(`${ddns_id}` 或 `${DDNS_ID}`,`${DDNS_LOG_LEVEL}`)
 
 
+> 📖 **环境变量详细配置**: 查看 [环境变量配置文档](doc/env.md) 了解所有环境变量的详细用法和示例
+
 <details open>
 <details open>
 <summary markdown="span">config.json 配置文件</summary>
 <summary markdown="span">config.json 配置文件</summary>
 
 

+ 361 - 0
doc/env.md

@@ -0,0 +1,361 @@
+# DDNS 环境变量配置文档
+
+## 概述
+
+DDNS 支持通过环境变量进行配置,环境变量的优先级为:**命令行参数 > 配置文件 > 环境变量**
+
+所有环境变量都以 `DDNS_` 为前缀,后跟参数名(推荐全大写),点号(`.`)替换为下划线(`_`)。
+
+## 环境变量命名规则
+
+| 配置参数 | 环境变量名称 | 示例 |
+|---------|-------------|------|
+| `id` | `DDNS_ID` 或 `ddns_id` | `DDNS_ID=12345` |
+| `token` | `DDNS_TOKEN` 或 `ddns_token` | `DDNS_TOKEN=mytokenkey` |
+| `log.level` | `DDNS_LOG_LEVEL` 或 `ddns_log_level` | `DDNS_LOG_LEVEL=DEBUG` |
+| `log.file` | `DDNS_LOG_FILE` 或 `ddns_log_file` | `DDNS_LOG_FILE=/var/log/ddns.log` |
+
+## 基础配置参数
+
+### 认证信息
+
+#### DDNS_ID
+
+- **类型**: 字符串
+- **必需**: 是(部分 DNS 服务商可选)
+- **说明**: API 访问 ID 或用户标识
+- **示例**:
+
+  ```bash
+  export DDNS_ID="12345"
+  # DNSPod 为用户 ID
+  # 阿里云为 Access Key ID
+  # CloudFlare 为邮箱地址(使用 Token 时可留空)
+  # HE.net 可留空
+  # 华为云为 Access Key ID (AK)
+  ```
+
+#### DDNS_TOKEN
+
+- **类型**: 字符串
+- **必需**: 是
+- **说明**: API 授权令牌或密钥
+- **示例**:
+
+  ```bash
+  export DDNS_TOKEN="your_api_token_here"
+  # 部分平台称为 Secret Key
+  # 注意:请妥善保管,不要泄露
+  ```
+
+### DNS 服务商
+
+#### DDNS_DNS
+
+- **类型**: 字符串
+- **必需**: 否
+- **默认值**: `dnspod`
+- **可选值**: `alidns`, `cloudflare`, `dnscom`, `dnspod`, `dnspod_com`, `he`, `huaweidns`, `callback`
+- **说明**: DNS 服务提供商
+- **示例**:
+
+  ```bash
+  export DDNS_DNS="alidns"        # 阿里云 DNS
+  export DDNS_DNS="cloudflare"    # CloudFlare
+  export DDNS_DNS="dnspod"        # DNSPod 国内版
+  export DDNS_DNS="dnspod_com"    # DNSPod 国际版
+  export DDNS_DNS="he"            # HE.net
+  export DDNS_DNS="huaweidns"     # 华为云 DNS
+  export DDNS_DNS="callback"      # 自定义回调
+  ```
+
+## 域名配置
+
+### IPv4 域名列表
+
+#### DDNS_IPV4
+
+- **类型**: 数组(支持 JSON/python 格式)
+- **必需**: 否
+- **默认值**: `[]`
+- **说明**: 需要更新 IPv4 记录的域名列表
+- **示例**:
+
+  ```bash
+  # JSON 数组格式(推荐)
+  export DDNS_IPV4='["example.com", "sub.example.com"]'
+  
+  # 单个域名
+  export DDNS_IPV4="example.com"
+  
+  # 禁用 IPv4 更新
+  export DDNS_IPV4="[]"
+  ```
+
+### IPv6 域名列表
+
+#### DDNS_IPV6
+
+- **类型**: 数组(支持 JSON / Python 格式)
+- **必需**: 否
+- **默认值**: `[]`
+- **说明**: 需要更新 IPv6 记录的域名列表
+- **示例**:
+
+  ```bash
+  # 单个域名
+  export DDNS_IPV6="ipv6.example.com"
+
+  # JSON 数组格式(推荐)
+  export DDNS_IPV6='["ipv6.example.com", "v6.example.com"]'
+
+  # python 列表格式
+  export DDNS_IPV6="['ipv6.example.com', 'v6.example.com']"
+
+  # 禁用 IPv6 更新
+  export DDNS_IPV6="[]"
+  ```
+
+## IP 获取方式
+
+### IPv4 获取方式
+
+#### DDNS_INDEX4
+
+- **类型**: 字符串或数组
+- **必需**: 否
+- **默认值**: `default`
+- **说明**: IPv4 地址获取方式
+- **示例**:
+
+  ```bash
+  # 默认方式(系统默认外网 IP)
+  export DDNS_INDEX4="default"
+  
+  # 公网 IP
+  export DDNS_INDEX4="public"
+  
+  # 指定网卡(第 0 个网卡)
+  export DDNS_INDEX4="0"
+  
+  # 自定义 URL 获取
+  export DDNS_INDEX4="url:http://ip.sb"
+  
+  # 正则匹配(注意转义)
+  export DDNS_INDEX4="regex:192\\.168\\..*"
+  
+  # 执行命令
+  export DDNS_INDEX4="cmd:curl -s http://ipv4.icanhazip.com"
+  
+  # Shell 命令
+  export DDNS_INDEX4="shell:ip route get 8.8.8.8 | awk '{print \$7}'"
+  
+  # 多种方式组合(JSON 数组)
+  export DDNS_INDEX4='["public", "regex:172\\..*"]'
+  
+  # 禁用 IPv4 获取
+  export DDNS_INDEX4="false"
+  ```
+
+### IPv6 获取方式
+
+#### DDNS_INDEX6
+
+- **类型**: 字符串或数组
+- **必需**: 否
+- **默认值**: `default`
+- **说明**: IPv6 地址获取方式(用法同 INDEX4)
+- **示例**:
+
+  ```bash
+  # 公网 IPv6
+  export DDNS_INDEX6="public"
+  
+  # 正则匹配 IPv6
+  export DDNS_INDEX6="regex:2001:.*"
+  
+  # 自定义 URL
+  export DDNS_INDEX6="url:http://ipv6.icanhazip.com"
+  
+  # 禁用 IPv6 获取
+  export DDNS_INDEX6="false"
+  ```
+
+## 网络配置
+
+### TTL 设置
+
+#### DDNS_TTL
+
+- **类型**: 整数
+- **必需**: 否
+- **默认值**: `null`(使用 DNS 默认值)
+- **说明**: DNS 记录的 TTL(生存时间),单位为秒
+- **示例**:
+
+  ```bash
+  export DDNS_TTL="600"     # 10 分钟
+  export DDNS_TTL="3600"    # 1 小时
+  export DDNS_TTL="86400"   # 24 小时
+  ```
+
+### 代理设置
+
+#### DDNS_PROXY
+
+- **类型**: 数组(支持 JSON 格式或分号分隔)
+- **必需**: 否
+- **默认值**: 无
+- **说明**: HTTP 代理设置,支持多代理轮换
+- **示例**:
+
+  ```bash
+  # 单个代理
+  export DDNS_PROXY="http://127.0.0.1:1080"
+  
+  # 多个代理(JSON 数组格式)
+  export DDNS_PROXY='["http://proxy1:8080", "http://proxy2:8080", "DIRECT"]'
+  
+  # 分号分隔格式
+  export DDNS_PROXY="http://proxy1:8080;http://proxy2:8080;DIRECT"
+  
+  # DIRECT 表示直连
+  export DDNS_PROXY="DIRECT"
+  ```
+
+## 系统配置
+
+### 缓存设置
+
+#### DDNS_CACHE
+
+- **类型**: 布尔值或字符串
+- **必需**: 否
+- **默认值**: `true`
+- **说明**: 启用缓存以减少 API 请求
+- **示例**:
+
+  ```bash
+  # 启用缓存(默认路径)
+  export DDNS_CACHE="true"
+  
+  # 禁用缓存
+  export DDNS_CACHE="false"
+  
+  # 自定义缓存文件路径
+  export DDNS_CACHE="/path/to/ddns.cache"
+  ```
+
+### 日志配置
+
+#### DDNS_LOG_LEVEL
+
+- **类型**: 字符串
+- **必需**: 否
+- **默认值**: `INFO`
+- **可选值**: `CRITICAL`, `FATAL`, `ERROR`, `WARN`, `WARNING`, `INFO`, `DEBUG`, `NOTSET`
+- **说明**: 日志级别
+- **示例**:
+
+  ```bash
+  export DDNS_LOG_LEVEL="DEBUG"    # 调试模式
+  export DDNS_LOG_LEVEL="INFO"     # 信息模式
+  export DDNS_LOG_LEVEL="ERROR"    # 仅错误
+  ```
+
+#### DDNS_LOG_FILE
+
+- **类型**: 字符串
+- **必需**: 否
+- **默认值**: 无(输出到控制台)
+- **说明**: 日志文件路径
+- **示例**:
+
+  ```bash
+  export DDNS_LOG_FILE="/var/log/ddns.log"
+  export DDNS_LOG_FILE="./ddns.log"
+  ```
+
+## 使用示例
+
+### 基础配置示例
+
+```bash
+#!/bin/bash
+# DNSPod 配置示例
+export DDNS_DNS="dnspod"
+export DDNS_ID="12345"
+export DDNS_TOKEN="your_token_here"
+export DDNS_IPV4='["example.com", "www.example.com"]'
+export DDNS_IPV6='["ipv6.example.com"]'
+export DDNS_TTL="600"
+
+# 运行 DDNS
+ddns
+```
+
+### Docker 环境变量示例
+
+```bash
+docker run -d \
+  -e DDNS_DNS=dnspod \
+  -e DDNS_ID=12345 \
+  -e DDNS_TOKEN=your_token_here \
+  -e DDNS_IPV4=example.com \
+  -e DDNS_IPV6='["ipv6.example.com","example.com"]' \
+  -e DDNS_INDEX4=["public", 0] \
+  -e DDNS_INDEX6=public \
+  -e DDNS_TTL=600 \
+  -e DDNS_LOG_LEVEL=INFO \
+  --network host \
+  newfuture/ddns
+```
+
+### 复杂配置示例
+
+```bash
+#!/bin/bash
+# 阿里云 DNS 高级配置
+export DDNS_DNS="alidns"
+export DDNS_ID="your_access_key_id"
+export DDNS_TOKEN="your_access_key_secret"
+
+# 多域名配置
+export DDNS_IPV4='["ddns.example.com", "home.example.com", "server.example.com"]'
+export DDNS_IPV6='["ipv6.example.com"]'
+
+# 多种 IP 获取方式
+export DDNS_INDEX4='["public", "regex:192\\.168\\.1\\..*", "cmd:curl -s ipv4.icanhazip.com"]'
+export DDNS_INDEX6="public"
+
+# 代理和缓存配置
+export DDNS_PROXY='["http://proxy.example.com:8080", "DIRECT"]'
+export DDNS_CACHE="/home/user/.ddns_cache"
+
+# 日志配置
+export DDNS_LOG_LEVEL="DEBUG"
+export DDNS_LOG_FILE="/var/log/ddns.log"
+
+# TTL 设置
+export DDNS_TTL="300"
+
+# 运行
+ddns
+```
+
+## 注意事项
+
+1. **数组参数格式**: `index4`, `index6`, `ipv4`, `ipv6`, `proxy` 等数组参数支持两种格式:
+   - JSON 数组格式:`'["item1", "item2"]'`(推荐)
+   - 逗号分隔格式:`"item1,item2"`
+
+2. **环境变量优先级**: 环境变量会被配置文件中的非 null 值覆盖
+
+3. **大小写兼容**: 环境变量名支持大写、小写或混合大小写
+
+4. **安全提醒**:
+   - 请妥善保管 `DDNS_TOKEN` 等敏感信息
+   - 在脚本中使用时避免明文存储
+   - 考虑使用 `.env` 文件或密钥管理系统
+
+5. **调试建议**: 出现问题时,可设置 `DDNS_LOG_LEVEL=DEBUG` 获取详细日志信息

+ 12 - 1
run.py

@@ -32,6 +32,17 @@ Copyright (c) New Future (MIT License)
 environ["DDNS_VERSION"] = "${BUILD_VERSION}"
 environ["DDNS_VERSION"] = "${BUILD_VERSION}"
 
 
 
 
+def is_false(value):
+    """
+    判断值是否为 False
+    字符串 'false', 或者 False, 或者 'none';
+    0 不是 False
+    """
+    if isinstance(value, str):
+        return value.strip().lower() in ['false', 'none']
+    return value is False
+
+
 def get_ip(ip_type, index="default"):
 def get_ip(ip_type, index="default"):
     """
     """
     get IP address
     get IP address
@@ -41,7 +52,7 @@ def get_ip(ip_type, index="default"):
     value = None
     value = None
     try:
     try:
         debug("get_ip(%s, %s)", ip_type, index)
         debug("get_ip(%s, %s)", ip_type, index)
-        if index is False:  # disabled
+        if is_false(index):  # disabled
             return False
             return False
         elif isinstance(index, list):  # 如果获取到的规则是列表,则依次判断列表中每一个规则,直到获取到IP
         elif isinstance(index, list):  # 如果获取到的规则是列表,则依次判断列表中每一个规则,直到获取到IP
             for i in index:
             for i in index:

+ 6 - 6
schema/v4.0.json

@@ -6,13 +6,13 @@
   "properties": {
   "properties": {
     "$schema": {
     "$schema": {
       "type": "string",
       "type": "string",
-      "title": "please use https://ddns.newfuture.cc/schema/v2.8.json",
-      "description": "请更换为 https://ddns.newfuture.cc/schema/v2.8.json",
-      "default": "https://ddns.newfuture.cc/schema/v2.8.json",
+      "title": "please use https://ddns.newfuture.cc/schema/v4.0.json",
+      "description": "请更换为 https://ddns.newfuture.cc/schema/v4.0.json",
+      "default": "https://ddns.newfuture.cc/schema/v4.0.json",
       "enum": [
       "enum": [
-        "https://ddns.newfuture.cc/schema/v2.8.json",
-        "http://ddns.newfuture.cc/schema/v2.8.json",
-        "./schema/v2.8.json"
+        "https://ddns.newfuture.cc/schema/v4.0.json",
+        "http://ddns.newfuture.cc/schema/v4.0.json",
+        "./schema/v4.0.json"
       ]
       ]
     },
     },
     "id": {
     "id": {

+ 41 - 7
util/config.py

@@ -4,6 +4,7 @@ from argparse import Action, ArgumentParser, Namespace, RawTextHelpFormatter
 from json import load as loadjson, dump as dumpjson
 from json import load as loadjson, dump as dumpjson
 from os import stat, environ, path
 from os import stat, environ, path
 from logging import error, getLevelName
 from logging import error, getLevelName
+from ast import literal_eval
 
 
 import sys
 import sys
 
 
@@ -13,6 +14,11 @@ __config = {}  # type: dict
 log_levels = ['CRITICAL', 'FATAL', 'ERROR',
 log_levels = ['CRITICAL', 'FATAL', 'ERROR',
               'WARN', 'WARNING', 'INFO', 'DEBUG', 'NOTSET']
               'WARN', 'WARNING', 'INFO', 'DEBUG', 'NOTSET']
 
 
+# 支持数组的参数列表
+ARRAY_PARAMS = ['index4', 'index6', 'ipv4', 'ipv6', 'proxy']
+# 简单数组,支持’,’, ‘;’ 分隔的参数列表
+SIMPLE_ARRAY_PARAMS = ['ipv4', 'ipv6', 'proxy']
+
 
 
 def str2bool(v):
 def str2bool(v):
     """
     """
@@ -35,6 +41,32 @@ def log_level(value):
     return getLevelName(value.upper())
     return getLevelName(value.upper())
 
 
 
 
+def parse_array_string(value, enable_simple_split):
+    """
+    解析数组字符串
+    仅当 trim 之后以 '[' 开头以 ']' 结尾时,才尝试使用 ast.literal_eval 解析
+    默认返回原始字符串
+    """
+    if not isinstance(value, str):
+        return value
+
+    trimmed = value.strip()
+    if trimmed.startswith('[') and trimmed.endswith(']'):
+        try:
+            # 尝试使用 ast.literal_eval 解析数组
+            parsed_value = literal_eval(trimmed)
+            # 确保解析结果是列表或元组
+            if isinstance(parsed_value, (list, tuple)):
+                return list(parsed_value)
+        except (ValueError, SyntaxError) as e:
+            # 解析失败时返回原始字符串
+            error('Failed to parse array string: %s. Exception: %s', value, e)
+    elif enable_simple_split and ',' in trimmed:
+        # 尝试使用逗号或分号分隔符解析
+        return [item.strip() for item in trimmed.split(',') if item.strip()]
+    return value
+
+
 def init_config(description, doc, version):
 def init_config(description, doc, version):
     """
     """
     配置
     配置
@@ -128,14 +160,16 @@ def get_config(key, default=None):
         return getattr(__cli_args, key)
         return getattr(__cli_args, key)
     if key in __config:
     if key in __config:
         return __config.get(key)
         return __config.get(key)
+    # 检查环境变量
     env_name = 'DDNS_' + key.replace('.', '_')  # type:str
     env_name = 'DDNS_' + key.replace('.', '_')  # type:str
-    if env_name in environ:  # 环境变量
-        return environ.get(env_name)
-    elif env_name.upper() in environ:  # 大写环境变量
-        return environ.get(env_name.upper())
-    elif env_name.lower() in environ:  # 小写环境变量
-        return environ.get(env_name.lower())
-    return default
+    variations = [env_name, env_name.upper(), env_name.lower()]
+    value = next((environ.get(v) for v in variations if v in environ), None)
+
+    # 如果找到环境变量值且参数支持数组,尝试解析为数组
+    if value is not None and key in ARRAY_PARAMS:
+        return parse_array_string(value, key in SIMPLE_ARRAY_PARAMS)
+
+    return value if value is not None else default
 
 
 
 
 class ExtendAction(Action):
 class ExtendAction(Action):