Skip to content

Async book export#17

Merged
driveton merged 9 commits intomainfrom
async-book-export
Feb 25, 2026
Merged

Async book export#17
driveton merged 9 commits intomainfrom
async-book-export

Conversation

@driveton
Copy link
Copy Markdown
Owner

Summary

Async book export with email download link

  • Replaces the synchronous export with a background job. POST /books/:id/exports creates a Book::Export record and enqueues Book::ExportJob. The job streams the ZIP via
  • ZipFile.create_for, attaches it with ActiveStorage, then emails the user a direct download link when ready. One export per user per book is kept previous exports are destroyed on each new request.

A completed export's filename also appears as a download link on the edit page for convenience. A CSS-only flash toast (fixed-position, fades out after 4s) confirms the export was queued.

driveton added 9 commits February 24, 2026 17:09
Writer now accepts an external IO (Tempfile) rather than managing its
own buffer. ZipKit::Streamer writes directly to that IO, avoiding
loading the whole archive in memory. Reader gains a block form that
yields a ZipFile::Reader::IO, a streaming IO wrapper around a single
zip entry backed by ZipKit's extractor.
EpubArchiver#generate now writes to a Tempfile instead of StringIO,
so large EPUBs are never held fully in memory. Archiver#write_epub
streams the Tempfile into the ZIP entry via IO.copy_stream with
compress: false — EPUBs are already ZIP-compressed, so double-
compressing is wasteful and slow.
Register IO as an inflection acronym so Zeitwerk resolves
reader/io.rb to ZipFile::Reader::IO instead of ZipFile::Reader::Io.

Update Book::ArchiverTest to pass a ZipFile::Writer to generate and
read back from the resulting Tempfile. Update EpubArchiverTest to
expect a Tempfile from generate instead of StringIO.
ZipFile::Writer is now an IO-like proxy: write() forwards to the
underlying IO while tracking byte_size and building an MD5 digest,
and the streamer is wired through self so tracking is accurate.
stream_to() and checksum() are restored.

Book::Exportable#export uses the Writer+Tempfile pattern to match
the updated Archiver#generate signature.
POST /books/:book_id/exports destroys any existing export for the
current user, creates a new Book::Export record (status: pending),
and enqueues Book::ExportJob. The job calls export.build, which
streams the ZIP via ZipFile.create_for and attaches it with
ActiveStorage. Status flows pending → processing → completed (or
failed). Only the create action is exposed — download is via email.
One export per user per book is kept.
Books::ExportMailer#ready sends a download link once the export job
completes. Views live under app/views/mailers/ and ApplicationMailer
uses append_view_path to find them there. The download link uses
rails_blob_url so it works from any mailer context.
BooksController#edit loads the current user's most recent export.
If it is completed, a download link showing the filename appears
next to the export button in the footer nav.
A fixed-position toast fades in and out over 4 seconds using a CSS
animation, with no JavaScript required. The layout renders it whenever
a notice flash is set.
@driveton driveton merged commit abf6f43 into main Feb 25, 2026
4 checks passed
@driveton driveton deleted the async-book-export branch February 25, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant