Choosing how to read
With no options, open_archive lets you read members in any order, with one member open at a time.
That suits most programs. Three options change it for the cases where the default is slow or not
enough: streaming, seekable_members and concurrent_operations. Opening an archive also applies a
few limits, such as how many members it may list, which Archives you
trust explains how to raise.
Solid archives
A ZIP compresses each member on its own, so archivey can jump straight to any of them. A
.tar.gz compresses the whole archive as one stream, so reaching a member in the middle means
decompressing everything before it.
An archive whose members are compressed together like this is called solid. Every
compressed TAR is solid, and so are most 7z archives, since 7-Zip creates solid archives by
default. RAR archives are solid only when created with that option. On a solid archive,
reading members by name can decompress the same data again and again. stream_members
decompresses it once. archivey tells you when this happens: on a solid 7z or RAR archive, the
first open that decompresses data an earlier open already decompressed logs one INFO message
on the archivey.backends logger. Reading the same member twice counts only the data before it in the archive. The message
shows only if your application turns logging on.
If your code needs to know whether an archive is solid, archive.cost tells you before you read
anything. Its access_cost is AccessCost.SOLID when reaching a member means decompressing others
first, and AccessCost.DIRECT when a member can be reached directly. Its listing_cost says what
listing the members takes. It's ListingCost.INDEXED when the archive keeps a list of its members,
as ZIP and 7z do when read from a file. It's ListingCost.REQUIRES_SCANNING when listing requires
jumping from one member's header to the next, as in a plain .tar, a RAR, or a ZIP read from a pipe.
It's ListingCost.REQUIRES_DECOMPRESSION when the headers are inside compressed data, as in a
.tar.gz. Its stream_capability is StreamCapability.SEEKABLE when the source you opened can
seek, and StreamCapability.FORWARD_ONLY when it can only be read forward, as a pipe.
archive.cost is worked out once, when the archive opens. It describes what each kind of read
costs, but it never allows or refuses one: the options below do that.
Reading once
import sys
with archivey.open_archive(sys.stdin.buffer, streaming=True) as archive:
for member, stream in archive.stream_members():
...
streaming=True is a promise to read the archive once, from start to end. When set, open, read
and members() raise, so an out-of-order read fails right away instead of quietly costing time. If
you plan to read an archive in one pass, setting it catches that mistake, even on an ordinary file.
You get one pass, through stream_members, for member in archive or extract_all, and it is
used up even if you leave the loop early. Inside a stream_members loop, open, read and
members() raise even without streaming=True, since the pass is still using the archive. A pass
you leave with break ends at once on CPython. One kept in a variable, or one an exception left,
holds the archive until you close it, so use it in a with block when the loop can stop early:
with archive.stream_members() as members: ends the pass at the end of the block, and with
iter(archive) as members: does the same for a loop over members. A pass or member stream that
nothing refers to any more never blocks the next call, on any Python: when a call finds the archive
held, archivey runs a garbage collection and tries once more before it raises. With
concurrent_operations=True they work, from any thread, on every format,
and so does extract_all.
Streams from the pass never seek.
A pipe, a socket or an HTTP response can only be read this way. TAR and ZIP archives and single compressed files can come from one. 7z, RAR and ISO archives need a file or another source that can seek.
A ZIP keeps the full details of its members, such as permissions, symlinks and comments, in an
index at its end. From a pipe, archivey finds each member by the short header in front of it, and
gives you the member with what that header says: no permissions, and a symlink looks like a file.
When the pass reaches the index, archivey fills in the rest on the members you already have.
Until then member.member_state_final is False. From a pipe, the pass reaches a ZIP's index only
when the sender closes the pipe, so the loop doesn't finish while the sender keeps it open.
A compressed file or a compressed tar from a pipe that the sender keeps open is decompressed from what has arrived. A short one can still wait at open, and a read past its last byte waits for the sender to close the pipe (Single-file compressors says where).
Why can't the others come from a pipe?
7z keeps its index at the end of the file, and reading an ISO means jumping between places in
it. RAR stores a header before each member, as ZIP does, so archivey could read it from a
pipe, copying what it reads to a temporary file for unrar as it goes. It doesn't yet.
If you have a 7z, RAR or ISO archive in a pipe, copying it to a file or to an io.BytesIO first
lets archivey read it.
Seeking inside a member
Some code needs to seek inside a member: a library that reads a ZIP stored inside the archive, a
Parquet reader, an image decoder. By default a member stream only moves forward, and seek raises.
With seekable_members=True, streams from open can seek, and an archive stored inside another one
opens from such a stream like from a file. Moving backwards in compressed data can
mean decompressing the member again from its start. If you install the seekable extra, archivey
reads bzip2 data, and large DEFLATE data, through an
accelerator that marks places it can restart from as it
reads, and every seek resumes from the nearest marked place before its target. DEFLATE is the
compression in gzip files and in most ZIP members. Without the extra, a gzip file made of several
members, such as one bgzip writes, resumes from the nearest member start. A zstd file made of
several frames, such as one in the Seekable Zstandard format, always resumes from the nearest frame
start. For a bgzip file or a Seekable Zstandard file, archivey finds those starts without
decompressing anything. A forward seek keeps the checksum check when reaching the target means
decoding the bytes in between, which is the usual case in compressed data. A seek that can jump
over those bytes instead, as the accelerator can, gives the check up once you read on from there,
until you seek back to a point you had already read up to. A seek to or past the member's end keeps
it either way. Damaged archives lists the other seeks
that jump, and what is still checked after one. If you'll seek a lot, extracting the member to a
file first is often faster.
Several members at once
By default only one member stream can be open at a time, and opening a second while the first is
still open raises. A thread pool that reads different members at once needs
concurrent_operations=True, which can't be combined with streaming=True. Each stream is opened the
same way open always opens one, so on a solid archive it may first decompress the members stored
before it. The streams share the source and take turns reading compressed data from it. On a ZIP, each
member is then decompressed on its own, so several of those can run at once. On a TAR, and on an
ISO, they share one file handle, so one read waits while another has it. Opening the archive once
per worker still reads the index each time, which these streams do not. The exception is a source
where each jump to a new position is costly, such as a member of another archive or a file read
over the network. There, streams that
take turns make the source jump back and forth, and each jump throws away what it had buffered.
With concurrent_operations=True, threads can also share the rest of the reader. They can list
members, iterate, run stream_members() passes and extract_all() at the same time, and if several
need the member list at once, one builds it while the others wait. Each member stream still belongs
to one thread. Opening the archive once per thread is also safe, as long as each thread opens it
from its own path or file: one file object belongs to one reader. close() waits for an open()
or get() running in another thread, then closes the streams still open on it. It does not wait
for reads on those streams: a read running in another thread returns what it already had or
raises ValueError, as a closed file does.
Without concurrent_operations=True the reader belongs to one thread. If another thread is inside
a reader call, such as members(), open(), or a stream_members() or extract_all() pass,
close() raises ArchiveyUsageError and the reader stays open. Otherwise the close goes ahead, and
the next read() on a member stream it closed raises ValueError, as a closed file does. Once a
close() has returned, calling it again does nothing; after one that raised, the reader is still
open and you still have to close it.
The functions you give archivey, such as on_diagnostic, on_progress, a password provider, or a
members= or filter= function, run on the thread whose call to archivey needs them, before that
call returns. When several threads wait for the member list, the thread that builds it runs the
functions that listing calls. One case falls outside your calls: a member stream or a
stream_members() pass that you don't close is closed when Python frees it, on whatever thread
Python frees it on, and a diagnostic that this close reports reaches on_diagnostic on that
thread. That is usually the thread that drops the last reference to it, but for an object caught
in a reference cycle it is the thread where the garbage collector happens to run. Close streams and passes yourself, or use with, to keep every call on your own
threads. Archivey never calls your code from a thread it started.
The same result everywhere
With the seekable extra installed, archivey reads large DEFLATE data, and bzip2 data opened with
seekable_members=True, through the rapidgzip accelerator,
which is faster on large files. It returns the same data and errors as the standard library and
tools such as 7-Zip and unzip, except for a few files built on purpose, which no known tool writes:
a ZIP or 7z member holding two DEFLATE streams under one CRC
(Where the guarantees stop), and a cut .gz whose last
bytes are made to look like its trailer
(Limits and accelerators). For the first, the result can also
change with how many cores the machine has and with the platform. If your code must give the same
answer as other tools, on every machine, and you would rather it be slower than differ, turn the
accelerator off:
config = archivey.ArchiveyConfig(
use_rapidgzip=archivey.AcceleratorMode.OFF,
use_indexed_bzip2=archivey.AcceleratorMode.OFF,
)
with archivey.open_archive("data.zip", config=config) as archive:
...
The standard library then decodes everything. A backward seek in gzip or bzip2 data decompresses again from the start of the member, and a large gzip file reads on one core.
Why these aren't on by default
Seeking and reading several members at once are off by default, and streaming=True turns off
out-of-order reads. Each of these can be slow in some cases, and nothing in the calling code shows
it. Some seeks also skip the check for damage. With these defaults, the risky pattern raises instead
of running.
A ZIP can be read out of order at no cost, but a .tar.gz can't. If archivey raised only on the
.tar.gz, code tested on ZIPs would first fail in production. So archivey raises on every
format, and the mistake shows up during development.
Summary
| If you want to | Open with | What it costs |
|---|---|---|
| Read a few members by name, or list and then read | nothing | Out-of-order reads can be slow on solid archives |
| Read everything once | streaming=True |
One pass only; no reads by name |
| Read from a pipe | streaming=True |
TAR, ZIP and single compressed files only |
| Seek inside a member | seekable_members=True |
A backward seek may decompress again, and a seek that jumps over data stops the checksum check |
| Read members from several threads | concurrent_operations=True |
Slow on sources where each jump is costly |
| Get the same result as other tools, on every machine | use_rapidgzip and use_indexed_bzip2 set to OFF in ArchiveyConfig |
Large gzip and bzip2 reads are slower |