File size: 2,366 Bytes
fa1140b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
"""Token 持久化与并发刷新锁(文件级 `fcntl.flock`)。

适用于 Supabase Auth 等 refresh_token 一次性的场景:多 worker 同时刷新会导致第二个请求使用
已失效的 refresh_token。用 :func:`locked_refresh` 把“读旧 token → 刷新 → 写新 token”包在
文件锁内,避免覆盖与竞态。

Windows 不支持 `fcntl.flock`;骨架目标运行环境为 Linux/Docker,如需跨平台可替换为 SQLite 或
进程级锁。
"""
from __future__ import annotations

import json
from collections.abc import Callable
from pathlib import Path
from typing import Any

from app._compat import flock_ex, flock_sh

TokenDict = dict[str, Any]


def load_token(path: str | Path) -> TokenDict:
    """读取 token 文件;文件不存在返回空字典。"""
    p = Path(path)
    if not p.is_file():
        return {}
    with p.open("r", encoding="utf-8") as f:
        # 共享锁即可,读时保证读到完整写完的内容
        flock_sh(f.fileno())
        try:
            return json.load(f)
        except (json.JSONDecodeError, ValueError):
            return {}


def save_token(path: str | Path, data: TokenDict) -> None:
    """原子写 token 文件(tmp + replace + 排他锁)。"""
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    tmp = p.with_suffix(p.suffix + ".tmp")
    with tmp.open("w", encoding="utf-8") as f:
        flock_ex(f.fileno())
        json.dump(data, f, ensure_ascii=False, indent=2)
        f.write("\n")
    tmp.replace(p)


def locked_refresh(
    path: str | Path,
    refresh_fn: Callable[[TokenDict], TokenDict],
    *,
    lock_path: str | Path | None = None,
) -> TokenDict:
    """在文件锁保护下读取旧 token、调用 ``refresh_fn(old)`` 并写入新 token。

    ``refresh_fn`` 内部应执行实际的刷新网络请求,并返回要持久化的新 token dict。
    锁默认使用 ``<token>.lock``,可通过 ``lock_path`` 自定义。
    """
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    lock = Path(lock_path) if lock_path is not None else p.with_suffix(p.suffix + ".lock")
    lock.parent.mkdir(parents=True, exist_ok=True)

    with lock.open("w", encoding="utf-8") as lf:
        flock_ex(lf.fileno())
        old = load_token(p)
        new = refresh_fn(old)
        save_token(p, new)
        return new