#!/usr/bin/env python3 """ Obsidian LiveSync CouchDB 配置生成器 此脚本用于生成运行 CouchDB 所需的配置文件,特别针对 Obsidian LiveSync 插件进行了优化配置。 """ import os import sys import re import getpass import platform from typing import Dict, Any, Optional, Tuple from pathlib import Path def get_current_user_ids() -> Tuple[str, str]: """ 获取当前用户的 UID 和 GID。 在 Windows 上返回默认值,在 Linux/macOS 上返回实际值。 """ if platform.system() in ['Linux', 'Darwin']: # Linux 或 macOS try: return str(os.getuid()), str(os.getgid()) except AttributeError: pass # Windows 或其他系统返回默认值 return "99", "100" # 常量定义 DEFAULT_CONFIG = { "couchdb_user": "obsidian_user", "host_port": "5984", "volume_name": "couchdb_data", "config_host_path": "./etc/couchdb/local.d", "single_node": "true", "cors_origins": "app://obsidian.md,capacitor://localhost,http://localhost", "puid": get_current_user_ids()[0], # 自动获取当前用户的 UID "pgid": get_current_user_ids()[1], # 自动获取当前用户的 GID "tz": "Asia/Shanghai" } COMMON_TIMEZONES = { "1": "Asia/Shanghai", "2": "Asia/Singapore", "3": "America/New_York", "4": "Europe/London", "5": "Asia/Tokyo" } def validate_port(port: str) -> bool: """验证端口号是否有效。""" try: port_num = int(port) return 1 <= port_num <= 65535 except ValueError: return False def validate_username(username: str) -> bool: """验证用户名是否有效。""" return bool(re.match(r'^[a-zA-Z0-9_-]{3,32}$', username)) def validate_password(password: str) -> bool: """验证密码是否满足最低安全要求。""" if len(password) < 8: return False if not re.search(r'[A-Z]', password): return False if not re.search(r'[a-z]', password): return False if not re.search(r'[0-9]', password): return False return True def get_input_with_default(prompt: str, default: str, validator: Optional[callable] = None) -> str: """获取用户输入,支持默认值和验证。""" while True: value = input(f"{prompt} [{default}]:").strip() if not value: value = default if validator and not validator(value): print(f"输入无效,请重试。") continue return value def get_password() -> str: """安全地获取密码输入并进行确认。""" while True: password = getpass.getpass("请输入密码:").strip() if not validate_password(password): print("密码必须至少8个字符,并包含大写字母、小写字母和数字。") continue confirm = getpass.getpass("请确认密码:").strip() if password == confirm: return password print("两次输入的密码不一致,请重试。") def get_timezone() -> str: """获取用户选择的时区。""" print("\n可用的时区:") for key, value in COMMON_TIMEZONES.items(): print(f" {key}. {value}") print(" (或输入自定义时区名称,例如:'Asia/Kuala_Lumpur')") while True: choice = input(f"选择时区 [{DEFAULT_CONFIG['tz']}]:").strip() if not choice: return DEFAULT_CONFIG['tz'] if choice in COMMON_TIMEZONES: return COMMON_TIMEZONES[choice] # 基本验证自定义时区 if '/' in choice and all(part.isalnum() or part in '_-' for part in choice.split('/')): return choice print("时区格式无效,请重试。") def generate_docker_compose(config: Dict[str, Any]) -> str: """生成 docker-compose.yml 内容。""" return f"""# 为 Obsidian LiveSync 生成的 docker-compose.yml # 使用最新的 Docker Compose 规范 services: couchdb-obsidian-livesync: container_name: couchdb-obsidian-livesync image: couchdb:3.3.3 environment: PUID: {config['puid']} PGID: {config['pgid']} UMASK: 0022 TZ: {config['tz']} COUCHDB_USER: {config['couchdb_user']} COUCHDB_PASSWORD: {config['couchdb_password']} volumes: - {config['volume_name']}:/opt/couchdb/data - {config['config_host_path']}:/opt/couchdb/etc/local.d ports: - "{config['host_port']}:5984" restart: unless-stopped networks: - couchdb_network volumes: {config['volume_name']}: networks: couchdb_network: driver: bridge """ def generate_couchdb_config(config: Dict[str, Any]) -> str: """生成 CouchDB local.ini 内容。""" return f"""[couchdb] single_node={config['single_node']} max_document_size = 50000000 [chttpd] require_valid_user = true max_http_request_size = 4294967296 [chttpd_auth] require_valid_user = true authentication_redirect = /_utils/session.html [httpd] WWW-Authenticate = Basic realm="couchdb" enable_cors = true [cors] origins = {config['cors_origins']} credentials = true headers = accept, authorization, content-type, origin, referer methods = GET, PUT, POST, HEAD, DELETE max_age = 3600 """ def save_config_files(config: Dict[str, Any]) -> None: """保存所有配置文件。""" # 创建配置目录 config_dir = Path(config['config_host_path']) config_dir.mkdir(parents=True, exist_ok=True) # 保存 docker-compose.yml with open('docker-compose.yml', 'w') as f: f.write(generate_docker_compose(config)) print(f"\n✓ 已生成 docker-compose.yml") # 保存 CouchDB 配置 config_path = config_dir / "my_obsidian_config.ini" with open(config_path, 'w') as f: f.write(generate_couchdb_config(config)) print(f"✓ 已生成 {config_path}") def main(): """主函数,运行配置生成器。""" print("=== Obsidian LiveSync CouchDB 配置生成器 ===") print("此脚本将帮助您设置用于 Obsidian LiveSync 的 CouchDB。") print("随时可以按 Ctrl+C 退出。\n") try: config = DEFAULT_CONFIG.copy() # 获取用户名 config['couchdb_user'] = get_input_with_default( "请输入 CouchDB 管理员用户名", DEFAULT_CONFIG['couchdb_user'], validate_username ) # 获取密码 print("\n请输入 CouchDB 管理员密码(必须至少8个字符,包含大写字母、小写字母和数字)") config['couchdb_password'] = get_password() # 获取时区 config['tz'] = get_timezone() # 获取端口 config['host_port'] = get_input_with_default( "请输入 CouchDB 主机端口", DEFAULT_CONFIG['host_port'], validate_port ) # 获取卷名称 config['volume_name'] = get_input_with_default( "请输入 CouchDB 数据卷名称", DEFAULT_CONFIG['volume_name'] ) # 获取配置路径 config['config_host_path'] = get_input_with_default( "请输入配置文件目录路径", DEFAULT_CONFIG['config_host_path'] ) # 获取单节点模式 single_node = get_input_with_default( "是否使用单节点模式(true/false)", DEFAULT_CONFIG['single_node'] ).lower() config['single_node'] = "true" if single_node in ["true", "t", "yes", "y"] else "false" # 获取 CORS 源 config['cors_origins'] = get_input_with_default( "请输入允许的 CORS 源(用逗号分隔)", DEFAULT_CONFIG['cors_origins'] ) # 获取 PUID/PGID current_uid, current_gid = get_current_user_ids() if platform.system() in ['Linux', 'Darwin']: print(f"\n检测到当前用户 ID:UID={current_uid}, GID={current_gid}") print("这些值将用于设置容器内的文件权限。") use_current = get_input_with_default( "是否使用当前用户的 UID/GID(推荐)", "yes" ).lower() in ["yes", "y", "true", "t"] if use_current: config['puid'] = current_uid config['pgid'] = current_gid else: print("\n请输入自定义的 PUID/PGID") config['puid'] = get_input_with_default( "请输入 PUID", current_uid, lambda x: x.isdigit() ) config['pgid'] = get_input_with_default( "请输入 PGID", current_gid, lambda x: x.isdigit() ) else: print("\n当前系统不支持自动获取用户 ID,将使用默认值(99/100)") print("如果需要自定义,请手动输入。") config['puid'] = get_input_with_default( "请输入 PUID", DEFAULT_CONFIG['puid'], lambda x: x.isdigit() ) config['pgid'] = get_input_with_default( "请输入 PGID", DEFAULT_CONFIG['pgid'], lambda x: x.isdigit() ) # 保存所有配置文件 save_config_files(config) print("\n=== 配置完成 ===") print(f"✓ CouchDB 将在 http://localhost:{config['host_port']}/_utils/ 访问") print(f"✓ 用户名:{config['couchdb_user']}") print(f"✓ 文件权限:UID={config['puid']}, GID={config['pgid']}") print("\n要启动 CouchDB,请运行:") print(" docker-compose up -d") print("\n要查看日志,请运行:") print(" docker-compose logs -f") except KeyboardInterrupt: print("\n\n配置已取消。") sys.exit(1) except Exception as e: print(f"\n错误:{str(e)}") sys.exit(1) if __name__ == "__main__": main()