fix: download every selected book, not just the last
The frontend built book_ids with `set` rather than `append`, so the archive only ever held one book. The proxy also sent an empty Content-Length, which is not valid HTTP/1.1, and buffered the whole zip before forwarding a byte of it. Entries now carry the file's own name, suffixed when two books claim the same one, and the books are resolved before the stream starts so a miss is a 404 instead of a truncated archive under a 200.
This commit is contained in:
@@ -221,7 +221,9 @@ class TestBookServiceCRUD:
|
||||
archive_bytes = b"".join(
|
||||
[
|
||||
chunk
|
||||
async for chunk in books_service.get_files([book.id], test_library.id)
|
||||
async for chunk in await books_service.get_files(
|
||||
[book.id], test_library.id
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -231,13 +233,79 @@ class TestBookServiceCRUD:
|
||||
names = archive.namelist()
|
||||
assert len(names) == len(contents)
|
||||
|
||||
# Entries are namespaced by the book's directory, so two books sharing a
|
||||
# filename cannot overwrite one another.
|
||||
assert {Path(name).parent.name for name in names} == {"Dracula"}
|
||||
# Entries carry the file's own name, with no directory in front of it.
|
||||
assert set(names) == set(contents)
|
||||
|
||||
for name, payload in contents.items():
|
||||
entry = next(n for n in names if Path(n).name == name)
|
||||
assert archive.read(entry) == payload
|
||||
assert archive.read(name) == payload
|
||||
|
||||
async def test_get_files_suffixes_a_name_claimed_twice(
|
||||
self, books_service: BookService, test_library: m.Library
|
||||
) -> None:
|
||||
"""Two books can each hold a `Dune.epub`; the archive must keep both.
|
||||
|
||||
Entries are named after the file, and a zip stores the same name twice
|
||||
happily — leaving whichever copy was written last as the one that survives
|
||||
extraction. The second is suffixed instead.
|
||||
"""
|
||||
payloads = {
|
||||
"Herbert": b"herbert-payload-" * 512,
|
||||
"Other": b"other-payload-" * 512,
|
||||
}
|
||||
book_ids = []
|
||||
|
||||
for author, payload in payloads.items():
|
||||
book_data = BookCreate(
|
||||
library_id=test_library.id,
|
||||
title="Dune",
|
||||
authors=[author],
|
||||
pages=412,
|
||||
)
|
||||
book = await books_service.to_model_on_create(book_data.model_dump())
|
||||
assert isinstance(book, m.Book)
|
||||
|
||||
book.path = f"{test_library.root_path}/{author}/Dune"
|
||||
await aios.makedirs(book.path) # type: ignore[arg-type]
|
||||
Path(book.path, "Dune.epub").write_bytes(payload)
|
||||
book.files.append(
|
||||
m.FileMetadata(
|
||||
path="Dune.epub",
|
||||
size=len(payload),
|
||||
hash=f"hash-{author}",
|
||||
content_type=None,
|
||||
)
|
||||
)
|
||||
|
||||
books_service.repository.session.add(book)
|
||||
await books_service.repository.session.commit()
|
||||
book_ids.append(book.id)
|
||||
|
||||
archive_bytes = b"".join(
|
||||
[
|
||||
chunk
|
||||
async for chunk in await books_service.get_files(
|
||||
book_ids, test_library.id
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
with zipfile.ZipFile(BytesIO(archive_bytes)) as archive:
|
||||
assert archive.testzip() is None
|
||||
assert sorted(archive.namelist()) == ["Dune (2).epub", "Dune.epub"]
|
||||
|
||||
stored = {archive.read(name) for name in archive.namelist()}
|
||||
assert stored == set(payloads.values())
|
||||
|
||||
async def test_get_files_refuses_a_selection_with_nothing_in_it(
|
||||
self, books_service: BookService, test_library: m.Library
|
||||
) -> None:
|
||||
"""A miss is a 404, raised before the response starts.
|
||||
|
||||
Resolving lazily inside the stream meant an empty selection produced a valid
|
||||
22-byte archive under a 200, which tells the caller nothing went wrong.
|
||||
"""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
await books_service.get_files([424242], test_library.id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user