Files
AgentLab/backend/app/core/atomic.py
T
ThearchyHelios dead51ae56 数据目录里的文件一律先写临时文件再改名:新增 core/atomic(同目录、带随机后缀的临时文件,写完 os.replace;只该生成一次的用 os.link 建,已有就不动)
- 工件(内容寻址):以前共用 <id>.tmp,两个进程同时写同一份内容时后到的会截断先到的半成品、先到的改名又让后到的改名失败;现在各写各的临时文件
- 加密密钥 .secret_key、断言签名私钥:几个进程同时第一次启动时只建出一把,后到的读先到的那一把(以前各生成一把,各自加密、签名的东西对方认不得),文件从建出来起就是 0600
- 附件抽出文字的缓存:临时文件名带随机后缀
- 文件工具(file_write)的覆盖和追加、三种系统沙箱写进工作目录的代码和输入文件、seatbelt 的策略文件:先写临时文件再改名
- 上传表格的版本库、原件存档、记录表本来就是这样写的,不动
2026-10-19 10:01:20 +08:00

81 lines
3.3 KiB
Python

"""数据目录里的文件一律先写临时文件(同目录、名字带随机后缀)再改名。
为什么:读的一方(同一台机器上的别的进程、共享存储另一头的机器)要么看到旧的完整文件,要么看到新的完整文件,
看不到写了一半的;进程写到一半没了,留下的是一个没人认的临时文件,不是半个正式文件。临时文件名带随机后缀:
两个进程同时写同一个文件(内容寻址的工件撞了同一份内容)各写各的,不会一个把另一个写到一半的截断。
- write_bytes / write_text:写完 os.replace 到目标(目标已有就替换);
- create_bytes / create_text:只在目标还没有时建(os.link,已有就不动、返回 False)——给只该生成一次的东西用
(加密密钥、签名私钥):两个进程同时第一次启动,后到的读先到的那一份,不会各拿一把。
上传表格的版本库、原件存档、记录表另有自己的写法(data/raw_store、data/table_versions,同样先写临时文件),不走这里。
"""
from __future__ import annotations
import contextlib
import os
import secrets
from pathlib import Path
#: 临时文件名里的记号:.<原名>.tmp-<随机>(同 data/raw_store.TMP_MARK 的写法)
TMP_MARK = ".tmp-"
def _tmp_for(path: Path) -> Path:
return path.parent / f".{path.name}{TMP_MARK}{secrets.token_hex(6)}"
def _write_tmp(path: Path, data: bytes, *, mode: int | None, fsync: bool) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
tmp = _tmp_for(path)
# 没给 mode 时照普通新建文件(0666 再受 umask 约束);给了的(密钥 0600)从建出来那一刻起就是那个权限,再 chmod
# 一次压过 umask
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o666 if mode is None else 0o600)
try:
with os.fdopen(fd, "wb") as fh:
fh.write(data)
fh.flush()
if fsync:
os.fsync(fh.fileno())
if mode is not None:
os.chmod(tmp, mode)
except BaseException:
with contextlib.suppress(OSError):
tmp.unlink()
raise
return tmp
def write_bytes(path: Path, data: bytes, *, mode: int | None = None, fsync: bool = False) -> None:
"""原子地写(替换)path。"""
tmp = _write_tmp(path, data, mode=mode, fsync=fsync)
try:
os.replace(tmp, path)
except BaseException:
with contextlib.suppress(OSError):
tmp.unlink()
raise
def write_text(path: Path, text: str, *, encoding: str = "utf-8", mode: int | None = None,
fsync: bool = False) -> None:
write_bytes(path, text.encode(encoding), mode=mode, fsync=fsync)
def create_bytes(path: Path, data: bytes, *, mode: int | None = None, fsync: bool = True) -> bool:
"""path 还没有时原子地建出来,返回 True;已经有了(别人先建了)什么都不动,返回 False。"""
tmp = _write_tmp(path, data, mode=mode, fsync=fsync)
try:
os.link(tmp, path)
return True
except FileExistsError:
return False
finally:
with contextlib.suppress(OSError):
tmp.unlink()
def create_text(path: Path, text: str, *, encoding: str = "utf-8", mode: int | None = None) -> bool:
return create_bytes(path, text.encode(encoding), mode=mode)