TASK-021: Implement atomic writes for set_jail_config_enabled and write_jail_config_file

This commit is contained in:
2026-04-26 14:27:33 +02:00
parent d476e9d611
commit ec253d9b7a
3 changed files with 58 additions and 21 deletions

View File

@@ -257,3 +257,38 @@ def _create_conf_file(subdir: Path, name: str, content: str) -> str:
raise ConfigFileWriteError(f"Cannot create {name!r}: {exc}") from exc
return target.name
def atomic_write(path: Path, content: str) -> None:
"""Write content to a file atomically using temp file + rename.
This function ensures that if the process is killed during the write,
the target file is not corrupted. Uses a temporary file in the same
directory as the target, then atomically renames it into place.
Args:
path: Target file path.
content: Content to write.
Raises:
ConfigFileWriteError: If the file cannot be written.
"""
_validate_content(content)
tmp_name: str | None = None
try:
with tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=path.parent,
delete=False,
suffix=".tmp",
) as tmp:
tmp.write(content)
tmp_name = tmp.name
os.replace(tmp_name, path)
except OSError as exc:
with contextlib.suppress(OSError):
if tmp_name is not None:
os.unlink(tmp_name)
raise ConfigFileWriteError(f"Cannot write {path.name!r}: {exc}") from exc

View File

@@ -15,19 +15,16 @@ traversal attacks.
from __future__ import annotations
from app.utils.async_utils import run_blocking
from app.exceptions import (
ConfigFileNameError,
ConfigFileNotFoundError,
ConfigFileWriteError,
)
import configparser
import re
from pathlib import Path
from typing import TYPE_CHECKING
import structlog
from app.exceptions import (
ConfigFileNameError,
ConfigFileNotFoundError,
)
from app.models.file_config import (
ConfFileContent,
ConfFileCreateRequest,
@@ -46,9 +43,13 @@ from app.services.config_file_helpers import (
_resolve_subdir,
_validate_content,
_write_conf_file,
atomic_write,
)
from app.utils.async_utils import run_blocking
if TYPE_CHECKING:
from pathlib import Path
from app.models.config import (
ActionConfig,
ActionConfigUpdate,
@@ -260,12 +261,7 @@ async def set_jail_config_enabled(
original = path.read_text(encoding="utf-8", errors="replace")
updated = _set_enabled_in_content(original, enabled)
try:
path.write_text(updated, encoding="utf-8")
except OSError as exc:
raise ConfigFileWriteError(
f"Cannot write {filename!r}: {exc}"
) from exc
atomic_write(path, updated)
log.info(
"jail_config_file_enabled_set",
filename=filename,
@@ -336,12 +332,7 @@ async def write_jail_config_file(
)
if not path.is_file():
raise ConfigFileNotFoundError(filename)
try:
path.write_text(req.content, encoding="utf-8")
except OSError as exc:
raise ConfigFileWriteError(
f"Cannot write {filename!r}: {exc}"
) from exc
atomic_write(path, req.content)
log.info("jail_config_file_written", filename=filename)
await run_blocking( _do)