Opening and listing
Open an archive — unlocking it first if it needs a password — and find out what is inside. Reading the bytes out is Reading members.
Open and list
import archivey
with archivey.open_archive("photos.zip") as reader:
for member in reader: # archive order
print(member.name, member.size, member.type)
members = reader.members() # the full list, or an error
info = reader.get("subdir/a.txt") # by name
print(reader.format, reader.cost)
By default you can open any member you like, in any order. That is what most callers want, and it is what the example above relies on. It is not always the cheapest way to read, though — Reading members covers when to make one forward pass instead.
If your source is a pipe or another non-seekable stream, pass streaming=True for a
forward-only single pass. Without it the open fails immediately rather than halfway
through — see What you can open for which formats can be read
this way at all.
open_archive or open_stream?
A .gz, .bz2, .xz, .zst and so on holds one compressed payload, with no
archive structure around it. Either entry point handles that; they differ in what you
get back:
archivey.open_archive("logs.tar.gz") # an archive: the files inside the tar
archivey.open_stream("access.log.gz") # a stream: the decompressed bytes
open_archive works on a plain .gz too — you get an archive with exactly one
member, named after the file. Use open_stream when you just want the bytes and
know there is no tar inside.
What you can open
| Source | What happens |
|---|---|
| A path to a file | Detected and opened |
| A path to a directory | Opens as a pseudo-archive, one member per file |
| An open binary stream | Any format if it is seekable; only some formats if not — see below |
| A sequence of paths or streams | The volumes of one multi-volume archive — see below |
Passing a format= that says anything other than a directory, for a path that is one,
raises ArchiveyUsageError rather than quietly reading the directory tree instead.
A seekable stream is read from wherever it currently is, through to the end. Archivey treats the current position as byte 0 of the archive, so an archive stored at a known offset inside a larger file opens without copying it out: seek to its first byte and hand the stream over. There is no matching end bound, so this works when the archive runs to the end of the stream; if something follows it, wrap the stream in your own bounded view first.
A non-seekable stream — a pipe, a socket, an HTTP response body — needs
streaming=True, and works for TAR (including compressed tar) and the single-file
compressors. ZIP, 7z, RAR and ISO keep their index at the end of the archive or
address it by offset, so they have to seek: opening one from a pipe raises
StreamNotSeekableError, and the fix is to buffer it to a file or a BytesIO first.
You do not have to find that out by trying. format_availability(fmt).required_source
is the weakest source shape the format can be read from, so "pipe it if you can,
otherwise spool it to disk" is a comparison rather than a try/except:
from archivey import StreamCapability, detect_format, format_availability
if format_availability(detect_format(head)).required_source <= StreamCapability.FORWARD_ONLY:
... # feed the pipe straight in with streaming=True
else:
... # spool to a file first
StreamCapability is ordered (FORWARD_ONLY < SEEKABLE), which is why <= reads as
"this source is strong enough" — and why the same comparison works against an already
open archive's reader.cost.stream_capability.
Multi-volume archives
Only 7z and RAR split across volumes. Pass the path of any one volume and Archivey finds the rest, in the naming schemes those tools produce:
| Scheme | Give it |
|---|---|
backup.7z.001 / backup.exe.001 / backup.zip.001, .002, … |
Any numbered part, or the stub backup.exe |
backup.part1.rar / backup.part1.sfx, .part2.rar, … |
Any part |
backup.rar / backup.exe / backup.sfx + backup.r00, .r01, … |
The .rar, the SFX stub, or any .rNN |
A 7z set is checked for completeness, so a missing middle part is an error rather
than a silent short read. The stub executable beside an SFX numbered set is not
concatenated into the volumes; opening it follows the first volume
(backup.exe.001, backup.7z.001, or backup.zip.001) so the same path works
for Linux 7-Zip and Windows 7-Zip, including when you pass format= after
detect_format. A file that is a self-extracting archive
(magic behind the stub) still opens as that archive, even if numbered parts sit
beside it.
The old RAR scheme needs a first volume either way: <base>.rar, or an SFX
<base>.exe / <base>.sfx beside the .rNN files. A .rNN on its own is read
as a lone file rather than as part of a set. A lone numbered part
(.7z.001 / .zip.001 / .exe.001 with no siblings) is an incomplete set,
not a silent mis-parse.
You can also pass the volumes yourself, as an ordered sequence of paths or open streams — useful when they are not siblings on disk, or not on disk at all. Do that and the order you give is the order used, with no discovery. A one-item sequence is treated as a single source, and a multi-volume sequence for any format other than 7z or RAR raises.
Detection
Most callers never need this: open_archive detects the format itself. Use
detect_format when you want to know what a file is before deciding to open it.
Content wins over filename. Archivey looks at the bytes first and falls back to
the extension only when they are inconclusive. When the two disagree it uses the
bytes and tells you, via a FORMAT_EXTENSION_CONFLICT
diagnostic naming both candidates — a .jpg that is
really a ZIP opens fine, and so does a .cbr that is a ZIP (the usual comic
mislabel). You can still find out that the name lied.
detect_format reports the same format open_archive would use, with one wrinkle
worth knowing. Telling a .tar.zst from a plain .zst means decompressing a little
of it to look for the tar header, so when that compressor's package is not installed
the check cannot run and the bare compressor is reported instead. You are not left
guessing: opening the file raises UnsupportedFormatError, naming the package to
install.
See Install and extras.
Passwords
archivey.open_archive("secret.7z", password="hunter2")
archivey.open_archive("secret.zip", password=["likely", "fallback"])
Put the most likely password first: every wrong candidate costs work before it is rejected, which can be expensive (especially on 7z).
Passing a password to a format that has no encryption at all — a tar, say — is
accepted and never consulted, and records a PASSWORD_ARGUMENT_UNUSED diagnostic
you can query on reader.diagnostics. That is deliberate. password= is a keyring you are offering, not a claim that this
archive is encrypted — "here are the twenty passwords we know, open whatever you can"
is the point of the list form — so one plain .tar in a batch should not stop the run.
All three forms behave alike here: a string, a list, and a PasswordProvider callable.
A wrong password on an archive that really is encrypted still fails loudly with
EncryptionError, which is the case that actually costs you something.
Damaged archives
members() and scan_members() give you the whole listing or raise — if the archive
is damaged partway through, you get an error, never a quietly shortened list.
members_report() is the other half of that deal: it hands back the members it did
manage to read together with the error that stopped it. Iterating yields members up
to the damage and then raises.
Errors and diagnostics has the recipe and what each failure means.
Duplicate names and is_current
Appending to a tarball, or updating a 7z, can leave the same member name in the
archive more than once. Archivey never hides the older copies — members() and
iteration return every entry — but it marks which one is live:
- The last entry with a given name has
is_current=True. - Earlier entries with that name have
is_current=False.
reader.get(name) and reader.open(name) follow the same rule: a name resolves to
the last entry, which is the live one.
extract_all also follows it. Superseded entries are skipped and reported as
ExtractionStatus.SUPERSEDED (distinct from NOT_OVERWRITTEN, which is about files
already on disk, and from OVERWRITTEN, which is a member that was written this run
and then had its destination taken by a later one), so what lands on disk matches a
fresh write.
Selecting members by name is the one place to be careful. A name in a selector —
extract_all(members=["notes.txt"]), stream_members(members=["notes.txt"]) —
matches every entry with that name, not just the live one. For extract_all that
is harmless, since the superseded ones are skipped anyway; stream_members has no
such skip and will hand you each version in turn. Pass the ArchiveMember itself
when you mean one specific entry — selectors match those by identity.
In your own code, filter for the live state:
Or keep every version, for a history view: