fix: --report darf keine erzeugte Datei ersetzen, Schreibfehler mit Exit 1 (QA2-07)
--report OUTPUT/manifest.json ersetzte das Manifest stillschweigend durch
den Bericht. Ein nicht beschreibbares --report-Ziel endete nach bereits
geschriebener Ausgabe mit Exit 4 ("Interner Fehler"). Jetzt: Abbruch mit
Exit 1 vor dem Schreiben bzw. verständliche Meldung mit Exit 1;
Überschreibverhalten in der README dokumentiert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7de7efaaa2
commit
ff1a0ae5e4
3 changed files with 51 additions and 3 deletions
|
|
@ -102,7 +102,7 @@ python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json
|
||||||
| `--force` | aus | Bestehendes Ausgabeverzeichnis ersetzen – siehe [Sicherheit](#sicherheit-und-fehlerbehandlung) |
|
| `--force` | aus | Bestehendes Ausgabeverzeichnis ersetzen – siehe [Sicherheit](#sicherheit-und-fehlerbehandlung) |
|
||||||
| `--verbose`, `-v` | aus | Verarbeitungsschritte und Warnungen auf stderr |
|
| `--verbose`, `-v` | aus | Verarbeitungsschritte und Warnungen auf stderr |
|
||||||
| `--dry-run` | aus | EPUB analysieren, geplante Kapiteldateien anzeigen, **nichts** schreiben (auch kein `--report`) |
|
| `--dry-run` | aus | EPUB analysieren, geplante Kapiteldateien anzeigen, **nichts** schreiben (auch kein `--report`) |
|
||||||
| `--report DATEI.json` | – | Bericht zusätzlich an diesen Ort schreiben. `OUTPUT/conversion-report.json` entsteht immer |
|
| `--report DATEI.json` | – | Bericht zusätzlich an diesen Ort schreiben. `OUTPUT/conversion-report.json` entsteht immer. Eine bestehende Datei `DATEI.json` wird ersetzt; Namen, die epub2md selbst im Ausgabeverzeichnis erzeugt (z. B. `manifest.json`), werden abgelehnt (Exit 1). Kann der Bericht nicht geschrieben werden: Exit 1, die Ausgabe ist trotzdem vollständig |
|
||||||
| `--version` | – | Programmversion |
|
| `--version` | – | Programmversion |
|
||||||
|
|
||||||
### Rückgabecodes
|
### Rückgabecodes
|
||||||
|
|
|
||||||
|
|
@ -141,14 +141,32 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
if opts.dry_run:
|
if opts.dry_run:
|
||||||
return Result(report=report, chapters=all_chapters, files=files, output_dir=output_dir)
|
return Result(report=report, chapters=all_chapters, files=files, output_dir=output_dir)
|
||||||
|
|
||||||
|
if opts.report:
|
||||||
|
_check_report_path(Path(opts.report), output_dir, files, rendered.media, opts)
|
||||||
_write_output(reader, input_path, output_dir, opts, files, rendered.media)
|
_write_output(reader, input_path, output_dir, opts, files, rendered.media)
|
||||||
if opts.report:
|
if opts.report:
|
||||||
report_path = Path(opts.report)
|
report_path = Path(opts.report)
|
||||||
|
try:
|
||||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
report_path.write_text(writer.to_json(report), encoding="utf-8")
|
report_path.write_text(writer.to_json(report), encoding="utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
raise InputError(f"Bericht (--report) konnte nicht geschrieben werden: {report_path} "
|
||||||
|
f"({exc.strerror or exc}); die Ausgabe in {output_dir} ist vollständig.") from exc
|
||||||
return Result(report=report, chapters=all_chapters, files=files, output_dir=output_dir)
|
return Result(report=report, chapters=all_chapters, files=files, output_dir=output_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_report_path(report_path: Path, output_dir: Path, files: dict[str, str], media: MediaRegistry,
|
||||||
|
opts: Options) -> None:
|
||||||
|
"""--report must not replace a file that epub2md itself writes into the output directory."""
|
||||||
|
if not is_inside(report_path, output_dir):
|
||||||
|
return
|
||||||
|
rel = report_path.resolve().relative_to(output_dir.resolve()).as_posix()
|
||||||
|
generated = set(files) | ({mf.output for mf in media.files.values()} if opts.extract_media else set())
|
||||||
|
if rel in generated:
|
||||||
|
raise InputError(f"--report {report_path} würde die erzeugte Datei {rel} überschreiben; "
|
||||||
|
"bitte einen anderen Dateinamen wählen.")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class _Rendered:
|
class _Rendered:
|
||||||
renderer: Renderer
|
renderer: Renderer
|
||||||
|
|
|
||||||
|
|
@ -107,3 +107,33 @@ def test_multiple_blank_lines_inside_code_are_kept(convert):
|
||||||
text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001")
|
text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001")
|
||||||
assert f"```python\n{code}\n```" in text, text
|
assert f"```python\n{code}\n```" in text, text
|
||||||
assert "\n\n\n" not in text.replace(code, "") # outside code blank lines are still normalized
|
assert "\n\n\n" not in text.replace(code, "") # outside code blank lines are still normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _simple() -> Book:
|
||||||
|
return Book(docs=[Doc("c.xhtml", "<h1>K</h1><p>Text.</p>")])
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_must_not_overwrite_generated_output_file(convert, tmp_path):
|
||||||
|
"""QA2-07: --report OUTPUT/manifest.json silently replaced the manifest with the report."""
|
||||||
|
out = tmp_path / "ziel"
|
||||||
|
run = convert(_simple(), "--report", str(out / "manifest.json"), out=out)
|
||||||
|
assert run.rc == 1
|
||||||
|
assert "--report" in run.stderr
|
||||||
|
assert not out.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_write_error_is_input_error_not_internal(convert, tmp_path):
|
||||||
|
"""QA2-07: an unwritable --report target ended with exit 4 ("Interner Fehler")."""
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
|
ro = tmp_path / "ro"
|
||||||
|
ro.mkdir()
|
||||||
|
os.chmod(ro, stat.S_IRUSR | stat.S_IXUSR)
|
||||||
|
try:
|
||||||
|
run = convert(_simple(), "--report", str(ro / "bericht.json"), out=tmp_path / "ziel")
|
||||||
|
finally:
|
||||||
|
os.chmod(ro, stat.S_IRWXU)
|
||||||
|
assert run.rc == 1, run.stderr
|
||||||
|
assert "Interner Fehler" not in run.stderr
|
||||||
|
assert "Bericht" in run.stderr
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue