Skip to content

Commit ae52b25

Browse files
committed
feat: File protocol tries to write the xrd checksum attriubute
1 parent 4f7aaa4 commit ae52b25

File tree

1 file changed

+121
-2
lines changed

1 file changed

+121
-2
lines changed

src/DIRAC/Resources/Storage/FileStorage.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,130 @@
11
"""
2-
This is the File StorageClass, only meant to be used localy
3-
"""
2+
This is the File StorageClass, only meant to be used localy
3+
"""
4+
45
import os
56
import shutil
67
import errno
78
import stat
9+
import struct
10+
import time
811

912
from DIRAC import gLogger, S_OK, S_ERROR
1013
from DIRAC.Resources.Storage.Utilities import checkArgumentFormat
1114
from DIRAC.Resources.Storage.StorageBase import StorageBase
1215
from DIRAC.Core.Utilities.Adler import fileAdler
1316

1417

18+
def set_xattr_adler32(path, checksum):
19+
"""
20+
Set the adler32 checksum extended attribute on a file.
21+
22+
This is needed for case where you write the data on a locally mounted
23+
file system, but then want to access it from outside via xroot (like the HLT farm)
24+
25+
Hopefully, this whole function will be part of xroot at some point
26+
https://github.com/xrootd/xrootd/pull/2650
27+
28+
29+
This function replicates the exact behavior of the C++ function fSetXattrAdler32
30+
31+
It writes the checksum in XrdCksData binary format with the following structure:
32+
- Name[16]: Algorithm name ("adler32"), null-padded
33+
- fmTime (8): File modification time (network byte order, int64)
34+
- csTime (4): Time delta from mtime (network byte order, int32)
35+
- Rsvd1 (2): Reserved (int16)
36+
- Rsvd2 (1): Reserved (uint8)
37+
- Length (1): Checksum length in bytes (uint8)
38+
- Value[64]: Binary checksum value (4 bytes for adler32)
39+
40+
Total structure size: 96 bytes
41+
42+
Parameters
43+
----------
44+
path : str
45+
Path to the file (must be a regular file on local filesystem)
46+
checksum : str
47+
8-character hexadecimal adler32 checksum (e.g., "deadbeef")
48+
49+
50+
Notes
51+
-----
52+
- The attribute is stored as "user.XrdCks.adler32"
53+
54+
"""
55+
# Validate checksum format
56+
if not isinstance(checksum, str) or len(checksum) != 8:
57+
raise ValueError("Checksum must be exactly 8 characters")
58+
59+
# Validate it's valid hex
60+
try:
61+
int(checksum, 16)
62+
except ValueError:
63+
raise ValueError(f"Checksum must be valid hexadecimal: {checksum}")
64+
65+
# Check file exists and is regular
66+
67+
st = os.stat(path)
68+
69+
# Import xattr module
70+
try:
71+
import xattr
72+
except ImportError:
73+
raise ImportError("The 'xattr' module is required. Install it with: pip install xattr")
74+
75+
# Build XrdCksData structure (96 bytes total)
76+
# Reference: src/XrdCks/XrdCksData.hh
77+
78+
# 1. Name[16] - Algorithm name, null-padded
79+
name = b"adler32"
80+
name_field = name.ljust(16, b"\x00")
81+
82+
# 2. fmTime (8 bytes) - File modification time (network byte order = big-endian)
83+
fm_time = int(st.st_mtime)
84+
fm_time_field = struct.pack(">q", fm_time) # signed 64-bit big-endian
85+
86+
# 3. csTime (4 bytes) - Delta from mtime to now (network byte order)
87+
cs_time = int(time.time()) - fm_time
88+
cs_time_field = struct.pack(">i", cs_time) # signed 32-bit big-endian
89+
90+
# 4. Rsvd1 (2 bytes) - Reserved, set to 0
91+
rsvd1_field = struct.pack(">h", 0) # signed 16-bit big-endian
92+
93+
# 5. Rsvd2 (1 byte) - Reserved, set to 0
94+
rsvd2_field = struct.pack("B", 0) # unsigned 8-bit
95+
96+
# 6. Length (1 byte) - Checksum length in bytes
97+
# Adler32 is 4 bytes (8 hex chars / 2)
98+
length_field = struct.pack("B", 4) # unsigned 8-bit
99+
100+
# 7. Value[64] - Binary checksum value
101+
# Convert hex string to 4 bytes, pad rest with zeros
102+
checksum_bytes = bytes.fromhex(checksum)
103+
value_field = checksum_bytes + b"\x00" * (64 - len(checksum_bytes))
104+
105+
# Assemble complete structure
106+
xrd_cks_data = (
107+
name_field # 16 bytes
108+
+ fm_time_field # 8 bytes
109+
+ cs_time_field # 4 bytes
110+
+ rsvd1_field # 2 bytes
111+
+ rsvd2_field # 1 byte
112+
+ length_field # 1 byte
113+
+ value_field # 64 bytes
114+
) # Total: 96 bytes
115+
116+
assert len(xrd_cks_data) == 96, f"Structure size mismatch: {len(xrd_cks_data)}"
117+
118+
# Set the extended attribute
119+
# XRootD uses "XrdCks.adler32" which becomes "user.XrdCks.adler32" on Linux
120+
attr_name = "user.XrdCks.adler32"
121+
122+
try:
123+
xattr.setxattr(path, attr_name, xrd_cks_data)
124+
except OSError as e:
125+
raise OSError(f"Failed to set extended attribute on {path}: {e}") from e
126+
127+
15128
class FileStorage(StorageBase):
16129
""".. class:: FileStorage
17130
@@ -165,6 +278,12 @@ def putFile(self, path, sourceSize=0):
165278
os.makedirs(dirname)
166279
shutil.copy2(src_file, dest_url)
167280
fileSize = os.path.getsize(dest_url)
281+
try:
282+
src_cks = fileAdler(src_file)
283+
set_xattr_adler32(dest_url, src_cks)
284+
except Exception as e:
285+
gLogger.warn("Could not set checksum", f"{e!r}")
286+
168287
if sourceSize and (sourceSize != fileSize):
169288
try:
170289
os.unlink(dest_url)

0 commit comments

Comments
 (0)