Handle corrupt/empty notebook files, atomic writes
_read_notebook_file returns None on empty or unparseable JSON instead of crashing with JSONDecodeError (surfaced as 500). save_notebook now writes to a temp file and atomically renames, preventing empty files from container restarts mid-write.
This commit is contained in:
parent
a2b76539da
commit
9781c9e676
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import tempfile
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -116,6 +118,9 @@ def list_notebooks(directory: Path) -> list[NotebookSummary]:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
nb = _read_notebook_file(path)
|
nb = _read_notebook_file(path)
|
||||||
|
if nb is None:
|
||||||
|
logger.warning("Skipping empty/corrupt notebook: %s", path)
|
||||||
|
continue
|
||||||
description, schematic_svg = _extract_card_data(nb)
|
description, schematic_svg = _extract_card_data(nb)
|
||||||
summaries.append(NotebookSummary(
|
summaries.append(NotebookSummary(
|
||||||
id=nb_id,
|
id=nb_id,
|
||||||
@ -152,10 +157,18 @@ def save_notebook(directory: Path, notebook_id: str, notebook: Notebook) -> None
|
|||||||
notebook.metadata.modified = _now_iso()
|
notebook.metadata.modified = _now_iso()
|
||||||
|
|
||||||
path = user_dir / f"{notebook_id}{SPICEBOOK_EXT}"
|
path = user_dir / f"{notebook_id}{SPICEBOOK_EXT}"
|
||||||
path.write_text(
|
content = notebook.model_dump_json(indent=2)
|
||||||
notebook.model_dump_json(indent=2),
|
# Atomic write: write to temp file then rename, so a crash can't leave an empty file
|
||||||
encoding="utf-8",
|
fd, tmp = tempfile.mkstemp(dir=user_dir, suffix=".tmp")
|
||||||
)
|
try:
|
||||||
|
os.write(fd, content.encode("utf-8"))
|
||||||
|
os.fsync(fd)
|
||||||
|
os.close(fd)
|
||||||
|
os.replace(tmp, path)
|
||||||
|
except BaseException:
|
||||||
|
os.close(fd)
|
||||||
|
os.unlink(tmp)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def create_notebook(directory: Path, title: str, engine: str = "ngspice") -> tuple[str, Notebook]:
|
def create_notebook(directory: Path, title: str, engine: str = "ngspice") -> tuple[str, Notebook]:
|
||||||
@ -196,8 +209,13 @@ def delete_notebook(directory: Path, notebook_id: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _read_notebook_file(path: Path) -> Notebook:
|
def _read_notebook_file(path: Path) -> Notebook | None:
|
||||||
"""Read and validate a .spicebook JSON file."""
|
"""Read and validate a .spicebook JSON file. Returns None on corrupt/empty files."""
|
||||||
raw = path.read_text(encoding="utf-8")
|
raw = path.read_text(encoding="utf-8")
|
||||||
data = json.loads(raw)
|
if not raw.strip():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
return Notebook.model_validate(data)
|
return Notebook.model_validate(data)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user