Skip to content

Commit 6a83ea1

Browse files
Merge pull request #8856 from ThomasWaldmann/tar-posix-ACLs
export-tar/import-tar: support for POSIX ACLs
2 parents eb136a3 + fe5a991 commit 6a83ea1

File tree

4 files changed

+103
-0
lines changed

4 files changed

+103
-0
lines changed

src/borg/archive.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,12 @@ def s_to_ns(s):
15661566
bkey = key.encode("utf-8", errors="surrogateescape")
15671567
bvalue = value.encode("utf-8", errors="surrogateescape")
15681568
xattrs[bkey] = bvalue
1569+
elif key == SCHILY_ACL_ACCESS:
1570+
# Process POSIX access ACL
1571+
item.acl_access = value.encode("utf-8", errors="surrogateescape")
1572+
elif key == SCHILY_ACL_DEFAULT:
1573+
# Process POSIX default ACL
1574+
item.acl_default = value.encode("utf-8", errors="surrogateescape")
15691575
if xattrs:
15701576
item.xattrs = xattrs
15711577
yield item, status

src/borg/archiver/tar_cmds.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ def item_to_paxheaders(format, item):
216216
key = SCHILY_XATTR + bkey.decode("utf-8", errors="surrogateescape")
217217
value = bvalue.decode("utf-8", errors="surrogateescape")
218218
ph[key] = value
219+
# Add POSIX access and default ACL if present
220+
acl_access = item.get("acl_access")
221+
if acl_access is not None:
222+
ph[SCHILY_ACL_ACCESS] = acl_access.decode("utf-8", errors="surrogateescape")
223+
acl_default = item.get("acl_default")
224+
if acl_default is not None:
225+
ph[SCHILY_ACL_DEFAULT] = acl_default.decode("utf-8", errors="surrogateescape")
219226
if format == "BORG": # BORG format additions
220227
ph["BORG.item.version"] = "1"
221228
# BORG.item.meta - just serialize all metadata we have:

src/borg/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@
125125

126126
# tar related
127127
SCHILY_XATTR = "SCHILY.xattr." # xattr key prefix in tar PAX headers
128+
SCHILY_ACL_ACCESS = "SCHILY.acl.access" # POSIX access ACL in tar PAX headers
129+
SCHILY_ACL_DEFAULT = "SCHILY.acl.default" # POSIX default ACL in tar PAX headers
128130

129131
# special tags
130132
# @PROT protects archives against accidential deletion or modification by delete, prune or recreate.

src/borg/testsuite/archiver/tar_cmds_test.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from . import assert_dirs_equal, _extract_hardlinks_setup, cmd, requires_hardlinks, RK_ENCRYPTION
1111
from . import create_test_files, create_regular_file
1212
from . import generate_archiver_tests
13+
from ...platform import acl_get, acl_set
14+
from ..platform.platform_test import skipif_not_linux, skipif_acls_not_working
1315

1416
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA
1517

@@ -269,3 +271,89 @@ def test_roundtrip_pax_xattrs(archivers, request):
269271
extracted_path = os.path.abspath("input/file")
270272
xa_value_extracted = xattr.getxattr(extracted_path.encode(), xa_key)
271273
assert xa_value_extracted == xa_value
274+
275+
276+
@skipif_not_linux
277+
@skipif_acls_not_working
278+
def test_acl_roundtrip(archivers, request):
279+
"""Test the complete workflow for POSIX ACLs with export-tar and import-tar.
280+
281+
This test follows the workflow:
282+
1. set filesystem ACLs
283+
2. create a Borg archive
284+
3. export-tar this archive
285+
4. import-tar the resulting tar file
286+
5. extract the imported archive
287+
6. check the expected ACLs in the filesystem
288+
"""
289+
archiver = request.getfixturevalue(archivers)
290+
291+
# Define helper functions for working with ACLs
292+
def get_acl(path):
293+
item = {}
294+
acl_get(path, item, os.stat(path))
295+
return item
296+
297+
def set_acl(path, access=None, default=None):
298+
item = {"acl_access": access, "acl_default": default}
299+
acl_set(path, item)
300+
301+
# Define example ACLs
302+
ACCESS_ACL = b"user::rw-\nuser:root:rw-:0\ngroup::r--\ngroup:root:r--:0\nmask::rw-\nother::r--"
303+
DEFAULT_ACL = b"user::rw-\nuser:root:r--:0\ngroup::r--\ngroup:root:r--:0\nmask::rw-\nother::r--"
304+
305+
# 1. Set filesystem ACLs
306+
# Create test files with ACLs
307+
create_regular_file(archiver.input_path, "file")
308+
os.mkdir(os.path.join(archiver.input_path, "dir"))
309+
310+
file_path = os.path.join(archiver.input_path, "file")
311+
dir_path = os.path.join(archiver.input_path, "dir")
312+
313+
# Set ACLs on the test files
314+
try:
315+
set_acl(file_path, access=ACCESS_ACL)
316+
set_acl(dir_path, access=ACCESS_ACL, default=DEFAULT_ACL)
317+
except OSError as e:
318+
pytest.skip(f"Failed to set ACLs: {e}")
319+
320+
file_acl = get_acl(file_path)
321+
dir_acl = get_acl(dir_path)
322+
323+
if not file_acl.get("acl_access") or not dir_acl.get("acl_access") or not dir_acl.get("acl_default"):
324+
pytest.skip("ACLs not supported or not working correctly")
325+
326+
# 2. Create a Borg archive
327+
cmd(archiver, "repo-create", "--encryption=none")
328+
cmd(archiver, "create", "original", "input")
329+
330+
# 3. export-tar this archive to a tar file
331+
cmd(archiver, "export-tar", "original", "acls.tar", "--tar-format=PAX")
332+
333+
# 4. import-tar the resulting tar file
334+
cmd(archiver, "import-tar", "imported", "acls.tar")
335+
336+
# 5. Extract the imported archive
337+
with changedir(archiver.output_path):
338+
cmd(archiver, "extract", "imported")
339+
340+
# 6. Check the expected ACLs in the filesystem
341+
extracted_file_path = os.path.abspath("input/file")
342+
extracted_dir_path = os.path.abspath("input/dir")
343+
344+
extracted_file_acl = get_acl(extracted_file_path)
345+
extracted_dir_acl = get_acl(extracted_dir_path)
346+
347+
# Check that access ACLs were preserved
348+
assert "acl_access" in extracted_file_acl
349+
assert extracted_file_acl["acl_access"] == file_acl["acl_access"]
350+
assert b"user:root:rw-" in file_acl["acl_access"]
351+
352+
assert "acl_access" in extracted_dir_acl
353+
assert extracted_dir_acl["acl_access"] == dir_acl["acl_access"]
354+
assert b"user:root:rw-" in dir_acl["acl_access"]
355+
356+
# Check that default ACLs were preserved for directories
357+
assert "acl_default" in extracted_dir_acl
358+
assert extracted_dir_acl["acl_default"] == dir_acl["acl_default"]
359+
assert b"user:root:r--" in dir_acl["acl_default"]

0 commit comments

Comments
 (0)