How Split ZIP is built
These are the engineering notes for Split ZIP: the technologies it is built on, what each one is, and how it is used in the tool.
Tech used
Independent parts, not a spanned archive
The classic way to split a ZIP is a spanned / multi-volume archive — .z01, .z02, … alongside a final .zip — where a single logical archive is cut across files, sometimes mid-entry. That format is fragile: every piece must be present and correctly named to open anything, and support across tools is uneven. This tool takes the other approach: each part is a normal, complete .zip that opens on its own, and together the parts contain every entry of the input. No piece depends on any other.
Bin-packing the entries
Deciding which entries go in which part is a bin-packing problem: items of known size assigned into bins of fixed capacity (the target part size), minimizing the number of bins. The tool uses first-fit-decreasing (FFD): sort the entries largest-first, and place each into the first part it still fits in, opening a new part only when none has room. FFD is deterministic and tends to produce few parts. An entry that alone exceeds the target can’t be split without spanning, so it is given its own part and flagged oversize.
Sizing by uncompressed size plus overhead
Packing is planned before any data is read, from each entry’s uncompressed size plus structural overhead — per-entry local and central headers, a UTF-8 extra field, a data-descriptor allowance, and a per-part end-of-central-directory record. Using the uncompressed size is deliberate: DEFLATE (from the Create ZIP notes) never expands data beyond its input by more than a small stored-block framing margin, so a part re-packed from entries whose uncompressed sizes fit the budget is guaranteed to end up at or below the target — whatever the input, however it was originally compressed. A well-compressed archive therefore often yields parts smaller than the target.
Re-packing with @zip.js/zip.js
Once the plan is fixed, the input’s central directory is read once with a @zip.js/zip.js ZipReader (from the Unzip notes), and each planned part gets its own ZipWriter(new BlobWriter('application/zip'), { useUnicodeFileNames: true }). The part’s assigned entries are streamed in — entry.getData(new BlobWriter()) out, writer.add(...) in — with directory entries re-added as directories and lastModDate preserved, and the UTF-8 filename flag (bit 11) set. close() yields each part as its own Blob, named archive-part-01.zip, archive-part-02.zip, and so on.
Shell
Same static Astro + Preact island and Service-Worker PWA shell as the other tools (see the HEIC notes); zip.js runs deflate in the Web Workers described in the Create ZIP notes.
Implementation & operational notes
The plan is an upper bound, so parts don’t overflow. The per-entry size estimate is a worst case — uncompressed data plus header and DEFLATE-framing margins — so the produced part is at or under the target rather than over it. The trade-off is that parts can come out noticeably smaller than the target when their entries compress well.
Oversize parts read predictably. Within a part, entries are ordered by their original position; oversize parts (a single file too big for the target) are sorted last, so the numbered parts stay in a readable order.
Read once, re-pack per part. The reader is opened a single time and closed in a finally; each part re-compresses its entries as it is written, so contents match the source while the compressed bytes may differ. An invalid or empty archive is rejected with a clear error before any part is produced.
Try it / source
- Tool: Split ZIP
- Source: github.com/GeppettoAndRomero/split-zip