add:同时upload和edit时,使用etag进行版本标记

This commit is contained in:
2026-04-02 10:52:59 +08:00
parent 0d178c748e
commit ac66c5dd08

View File

@@ -475,6 +475,40 @@ def _cleanup_temp_file(file_path: str) -> None:
pass
def _compute_file_etag(file_path: str) -> str:
"""计算文件内容的 SHA-256 哈希,作为并发控制的 ETag。"""
h = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
# 内部版本注册表:记录每个文件最后一次 upload 或 edit_docx 之后的 etag。
# 所有读写必须持有对应文件的 _file_lock无需额外线程锁。
_file_etag_registry: Dict[str, str] = {}
def _register_etag(abs_path: str, etag: str) -> None:
_file_etag_registry[abs_path] = etag
def _check_etag(abs_path: str) -> None:
"""
在文件锁内调用:若注册表中存在该文件的 etag则校验当前磁盘文件是否匹配。
不匹配说明文件在本次操作排队期间已被其他操作(如并发 upload修改。
"""
known = _file_etag_registry.get(abs_path)
if not known:
return
current = _compute_file_etag(abs_path)
if current != known:
raise ValueError(
f"文件已被其他操作修改(版本冲突),请确认最新上传后重试。"
f"已知: {known[:12]}…,当前: {current[:12]}"
)
def _validate_docx_file(file_path: str) -> None:
if not os.path.exists(file_path):
raise FileNotFoundError(f"输入 DOCX 文件不存在: {file_path}")
@@ -536,7 +570,7 @@ def _edit_docx_core(
对 DOCX 文件进行编辑(与 HTTP /edit_docx 共用逻辑)。
返回:
- {"output_path": 绝对路径, "output_url": URL 或 None}
- {"output_path": 绝对路径, "output_url": URL 或 None, "etag": 新文件哈希}
"""
print(f"edit_docx: input_docx_path: {input_docx_path}, replacements: {replacements}")
upload_dir = _get_upload_dir()
@@ -550,6 +584,9 @@ def _edit_docx_core(
_validate_docx_file(local_input)
# 版本校验:在锁内对比注册表 etag检测并发 upload 导致的版本冲突
_check_etag(os.path.abspath(local_input))
if replacements is None:
replacements = []
@@ -591,10 +628,13 @@ def _edit_docx_core(
os.replace(output_docx, local_input)
abs_out = os.path.abspath(local_input)
new_etag = _compute_file_etag(abs_out)
_register_etag(abs_out, new_etag)
return {
"output_path": abs_out,
"output_url": _build_output_url(abs_out),
"etag": new_etag,
}
except Exception:
if 'output_docx' in locals() and os.path.exists(output_docx):
@@ -758,8 +798,10 @@ async def upload_handler(request: Request):
"success": False,
"message": f"上传文件为空: {filename}"
}, status_code=400)
abs_file_path = os.path.abspath(file_path)
with _file_lock(file_path):
_write_bytes_atomic(file_path, content)
_register_etag(abs_file_path, _compute_file_etag(abs_file_path))
return JSONResponse({
"success": True,