Skip to content

Commit 64d3962

Browse files
committed
Removed getxattr in favor of statx returning all xattrs
The test has been adapted to require xattrs in the stat output
1 parent 3198389 commit 64d3962

File tree

5 files changed

+59
-77
lines changed

5 files changed

+59
-77
lines changed

src/core/cs3iface.py

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ def init(inconfig, inlog):
4040
ctx['lockasattr'] = inconfig.getboolean('cs3', 'lockasattr', fallback=False)
4141
ctx['locknotimpl'] = False
4242
ctx['revagateway'] = inconfig.get('cs3', 'revagateway')
43-
ctx['xattrcache'] = {} # this is a map cs3ref -> arbitrary_metadata as returned by Stat()
4443
ctx['grpc_timeout'] = inconfig.getint('cs3', "grpctimeout", fallback=10)
4544
ctx['http_timeout'] = inconfig.getint('cs3', "httptimeout", fallback=10)
4645
# prepare the gRPC channel and validate that the revagateway gRPC server is ready
@@ -97,11 +96,6 @@ def _getcs3reference(endpoint, fileref):
9796
return ref
9897

9998

100-
def _hashedref(endpoint, fileref):
101-
'''Returns an hashable key for the given endpoint and file reference'''
102-
return str(endpoint) + str(fileref)
103-
104-
10599
def authenticate_for_test(userid, userpwd):
106100
'''Use basic authentication against Reva for testing purposes'''
107101
authReq = cs3gw.AuthenticateRequest(type='basic', client_id=userid, client_secret=userpwd)
@@ -148,14 +142,14 @@ def stat(endpoint, fileref, userid, versioninv=1):
148142
log.info('msg="Invoked stat" fileref="%s" trace="%s" inode="%s" filepath="%s" elapsedTimems="%.1f"' %
149143
(fileref, statInfo.status.trace, inode, filepath, (tend-tstart)*1000))
150144
# cache the xattrs map prior to returning; note we're never cleaning this cache and let it grow indefinitely
151-
ctx['xattrcache'][_hashedref(endpoint, fileref)] = statInfo.info.arbitrary_metadata.metadata
152145
return {
153146
'inode': inode,
154147
'filepath': filepath,
155148
'ownerid': statInfo.info.owner.opaque_id + '@' + statInfo.info.owner.idp,
156149
'size': statInfo.info.size,
157150
'mtime': statInfo.info.mtime.seconds,
158151
'etag': statInfo.info.etag,
152+
'xattrs': statInfo.info.arbitrary_metadata.metadata
159153
}
160154

161155

@@ -169,11 +163,6 @@ def setxattr(endpoint, filepath, userid, key, value, lockmd):
169163
ref = _getcs3reference(endpoint, filepath)
170164
md = cs3spr.ArbitraryMetadata()
171165
md.metadata.update({key: str(value)}) # pylint: disable=no-member
172-
try:
173-
ctx['xattrcache'][_hashedref(endpoint, filepath)][key] = str(value)
174-
except KeyError:
175-
# we did not have this file in the cache, ignore
176-
pass
177166
lockid = None
178167
if lockmd:
179168
_, lockid = lockmd
@@ -192,38 +181,6 @@ def setxattr(endpoint, filepath, userid, key, value, lockmd):
192181
log.debug(f'msg="Invoked setxattr" result="{res}"')
193182

194183

195-
def getxattr(endpoint, filepath, userid, key):
196-
'''Get the extended attribute <key> using the given userid as access token'''
197-
ref = _getcs3reference(endpoint, filepath)
198-
statInfo = None
199-
href = _hashedref(endpoint, filepath)
200-
if href not in ctx['xattrcache']:
201-
# cache miss, go for Stat and refresh cache
202-
tstart = time.time()
203-
statInfo = ctx['cs3gw'].Stat(request=cs3sp.StatRequest(ref=ref), metadata=[('x-access-token', userid)])
204-
tend = time.time()
205-
if statInfo.status.code == cs3code.CODE_NOT_FOUND:
206-
log.debug(f'msg="Invoked stat for getxattr on missing file" filepath="{filepath}"')
207-
return None
208-
if statInfo.status.code != cs3code.CODE_OK:
209-
log.error('msg="Failed to stat" filepath="%s" userid="%s" trace="%s" key="%s" reason="%s"' %
210-
(filepath, userid[-20:], statInfo.status.trace, key, statInfo.status.message.replace('"', "'")))
211-
raise IOError(statInfo.status.message)
212-
log.debug(f'msg="Invoked stat for getxattr" filepath="{filepath}" elapsedTimems="{(tend - tstart) * 1000:.1f}"')
213-
ctx['xattrcache'][href] = statInfo.info.arbitrary_metadata.metadata
214-
try:
215-
xattrvalue = ctx['xattrcache'][href][key]
216-
if xattrvalue == '':
217-
raise KeyError
218-
if not statInfo:
219-
log.debug(f'msg="Returning cached attr on getxattr" filepath="{filepath}" key="{key}"')
220-
return xattrvalue
221-
except KeyError:
222-
log.info('msg="Empty value or key not found in getxattr" filepath="%s" key="%s" trace="%s" metadata="%s"' %
223-
(filepath, key, statInfo.status.trace if statInfo else 'N/A', ctx['xattrcache'][href]))
224-
return None
225-
226-
227184
def rmxattr(endpoint, filepath, userid, key, lockmd):
228185
'''Remove the extended attribute <key> using the given userid as access token'''
229186
ref = _getcs3reference(endpoint, filepath)
@@ -240,11 +197,6 @@ def rmxattr(endpoint, filepath, userid, key, lockmd):
240197
log.error('msg="Failed to rmxattr" filepath="%s" trace="%s" key="%s" reason="%s"' %
241198
(filepath, key, res.status.trace, res.status.message.replace('"', "'")))
242199
raise IOError(res.status.message)
243-
try:
244-
del ctx['xattrcache'][_hashedref(endpoint, filepath)][key]
245-
except KeyError:
246-
# we did not have this file in the cache, ignore
247-
pass
248200
log.debug(f'msg="Invoked rmxattr" result="{res.status}"')
249201

250202

@@ -253,7 +205,8 @@ def setlock(endpoint, filepath, userid, appname, value):
253205
if ctx['lockasattr'] and ctx['locknotimpl']:
254206
log.debug(f'msg="Using xattrs to execute setlock" filepath="{filepath}" value="{value}"')
255207
try:
256-
currvalue = getxattr(endpoint, filepath, userid, LOCK_ATTR_KEY)
208+
filemd = stat(endpoint, filepath, userid)
209+
currvalue = filemd['xattrs'][LOCK_ATTR_KEY]
257210
log.info('msg="Invoked setlock on an already locked entity" filepath="%s" appname="%s" previouslock="%s"' %
258211
(filepath, appname, currvalue))
259212
raise IOError(common.EXCL_ERROR)
@@ -287,7 +240,8 @@ def getlock(endpoint, filepath, userid):
287240
if ctx['lockasattr'] and ctx['locknotimpl']:
288241
log.debug(f'msg="Using xattrs to execute getlock" filepath="{filepath}"')
289242
try:
290-
currvalue = getxattr(endpoint, filepath, userid, LOCK_ATTR_KEY)
243+
filemd = stat(endpoint, filepath, userid)
244+
currvalue = filemd['xattrs'][LOCK_ATTR_KEY]
291245
return {
292246
'lock_id': currvalue.split('!')[1],
293247
'type': 2, # LOCK_TYPE_WRITE, though this is advisory!
@@ -333,7 +287,8 @@ def refreshlock(endpoint, filepath, userid, appname, value, oldvalue=None):
333287
if ctx['lockasattr'] and ctx['locknotimpl']:
334288
log.debug(f'msg="Using xattrs to execute setlock" filepath="{filepath}" value="{value}"')
335289
try:
336-
currvalue = getxattr(endpoint, filepath, userid, LOCK_ATTR_KEY)
290+
filemd = stat(endpoint, filepath, userid)
291+
currvalue = filemd['xattrs'][LOCK_ATTR_KEY]
337292
if currvalue.split('!')[0] == appname and (not oldvalue or currvalue.split('!')[1] == oldvalue):
338293
raise KeyError
339294
log.info('msg="Failed precondition on refreshlock" filepath="%s" appname="%s" previouslock="%s"' %
@@ -369,7 +324,8 @@ def unlock(endpoint, filepath, userid, appname, value):
369324
if ctx['lockasattr'] and ctx['locknotimpl']:
370325
log.debug(f'msg="Using xattrs to execute unlock" filepath="{filepath}" value="{value}"')
371326
try:
372-
currvalue = getxattr(endpoint, filepath, userid, LOCK_ATTR_KEY)
327+
filemd = stat(endpoint, filepath, userid)
328+
currvalue = filemd['xattrs'][LOCK_ATTR_KEY]
373329
if currvalue.split('!')[0] == appname and currvalue.split('!')[1] == value:
374330
raise KeyError
375331
log.info('msg="Failed precondition on unlock" filepath="%s" appname="%s" previouslock="%s"' %

src/core/localiface.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,23 @@ def stat(_endpoint, filepath, _userid):
101101
(statInfo.st_ino, _getfilepath(filepath), (tend - tstart) * 1000))
102102
if S_ISDIR(statInfo.st_mode):
103103
raise IOError('Is a directory')
104+
try:
105+
xattrs = {
106+
k.strip('user.'): os.getxattr(_getfilepath(filepath), k).decode()
107+
for k in os.listxattr(_getfilepath(filepath))
108+
}
109+
except OSError as e:
110+
log.info('msg="Failed to invoke listxattr/getxattr" inode="%d" filepath="%s"' %
111+
statInfo.st_ino, _getfilepath(filepath))
112+
xattrs = {}
104113
return {
105114
'inode': common.encodeinode('local', str(statInfo.st_ino)),
106115
'filepath': filepath,
107116
'ownerid': str(statInfo.st_uid) + ':' + str(statInfo.st_gid),
108117
'size': statInfo.st_size,
109118
'mtime': statInfo.st_mtime,
110119
'etag': str(statInfo.st_mtime),
120+
'xattrs': xattrs,
111121
}
112122
except (FileNotFoundError, PermissionError) as e:
113123
raise IOError(e) from e
@@ -147,8 +157,8 @@ def setxattr(endpoint, filepath, userid, key, value, lockmd):
147157
raise IOError(e) from e
148158

149159

150-
def getxattr(_endpoint, filepath, _userid, key):
151-
'''Get the extended attribute <key> on behalf of the given userid. Do not raise exceptions'''
160+
def _getxattr(filepath, key):
161+
'''Internal only: get the extended attribute <key>, do not raise exceptions'''
152162
try:
153163
return os.getxattr(_getfilepath(filepath), 'user.' + key).decode('UTF-8')
154164
except OSError as e:
@@ -184,7 +194,7 @@ def setlock(endpoint, filepath, userid, appname, value):
184194

185195
def getlock(endpoint, filepath, _userid):
186196
'''Get the lock metadata as an xattr on behalf of the given userid'''
187-
rawl = getxattr(endpoint, filepath, '0:0', common.LOCKKEY)
197+
rawl = _getxattr(filepath, common.LOCKKEY)
188198
if rawl:
189199
lock = common.retrieverevalock(rawl)
190200
if lock['expiration']['seconds'] > time.time():

src/core/wopi.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,7 @@ def checkFileInfo(fileid, acctok):
105105
fmd['SupportsRename'] = fmd['UserCanRename'] = enablerename and \
106106
acctok['viewmode'] in (utils.ViewMode.READ_WRITE, utils.ViewMode.PREVIEW)
107107
fmd['SupportsUserInfo'] = True
108-
uinfo = st.getxattr(acctok['endpoint'], acctok['filename'], acctok['userid'],
109-
utils.USERINFOKEY + '.' + acctok['wopiuser'].split('!')[0])
108+
uinfo = statInfo['xattrs'].get(utils.USERINFOKEY + '.' + acctok['wopiuser'].split('!')[0])
110109
if uinfo:
111110
fmd['UserInfo'] = uinfo
112111
if srv.config.get('general', 'earlyfeatures', fallback='False').upper() == 'TRUE':
@@ -194,7 +193,7 @@ def setLock(fileid, reqheaders, acctok):
194193

195194
if retrievedLock or op == 'REFRESH_LOCK':
196195
# useful for later checks
197-
savetime = st.getxattr(acctok['endpoint'], fn, acctok['userid'], utils.LASTSAVETIMEKEY)
196+
savetime = statInfo['xattrs'].get(utils.LASTSAVETIMEKEY)
198197
if savetime and (not savetime.isdigit() or int(savetime) < int(statInfo['mtime'])):
199198
# we had stale information, discard
200199
log.warning('msg="Detected external modification" filename="%s" savetime="%s" mtime="%s" token="%s"' %
@@ -600,9 +599,9 @@ def putFile(fileid, acctok):
600599
try:
601600
if srv.config.get('general', 'detectexternalmodifications', fallback='True').upper() == 'TRUE':
602601
# check now the destination file against conflicts if required
603-
savetime = st.getxattr(acctok['endpoint'], acctok['filename'], acctok['userid'], utils.LASTSAVETIMEKEY)
604-
mtime = None
605-
mtime = st.stat(acctok['endpoint'], acctok['filename'], acctok['userid'])['mtime']
602+
statInfo = st.statx(acctok['endpoint'], acctok['filename'], acctok['userid'], versioninv=1)
603+
savetime = statInfo['xattrs'].get(utils.LASTSAVETIMEKEY)
604+
mtime = statInfo['mtime']
606605
if not savetime or not savetime.isdigit() or int(savetime) < int(mtime):
607606
# no xattr was there or we got our xattr but mtime is more recent: someone may have updated the file from
608607
# a different source (e.g. FUSE or SMB mount), therefore force conflict and return failure to the application

src/core/xrootiface.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -218,28 +218,41 @@ def statx(endpoint, fileref, userid, versioninv=1):
218218
filepath = filepath.decode().replace(EOSVERSIONPREFIX, '').replace('#and#', '&')
219219
else:
220220
filepath = fileref
221+
221222
# stat with the -m flag, so to obtain a k=v list
222223
statInfo = _xrootcmd(endpoint, 'fileinfo', '', userid, 'mgm.path=' + _getfilepath(filepath, encodeamp=True)
223224
+ '&mgm.pcmd=fileinfo&mgm.file.info.option=-m')
225+
xattrs = {}
224226
try:
225227
# output looks like:
226-
# keylength.file=35 file=/eos/.../filename size=2915 mtime=1599649863.0 ctime=1599649866.280468540
227-
# btime=1599649866.280468540 clock=0 mode=0644 uid=xxxx gid=xxxx fxid=19ab8b68 fid=430672744 ino=115607834422411264
228-
# pid=1713958 pxid=001a2726 xstype=adler xs=a2dfcdf9 etag="115607834422411264:a2dfcdf9" detached=0 layout=replica
229-
# nstripes=2 lid=00100112 nrep=2 xattrn=sys.eos.btime xattrv=1599649866.280468540 uid:xxxx[username] gid:xxxx[group]
230-
# tident:xxx name:username dn: prot:https host:xxxx.cern.ch domain:cern.ch geo: sudo:0 fsid=305 fsid=486
228+
# keylength.file=60 file=/eos/.../file name with spaces size=49322 status=healthy mtime=1722868599.312924544
229+
# ctime=1722868599.371644799 btime=1722868599.312743733 atime=1722868599.312744021 clock=0 mode=0644 uid=55375 gid=2763
230+
# fxid=1fef0b88 fid=535759752 ino=143816913334566912 pid=21195331 pxid=01436a43 xstype=adler xs=7fd2729c
231+
# etag="143816913334566912:7fd2729c" detached=0 layout=replica nstripes=2 lid=00100112 nrep=2
232+
# xattrn=sys.eos.btime xattrv=1722868599.312743733
233+
# xattrn=sys.fs.tracking xattrv=+648+260
234+
# xattrn=sys.fusex.state xattrv=<non-decodable>
235+
# xattrn=sys.utrace xattrv=2012194c-5338-11ef-b295-a4bf0179682d
236+
# xattrn=sys.vtrace xattrv=[Mon Aug 5 16:36:39 2024] uid:55375[xxxx] gid:2763[xx] tident:root.11:2881@...
237+
# xattrn=user.iop.wopi.lastwritetime xattrv=1722868599
238+
# fsid=648 fsid=260
231239
# cf. https://gitlab.cern.ch/dss/eos/-/blob/master/archive/eosarch/utils.py
232240
kvlist = [kv.split(b'=') for kv in statInfo.split()]
233-
# extract the key-value pairs, but drop the xattrn/xattrv ones as not needed and potentially containing
234-
# non-unicode-decodable content (cf. CERNBOX-3514)
241+
# extract the key-value pairs for the core metadata and the user xattrs; drop the rest, don't decode as
242+
# some sys xattrs may contain non-unicode-decodable content (cf. CERNBOX-3514)
235243
statxdata = {k.decode(): v.decode().strip('"') for k, v in
236244
[kv for kv in kvlist if len(kv) == 2 and kv[0].find(b'xattr') == -1]}
245+
for ikv, kv in enumerate(kvlist):
246+
if len(kv) == 2 and kv[0].find(b'xattrn=user.'):
247+
xattrs[kv[1].decode().split('user.')] = kvlist[ikv+1][1].decode()
248+
237249
except ValueError as e:
238250
# UnicodeDecodeError exceptions would fall here
239251
log.error(f'msg="Invoked fileinfo but failed to parse output" result="{statInfo}" exception="{e}"')
240252
raise IOError('Failed to parse fileinfo response') from e
241253
if 'treesize' in statxdata:
242254
raise IOError('Is a directory') # EISDIR
255+
243256
if versioninv == 0:
244257
# statx info of the given file:
245258
# we extract the eosinstance from endpoint, which looks like e.g. root://eosinstance[.cern.ch]
@@ -253,6 +266,7 @@ def statx(endpoint, fileref, userid, versioninv=1):
253266
'size': int(statxdata['size']),
254267
'mtime': int(float(statxdata['mtime'])),
255268
'etag': statxdata['etag'],
269+
'xattrs': xattrs,
256270
}
257271
# now stat the corresponding version folder to get an inode invariant to save operations, see CERNBOX-1216
258272
# also, use the owner's as opposed to the user's credentials to bypass any restriction (e.g. with single-share files)
@@ -261,6 +275,7 @@ def statx(endpoint, fileref, userid, versioninv=1):
261275
rcv, infov = _getxrdfor(endpoint).query(QueryCode.OPAQUEFILE, _getfilepath(verFolder) + ownerarg + '&mgm.pcmd=stat',
262276
timeout=timeout)
263277
tend = time.time()
278+
264279
try:
265280
if not infov:
266281
raise IOError(f'xrdquery returned nothing, rcv={rcv}')
@@ -286,6 +301,7 @@ def statx(endpoint, fileref, userid, versioninv=1):
286301
except (IOError, UnicodeDecodeError) as e:
287302
log.error(f'msg="Failed to mkdir/stat version folder" filepath="{_getfilepath(filepath)}" error="{e}"')
288303
raise IOError(e) from e
304+
289305
# return the metadata of the given file, with the inode taken from the version folder
290306
endpoint = _geturlfor(endpoint)
291307
inode = common.encodeinode(endpoint[7:] if endpoint.find('.') == -1 else endpoint[7:endpoint.find('.')], statxdata['ino'])
@@ -297,6 +313,7 @@ def statx(endpoint, fileref, userid, versioninv=1):
297313
'size': int(statxdata['size']),
298314
'mtime': int(float(statxdata['mtime'])),
299315
'etag': statxdata['etag'],
316+
'xattrs': xattrs,
300317
}
301318

302319

@@ -313,9 +330,8 @@ def setxattr(endpoint, filepath, _userid, key, value, lockmd):
313330
+ '&mgm.path=' + _getfilepath(filepath, encodeamp=True), appname)
314331

