Skip to content

Commit c3963c2

Browse files
korydraughnalanking
authored andcommitted
[#525] Add support for touch API operation.
1 parent 71d787f commit c3963c2

10 files changed

Lines changed: 270 additions & 2 deletions

File tree

irods/api_number.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@
182182
"ATOMIC_APPLY_METADATA_OPERATIONS_APN": 20002,
183183
"GET_FILE_DESCRIPTOR_INFO_APN": 20000,
184184
"REPLICA_CLOSE_APN": 20004,
185+
"TOUCH_APN": 20007,
185186

186187
"AUTH_PLUG_REQ_AN": 1201
187188
}

irods/data_object.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def __init__(self, manager, parent=None, results=None):
7070
r[DataObject.resc_hier],
7171
checksum=r[DataObject.checksum],
7272
size=r[DataObject.size],
73-
comments=r[DataObject.comments]
73+
comments=r[DataObject.comments],
74+
create_time=r[DataObject.create_time],
75+
modify_time=r[DataObject.modify_time]
7476
) for r in replicas]
7577
self._meta = None
7678

irods/exception.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ class DoesNotExist(PycommandsException):
2323
pass
2424

2525

26+
class InvalidInputArgument(PycommandsException):
27+
pass
28+
29+
2630
class DataObjectDoesNotExist(DoesNotExist):
2731
pass
2832

irods/manager/_internal/__init__.py

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from irods.api_number import api_number
2+
from irods.message import iRODSMessage, JSON_Message
3+
4+
def _touch_impl(session, path, **options):
5+
with session.pool.get_connection() as conn:
6+
message_body = JSON_Message(
7+
{'logical_path': path, 'options': options},
8+
conn.server_version)
9+
message = iRODSMessage('RODS_API_REQ', msg=message_body,
10+
int_info=api_number['TOUCH_APN'])
11+
conn.send(message)
12+
response = conn.recv()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from irods.exception import CollectionDoesNotExist
2+
3+
def _is_collection(session, path):
4+
"""Return True if the logical path points to a collection, else False.
5+
6+
Parameters
7+
----------
8+
session: iRODSSession
9+
The session object.
10+
11+
path: string
12+
The absolute logical path to a collection.
13+
"""
14+
try:
15+
session.collections.get(path)
16+
return True
17+
except CollectionDoesNotExist:
18+
pass
19+
return False

irods/manager/collection_manager.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import
22
from irods.models import Collection, DataObject
33
from irods.manager import Manager
4+
from irods.manager._internal import _api_impl
45
from irods.message import iRODSMessage, CollectionRequest, FileOpenRequest, ObjCopyRequest, StringStringMap
56
from irods.exception import CollectionDoesNotExist, NoResultFound
67
from irods.api_number import api_number
@@ -150,3 +151,40 @@ def register(self, dir_path, coll_path, **options):
150151
with self.sess.pool.get_connection() as conn:
151152
conn.send(message)
152153
response = conn.recv()
154+
155+
def touch(self, path, **options):
156+
"""Change the mtime of an existing collection.
157+
158+
Parameters
159+
----------
160+
path: string
161+
The absolute logical path of a collection.
162+
163+
seconds_since_epoch: integer, optional
164+
The number of seconds since epoch representing the new mtime. Cannot
165+
be used with "reference" parameter.
166+
167+
reference: string, optional
168+
Use the mtime of the logical path to the data object or collection
169+
identified by this option. Cannot be used with "seconds_since_epoch"
170+
parameter.
171+
172+
Raises
173+
------
174+
CollectionDoesNotExist
175+
If the target collection does not exist or does not point to a
176+
collection.
177+
"""
178+
# Attempt to lookup the collection. If it does not exist, the call
179+
# will raise an exception.
180+
#
181+
# Enforces the requirement that collections must exist before this
182+
# operation is invoked.
183+
self.get(path)
184+
185+
# The following options to the touch API are not allowed for collections.
186+
options.pop('no_create', None)
187+
options.pop('replica_number', None)
188+
options.pop('leaf_resource_name', None)
189+
190+
_api_impl._touch_impl(self.sess, path, no_create=True, **options)

