diff --git a/dbSer/couchdb/env.cfg b/dbSer/couchdb/env.cfg new file mode 100644 index 0000000..20215fa --- /dev/null +++ b/dbSer/couchdb/env.cfg @@ -0,0 +1,2 @@ +IMAGE_TAG=couchdb:3.5 + diff --git a/dbSer/couchdb/etc/couchdb/local.d/local.ini b/dbSer/couchdb/etc/couchdb/local.d/local.ini new file mode 100644 index 0000000..df134ee --- /dev/null +++ b/dbSer/couchdb/etc/couchdb/local.d/local.ini @@ -0,0 +1,38 @@ +# CouchDB 配置文件 +# 此文件包含 Obsidian LiveSync 所需的 CouchDB 配置 + +[couchdb] +# 单节点模式设置 +single_node=true +# 最大文档大小(字节) +max_document_size = 50000000 + +[chttpd] +# 要求有效用户认证 +require_valid_user = true +# 最大 HTTP 请求大小(字节) +max_http_request_size = 4294967296 + +[chttpd_auth] +# 要求有效用户认证 +require_valid_user = true +# 认证重定向页面 +authentication_redirect = /_utils/session.html + +[httpd] +# 基本认证领域 +WWW-Authenticate = Basic realm="couchdb" +# 启用 CORS +enable_cors = true + +[cors] +# 允许的源(用逗号分隔) +origins = app://obsidian.md,capacitor://localhost,http://localhost +# 允许发送认证信息 +credentials = true +# 允许的请求头 +headers = accept, authorization, content-type, origin, referer +# 允许的 HTTP 方法 +methods = GET, PUT, POST, HEAD, DELETE +# CORS 预检请求缓存时间(秒) +max_age = 3600 \ No newline at end of file diff --git a/dbSer/couchdb/generate_config.py b/dbSer/couchdb/generate_config.py new file mode 100644 index 0000000..f971de6 --- /dev/null +++ b/dbSer/couchdb/generate_config.py @@ -0,0 +1,305 @@ +#!/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() \ No newline at end of file diff --git a/dbSer/couchdb/stack.yml b/dbSer/couchdb/stack.yml new file mode 100644 index 0000000..781b05d --- /dev/null +++ b/dbSer/couchdb/stack.yml @@ -0,0 +1,17 @@ +services: + couchdb: + image: ${IMAGE_TAG:-couchdb:3.5} + container_name: couchdb + restart: unless-stopped + ports: + - 5984:5984 + volumes: + - ./etc/couchdb/local.d:/opt/couchdb/etc/local.d + - ./couchdb:/opt/couchdb/data + environment: + TZ: Asia/Shanghai + PUID: 99 + PGID: 100 + UMASK: 0022 + COUCHDB_USER: your_custom_user + COUCHDB_PASSWORD: your_strong_password