via a special open.
The userid is overridden to make sure it also works on shared files.'''
_xrootcmd(endpoint, 'attr', 'rm', '0:0', 'mgm.attr.key=user.' + key + '&mgm.path=' + _getfilepath(filepath, encodeamp=True))
-def _getLegacyLockName(filename):
- '''Generates the legacy hidden filename used to store the WOPI locks'''
- return os.path.dirname(filename) + os.path.sep + '.sys.wopilock.' + os.path.basename(filename) + '.'
-
-
def setlock(endpoint, filepath, userid, appname, value):
'''Set a lock as an xattr with the given value metadata and appname as holder.
The special option "c" (create-if-not-exists) is used to be atomic'''
try:
- log.debug('msg="Invoked setlock" filepath="%s"' % filepath)
- setxattr(endpoint, filepath, userid, common.LOCKKEY, \
- urlsafe_b64encode(common.genrevalock(appname, value).encode()).decode() + '&mgm.option=c')
+ log.debug('msg="Invoked setlock" filepath="%s" value="%s"' % (filepath, value))
+ setxattr(endpoint, filepath, userid, common.LOCKKEY, common.genrevalock(appname, value) + '&mgm.option=c', None)
except IOError as e:
if EXCL_XATTR_MSG in str(e):
raise IOError(common.EXCL_ERROR)
@@ -291,42 +283,42 @@ def getlock(endpoint, filepath, userid):
'''Get the lock metadata as an xattr'''
l = getxattr(endpoint, filepath, userid, common.LOCKKEY)
if l:
- return json.loads(urlsafe_b64decode(l).decode())
- # try and read it from the legacy lock file for the time being
- l = b''
- for line in readfile(endpoint, _getLegacyLockName(filepath), '0:0'):
- if isinstance(line, IOError):
- return None # no pre-existing lock found, or error attempting to read it: assume it does not exist
- # the following check is necessary as it happens to get a str instead of bytes
- l += line if isinstance(line, type(l)) else line.encode()
- return {'h': 'wopi', 'md' : l} # this is temporary
+ return common.retrieverevalock(l)
+ return None # no pre-existing lock found, or error attempting to read it: assume it does not exist
def refreshlock(endpoint, filepath, userid, appname, value):
'''Refresh the lock value as an xattr'''
- log.debug('msg="Invoked refreshlock" filepath="%s"' % filepath)
l = getlock(endpoint, filepath, userid)
if not l:
+ log.warning('msg="Failed to refreshlock" filepath="%s" appname="%s" reason="%s"' %
+ (filepath, appname, 'File is not locked'))
raise IOError('File was not locked')
- if l['h'] != appname and l['h'] != 'wopi':
- raise IOError('File is locked by %s' % l['h'])
+ if l['app_name'] != appname and l['app_name'] != 'wopi':
+ log.warning('msg="Failed to refreshlock" filepath="%s" appname="%s" reason="%s"' %
+ (filepath, appname, 'File is locked by %s' % l['app_name']))
+ raise IOError('File is locked by %s' % l['app_name'])
+ log.debug('msg="Invoked refreshlock" filepath="%s" value="%s"' % (filepath, value))
# this is non-atomic, but the lock was already held
- setxattr(endpoint, filepath, userid, common.LOCKKEY, \
- urlsafe_b64encode(common.genrevalock(appname, value).encode()).decode())
+ setxattr(endpoint, filepath, userid, common.LOCKKEY, common.genrevalock(appname, value), None)
-def unlock(endpoint, filepath, userid, _appname):
+def unlock(endpoint, filepath, userid, appname, value):
'''Remove a lock as an xattr'''
- log.debug('msg="Invoked unlock" filepath="%s"' % filepath)
- try:
- # try and remove the legacy lock file as well for the time being
- removefile(endpoint, _getLegacyLockName(filepath), userid, force=True)
- except IOError:
- pass
- rmxattr(endpoint, filepath, userid, common.LOCKKEY)
+ l = getlock(endpoint, filepath, userid)
+ if not l:
+ log.warning('msg="Failed to unlock" filepath="%s" appname="%s" reason="%s"' %
+ (filepath, appname, 'File is not locked'))
+ raise IOError('File was not locked')
+ if l['app_name'] != appname and l['app_name'] != 'wopi':
+ log.warning('msg="Failed to unlock" filepath="%s" appname="%s" reason="%s"' %
+ (filepath, appname, 'File is locked by %s' % l['app_name']))
+ raise IOError('File is locked by %s' % l['app_name'])
+ log.debug('msg="Invoked unlock" filepath="%s" value="%s' % (filepath, value))
+ rmxattr(endpoint, filepath, userid, common.LOCKKEY, None)
-def readfile(endpoint, filepath, userid):
+def readfile(endpoint, filepath, userid, _lockid):
'''Read a file via xroot on behalf of the given userid. Note that the function is a generator, managed by Flask.'''
log.debug('msg="Invoking readFile" filepath="%s"' % filepath)
with XrdClient.File() as f:
@@ -353,7 +345,7 @@ def readfile(endpoint, filepath, userid):
yield chunk
-def writefile(endpoint, filepath, userid, content, islock=False):
+def writefile(endpoint, filepath, userid, content, _lockid, islock=False):
'''Write a file via xroot on behalf of the given userid. The entire content is written
and any pre-existing file is deleted (or moved to the previous version if supported).
With islock=True, the write explicitly disables versioning, and the file is opened with
@@ -390,7 +382,7 @@ def writefile(endpoint, filepath, userid, content, islock=False):
(filepath, (tend-tstart)*1000, islock))
-def renamefile(endpoint, origfilepath, newfilepath, userid):
+def renamefile(endpoint, origfilepath, newfilepath, userid, _lockid):
'''Rename a file via a special open from origfilepath to newfilepath on behalf of the given userid.'''
_xrootcmd(endpoint, 'file', 'rename', userid, 'mgm.path=' + _getfilepath(origfilepath, encodeamp=True) + \
'&mgm.file.source=' + _getfilepath(origfilepath, encodeamp=True) + \
diff --git a/src/wopiserver.py b/src/wopiserver.py
index ae6e7ac4..c8357420 100755
--- a/src/wopiserver.py
+++ b/src/wopiserver.py
@@ -4,7 +4,7 @@
The Web-application Open Platform Interface (WOPI) gateway for the ScienceMesh IOP
-Author: Giuseppe Lo Presti (@glpatcern), CERN/IT-ST
+Main author: Giuseppe.LoPresti@cern.ch, CERN/IT-ST
Contributions: see README.md
'''
@@ -123,10 +123,12 @@ def init(cls):
cls.log.error('msg="Failed to open the provided certificate or key to start in https mode"')
raise
cls.wopiurl = cls.config.get('general', 'wopiurl')
- if cls.config.has_option('general', 'lockpath'):
- cls.lockpath = cls.config.get('general', 'lockpath')
- else:
- cls.lockpath = ''
+ cls.conflictpath = cls.config.get('general', 'conflictpath', fallback='/')
+ cls.recoverypath = cls.config.get('io', 'recoverypath', fallback='/var/spool/wopirecovery')
+ try:
+ os.makedirs(cls.recoverypath)
+ except FileExistsError as e:
+ pass
_ = cls.config.get('general', 'downloadurl') # make sure this is defined
# WOPI proxy configuration (optional)
cls.wopiproxy = cls.config.get('general', 'wopiproxy', fallback='')
@@ -208,7 +210,7 @@ def index():
ScienceMesh WOPI Server
- This is the ScienceMesh IOP
WOPI server to support online office-like editors.
+ This is the ScienceMesh IOP
WOPI server to support online office-like editors.
The service includes support for non-WOPI-native apps through a bridge extension.
To use this service, please log in to your EFSS Storage and click on a supported document.
@@ -380,6 +382,36 @@ def iopGetOpenFiles():
return flask.Response(json.dumps(jlist), mimetype='application/json')
+@Wopi.app.route("/wopi/iop/test", methods=['GET'])
+def iopWopiTest():
+ '''Returns a WOPI_URL and a WOPI_TOKEN values suitable as input for the WOPI validator test suite.
+ This call is protected by the same shared secret as the /wopi/iop/openinapp call.
+ Request arguments:
+ - string filepath: the full path to the file used for the test. The file must exist
+ - string endpoint (optional): the storage endpoint, defaults to 'default'
+ - string usertoken: the credentials to access the given file (uid:gid for xrootd, bearer token for Reva)
+ '''
+ req = flask.request
+ if req.headers.get('Authorization') != 'Bearer ' + Wopi.iopsecret:
+ Wopi.log.warning('msg="iopWopiTest: unauthorized access attempt, missing authorization token" ' \
+ 'client="%s"' % req.remote_addr)
+ return UNAUTHORIZED
+ # the Microsoft WOPI validator test suite requires to issue an access token for a predefined test file
+ filepath = req.args.get('filepath', '')
+ endpoint = req.args.get('endpoint', 'default')
+ usertoken = req.args.get('usertoken', '')
+ if not filepath or not usertoken:
+ return 'Missing arguments', http.client.BAD_REQUEST
+ if Wopi.useHttps:
+ return 'WOPI validator not supported in https mode', http.client.BAD_REQUEST
+ inode, acctok = utils.generateAccessToken(usertoken, filepath, utils.ViewMode.READ_WRITE, ('test', usertoken),
+ 'http://folderurlfortestonly/', endpoint,
+ ('WOPI validator', 'http://fortestonly/', 'http://fortestonly/'))
+ Wopi.log.info('msg="iopWopiTest: preparing test via WOPI validator" client="%s"' % req.remote_addr)
+ return '-e WOPI_URL=http://localhost:%d/wopi/files/%s -e WOPI_TOKEN=%s' % (Wopi.port, inode, acctok)
+
+
+
#
# WOPI protocol implementation
#
@@ -431,7 +463,7 @@ def wopiFilesPost(fileid):
except KeyError as e:
Wopi.log.warning('msg="Missing argument" client="%s" requestedUrl="%s" error="%s" token="%s"' %
(flask.request.remote_addr, flask.request.base_url, e, flask.request.args.get('access_token')))
- return 'Missing argument: %s' % e, http.client.BAD_REQUEST
+ return 'Missing argument', http.client.BAD_REQUEST
@Wopi.app.route("/wopi/files//contents", methods=['POST'])
@@ -441,7 +473,7 @@ def wopiPutFile(fileid):
#
-# IOP lock endpoints
+# interoperable lock endpoints
#
@Wopi.app.route("/wopi/cbox/lock", methods=['GET', 'POST'])
@Wopi.metrics.counter('lock_by_ext', 'Number of /lock calls by file extension',
diff --git a/test/test_storageiface.py b/test/test_storageiface.py
index a737222c..1df301c9 100644
--- a/test/test_storageiface.py
+++ b/test/test_storageiface.py
@@ -4,6 +4,8 @@
Basic unit testing of the storage interfaces. To run them, please make sure the
wopiserver-test.conf is correctly configured (see /README.md). The storage layer
to be tested can be overridden by the WOPI_STORAGE env variable.
+
+Main author: Giuseppe.LoPresti@cern.ch, CERN/IT-ST
'''
import unittest
@@ -12,16 +14,20 @@
import sys
import os
from threading import Thread
-sys.path.append('src/core') # for tests out of the git repo
+import pytest
+sys.path.append('src') # for tests out of the git repo
sys.path.append('/app') # for tests within the Docker image
+from core.commoniface import EXCL_ERROR, ENOENT_MSG
+databuf = b'ebe5tresbsrdthbrdhvdtr'
class TestStorage(unittest.TestCase):
'''Simple tests for the storage layers of the WOPI server. See README for how to run the tests for each storage provider'''
+ initialized = False
- def __init__(self, *args, **kwargs):
+ @classmethod
+ def globalinit(cls):
'''One-off initialization of the test environment: create mock logging and import the library'''
- super(TestStorage, self).__init__(*args, **kwargs)
loghandler = logging.FileHandler('/tmp/wopiserver-test.log')
loghandler.setFormatter(logging.Formatter(fmt='%(asctime)s %(name)s[%(process)d] %(levelname)-8s %(message)s',
datefmt='%Y-%m-%dT%H:%M:%S'))
@@ -35,35 +41,47 @@ def __init__(self, *args, **kwargs):
storagetype = os.environ.get('WOPI_STORAGE')
if not storagetype:
storagetype = config.get('general', 'storagetype')
- self.userid = config.get(storagetype, 'userid')
- self.endpoint = config.get(storagetype, 'endpoint')
+ cls.userid = config.get(storagetype, 'userid')
+ cls.endpoint = config.get(storagetype, 'endpoint')
except (KeyError, configparser.NoOptionError):
print("Missing option or missing configuration, check the wopiserver-test.conf file")
raise
- # this is taken from wopiserver.py::storage_layer_import
+ # this is adapted from wopiserver.py::storage_layer_import
if storagetype in ['local', 'xroot', 'cs3']:
storagetype += 'iface'
else:
raise ImportError('Unsupported/Unknown storage type %s' % storagetype)
try:
- self.storage = __import__(storagetype, globals(), locals())
- self.storage.init(config, log)
- self.homepath = ''
- if storagetype == 'cs3iface':
+ cls.storage = __import__('core.' + storagetype, globals(), locals(), [storagetype])
+ cls.storage.init(config, log)
+ cls.homepath = ''
+ cls.username = ''
+ if 'cs3' in storagetype:
# we need to login for this case
- self.username = self.userid
- self.userid = self.storage.authenticate_for_test(self.userid, config.get('cs3', 'userpwd'))
- self.homepath = config.get('cs3', 'storagehomepath')
+ cls.username = cls.userid
+ cls.userid = cls.storage.authenticate_for_test(cls.userid, config.get('cs3', 'userpwd'))
+ cls.homepath = config.get('cs3', 'storagehomepath')
except ImportError:
- print("Missing module when attempting to import {}. Please make sure dependencies are met.", storagetype)
+ print("Missing module when attempting to import %s. Please make sure dependencies are met." % storagetype)
raise
+ print('Global initialization succeded for storage interface %s, starting unit tests' % storagetype)
+ cls.initialized = True
+ def __init__(self, *args, **kwargs):
+ '''Initialization of a test'''
+ super(TestStorage, self).__init__(*args, **kwargs)
+ if not TestStorage.initialized:
+ TestStorage.globalinit()
+ self.userid = TestStorage.userid
+ self.endpoint = TestStorage.endpoint
+ self.storage = TestStorage.storage
+ self.homepath = TestStorage.homepath
+ self.username = TestStorage.username
def test_stat(self):
'''Call stat() and assert the path matches'''
- buf = b'bla\n'
- self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, buf)
+ self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, databuf, None)
statInfo = self.storage.stat(self.endpoint, self.homepath + '/test.txt', self.userid)
self.assertIsInstance(statInfo, dict)
self.assertTrue('mtime' in statInfo, 'Missing mtime from stat output')
@@ -72,8 +90,7 @@ def test_stat(self):
def test_statx_fileid(self):
'''Call statx() and test if fileid-based stat is supported'''
- buf = b'bla\n'
- self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, buf)
+ self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, databuf, None)
statInfo = self.storage.statx(self.endpoint, self.homepath + '/test.txt', self.userid)
if self.endpoint in str(statInfo['inode']):
# detected CS3 storage, test if fileid-based stat is supported
@@ -84,13 +101,11 @@ def test_statx_fileid(self):
def test_statx_invariant_fileid(self):
'''Call statx() before and after updating a file, and assert the inode did not change'''
- buf = b'bla\n'
- self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, buf)
+ self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, databuf, None)
statInfo = self.storage.statx(self.endpoint, self.homepath + '/test.txt', self.userid, versioninv=1)
self.assertIsInstance(statInfo, dict)
inode = statInfo['inode']
- buf = b'blabla\n'
- self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, buf)
+ self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, databuf, None)
statInfo = self.storage.statx(self.endpoint, self.homepath + '/test.txt', self.userid, versioninv=1)
self.assertIsInstance(statInfo, dict)
self.assertEqual(statInfo['inode'], inode, 'Fileid is not invariant to multiple write operations')
@@ -100,31 +115,30 @@ def test_stat_nofile(self):
'''Call stat() and assert the exception is as expected'''
with self.assertRaises(IOError) as context:
self.storage.stat(self.endpoint, self.homepath + '/hopefullynotexisting', self.userid)
- self.assertIn('No such file or directory', str(context.exception))
+ self.assertIn(ENOENT_MSG, str(context.exception))
def test_statx_nofile(self):
'''Call statx() and assert the exception is as expected'''
with self.assertRaises(IOError) as context:
self.storage.statx(self.endpoint, self.homepath + '/hopefullynotexisting', self.userid)
- self.assertIn('No such file or directory', str(context.exception))
+ self.assertIn(ENOENT_MSG, str(context.exception))
def test_readfile_bin(self):
'''Writes a binary file and reads it back, validating that the content matches'''
- content = b'bla'
- self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, content)
+ self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, databuf, None)
content = ''
- for chunk in self.storage.readfile(self.endpoint, self.homepath + '/test.txt', self.userid):
+ for chunk in self.storage.readfile(self.endpoint, self.homepath + '/test.txt', self.userid, None):
self.assertNotIsInstance(chunk, IOError, 'raised by storage.readfile')
content += chunk.decode('utf-8')
- self.assertEqual(content, 'bla', 'File test.txt should contain the string "bla"')
+ self.assertEqual(content, databuf.decode(), 'File test.txt should contain the string "%s"' % databuf.decode())
self.storage.removefile(self.endpoint, self.homepath + '/test.txt', self.userid)
def test_readfile_text(self):
'''Writes a text file and reads it back, validating that the content matches'''
content = 'bla\n'
- self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, content)
+ self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, content, None)
content = ''
- for chunk in self.storage.readfile(self.endpoint, self.homepath + '/test.txt', self.userid):
+ for chunk in self.storage.readfile(self.endpoint, self.homepath + '/test.txt', self.userid, None):
self.assertNotIsInstance(chunk, IOError, 'raised by storage.readfile')
content += chunk.decode('utf-8')
self.assertEqual(content, 'bla\n', 'File test.txt should contain the text "bla\\n"')
@@ -133,8 +147,8 @@ def test_readfile_text(self):
def test_readfile_empty(self):
'''Writes an empty file and reads it back, validating that the read does not fail'''
content = ''
- self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, content)
- for chunk in self.storage.readfile(self.endpoint, self.homepath + '/test.txt', self.userid):
+ self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, content, None)
+ for chunk in self.storage.readfile(self.endpoint, self.homepath + '/test.txt', self.userid, None):
self.assertNotIsInstance(chunk, IOError, 'raised by storage.readfile')
content += chunk.decode('utf-8')
self.assertEqual(content, '', 'File test.txt should be empty')
@@ -142,14 +156,13 @@ def test_readfile_empty(self):
def test_read_nofile(self):
'''Test reading of a non-existing file'''
- readex = next(self.storage.readfile(self.endpoint, self.homepath + '/hopefullynotexisting', self.userid))
+ readex = next(self.storage.readfile(self.endpoint, self.homepath + '/hopefullynotexisting', self.userid, None))
self.assertIsInstance(readex, IOError, 'readfile returned %s' % readex)
- self.assertEqual(str(readex), 'No such file or directory', 'readfile returned %s' % readex)
+ self.assertEqual(str(readex), ENOENT_MSG, 'readfile returned %s' % readex)
def test_write_remove_specialchars(self):
'''Test write and removal of a file with special chars'''
- buf = b'ebe5tresbsrdthbrdhvdtr'
- self.storage.writefile(self.endpoint, self.homepath + '/testwrite&rm', self.userid, buf)
+ self.storage.writefile(self.endpoint, self.homepath + '/testwrite&rm', self.userid, databuf, None)
statInfo = self.storage.stat(self.endpoint, self.homepath + '/testwrite&rm', self.userid)
self.assertIsInstance(statInfo, dict)
self.storage.removefile(self.endpoint, self.homepath + '/testwrite&rm', self.userid)
@@ -158,60 +171,145 @@ def test_write_remove_specialchars(self):
def test_write_islock(self):
'''Test double write with the islock flag'''
- buf = b'ebe5tresbsrdthbrdhvdtr'
try:
self.storage.removefile(self.endpoint, self.homepath + '/testoverwrite', self.userid)
except IOError:
pass
- self.storage.writefile(self.endpoint, self.homepath + '/testoverwrite', self.userid, buf, islock=True)
+ self.storage.writefile(self.endpoint, self.homepath + '/testoverwrite', self.userid, databuf, None, islock=True)
statInfo = self.storage.stat(self.endpoint, self.homepath + '/testoverwrite', self.userid)
self.assertIsInstance(statInfo, dict)
with self.assertRaises(IOError) as context:
- self.storage.writefile(self.endpoint, self.homepath + '/testoverwrite', self.userid, buf, islock=True)
- self.assertIn('File exists and islock flag requested', str(context.exception))
+ self.storage.writefile(self.endpoint, self.homepath + '/testoverwrite', self.userid, databuf, None, islock=True)
+ self.assertIn(EXCL_ERROR, str(context.exception))
self.storage.removefile(self.endpoint, self.homepath + '/testoverwrite', self.userid)
+ @pytest.mark.skip(reason="Unhandled race on localstorage")
def test_write_race(self):
- '''Test multithreaded double write with the islock flag'''
- buf = b'ebe5tresbsrdthbrdhvdtr'
+ '''Test multithreaded double write with the islock flag. Randomly fails on localstorage, should not on cs3 nor xroot'''
try:
self.storage.removefile(self.endpoint, self.homepath + '/testwriterace', self.userid)
except IOError:
pass
t = Thread(target=self.storage.writefile,
- args=[self.endpoint, self.homepath + '/testwriterace', self.userid, buf], kwargs={'islock': True})
+ args=[self.endpoint, self.homepath + '/testwriterace', self.userid, databuf, None], kwargs={'islock': True})
t.start()
with self.assertRaises(IOError) as context:
- self.storage.writefile(self.endpoint, self.homepath + '/testwriterace', self.userid, buf, islock=True)
- self.assertIn('File exists and islock flag requested', str(context.exception))
+ self.storage.writefile(self.endpoint, self.homepath + '/testwriterace', self.userid, databuf, None, islock=True)
+ self.assertIn(EXCL_ERROR, str(context.exception))
t.join()
self.storage.removefile(self.endpoint, self.homepath + '/testwriterace', self.userid)
+ def test_lock(self):
+ '''Test setting lock'''
+ try:
+ self.storage.removefile(self.endpoint, self.homepath + '/testlock', self.userid)
+ except IOError:
+ pass
+ self.storage.writefile(self.endpoint, self.homepath + '/testlock', self.userid, databuf, None)
+ statInfo = self.storage.stat(self.endpoint, self.homepath + '/testlock', self.userid)
+ self.assertIsInstance(statInfo, dict)
+ self.storage.setlock(self.endpoint, self.homepath + '/testlock', self.userid, 'myapp', 'testlock')
+ l = self.storage.getlock(self.endpoint, self.homepath + '/testlock', self.userid)
+ self.assertIsInstance(l, dict)
+ self.assertEqual(l['lock_id'], 'testlock')
+ self.assertEqual(l['app_name'], 'myapp')
+ self.assertIsInstance(l['expiration'], dict)
+ self.assertIsInstance(l['expiration']['seconds'], int)
+ with self.assertRaises(IOError) as context:
+ self.storage.setlock(self.endpoint, self.homepath + '/testlock', self.userid, 'myapp', 'testlock2')
+ self.assertIn(EXCL_ERROR, str(context.exception))
+ self.storage.unlock(self.endpoint, self.homepath + '/testlock', self.userid, 'myapp', 'testlock')
+ self.storage.removefile(self.endpoint, self.homepath + '/testlock', self.userid)
+
+ def test_refresh_lock(self):
+ '''Test refreshing lock'''
+ try:
+ self.storage.removefile(self.endpoint, self.homepath + '/testrlock', self.userid)
+ except IOError:
+ pass
+ self.storage.writefile(self.endpoint, self.homepath + '/testrlock', self.userid, databuf, None)
+ statInfo = self.storage.stat(self.endpoint, self.homepath + '/testrlock', self.userid)
+ self.assertIsInstance(statInfo, dict)
+ with self.assertRaises(IOError) as context:
+ self.storage.refreshlock(self.endpoint, self.homepath + '/testrlock', self.userid, 'myapp', 'testlock')
+ self.assertIn('File was not locked', str(context.exception))
+ self.storage.setlock(self.endpoint, self.homepath + '/testrlock', self.userid, 'myapp', 'testlock')
+ self.storage.refreshlock(self.endpoint, self.homepath + '/testrlock', self.userid, 'myapp', 'testlock2')
+ l = self.storage.getlock(self.endpoint, self.homepath + '/testrlock', self.userid)
+ self.assertIsInstance(l, dict)
+ self.assertEqual(l['lock_id'], 'testlock2')
+ self.assertEqual(l['app_name'], 'myapp')
+ self.assertIsInstance(l['expiration'], dict)
+ self.assertIsInstance(l['expiration']['seconds'], int)
+ with self.assertRaises(IOError) as context:
+ self.storage.refreshlock(self.endpoint, self.homepath + '/testrlock', self.userid, 'myapp2', 'testlock2')
+ self.assertIn('File is locked by myapp', str(context.exception))
+ self.storage.removefile(self.endpoint, self.homepath + '/testrlock', self.userid)
+
+ @pytest.mark.skip(reason="Unhandled race on localstorage")
+ def test_lock_race(self):
+ '''Test multithreaded setting lock. Expected to fail on localstorage, not on cs3 nor xroot'''
+ try:
+ self.storage.removefile(self.endpoint, self.homepath + '/testlockrace', self.userid)
+ except IOError:
+ pass
+ self.storage.writefile(self.endpoint, self.homepath + '/testlockrace', self.userid, databuf, None)
+ statInfo = self.storage.stat(self.endpoint, self.homepath + '/testlockrace', self.userid)
+ self.assertIsInstance(statInfo, dict)
+ t = Thread(target=self.storage.setlock,
+ args=[self.endpoint, self.homepath + '/testlockrace', self.userid, 'myapp', 'testlock'])
+ t.start()
+ with self.assertRaises(IOError) as context:
+ self.storage.setlock(self.endpoint, self.homepath + '/testlockrace', self.userid, 'myapp', 'testlock2')
+ self.assertIn(EXCL_ERROR, str(context.exception))
+ self.storage.removefile(self.endpoint, self.homepath + '/testlockrace', self.userid)
+
+ @unittest.expectedFailure
+ def test_lock_operations(self):
+ '''Test file operations on locked file. Eexpected to fail until locks are enforced by the storage'''
+ try:
+ self.storage.removefile(self.endpoint, self.homepath + '/testlockop', self.userid)
+ except IOError:
+ pass
+ self.storage.writefile(self.endpoint, self.homepath + '/testlockop', self.userid, databuf, None)
+ statInfo = self.storage.stat(self.endpoint, self.homepath + '/testlockop', self.userid)
+ self.assertIsInstance(statInfo, dict)
+ self.storage.setlock(self.endpoint, self.homepath + '/testlockop', self.userid, 'myapp', 'testlock')
+ self.storage.writefile(self.endpoint, self.homepath + '/testlockop', self.userid, databuf, 'testlock')
+ self.storage.setxattr(self.endpoint, self.homepath + '/testlockop', self.userid, 'testkey', 123, 'testlock')
+ self.storage.renamefile(self.endpoint, self.homepath + '/testlockop', self.homepath + '/testlockop_renamed', self.userid, 'testlock')
+ with self.assertRaises(IOError):
+ self.storage.writefile(self.endpoint, self.homepath + '/testlockop_renamed', self.userid, databuf, None)
+ with self.assertRaises(IOError):
+ self.storage.setxattr(self.endpoint, self.homepath + '/testlockop_renamed', self.userid, 'testkey', 123, None)
+ with self.assertRaises(IOError):
+ self.storage.renamefile(self.endpoint, self.homepath + '/testlockop_renamed', self.homepath + '/testlockop', self.userid, None)
+ self.storage.removefile(self.endpoint, self.homepath + '/testlockop_renamed', self.userid)
+
def test_remove_nofile(self):
'''Test removal of a non-existing file'''
- with self.assertRaises(IOError):
+ with self.assertRaises(IOError) as context:
self.storage.removefile(self.endpoint, self.homepath + '/hopefullynotexisting', self.userid)
+ self.assertIn(ENOENT_MSG, str(context.exception))
def test_xattr(self):
'''Test all xattr methods with special chars'''
- buf = b'bla\n'
- self.storage.writefile(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, buf)
- self.storage.setxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey', 123)
+ self.storage.writefile(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, databuf, None)
+ self.storage.setxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey', 123, None)
v = self.storage.getxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey')
self.assertEqual(v, '123')
- self.storage.rmxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey')
+ self.storage.rmxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey', None)
v = self.storage.getxattr(self.endpoint, self.homepath + '/test&xattr.txt', self.userid, 'testkey')
self.assertEqual(v, None)
self.storage.removefile(self.endpoint, self.homepath + '/test&xattr.txt', self.userid)
def test_rename_statx(self):
'''Test renaming and statx of a file with special chars'''
- buf = b'bla\n'
- self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, buf)
- self.storage.renamefile(self.endpoint, self.homepath + '/test.txt', self.homepath + '/test&renamed.txt', self.userid)
+ self.storage.writefile(self.endpoint, self.homepath + '/test.txt', self.userid, databuf, None)
+ self.storage.renamefile(self.endpoint, self.homepath + '/test.txt', self.homepath + '/test&renamed.txt', self.userid, None)
statInfo = self.storage.statx(self.endpoint, self.homepath + '/test&renamed.txt', self.userid)
self.assertEqual(statInfo['filepath'], self.homepath + '/test&renamed.txt')
- self.storage.renamefile(self.endpoint, self.homepath + '/test&renamed.txt', self.homepath + '/test.txt', self.userid)
+ self.storage.renamefile(self.endpoint, self.homepath + '/test&renamed.txt', self.homepath + '/test.txt', self.userid, None)
statInfo = self.storage.statx(self.endpoint, self.homepath + '/test.txt', self.userid)
self.assertEqual(statInfo['filepath'], self.homepath + '/test.txt')
self.storage.removefile(self.endpoint, self.homepath + '/test.txt', self.userid)
diff --git a/test/wopi-validator.md b/test/wopi-validator.md
index 1777332f..686418fd 100644
--- a/test/wopi-validator.md
+++ b/test/wopi-validator.md
@@ -1,15 +1,19 @@
-# How to run the wopi-validator on a local setup for testing
+## How to run the wopi-validator on a local setup for testing
These notes have been adaped from the enterprise ownCloud WOPI implementation, credits to @deepdiver1975.
1. Setup your WOPI server as well as Reva as required. Make sure the WOPI storage interface unit tests pass.
-2. Create an empty file named `test.wopitest` in the user folder, e.g. `touch /var/tmp/reva/einstein/test.wopitest` for a local Reva setup.
+2. Create an empty folder and touch an file named `test.wopitest` in that folder. For a local Reva setup:
-3. Generate a WOPI URL and token for that file, e.g. `wopiopen.py /test.wopitest 1000 1000`. Remote setups would require appropriate userids and file paths.
+ `mkdir /var/tmp/reva/data/einstein/wopivalidator && touch /var/tmp/reva/data/einstein/wopivalidator/test.wopitest`.
-4. Amend the `WOPI_URL` env variable such that it looks like `http://localhost:/wopi/files/`. Note the hardcoded `localhost`.
+3. Ensure you run your WOPI server in http mode, that is you have `usehttps = no` in your configuration.
-5. Run the testsuite (you can select a specific test group with e.g. `-e WOPI_TESTGROUP=FileVersion`):
-`docker run --add-host="localhost:" -e WOPI_URL=$WOPI_URL -e WOPI_TOKEN=$WOPI_TOKEN deepdiver/wopi-validator-core-docker:use-different-branch-to-make-ci-finally-green`
+4. Generate the input for the test suite:
+ `curl -H "Authorization: Bearer " "http://your_wopi_server:port/wopi/iop/test?filepath=&endpoint=&usertoken="`
+
+5. Run the testsuite (you can select a specific test group passing as well e.g. `-e WOPI_TESTGROUP=FileVersion`):
+
+ `docker run --rm --add-host="localhost:"