Skip to content

Latest commit

 

History

25 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PharoArchive

A high-level archive library for Pharo, backed by libarchive through FFI. It mirrors the Compression package's Archive document model, so common operations feel idiomatic, while the heavy lifting (read/write/extract for many container formats) is delegated to libarchive — preserving POSIX permissions and modification times for free.

  • Document modelLAArchive builds, inspects, edits and round-trips an archive in memory.
  • Streaming read/writeLAArchiveReader and LAArchiveWriter work directly with files, streams or byte buffers.
  • Native extraction — permissions and timestamps are preserved with no extra work.
  • Format auto-detection on read — you just open the archive; the format is detected.
  • Many writer formats — zip, tar (pax), gnutar, 7zip, cpio, xar and iso.

Requirements

  • A Pharo image (developed and tested against Pharo 14).
  • The libarchive native library. The FFI resolves it from CIG/lib relative to the Pharo working directory (i.e. ./CIG/lib/libarchive.{so|dylib|dll}). See Development for how CI supplies it.
  • The pharo-cig/pharo-archive (Compression) baseline, which provides the FFI plumbing (LibArchive, ArchiveEntry) and the ArchiveConstants pool this project builds on. It is pulled in automatically by the baseline below.

Installation

Load the project and its tests with Metacello:

Metacello new
	baseline: 'PharoArchive';
	repository: 'github://demarey/libarchive:main/src';
	load.

This loads the whole project including tests (the default group). To load only the core:

Metacello new
	baseline: 'PharoArchive';
	repository: 'github://demarey/libarchive:main/src';
	load: #PharoArchive.

Quick start

One-shot: build & extract a zip

If you just need to compress a folder into a zip, or unpack a zip — both in a single expression — this is the API for you. zip is libarchive's default on write and is auto-detected on read.

Build a zip from a directory and all of its subdirectories (one expression):

(LAArchive new
	addEntriesFromDirectory: '/path/to/folder';
	writeToFile: '/tmp/folder.zip')

Unpack that zip back, preserving permissions and modification times (one expression):

(LAArchive new
	readFrom: '/tmp/folder.zip';
	extractAllTo: '/tmp/extracted')

Building records each file's POSIX permission and modification-time metadata; extracting restores them natively. The subsections below keep the more granular, step-by-step coverage.

Build an archive from the document model and write it

| archive |
archive := LAArchive new.
archive
	addDirectory: 'docs';
	addString: 'Hello document!' as: 'hello.txt';
	addString: ('# Title', String lf) as: 'docs/notes.md';
	addFile: '/path/to/a-file.txt' as: 'assets/a-file.txt'.
archive writeToFile: '/tmp/example.zip'.

Read an archive back, inspect it and extract everything

| archive |
archive := (LAArchive new) readFrom: '/tmp/example.zip'.
archive memberNames.                                   "-> #('docs' 'docs/notes.md' 'hello.txt' 'assets/a-file.txt')"
archive contentsOf: 'hello.txt'.                       "a ByteArray"
archive extractAllTo: '/tmp/extracted'.

The extraction directory now contains the archive contents with their original permissions and modification times.

Stream an archive without building a document model

| target |
target := '/tmp/stream.zip'.
LAArchiveWriter writeToFile: target format: #zip during: [ :writer |
	writer
		addDirectory: 'docs';
		addFileNamed: 'hello.txt' content: 'file contents' asByteArray;
		addFileNamed: 'docs/notes.md' content: '# Title' asByteArray ].

Pack a whole directory tree

| archive |
archive := LAArchive new.
archive addEntriesFromDirectory: '/path/to/payload'.   "recursively walks the tree"
archive writeToFile: '/tmp/payload.tar'.               "choose any supported format"

Because the members are taken from real files, their POSIX permissions and mtimes are recorded in the archive.

Read a single entry's bytes and extract a single member

| reader entry |
reader := LAArchiveReader onFile: '/tmp/example.zip'.
entry := reader entries detect: [ :each | each pathname = 'hello.txt' ].
reader contentsOf: entry.                              "the raw bytes of that entry"

reader extractTo: '/tmp/all'.                          "native, perms + mtimes preserved"

| model |
model := LAArchive new readFrom: '/tmp/example.zip'.
model extractMember: 'hello.txt' to: '/tmp/single'.    "just one member"

Choose a different output format

Reads are format-agnostic (libarchive auto-detects). Writes pick a format by name:

| archive |
archive := LAArchive new.
archive
	addString: 'alpha' as: 'a.txt';
	addString: 'beta' as: 'b.txt'.
archive writeToFile: '/tmp/out.zip'.    "zip"
archive writeToFile: '/tmp/out.tar'.    "tar (pax)"
archive writeTo: '/tmp/out.7z' format: #sevenzip.
archive writeTo: '/tmp/out.cpio' format: #cpio.

Document model vs. backing file

LAArchive is both a document model and, after readFrom:/writeToFile:, a wrapper over a backing file. Mutations (addMember:/addFile:/addString:/addDirectory:, removeMember:, replaceMember:, setContentsOf:) change only the in-memory model; the backing file is untouched. This divergence is tracked with an internal isDirty flag:

  • extractAllTo: extracts natively from the backing file while the model still matches it (the fast path, preserving permissions/timestamps). Once the model has been edited, the model is the source of truth and is extracted instead, so your edits are honored.
  • writeTo:/writeToFile: serializes the (possibly edited) model and resets the flag — it produces an archive that faithfully reflects the document model.
  • contentsOf: and extractMember: always honor the model, reading on demand from the backing file when content was not explicitly set.

Example of editing a read archive and having the edits reflected on extraction:

| archive |
archive := LAArchive new readFrom: '/tmp/example.zip'.
archive setContentsOf: 'hello.txt' to: 'edited content'.
archive removeMember: 'docs/notes.md'.
archive addString: 'a new file' as: 'new.txt'.
archive extractAllTo: '/tmp/edited'.    "hello.txt has 'edited content'; notes.md is gone; new.txt present"

API overview

Class Responsibility Notable selectors
LAArchive High-level in-memory document model; round-trip to/from disk addMember: addFile: addString: addDirectory: addEntriesFromDirectory: removeMember: replaceMember: setContentsOf: contentsOf: extractAllTo: extractMember: readFrom: writeTo: writeToFile: members memberNamed: memberNames numberOfMembers isDirty
LAArchiveReader Opens an existing archive; enumerate, read bytes, extract natively onFile: onBytes: onStream: entries contentsOf: extractTo:
LAArchiveWriter Builds a new archive; streaming or block-based toFile: toStream: writeToFile:format:during: writeToStream:format:during: addEntryNamed: addFileNamed:content: addDirectory: addEntriesFromDirectory: finish
LAArchiveMember Immutable description of a single entry pathname size mtime mode filetype isDirectory isFile isSymlink symlink
LAArchiveFormat Value describing an output container format named: zip tar gnutar sevenZip cpio xar iso
LAArchiveError Raised when libarchive reports a failure (carries a readable message) signalOn:errorString:
LAArchiveWorkingDirectory Switches CWD around native extraction (thread-safe restore) in:during:

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages