add:同时upload和edit时,使用etag进行版本标记
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user