315332

316-
def getxattr(endpoint, filepath, _userid, key):
317-
'''Get the extended attribute <key> via a special open.
318-
The userid is overridden to make sure it also works on shared files.'''
333+
def _getxattr(endpoint, filepath, key):
334+
'''Internal only: get the extended attribute <key> via a special open.'''
319335
if 'user' not in key and 'sys' not in key:
320336
# if nothing is given, assume it's a user attr
321337
key = 'user.' + key
@@ -364,7 +380,7 @@ def setlock(endpoint, filepath, userid, appname, value, recurse=False):
364380

365381
def getlock(endpoint, filepath, userid):
366382
'''Get the lock metadata as an xattr'''
367-
rawl = getxattr(endpoint, filepath, userid, common.LOCKKEY)
383+
rawl = _getxattr(endpoint, filepath, common.LOCKKEY)
368384
if rawl:
369385
lock = common.retrieverevalock(rawl)
370386
if lock['expiration']['seconds'] > time.time():

test/test_storageiface.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def test_statx_fileid(self):
105105
self.assertTrue('size' in statInfo, 'Missing size from stat output')
106106
self.assertTrue('mtime' in statInfo, 'Missing mtime from stat output')
107107
self.assertTrue('etag' in statInfo, 'Missing etag from stat output')
108+
self.assertTrue('xattrs' in statInfo, 'Missing xattrs from stat output')
108109
self.storage.removefile(self.endpoint, self.homepath + '/test.txt', self.userid)
109110

110111
def test_statx_invariant_fileid(self):
@@ -372,11 +373,11 @@ def test_xattr(self):
372373
self.storage.setlock(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'test app', 'xattrlock')
373374
self.storage.setxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey', 123,
374375
('test app', 'xattrlock'))
375-
v = self.storage.getxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey')
376-
self.assertEqual(v, '123')
376+
fmd = self.storage.statx(self.endpoint, self.homepath + '/test&xattr.txt', self.userid)
377+
self.assertEqual(fmd['xattrs']['testkey'], '123')
377378
self.storage.rmxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey', ('test app', 'xattrlock'))
378-
v = self.storage.getxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey')
379-
self.assertEqual(v, None)
379+
fmd = self.storage.statx(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey')
380+
self.assertIsNone(fmd['xattrs'].get('testkey'))
380381
self.storage.removefile(self.endpoint, self.homepath + '/test&xattr.txt', self.userid)
381382

382383
def test_rename_statx(self):

0 commit comments

Comments
 (0)