fix: resolve file paths and stream the multi-book zip download
file.path is relative to book.path, so the archive matched nothing on disk and came out empty. Also namespace entries per book to stop filename collisions, and stream the zip instead of buffering it whole.
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
"""Tests for BookService"""
|
||||
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import aiofiles.os as aios
|
||||
|
||||
@@ -138,3 +142,63 @@ class TestBookServiceCRUD:
|
||||
updated_book = await books_service.get(book.id)
|
||||
assert len(updated_book.identifiers) == 1
|
||||
assert updated_book.identifiers[0].value == "9780261102999"
|
||||
|
||||
async def test_get_files_zips_every_book_file(
|
||||
self, books_service: BookService, test_library: m.Library
|
||||
) -> None:
|
||||
"""The multi-book download must contain the real bytes of each file.
|
||||
|
||||
`file.path` holds a bare filename relative to `book.path`, so resolving it on
|
||||
its own matched nothing on disk and yielded a valid but empty archive.
|
||||
"""
|
||||
contents = {
|
||||
"Dracula.epub": b"epub-payload-" * 512,
|
||||
"Dracula.pdf": b"pdf-payload-" * 512,
|
||||
}
|
||||
|
||||
book_data = BookCreate(
|
||||
library_id=test_library.id,
|
||||
title="Dracula",
|
||||
authors=["Bram Stoker"],
|
||||
pages=418,
|
||||
)
|
||||
book = await books_service.to_model_on_create(book_data.model_dump())
|
||||
assert isinstance(book, m.Book)
|
||||
|
||||
book.path = f"{test_library.root_path}/Bram Stoker/Dracula"
|
||||
await aios.makedirs(book.path) # type: ignore[arg-type]
|
||||
|
||||
for name, payload in contents.items():
|
||||
Path(book.path, name).write_bytes(payload)
|
||||
book.files.append(
|
||||
m.FileMetadata(
|
||||
path=name,
|
||||
size=len(payload),
|
||||
hash=f"hash-{name}",
|
||||
content_type=None,
|
||||
)
|
||||
)
|
||||
|
||||
books_service.repository.session.add(book)
|
||||
await books_service.repository.session.commit()
|
||||
|
||||
archive_bytes = b"".join(
|
||||
[
|
||||
chunk
|
||||
async for chunk in books_service.get_files([book.id], test_library.id)
|
||||
]
|
||||
)
|
||||
|
||||
with zipfile.ZipFile(BytesIO(archive_bytes)) as archive:
|
||||
assert archive.testzip() is None
|
||||
|
||||
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"}
|
||||
|
||||
for name, payload in contents.items():
|
||||
entry = next(n for n in names if Path(n).name == name)
|
||||
assert archive.read(entry) == payload
|
||||
|
||||
Reference in New Issue
Block a user