irods/manager/data_object_manager.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io
44
from irods.models import DataObject, Collection
55
from irods.manager import Manager
6+
from irods.manager._internal import _api_impl, _logical_path
67
from irods.message import (
78
iRODSMessage, FileOpenRequest, ObjCopyRequest, StringStringMap, DataObjInfo, ModDataObjMeta,
89
DataObjChksumRequest, DataObjChksumResponse, RErrorStack, STR_PI
@@ -726,3 +727,48 @@ def modDataObjMeta(self, data_obj_info, meta_dict, **options):
726727
with self.sess.pool.get_connection() as conn:
727728
conn.send(message)
728729
response = conn.recv()
730+
731+
def touch(self, path, **options):
732+
"""Change the mtime of a data object.
733+
734+
A path argument that does not exist will be created as an empty data
735+
object, unless "no_create=True" is supplied.
736+
737+
Parameters
738+
----------
739+
path: string
740+
The absolute logical path of a data object.
741+
742+
no_create: boolean, optional
743+
Instructs the system not to create a data object when it does not
744+
exist.
745+
746+
replica_number: integer, optional
747+
The replica number of the replica to update. Replica numbers cannot
748+
be used to create data objects or additional replicas. Cannot be used
749+
with "leaf_resource_name".
750+
751+
leaf_resource_name: string, optional
752+
The name of the leaf resource containing the replica to update. If
753+
the object identified by the "path" parameter does not exist and this
754+
parameter holds a valid resource, the data object will be created at
755+
the specified resource. Cannot be used with "replica_number" parameter.
756+
757+
seconds_since_epoch: integer, optional
758+
The number of seconds since epoch representing the new mtime. Cannot
759+
be used with "reference" parameter.
760+
761+
reference: string, optional
762+
Use the mtime of the logical path to the data object or collection
763+
identified by this option. Cannot be used with "seconds_since_epoch"
764+
parameter.
765+
766+
Raises
767+
------
768+
InvalidInputArgument
769+
If the path points to a collection.
770+
"""
771+
if _logical_path._is_collection(self.sess, path):
772+
raise ex.InvalidInputArgument()
773+
774+
_api_impl._touch_impl(self.sess, path, **options)

irods/test/collection_test.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#! /usr/bin/env python
22
from __future__ import absolute_import
3+
from datetime import datetime
34
import os
45
import sys
56
import socket
@@ -15,13 +16,31 @@
1516
from irods.test.helpers import my_function_name, unique_name
1617
from irods.collection import iRODSCollection
1718

19+
RODSUSER = 'nonadmin'
1820

1921
class TestCollection(unittest.TestCase):
2022

23+
class WrongUserType(RuntimeError): pass
24+
25+
@classmethod
26+
def setUpClass(cls):
27+
adm = helpers.make_session()
28+
if adm.users.get(adm.username).type != 'rodsadmin':
29+
raise cls.WrongUserType('Must be an iRODS admin to run tests in class {0.__name__}'.format(cls))
30+
cls.logins = helpers.iRODSUserLogins(adm)
31+
cls.logins.create_user(RODSUSER, 'abc123')
32+
33+
34+
@classmethod
35+
def tearDownClass(cls):
36+
# TODO(#553): Skipping this will result in an interpreter seg fault for Py3.6 but not 3.11; why?
37+
del cls.logins
38+
39+
2140
def setUp(self):
2241
self.sess = helpers.make_session()
23-
self.test_coll_path = '/{}/home/{}/test_dir'.format(self.sess.zone, self.sess.username)
2442

43+
self.test_coll_path = '/{}/home/{}/test_dir'.format(self.sess.zone, self.sess.username)
2544
self.test_coll = self.sess.collections.create(self.test_coll_path)
2645

2746

@@ -380,6 +399,96 @@ def test_object_paths_with_dot_and_dotdot__323(self):
380399
home2 = normalize('/zone','holmes','..','home','public','..','user')
381400
self.assertEqual(home2, '/zone/home/user')
382401

402+
def test_update_mtime_of_collection_using_touch_operation_as_non_admin__525(self):
403+
user_session = self.logins.session_for_user(RODSUSER)
404+
405+
# Capture mtime of the home collection.
406+
home_collection_path = helpers.home_collection(user_session)
407+
collection = user_session.collections.get(home_collection_path)
408+
old_mtime = collection.modify_time
409+
410+
# Set the mtime to an earlier time.
411+
new_mtime = 1400000000
412+
user_session.collections.touch(home_collection_path, seconds_since_epoch=new_mtime)
413+
414+
# Compare mtimes for correctness.
415+
collection = user_session.collections.get(home_collection_path)
416+
self.assertEqual(datetime.utcfromtimestamp(new_mtime), collection.modify_time)
417+
self.assertGreater(old_mtime, collection.modify_time)
418+
419+
def test_touch_operation_does_not_create_new_collections__525(self):
420+
user_session = self.logins.session_for_user(RODSUSER)
421+
422+
# The collection should not exist.
423+
collection_path = f'{helpers.home_collection(user_session)}/test_touch_operation_does_not_create_new_collections__525'
424+
with self.assertRaises(CollectionDoesNotExist):
425+
user_session.collections.get(collection_path)
426+
427+
# Show the touch operation throws an exception if the target collection
428+
# does not exist.
429+
with self.assertRaises(CollectionDoesNotExist):
430+
user_session.collections.touch(collection_path)
431+
432+
# Show the touch operation did not create a new collection.
433+
with self.assertRaises(CollectionDoesNotExist):
434+
user_session.collections.get(collection_path)
435+
436+
def test_touch_operation_does_not_work_when_given_a_data_object__525(self):
437+
try:
438+
user_session = self.logins.session_for_user(RODSUSER)
439+
440+
# Create a data object.
441+
data_object_path = f'{helpers.home_collection(user_session)}/test_touch_operation_does_not_work_when_given_a_data_object__525.txt'
442+
self.assertFalse(user_session.data_objects.exists(data_object_path))
443+
user_session.data_objects.touch(data_object_path)
444+
self.assertTrue(user_session.data_objects.exists(data_object_path))
445+
446+
# Show the touch operation for collections throws an exception when
447+
# given a path pointing to a data object.
448+
with self.assertRaises(CollectionDoesNotExist):
449+
user_session.collections.touch(data_object_path)
450+
451+
finally:
452+
user_session.data_objects.unlink(data_object_path, force=True)
453+
454+
def test_touch_operation_ignores_unsupported_options__525(self):
455+
user_session = self.logins.session_for_user(RODSUSER)
456+
path = f'{helpers.home_collection(user_session)}/test_touch_operation_ignores_unsupported_options__525'
457+
458+
try:
459+
# Capture mtime of the home collection.
460+
collection = user_session.collections.create(path)
461+
old_mtime = collection.modify_time
462+
463+
# Capture the current time.
464+
time.sleep(2) # Guarantees the mtime is different.
465+
new_mtime = int(time.time())
466+
467+
# The touch API for the iRODS server will attempt to create a new data object
468+
# if the "no_create" option is set to false. The PRC's collection interface will
469+
# ignore that option if passed.
470+
#
471+
# The following arguments don't make sense for collections and will also be ignored.
472+
#
473+
# - replica_number
474+
# - leaf_resource_name
475+
#
476+
# They are included to prove the PRC handles them appropriately (i.e. unsupported
477+
# parameters are removed from the request).
478+
user_session.collections.touch(path,
479+
no_create=False,
480+
replica_number=525,
481+
seconds_since_epoch=new_mtime,
482+
leaf_resource_name='ufs525')
483+
484+
# Compare mtimes for correctness.
485+
collection = user_session.collections.get(path)
486+
self.assertEqual(datetime.utcfromtimestamp(int(new_mtime)), collection.modify_time)
487+
488+
finally:
489+
if collection:
490+
user_session.collections.remove(path, recurse=True, force=True)
491+
383492

384493
if __name__ == "__main__":
385494
# let the tests find the parent irods lib

irods/test/data_obj_test.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#! /usr/bin/env python
22
from __future__ import absolute_import
3+
from datetime import datetime
34
import base64
45
import concurrent.futures
56
import contextlib # check if redundant
@@ -2044,6 +2045,42 @@ def test_append_mode_will_append_to_data_object__issue_495(self):
20442045
if data.exists(testfile):
20452046
data.unlink(testfile,force=True)
20462047

2048+
def test_update_mtime_of_data_object_using_touch_operation_as_non_admin__525(self):
2049+
try:
2050+
user_session = self.logins.session_for_user(RODSUSER)
2051+
2052+
# Create a data object.
2053+
data_object_path = f'{helpers.home_collection(user_session)}/test_update_mtime_of_data_object_using_touch_operation__525.txt'
2054+
self.assertFalse(user_session.data_objects.exists(data_object_path))
2055+
user_session.data_objects.touch(data_object_path)
2056+
self.assertTrue(user_session.data_objects.exists(data_object_path))
2057+
2058+
# Capture mtime of data object.
2059+
data_object = user_session.data_objects.get(data_object_path)
2060+
old_mtime = data_object.replicas[0].modify_time
2061+
2062+
# Set the mtime to an earlier time.
2063+
new_mtime = 1400000000
2064+
user_session.data_objects.touch(data_object_path, seconds_since_epoch=new_mtime)
2065+
2066+
# Compare mtimes for correctness.
2067+
data_object = user_session.data_objects.get(data_object_path)
2068+
self.assertEqual(datetime.utcfromtimestamp(int(new_mtime)), data_object.replicas[0].modify_time)
2069+
self.assertGreater(old_mtime, data_object.replicas[0].modify_time)
2070+
2071+
finally:
2072+
if data_object:
2073+
user_session.data_objects.unlink(data_object.path, force=True)
2074+
2075+
def test_touch_operation_does_not_work_when_given_a_collection__525(self):
2076+
user_session = self.logins.session_for_user(RODSUSER)
2077+
2078+
# Show the touch operation for data objects throws an exception when
2079+
# given a path pointing to a collection.
2080+
home_collection_path = helpers.home_collection(user_session)
2081+
with self.assertRaises(ex.InvalidInputArgument):
2082+
user_session.data_objects.touch(home_collection_path)
2083+
20472084

20482085
if __name__ == '__main__':
20492086
# let the tests find the parent irods lib

0 commit comments

Comments
 (0)