TASK-018: Make config file writes atomic using write-to-temp + rename
- Replace Path.write_text() with tempfile.NamedTemporaryFile + os.replace() in _write_conf_file() and _create_conf_file() - Ensures atomic operations on same filesystem (temp file in target.parent) - Prevents config corruption if process is killed mid-write - Follows existing pattern in jail_config_service.py - Add proper cleanup of temp files on error with contextlib.suppress() - Document atomic file write conventions in Backend-Development.md This prevents fail2ban config files (especially jail.d/*.conf) from being left in a truncated or corrupt state, which could disable active protection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,10 @@ and creation helpers used by :mod:`app.services.raw_config_io_service`.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from app.exceptions import (
|
||||
@@ -186,9 +189,22 @@ def _write_conf_file(subdir: Path, name: str, content: str) -> Path:
|
||||
if target is None:
|
||||
raise ConfigFileNotFoundError(name)
|
||||
|
||||
tmp_name: str | None = None
|
||||
try:
|
||||
target.write_text(content, encoding="utf-8")
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
dir=target.parent,
|
||||
delete=False,
|
||||
suffix=".tmp",
|
||||
) as tmp:
|
||||
tmp.write(content)
|
||||
tmp_name = tmp.name
|
||||
os.replace(tmp_name, target)
|
||||
except OSError as exc:
|
||||
with contextlib.suppress(OSError):
|
||||
if tmp_name is not None:
|
||||
os.unlink(tmp_name)
|
||||
raise ConfigFileWriteError(f"Cannot write {name!r}: {exc}") from exc
|
||||
|
||||
return target
|
||||
@@ -222,9 +238,22 @@ def _create_conf_file(subdir: Path, name: str, content: str) -> str:
|
||||
|
||||
target = (resolved_subdir / (name + ".conf")).resolve()
|
||||
_assert_within(resolved_subdir, target)
|
||||
tmp_name: str | None = None
|
||||
try:
|
||||
target.write_text(content, encoding="utf-8")
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
dir=target.parent,
|
||||
delete=False,
|
||||
suffix=".tmp",
|
||||
) as tmp:
|
||||
tmp.write(content)
|
||||
tmp_name = tmp.name
|
||||
os.replace(tmp_name, target)
|
||||
except OSError as exc:
|
||||
with contextlib.suppress(OSError):
|
||||
if tmp_name is not None:
|
||||
os.unlink(tmp_name)
|
||||
raise ConfigFileWriteError(f"Cannot create {name!r}: {exc}") from exc
|
||||
|
||||
return target.name
|
||||
|
||||
Reference in New Issue
Block a user