Do not make the ingest folder

A folder that Bindery made hid a missing volume mount. The books went
to a folder inside the container, and CWA never saw them. A missing
folder is now an error in the log at the start, and each upload fails
with a message that names the mount.
This commit is contained in:
tim
2026-10-02 23:37:33 +02:00
parent 679c1ede9f
commit 869e56eaf0
13 changed files with 81 additions and 16 deletions
+1
View File
@@ -7,6 +7,7 @@ from pathlib import Path
_tmp = Path(tempfile.mkdtemp(prefix="bindery-tests-"))
for name, sub in [("CONFIG_DIR", "config"), ("INGEST_DIR", "ingest"), ("TMP_DIR", "tmp")]:
os.environ[name] = str(_tmp / sub)
(_tmp / "ingest").mkdir() # Bindery does not make the ingest folder
os.environ["BINDERY_START_THREADS"] = "false"
os.environ["UPDATE_INTERVAL_HOURS"] = "0"
for name in (
+15
View File
@@ -279,3 +279,18 @@ def test_the_start_deletes_uploads_that_no_job_needs(builds, jobs, uploads, stor
orchestrator.queue_epub_file(waiting, "story.epub")
orchestrator.remove_stale_uploads()
assert list(uploads.iterdir()) == [waiting]
def test_a_missing_ingest_folder_fails_the_job_until_the_folder_exists(builds, jobs, monkeypatch, tmp_path):
monkeypatch.setattr(config, "INGEST_DIR", tmp_path / "not-mounted")
story_id = linked_story(ONE)
site(monkeypatch, ONE)
monkeypatch.setattr(orchestrator.time, "sleep", Mock())
jobs.add(Job(mode=DownloadMode.NEW, story_id=story_id))
job = orchestrator._run_next_job()
assert job.status == "error" and "does not exist. Check INGEST_DIR and the volume mount." in job.message
assert story(story_id)["status"] == "error"
assert not config.INGEST_DIR.exists()
config.INGEST_DIR.mkdir()
assert "library book 1" in update(story_id)
assert builds.call_count == 1
+1
View File
@@ -250,6 +250,7 @@ def test_library_database_failure_keeps_the_http_lookup(library_db, monkeypatch,
def test_database_book_id_reaches_the_epub_sidecar(client, library_db, monkeypatch, tmp_path, saved_id, saved_uuid):
monkeypatch.setattr(config, "CALIBRE_DB_PATH", library_db)
monkeypatch.setattr(config, "INGEST_DIR", tmp_path / "ingest")
config.INGEST_DIR.mkdir(exist_ok=True)
remote = CwaClient("")
remote.session.get = Mock(side_effect=AssertionError("No HTTP lookup needed"))
monkeypatch.setattr(orchestrator, "cwa", lambda: remote)
+13
View File
@@ -188,3 +188,16 @@ def test_prepare_warns_about_a_cwa_password_without_a_user_name(monkeypatch, tmp
with caplog.at_level("WARNING"):
config.prepare()
assert ("CWA_PASSWORD is set" in caplog.text) is warns
@pytest.mark.parametrize("exists", [False, True])
def test_prepare_does_not_make_the_ingest_folder(monkeypatch, tmp_path, caplog, exists):
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
for name, folder in [("CONFIG_DIR", "config"), ("UPLOAD_DIR", "uploads"), ("TMP_DIR", "tmp"), ("INGEST_DIR", "in")]:
monkeypatch.setattr(config, name, tmp_path / folder)
monkeypatch.setattr(config, "PERSONAL_INI", tmp_path / "config" / "personal.ini")
if exists:
config.INGEST_DIR.mkdir()
config.prepare()
assert config.INGEST_DIR.exists() is exists
assert ("does not exist. Check INGEST_DIR and the volume mount" in caplog.text) is not exists
+13 -1
View File
@@ -9,7 +9,7 @@ from test_build import URL, chapters, metahtml
from bindery import fff
from bindery.cwa import BookFinder, CwaClient, CwaError, plain_title, search_text, title_rank
from bindery.epub import epub_metadata, epub_urls
from bindery.ingest import MAX_NAME_LENGTH, IngestFolder
from bindery.ingest import MAX_NAME_LENGTH, IngestError, IngestFolder
STORY_URL = "https://www.royalroad.com/fiction/1234"
@@ -187,6 +187,7 @@ def test_epub_reader(story_epub, tmp_path):
def test_ingest_uses_part_names_and_unique_names(tmp_path):
folder = IngestFolder(tmp_path / "ingest")
folder.path.mkdir()
epub = tmp_path / "x.epub"
epub.write_bytes(b"PK")
first = folder.ingest(epub, 'A: "Story" / Name')
@@ -198,6 +199,7 @@ def test_ingest_uses_part_names_and_unique_names(tmp_path):
def test_send_update_writes_sidecar(tmp_path):
folder = IngestFolder(tmp_path / "ingest")
folder.path.mkdir()
epub = tmp_path / "x.epub"
epub.write_bytes(b"PK")
path = folder.send_update(epub, 7, "T" * 300)
@@ -206,3 +208,13 @@ def test_send_update_writes_sidecar(tmp_path):
manifest = json.loads((folder.path / (path.name + ".cwa.json")).read_text())
assert manifest == {"action": "add_format", "book_id": 7}
assert path.read_bytes() == b"PK"
def test_a_missing_ingest_folder_is_an_error_and_is_not_made(tmp_path, story_epub):
source = tmp_path / "book.epub"
source.write_bytes(story_epub)
folder = IngestFolder(tmp_path / "missing" / "ingest")
for send in (lambda: folder.ingest(source, "Title"), lambda: folder.send_update(source, 5, "Title")):
with pytest.raises(IngestError, match="does not exist. Check INGEST_DIR and the volume mount."):
send()
assert not (tmp_path / "missing").exists()
+1
View File
@@ -35,6 +35,7 @@ def fake(story_epub, monkeypatch, tmp_path):
monkeypatch.setattr(config, "CWA_PASSWORD", "secret")
monkeypatch.setattr(orchestrator, "store", Store(tmp_path / "bindery.db"))
monkeypatch.setattr(config, "INGEST_DIR", tmp_path / "ingest")
config.INGEST_DIR.mkdir()
monkeypatch.setattr(orchestrator, "finder", orchestrator.BookFinder())
yield server
server.close()
+7 -6
View File
@@ -21,6 +21,7 @@ from bindery.store import Store
@pytest.fixture(autouse=True)
def isolated(client, jobs, monkeypatch, tmp_path):
monkeypatch.setattr(config, "INGEST_DIR", tmp_path / "ingest")
config.INGEST_DIR.mkdir()
monkeypatch.setattr(config, "TMP_DIR", tmp_path / "tmp")
config.TMP_DIR.mkdir()
monkeypatch.setattr(config, "CWA_URL", "")
@@ -151,7 +152,7 @@ def test_build_without_ingest_succeeds_and_keeps_the_book_local(monkeypatch, tmp
assert story["status"] == "ok"
assert story["ingested_at"] == ("2026-01-01" if existing else None)
assert list(config.TMP_DIR.glob(f"story-{story_id}-*.epub"))
assert not (tmp_path / "ingest").exists()
assert not list((tmp_path / "ingest").iterdir())
details = orchestrator.story_details(story_id)
assert details["uploads_paused"] is True
assert details["story"]["upload_held"] is True
@@ -184,7 +185,7 @@ def test_linking_off_exports_books_and_respects_pause(monkeypatch, tmp_path, con
assert orchestrator.store.get_setting(story_id, "upload_held") is None
else:
assert message.startswith("Not sent.")
assert not config.INGEST_DIR.exists()
assert not list(config.INGEST_DIR.iterdir())
assert orchestrator.store.get_setting(story_id, "upload_held") is True
@@ -221,7 +222,7 @@ def test_enabling_linking_restores_the_saved_approval_preference(library):
assert orchestrator.get_settings()["hold_new_links"] is True
assert orchestrator.story_details(story_id)["story"]["needs_approval"] is True
assert "Approve link" in rebuild(story_id)
assert not config.INGEST_DIR.exists()
assert not list(config.INGEST_DIR.iterdir())
def test_linking_off_exports_every_rebuild_without_an_id_or_approval():
@@ -294,7 +295,7 @@ def test_paused_unlinked_update_does_not_raise_not_found(monkeypatch):
story_id = add_story(ingested_at="2026-01-01")
assert rebuild(story_id) == "Not sent. Turn off pause to send."
assert orchestrator.store.get_story(story_id)["status"] == "ok"
assert not config.INGEST_DIR.exists()
assert not list(config.INGEST_DIR.iterdir())
def test_enabling_linking_rebuilds_held_books_and_matches_again(monkeypatch, jobs, tmp_path):
@@ -335,7 +336,7 @@ def test_upload_settings_matrix(client, link_books, hold_new_links, paused, save
assert listed["upload_held"] is pending
assert listed["approval_held"] is (link_books and hold_new_links and not paused)
assert bool(orchestrator.store.get_setting(story_id, "upload_held")) is pending
assert config.INGEST_DIR.exists() is (not pending)
assert bool(list(config.INGEST_DIR.iterdir())) is (not pending)
@pytest.mark.parametrize("missing_ingest", [False, True])
@@ -376,7 +377,7 @@ def test_resuming_uploads_does_not_bypass_approval(client, jobs, library):
assert client.get("/api/stories").json[0]["approval_held"] is True
assert "Approve link" in orchestrator._run_rebuild(jobs.get_next())
assert orchestrator.store.get_setting(story_id, "upload_held") is True
assert not config.INGEST_DIR.exists()
assert not list(config.INGEST_DIR.iterdir())
def test_missing_library_connection_is_not_an_approval_hold(client, monkeypatch):