Merge pull request #9034 from ThomasWaldmann/format-inode

list --format: add "inode" support to formatter, add tests
This commit is contained in:
TW 2025-10-02 12:33:06 +02:00 committed by GitHub
commit a8e10c4a5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 47 additions and 2 deletions

View file

@ -860,6 +860,7 @@ class ItemFormatter(BaseFormatter):
"path": "file path",
"target": "link target for symlinks",
"hlid": "hard link identity (same if hardlinking same fs object)",
"inode": "inode number",
"flags": "file flags",
"extra": 'prepends {target} with " -> " for soft links and " link to " for hard links',
"size": "file size",
@ -875,7 +876,7 @@ class ItemFormatter(BaseFormatter):
"archivename": "name of the archive",
}
KEY_GROUPS = (
("type", "mode", "uid", "gid", "user", "group", "path", "target", "hlid", "flags"),
("type", "mode", "uid", "gid", "user", "group", "path", "target", "hlid", "inode", "flags"),
("size", "num_chunks"),
("mtime", "ctime", "atime", "isomtime", "isoctime", "isoatime"),
tuple(sorted(hash_algorithms)),
@ -937,6 +938,8 @@ class ItemFormatter(BaseFormatter):
item_data.update(text_to_json("group", item.get("group", str(item_data["gid"]))))
item_data["flags"] = item.get("bsdflags") # int if flags known, else (if flags unknown) None
# inode number from source filesystem (may be absent on some platforms)
item_data["inode"] = item.get("inode")
for key in self.used_call_keys:
item_data[key] = self.call_keys[key](item)
return item_data

View file

@ -1,8 +1,9 @@
import json
import os
import pytest
from ...constants import * # NOQA
from . import src_dir, cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION
from . import src_dir, cmd, create_regular_file, generate_archiver_tests, RK_ENCRYPTION, requires_hardlinks
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA
@ -137,3 +138,43 @@ def test_list_depth(archivers, request):
assert "input/dir1/file_at_depth_2.txt" in output_no_depth
assert "input/dir1/dir2" in output_no_depth
assert "input/dir1/dir2/file_at_depth_3.txt" in output_no_depth
@requires_hardlinks
def test_list_inode_hardlinks(archivers, request):
archiver = request.getfixturevalue(archivers)
# Prepare repository and input files: two hardlinks to same file and one separate file
cmd(archiver, "repo-create", RK_ENCRYPTION)
create_regular_file(archiver.input_path, "fileA", contents=b"DATA")
os.link(os.path.join(archiver.input_path, "fileA"), os.path.join(archiver.input_path, "fileB"))
create_regular_file(archiver.input_path, "fileC", contents=b"DATA")
# Create archive
cmd(archiver, "create", "test", "input")
# Use ItemFormatter via list --format to output {inode}
output = cmd(archiver, "list", "test", "--format", "{path} {inode}{NL}")
# Parse output lines and collect inode numbers for our files
inodes = {}
for line in output.splitlines():
try:
path, inode_str = line.rsplit(" ", 1)
except ValueError:
continue
if path in {"input/fileA", "input/fileB", "input/fileC"}:
# inode may be missing (None) on some platforms; convert to int if possible
inode = None if inode_str in ("", "None") else int(inode_str)
inodes[path] = inode
# Ensure we captured all three files
assert set(inodes) == {"input/fileA", "input/fileB", "input/fileC"}
# On platforms where inode is available, verify hardlinks share same inode
# If inode is None, the formatter still worked, but platform didn't provide an inode; skip in that case.
if inodes["input/fileA"] is not None and inodes["input/fileB"] is not None and inodes["input/fileC"] is not None:
assert inodes["input/fileA"] == inodes["input/fileB"]
assert inodes["input/fileA"] != inodes["input/fileC"]
else:
pytest.skip("Platform does not provide inode numbers for items")

View file

@ -156,6 +156,7 @@ def test_transfer_upgrade(archivers, request, monkeypatch):
del e["mode"], g["mode"]
del e["healthy"] # not supported anymore
del g["inode"] # new in borg2
assert g == e