feat: switch config to JSONC + split secrets.json
Load business config as JSONC (comments + trailing commas) via an in-repo stripper then stdlib json — no new deps. Default path prefers config.jsonc, falls back to legacy config.json. Auth write-back targets only sibling secrets.json so JSONC comments are never wiped; secrets overlay merges on load. Examples, compose, Docker, docs, favsort and tools all share the same loader. Phase A only — no DB settings migration.
This commit was merged in pull request #4.
This commit is contained in:
1 parent
b4f0f45c4e
commit
8e7ead1669
21 files changed
+652
-182
No files matched your search
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""扫码授权流程;仅由 ``python app.py auth`` 调用。
|
||||
|
||||
认证结果只写入指定的 ``bilibili.read`` 或 ``bilibili.write`` 段,两个账号的
|
||||
身份及设备指纹 Cookie 绝不互相覆盖。
|
||||
认证结果只写入凭据文件(默认与主配置同目录的 ``secrets.json``)里指定的
|
||||
``bilibili.read`` 或 ``bilibili.write`` 段,两个账号的身份及设备指纹 Cookie
|
||||
绝不互相覆盖。主配置(可为 JSONC)不会被回写,注释得以保留。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -21,6 +22,8 @@ import zlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Mapping
|
||||
|
||||
from ..config import resolve_secrets_path
|
||||
from .. import jsonc
|
||||
from .auth import (
|
||||
IDENTITY_KEYS,
|
||||
cookie_value,
|
||||
@@ -154,22 +157,71 @@ class AuthService:
|
||||
def __init__(self, config_path: str | Path, *, account: str, logger=None) -> None:
|
||||
if account not in {"read", "write"}:
|
||||
raise ValueError(f"未知账号角色: {account}")
|
||||
self.path = Path(config_path)
|
||||
self.config_path = Path(config_path)
|
||||
# 兼容旧属性名:历史代码/测试可能读 path;现指向凭据文件
|
||||
self.account = account
|
||||
self.logger = logger
|
||||
self.secrets_path = self._resolve_secrets_path()
|
||||
self.path = self.secrets_path
|
||||
|
||||
def _resolve_secrets_path(self) -> Path:
|
||||
raw: dict[str, Any] | None = None
|
||||
if self.config_path.exists():
|
||||
try:
|
||||
loaded = jsonc.load(self.config_path)
|
||||
except (OSError, ValueError):
|
||||
loaded = None
|
||||
if isinstance(loaded, dict):
|
||||
raw = loaded
|
||||
return resolve_secrets_path(self.config_path, raw)
|
||||
|
||||
@property
|
||||
def backup_path(self) -> Path:
|
||||
return self.path.with_name(self.path.name + ".bak")
|
||||
return self.secrets_path.with_name(self.secrets_path.name + ".bak")
|
||||
|
||||
def _empty_account(self) -> dict[str, Any]:
|
||||
return {"mid": 0, "csrf": "", "cookie": ""}
|
||||
|
||||
def _load_raw(self) -> dict[str, Any]:
|
||||
with self.path.open(encoding="utf-8") as fh:
|
||||
raw = json.load(fh)
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("配置文件顶层必须是对象")
|
||||
"""加载凭据文件;若尚不存在则从主配置 bilibili 段迁移种子。"""
|
||||
if self.secrets_path.exists():
|
||||
raw = jsonc.load(self.secrets_path)
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("凭据文件顶层必须是对象")
|
||||
else:
|
||||
raw = {}
|
||||
if self.config_path.exists():
|
||||
try:
|
||||
main = jsonc.load(self.config_path)
|
||||
except (OSError, ValueError):
|
||||
main = None
|
||||
if isinstance(main, dict) and isinstance(main.get("bilibili"), dict):
|
||||
# 仅迁移凭据段,不把整份主配置拷进 secrets
|
||||
raw["bilibili"] = main["bilibili"]
|
||||
|
||||
section = raw.get("bilibili")
|
||||
if not isinstance(section, dict) or not isinstance(section.get(self.account), dict):
|
||||
raise ValueError(f"缺少 bilibili.{self.account} 账号配置")
|
||||
if not isinstance(section, dict):
|
||||
section = {}
|
||||
raw["bilibili"] = section
|
||||
if not isinstance(section.get(self.account), dict):
|
||||
section[self.account] = self._empty_account()
|
||||
# 保证另一账号键存在,避免后续手工编辑困惑
|
||||
other = "write" if self.account == "read" else "read"
|
||||
if not isinstance(section.get(other), dict):
|
||||
# 尝试从主配置补齐;否则空壳
|
||||
filled = False
|
||||
if self.config_path.exists():
|
||||
try:
|
||||
main = jsonc.load(self.config_path)
|
||||
except (OSError, ValueError):
|
||||
main = None
|
||||
if isinstance(main, dict) and isinstance(main.get("bilibili"), dict):
|
||||
other_cred = main["bilibili"].get(other)
|
||||
if isinstance(other_cred, dict):
|
||||
section[other] = dict(other_cred)
|
||||
filled = True
|
||||
if not filled:
|
||||
section[other] = self._empty_account()
|
||||
return raw
|
||||
|
||||
@staticmethod
|
||||
@@ -209,7 +261,9 @@ class AuthService:
|
||||
if new_mid.isdigit():
|
||||
credentials["mid"] = int(new_mid)
|
||||
|
||||
shutil.copy2(self.path, self.backup_path)
|
||||
had_secrets = self.secrets_path.exists()
|
||||
if had_secrets:
|
||||
shutil.copy2(self.secrets_path, self.backup_path)
|
||||
self._write_raw(raw)
|
||||
try:
|
||||
nav = self._get_json(credentials, f"{BASE}/x/web-interface/nav", True)
|
||||
@@ -218,8 +272,11 @@ class AuthService:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
data, ok = {"uname": str(exc)}, False
|
||||
if not ok:
|
||||
shutil.copy2(self.backup_path, self.path)
|
||||
print(f"[auth] {self.account} 登录校验失败,已回滚 config.json")
|
||||
if had_secrets and self.backup_path.exists():
|
||||
shutil.copy2(self.backup_path, self.secrets_path)
|
||||
elif self.secrets_path.exists():
|
||||
self.secrets_path.unlink()
|
||||
print(f"[auth] {self.account} 登录校验失败,已回滚 {self.secrets_path.name}")
|
||||
return 4
|
||||
|
||||
if isinstance(data.get("mid"), int):
|
||||
|
||||
Reference in new issue
Block a user