Archive API
glyphive.archive defines the binary-safe archive record stream. Compression
is a separate stage.
glyphive.archive
glyphive — file tree ⇄ flat, binary-safe byte stream, plus the ignore filter and the (separate) compression stage.
This module turns a directory tree into one deterministic bytes blob that
:mod:glyphive.codec encodes for print and :mod:glyphive.layout lays out on
pages; it also provides the inverse parser, iter_records (consumed by
:mod:glyphive.restore.unarchive). Serialization is flat and relpath-keyed and
restore recreates directories. Every field is length-prefixed binary framing, so
the stream survives arbitrary bytes — including a file whose own content happens
to contain the magic or any framing bytes — rather than relying on a text
delimiter that a payload could collide with.
All filesystem access goes through pathlib_next.Path (never os /
pathlib directly).
Wire format (the "archive stream")
Everything is little-endian. struct format codes are given in parentheses.
┌──────────────────────────────────────────────────────────────────────┐
│ MAGIC 8 bytes = b"GLYPHIV1" │
│ VERSION 1 byte (B) = 2 │
│ META_FLAGS 1 byte (B) 0 = none, 1 = basic │
│ REC_COUNT 4 bytes (I) number of records that follow │
│ record * REC_COUNT │
└──────────────────────────────────────────────────────────────────────┘
Each record is:
┌──────────────────────────────────────────────────────────────────────┐
│ REC_TYPE 1 byte (B) 0 = file, 1 = explicitly-empty directory │
│ PATH_LEN 2 bytes (H) length of the UTF-8 relpath in bytes │
│ PATH PATH_LEN bytes relpath, UTF-8, "/"-separated (POSIX) │
│ [BASIC] MODE 2 bytes (H) permission bits (mode & 0o7777) │
│ [BASIC] MTIME 8 bytes (q) signed Unix milliseconds │
│ CONTENT_LEN 8 bytes (Q) content length in bytes (0 for a dir) │
│ CONTENT CONTENT_LEN bytes raw file bytes (absent for a dir) │
└──────────────────────────────────────────────────────────────────────┘
Version 1 remains readable. Its header is VERSION + REC_COUNT and
every record unconditionally contains the historical 4-byte st_mode and
8-byte float st_mtime fields. Version 2 is the default for new archives;
none omits optional metadata fields and basic stores only the ordinary
permission bits plus mtime rounded to integer milliseconds.
Because every variable-length field is length-prefixed, the content bytes are
never scanned for a delimiter and may contain any byte sequence, including
the magic or any framing bytes. Records are emitted in ascending relpath
order (Python string sort on the POSIX relpath) so the stream is deterministic
for a given tree.
Directories are normally reconstructable from the file relpaths, so only
explicitly-empty directories (a directory with no files anywhere beneath
it) get their own REC_TYPE == 1 record — this is what lets an empty dir
round-trip. Non-empty directories are implied by their files' paths.
Ignore filter
When use_ignore is true (the default, matching the CLI's normal mode), a
pathspec.PathSpec (gitignore) is built from .gitignore and
.ignore found at the tree root only and from any extra_ignore
pattern lines. Paths matching the spec (evaluated relative to the root, with a
trailing / for directories so dir/ patterns match) are skipped, and the
.git/ directory is always skipped regardless. use_ignore=False disables
all ignore filtering (the CLI's --no-ignore).
Limitation (v1): only root-level .gitignore / .ignore are read.
Nested .gitignore files deeper in the tree are not honored — out of scope
for v1.
Compression stage
Compression is a separate stage from serialization. The whole
archive_tree output is compressed by the caller, never per file.
compress / decompress support "none" (passthrough), "gzip"
(stdlib), and "zstd" (via the optional zstandard dependency, imported
lazily and only when requested). Which method was used is recorded in the page
header by :mod:glyphive.layout, not here.
gzip/deflate has no incremental validation — a single early error invalidates everything downstream — so integrity is provided around the compressed stream by the codec's per-line CRC and layout's per-page hashes, not by dropping compression or adding per-file checks.
FORMAT_VERSION = 2
module-attribute
MAGIC = b'GLYPHIV1'
module-attribute
METADATA_PROFILES = ('none', 'basic')
module-attribute
PathLike = _ty.Union[str, 'Path', _ty.Any]
module-attribute
REC_EMPTY_DIR = 1
module-attribute
REC_FILE = 0
module-attribute
V1_FORMAT_VERSION = 1
module-attribute
__all__ = ['MAGIC', 'FORMAT_VERSION', 'V1_FORMAT_VERSION', 'METADATA_PROFILES', 'REC_FILE', 'REC_EMPTY_DIR', 'Record', 'ArchiveMetadata', 'RecordHeader', 'RecordChunk', 'stream_metadata', 'write_archive', 'archive_tree', 'list_paths', 'iter_records', 'iter_record_events']
module-attribute
ArchiveMetadata
Bases: NamedTuple
Metadata parsed from an archive stream header.
metadata
instance-attribute
version
instance-attribute
Record
Bases: NamedTuple
One parsed entry from an archive stream.
mode and mtime are zero for version 2 metadata='none' records.
Version 1 records retain their historical metadata values. content is
b"" for an empty-directory record (type == REC_EMPTY_DIR).
content
instance-attribute
mode
instance-attribute
mtime
instance-attribute
path
instance-attribute
type
instance-attribute
RecordChunk
Bases: NamedTuple
A bounded piece of the content belonging to the preceding header.
data
instance-attribute
RecordHeader
Bases: NamedTuple
Metadata preceding one record's streamed content.
content_length
instance-attribute
mode
instance-attribute
mtime
instance-attribute
path
instance-attribute
type
instance-attribute
archive_tree(root, *, use_ignore=True, extra_ignore=None, metadata='none')
Serialize the tree at root into one deterministic archive-stream bytes.
root may be a pathlib_next.Path, a str, or any os.PathLike
(coerced). The tree is walked with root.walk(); the ignore filter and
.git pruning follow the module docstring. Output is byte-for-byte
deterministic for a given tree (records sorted by relpath).
metadata is "none" by default and may be "basic" to capture
ordinary permission bits and mtime. Compression is not applied here — the
caller compresses the whole result via :func:compress.
Source code in src/glyphive/archive.py
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 | |
iter_record_events(source, *, chunk_size=1024 * 1024, max_content_bytes=None)
Yield record headers and bounded content chunks from a binary source.
A :class:RecordHeader is followed by zero or more :class:RecordChunk
events totaling exactly content_length. max_content_bytes rejects a
declared per-record size before payload bytes are consumed.
Source code in src/glyphive/archive.py
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 | |
iter_records(data)
Parse an archive stream, yielding :class:Record in stream order.
Raises :class:ValueError on a truncated stream, unknown version/profile,
unknown record type, or trailing bytes. The full stream is validated by
restore before any records are materialized on disk.
Source code in src/glyphive/archive.py
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 | |
list_paths(root, *, use_ignore=True, extra_ignore=None)
Return the sorted POSIX relpaths that :func:archive_tree would archive.
Reuses the same walk + ignore logic, so the manifest :mod:glyphive.layout prints in the
page header exactly matches the archived records. Empty-directory entries
appear with a trailing / to distinguish them from files.
Source code in src/glyphive/archive.py
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 | |
stream_metadata(data)
Parse and return the archive version and explicit metadata profile.
Version 1 is reported as metadata='basic' because its historical wire
format always carried mode and mtime fields.
Source code in src/glyphive/archive.py
552 553 554 555 556 557 558 559 | |
write_archive(root, sink, *, use_ignore=True, extra_ignore=None, metadata='none', chunk_size=1024 * 1024)
Serialize root to a binary sink without buffering file contents.
The sorted manifest is retained in memory, but payload bytes are copied in chunks. File length is captured before the header is emitted; a concurrent truncation or growth raises instead of producing a malformed archive.
Source code in src/glyphive/archive.py
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | |