add:同时upload和edit时,使用etag进行版本标记
This commit is contained in:
@@ -475,6 +475,40 @@ def _cleanup_temp_file(file_path: str) -> None:
|
|||||||
pass
|
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:
|
def _validate_docx_file(file_path: str) -> None:
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
raise FileNotFoundError(f"输入 DOCX 文件不存在: {file_path}")
|
raise FileNotFoundError(f"输入 DOCX 文件不存在: {file_path}")
|
||||||
@@ -536,7 +570,7 @@ def _edit_docx_core(
|
|||||||
对 DOCX 文件进行编辑(与 HTTP /edit_docx 共用逻辑)。
|
对 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}")
|
print(f"edit_docx: input_docx_path: {input_docx_path}, replacements: {replacements}")
|
||||||
upload_dir = _get_upload_dir()
|
upload_dir = _get_upload_dir()
|
||||||
@@ -550,6 +584,9 @@ def _edit_docx_core(
|
|||||||
|
|
||||||
_validate_docx_file(local_input)
|
_validate_docx_file(local_input)
|
||||||
|
|
||||||
|
# 版本校验:在锁内对比注册表 etag,检测并发 upload 导致的版本冲突
|
||||||
|
_check_etag(os.path.abspath(local_input))
|
||||||
|
|
||||||
if replacements is None:
|
if replacements is None:
|
||||||
replacements = []
|
replacements = []
|
||||||
|
|
||||||
@@ -591,10 +628,13 @@ def _edit_docx_core(
|
|||||||
|
|
||||||
os.replace(output_docx, local_input)
|
os.replace(output_docx, local_input)
|
||||||
abs_out = os.path.abspath(local_input)
|
abs_out = os.path.abspath(local_input)
|
||||||
|
new_etag = _compute_file_etag(abs_out)
|
||||||
|
_register_etag(abs_out, new_etag)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"output_path": abs_out,
|
"output_path": abs_out,
|
||||||
"output_url": _build_output_url(abs_out),
|
"output_url": _build_output_url(abs_out),
|
||||||
|
"etag": new_etag,
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception:
|
||||||
if 'output_docx' in locals() and os.path.exists(output_docx):
|
if 'output_docx' in locals() and os.path.exists(output_docx):
|
||||||
@@ -758,8 +798,10 @@ async def upload_handler(request: Request):
|
|||||||
"success": False,
|
"success": False,
|
||||||
"message": f"上传文件为空: {filename}"
|
"message": f"上传文件为空: {filename}"
|
||||||
}, status_code=400)
|
}, status_code=400)
|
||||||
|
abs_file_path = os.path.abspath(file_path)
|
||||||
with _file_lock(file_path):
|
with _file_lock(file_path):
|
||||||
_write_bytes_atomic(file_path, content)
|
_write_bytes_atomic(file_path, content)
|
||||||
|
_register_etag(abs_file_path, _compute_file_etag(abs_file_path))
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|||||||
Reference in New Issue
Block a user