Skip to content

Commit e3b7d83

Browse files
authored
Merge pull request #137 from nvinson/symlink-mode-support
2 parents 1a5b505 + b313c1d commit e3b7d83

File tree

4 files changed

+58
-1
lines changed

4 files changed

+58
-1
lines changed

README.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ and the optional third argument is the compression format (called “filter” i
114114
libarchive). The acceptable values are listed in ``libarchive.ffi.WRITE_FORMATS``
115115
and ``libarchive.ffi.WRITE_FILTERS``.
116116

117+
Symbolic links
118+
~~~~~~~~~~~~~~
119+
120+
By default, libarchive preserves symbolic links. If you want it to resolve the
121+
links and archive the files they point to instead, pass ``symlink_mode='logical'``
122+
when calling the ``add_files`` method. If you do that, an ``ArchiveError``
123+
exception will be raised when a symbolic link points to a nonexistent file.
124+
117125
File metadata codecs
118126
--------------------
119127

libarchive/ffi.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,9 @@ def get_write_filter_function(filter_name):
286286
ffi('read_disk_open', [c_archive_p, c_char_p], c_int, check_int)
287287
ffi('read_disk_open_w', [c_archive_p, c_wchar_p], c_int, check_int)
288288
ffi('read_disk_descend', [c_archive_p], c_int, check_int)
289+
ffi('read_disk_set_symlink_hybrid', [c_archive_p], c_int, check_int)
290+
ffi('read_disk_set_symlink_logical', [c_archive_p], c_int, check_int)
291+
ffi('read_disk_set_symlink_physical', [c_archive_p], c_int, check_int)
289292

290293
# archive_read_data
291294

libarchive/write.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def add_entries(self, entries):
4646

4747
def add_files(
4848
self, *paths, flags=0, lookup=False, pathname=None, recursive=True,
49-
**attributes
49+
symlink_mode=None, **attributes
5050
):
5151
"""Read files through the OS and add them to the archive.
5252
@@ -63,6 +63,9 @@ def add_files(
6363
recursive (bool):
6464
when False, if a path in `paths` is a directory,
6565
only the directory itself is added.
66+
symlink_mode (Literal['hybrid', 'logical', 'physical'] | None):
67+
how symbolic links should be handled; see `man archive_read_disk`
68+
for meanings
6669
attributes (dict): passed to `ArchiveEntry.modify()`
6770
6871
Raises:
@@ -75,10 +78,23 @@ def add_files(
7578
if block_size <= 0:
7679
block_size = 10240 # pragma: no cover
7780

81+
set_symlink_mode = None
82+
if symlink_mode:
83+
try:
84+
set_symlink_mode = getattr(
85+
ffi, f'read_disk_set_symlink_{symlink_mode}'
86+
)
87+
except AttributeError:
88+
raise ValueError(
89+
f"symlink_mode value {symlink_mode!r} is invalid"
90+
) from None
91+
7892
entry = ArchiveEntry(header_codec=self.header_codec)
7993
entry_p = entry._entry_p
8094
for path in paths:
8195
with new_archive_read_disk(path, flags, lookup) as read_p:
96+
if set_symlink_mode:
97+
set_symlink_mode(read_p)
8298
while 1:
8399
r = read_next_header2(read_p, entry_p)
84100
if r == ARCHIVE_EOF:

tests/test_rwx.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io
44
import json
5+
import os
56

67
import libarchive
78
from libarchive.entry import format_time
@@ -181,3 +182,32 @@ def write_callback(data):
181182
)
182183
assert archive_entry.uid == 1000
183184
assert archive_entry.gid == 1000
185+
186+
187+
def test_symlinks(tmpdir):
188+
os.chdir(tmpdir)
189+
with open('empty', 'w'):
190+
pass
191+
with open('unreadable', 'w') as f:
192+
f.write('secret')
193+
os.chmod('unreadable', 0)
194+
195+
os.symlink('empty', 'symlink-to-empty')
196+
os.symlink('unreadable', 'symlink-to-unreadable')
197+
198+
with libarchive.file_writer('archive.tar', 'gnutar') as archive:
199+
archive.add_files('symlink-to-empty', symlink_mode='hybrid')
200+
with pytest.raises(libarchive.ArchiveError):
201+
archive.add_files('symlink-to-unreadable', symlink_mode='logical')
202+
archive.add_files('symlink-to-unreadable', symlink_mode='physical')
203+
204+
with libarchive.file_reader('archive.tar') as archive:
205+
entries = iter(archive)
206+
e1 = next(entries)
207+
assert e1.pathname == 'symlink-to-empty'
208+
assert e1.isreg
209+
assert e1.size == 0
210+
e2 = next(entries)
211+
assert e2.pathname == 'symlink-to-unreadable'
212+
assert e2.issym
213+
assert e2.linkpath == 'unreadable'

0 commit comments

Comments
 (0)