Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ and the optional third argument is the compression format (called “filter” i
libarchive). The acceptable values are listed in ``libarchive.ffi.WRITE_FORMATS``
and ``libarchive.ffi.WRITE_FILTERS``.

Symbolic links
~~~~~~~~~~~~~~

By default, libarchive preserves symbolic links. If you want it to resolve the
links and archive the files they point to instead, pass ``symlink_mode='logical'``
when calling the ``add_files`` method. If you do that, an ``ArchiveError``
exception will be raised when a symbolic link points to a nonexistent file.

File metadata codecs
--------------------

Expand Down
3 changes: 3 additions & 0 deletions libarchive/ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ def get_write_filter_function(filter_name):
ffi('read_disk_open', [c_archive_p, c_char_p], c_int, check_int)
ffi('read_disk_open_w', [c_archive_p, c_wchar_p], c_int, check_int)
ffi('read_disk_descend', [c_archive_p], c_int, check_int)
ffi('read_disk_set_symlink_hybrid', [c_archive_p], c_int, check_int)
ffi('read_disk_set_symlink_logical', [c_archive_p], c_int, check_int)
ffi('read_disk_set_symlink_physical', [c_archive_p], c_int, check_int)

# archive_read_data

Expand Down
18 changes: 17 additions & 1 deletion libarchive/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def add_entries(self, entries):

def add_files(
self, *paths, flags=0, lookup=False, pathname=None, recursive=True,
**attributes
symlink_mode=None, **attributes
):
"""Read files through the OS and add them to the archive.

Expand All @@ -63,6 +63,9 @@ def add_files(
recursive (bool):
when False, if a path in `paths` is a directory,
only the directory itself is added.
symlink_mode (Literal['hybrid', 'logical', 'physical'] | None):
how symbolic links should be handled; see `man archive_read_disk`
for meanings
attributes (dict): passed to `ArchiveEntry.modify()`

Raises:
Expand All @@ -75,10 +78,23 @@ def add_files(
if block_size <= 0:
block_size = 10240 # pragma: no cover

set_symlink_mode = None
if symlink_mode:
try:
set_symlink_mode = getattr(
ffi, f'read_disk_set_symlink_{symlink_mode}'
)
except AttributeError:
raise ValueError(
f"symlink_mode value {symlink_mode!r} is invalid"
) from None

entry = ArchiveEntry(header_codec=self.header_codec)
entry_p = entry._entry_p
for path in paths:
with new_archive_read_disk(path, flags, lookup) as read_p:
if set_symlink_mode:
set_symlink_mode(read_p)
while 1:
r = read_next_header2(read_p, entry_p)
if r == ARCHIVE_EOF:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_rwx.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io
import json
import os

import libarchive
from libarchive.entry import format_time
Expand Down Expand Up @@ -181,3 +182,32 @@ def write_callback(data):
)
assert archive_entry.uid == 1000
assert archive_entry.gid == 1000


def test_symlinks(tmpdir):
os.chdir(tmpdir)
with open('empty', 'w'):
pass
with open('unreadable', 'w') as f:
f.write('secret')
os.chmod('unreadable', 0)

os.symlink('empty', 'symlink-to-empty')
os.symlink('unreadable', 'symlink-to-unreadable')

with libarchive.file_writer('archive.tar', 'gnutar') as archive:
archive.add_files('symlink-to-empty', symlink_mode='hybrid')
with pytest.raises(libarchive.ArchiveError):
archive.add_files('symlink-to-unreadable', symlink_mode='logical')
archive.add_files('symlink-to-unreadable', symlink_mode='physical')

with libarchive.file_reader('archive.tar') as archive:
entries = iter(archive)
e1 = next(entries)
assert e1.pathname == 'symlink-to-empty'
assert e1.isreg
assert e1.size == 0
e2 = next(entries)
assert e2.pathname == 'symlink-to-unreadable'
assert e2.issym
assert e2.linkpath == 'unreadable'