Skip to content

Commit a66051a

Browse files
committed
Added support for List/Close File Handles APIs
1 parent 588c11a commit a66051a

11 files changed

+672
-1
lines changed

azure-storage-file/ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
## Version XX.XX.XX:
66
- Support for 2018-11-09 REST version. Please see our REST API documentation and blogs for information about the related added features.
77
- Added an option to get share stats in bytes.
8+
- Added support for List/Close File Handles APIs.
89

910
## Version 1.4.0:
1011

azure-storage-file/azure/storage/file/_deserialization.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Share,
1414
Directory,
1515
File,
16+
Handle,
1617
FileProperties,
1718
FileRange,
1819
ShareProperties,
@@ -31,6 +32,7 @@
3132
_to_str,
3233
)
3334

35+
3436
def _parse_snapshot_share(response, name):
3537
'''
3638
Extracts snapshot return header.
@@ -39,6 +41,7 @@ def _parse_snapshot_share(response, name):
3941

4042
return _parse_share(response, name, snapshot)
4143

44+
4245
def _parse_share(response, name, snapshot=None):
4346
if response is None:
4447
return None
@@ -197,6 +200,71 @@ def _convert_xml_to_directories_and_files(response):
197200
return entries
198201

199202

203+
def _convert_xml_to_handles(response):
204+
"""
205+
<?xml version="1.0" encoding="utf-8"?>
206+
<EnumerationResults>
207+
<HandleList>
208+
<Handle>
209+
<HandleId>handle-id</HandleId>
210+
<Path>file-or-directory-name-including-full-path</Path>
211+
<FileId>file-id</FileId>
212+
<ParentId>parent-file-id</ParentId>
213+
<SessionId>session-id</SessionId>
214+
<ClientIp>client-ip</ClientIp>
215+
<OpenTime>opened-time</OpenTime>
216+
<LastReconnectTime>lastreconnect-time</LastReconnectTime>
217+
</Handle>
218+
...
219+
</HandleList>
220+
<NextMarker>next-marker</NextMarker>
221+
</EnumerationResults>
222+
"""
223+
if response is None or response.body is None:
224+
return None
225+
226+
entries = _list()
227+
list_element = ETree.fromstring(response.body)
228+
229+
# Set next marker
230+
next_marker = list_element.findtext('NextMarker') or None
231+
setattr(entries, 'next_marker', next_marker)
232+
233+
handles_list_element = list_element.find('Entries')
234+
235+
for handle_element in handles_list_element.findall('Handle'):
236+
# Name element
237+
handle = Handle()
238+
handle.handle_id = handle_element.findtext('HandleId')
239+
handle.path = handle_element.findtext('Path')
240+
handle.file_id = handle_element.findtext('FileId')
241+
handle.parent_id = handle_element.findtext('ParentId')
242+
handle.session_id = handle_element.findtext('SessionId')
243+
handle.client_ip = handle_element.findtext('ClientIp')
244+
handle.open_time = parser.parse(handle_element.findtext('OpenTime'))
245+
246+
last_connect_time_string = handle_element.findtext('LastReconnectTime')
247+
if last_connect_time_string is not None:
248+
handle.last_reconnect_time = parser.parse(last_connect_time_string)
249+
250+
# Add file to list
251+
entries.append(handle)
252+
253+
return entries
254+
255+
256+
def _parse_close_handle_response(response):
257+
if response is None or response.body is None:
258+
return 0
259+
260+
results = _list()
261+
results.append(int(response.headers['x-ms-number-of-handles-closed']))
262+
263+
next_marker = None if 'x-ms-marker' not in response.headers else response.headers['x-ms-marker']
264+
setattr(results, 'next_marker', next_marker)
265+
return results
266+
267+
200268
def _convert_xml_to_ranges(response):
201269
'''
202270
<?xml version="1.0" encoding="utf-8"?>

azure-storage-file/azure/storage/file/fileservice.py

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
from ._deserialization import (
6363
_convert_xml_to_shares,
6464
_convert_xml_to_directories_and_files,
65+
_convert_xml_to_handles,
66+
_parse_close_handle_response,
6567
_convert_xml_to_ranges,
6668
_convert_xml_to_share_stats,
6769
_parse_file,
@@ -898,7 +900,7 @@ def get_share_stats(self, share_name, timeout=None):
898900
}
899901

900902
usage = self._perform_request(request, _convert_xml_to_share_stats)
901-
return int(math.ceil(float(usage)/_GB))
903+
return int(math.ceil(float(usage) / _GB))
902904

903905
def get_share_stats_in_bytes(self, share_name, timeout=None):
904906
"""
@@ -1259,6 +1261,187 @@ def _list_directories_and_files(self, share_name, directory_name=None,
12591261
return self._perform_request(request, _convert_xml_to_directories_and_files,
12601262
operation_context=_context)
12611263

1264+
def list_handles(self, share_name, directory_name=None, file_name=None, recursive=None,
1265+
max_results=None, marker=None, snapshot=None, timeout=None):
1266+
1267+
"""
1268+
Returns a generator to list opened handles on a directory or a file under the specified share.
1269+
The generator will lazily follow the continuation tokens returned by
1270+
the service and stop when all handles have been returned or
1271+
num_results is reached.
1272+
1273+
If num_results is specified and the share has more than that number of
1274+
files and directories, the generator will have a populated next_marker
1275+
field once it finishes. This marker can be used to create a new generator
1276+
if more results are desired.
1277+
1278+
:param str share_name:
1279+
Name of existing share.
1280+
:param str directory_name:
1281+
The path to the directory.
1282+
:param str file_name:
1283+
Name of existing file.
1284+
:param bool recursive:
1285+
Boolean that specifies if operation should apply to the directory specified in the URI,
1286+
its files, its subdirectories and their files.
1287+
:param int max_results:
1288+
Specifies the maximum number of handles taken on files and/or directories to return.
1289+
If the request does not specify max_results or specifies a value greater than 5,000,
1290+
the server will return up to 5,000 items.
1291+
Setting max_results to a value less than or equal to zero results in error response code 400 (Bad Request).
1292+
:param str marker:
1293+
An opaque continuation token. This value can be retrieved from the
1294+
next_marker field of a previous generator object if max_results was
1295+
specified and that generator has finished enumerating results. If
1296+
specified, this generator will begin returning results from the point
1297+
where the previous generator stopped.
1298+
:param str snapshot:
1299+
A string that represents the snapshot version, if applicable.
1300+
:param int timeout:
1301+
The timeout parameter is expressed in seconds.
1302+
"""
1303+
operation_context = _OperationContext(location_lock=True)
1304+
args = (share_name, directory_name, file_name)
1305+
kwargs = {'marker': marker, 'max_results': max_results, 'timeout': timeout, 'recursive': recursive,
1306+
'_context': operation_context, 'snapshot': snapshot}
1307+
1308+
resp = self._list_handles(*args, **kwargs)
1309+
1310+
return ListGenerator(resp, self._list_handles, args, kwargs)
1311+
1312+
def _list_handles(self, share_name, directory_name=None, file_name=None, recursive=None,
1313+
marker=None, max_results=None, timeout=None, _context=None, snapshot=None):
1314+
"""
1315+
Returns a list of the directories and files under the specified share.
1316+
1317+
:param str share_name:
1318+
Name of existing share.
1319+
:param str directory_name:
1320+
The path to the directory.
1321+
:param str file_name:
1322+
Name of existing file.
1323+
:param bool recursive:
1324+
Boolean that specifies if operation should apply to the directory specified in the URI,
1325+
its files, its subdirectories and their files.
1326+
:param str marker:
1327+
An opaque continuation token. This value can be retrieved from the
1328+
next_marker field of a previous generator object if max_results was
1329+
specified and that generator has finished enumerating results. If
1330+
specified, this generator will begin returning results from the point
1331+
where the previous generator stopped.
1332+
:param int max_results:
1333+
Specifies the maximum number of handles to return,
1334+
including all directory elements. If the request does not specify
1335+
max_results or specifies a value greater than 5,000, the server will
1336+
return up to 5,000 items. Setting max_results to a value less than
1337+
or equal to zero results in error response code 400 (Bad Request).
1338+
:param int timeout:
1339+
The timeout parameter is expressed in seconds.
1340+
:param str snapshot:
1341+
A string that represents the snapshot version, if applicable.
1342+
"""
1343+
_validate_not_none('share_name', share_name)
1344+
request = HTTPRequest()
1345+
request.method = 'GET'
1346+
request.host_locations = self._get_host_locations()
1347+
request.path = _get_path(share_name, directory_name, file_name)
1348+
request.query = {
1349+
'comp': 'listhandles',
1350+
'marker': _to_str(marker),
1351+
'maxresults': _int_to_str(max_results),
1352+
'timeout': _int_to_str(timeout),
1353+
'sharesnapshot': _to_str(snapshot)
1354+
}
1355+
request.headers = {
1356+
'x-ms-recursive': _to_str(recursive)
1357+
}
1358+
1359+
return self._perform_request(request, _convert_xml_to_handles,
1360+
operation_context=_context)
1361+
1362+
def close_handles(self, share_name, directory_name=None, file_name=None, recursive=None,
1363+
handle_id=None, marker=None, snapshot=None, timeout=None):
1364+
1365+
"""
1366+
Returns a generator to close opened handles on a directory or a file under the specified share.
1367+
The generator will lazily follow the continuation tokens returned by
1368+
the service and stop when all handles have been closed.
1369+
The yielded values represent the number of handles that were closed.
1370+
1371+
1372+
:param str share_name:
1373+
Name of existing share.
1374+
:param str directory_name:
1375+
The path to the directory.
1376+
:param str file_name:
1377+
Name of existing file.
1378+
:param bool recursive:
1379+
Boolean that specifies if operation should apply to the directory specified in the URI,
1380+
its files, its subdirectories and their files.
1381+
:param str handle_id:
1382+
Required. Specifies handle ID opened on the file or directory to be closed.
1383+
Astrix (‘*’) is a wildcard that specifies all handles.
1384+
:param str marker:
1385+
An opaque continuation token. This value can be retrieved from the
1386+
next_marker field of a previous generator object if it has not finished closing handles. If
1387+
specified, this generator will begin closing handles from the point
1388+
where the previous generator stopped.
1389+
:param str snapshot:
1390+
A string that represents the snapshot version, if applicable.
1391+
:param int timeout:
1392+
The timeout parameter is expressed in seconds.
1393+
"""
1394+
operation_context = _OperationContext(location_lock=True)
1395+
args = (share_name, directory_name, file_name)
1396+
kwargs = {'marker': marker, 'handle_id': handle_id, 'timeout': timeout, 'recursive': recursive,
1397+
'_context': operation_context, 'snapshot': snapshot}
1398+
1399+
resp = self._close_handles(*args, **kwargs)
1400+
1401+
return ListGenerator(resp, self._close_handles, args, kwargs)
1402+
1403+
def _close_handles(self, share_name, directory_name=None, file_name=None, recursive=None, handle_id=None,
1404+
marker=None, timeout=None, _context=None, snapshot=None):
1405+
"""
1406+
Returns the number of handles that got closed.
1407+
1408+
:param str share_name:
1409+
Name of existing share.
1410+
:param str directory_name:
1411+
The path to the directory.
1412+
:param str file_name:
1413+
Name of existing file.
1414+
:param bool recursive:
1415+
Boolean that specifies if operation should apply to the directory specified in the URI,
1416+
its files, its subdirectories and their files.
1417+
:param str handle_id:
1418+
Required. Specifies handle ID opened on the file or directory to be closed.
1419+
Astrix (‘*’) is a wildcard that specifies all handles.
1420+
:param str marker:
1421+
Specifies the maximum number of handles taken on files and/or directories to return.
1422+
:param int timeout:
1423+
The timeout parameter is expressed in seconds.
1424+
:param str snapshot:
1425+
A string that represents the snapshot version, if applicable.
1426+
"""
1427+
_validate_not_none('share_name', share_name)
1428+
request = HTTPRequest()
1429+
request.method = 'PUT'
1430+
request.host_locations = self._get_host_locations()
1431+
request.path = _get_path(share_name, directory_name, file_name)
1432+
request.query = {
1433+
'comp': 'forceclosehandles',
1434+
'marker': _to_str(marker),
1435+
'timeout': _int_to_str(timeout),
1436+
'sharesnapshot': _to_str(snapshot)
1437+
}
1438+
request.headers = {
1439+
'x-ms-recursive': _to_str(recursive),
1440+
'x-ms-handle-id': _to_str(handle_id),
1441+
}
1442+
1443+
return self._perform_request(request, _parse_close_handle_response, operation_context=_context)
1444+
12621445
def get_file_properties(self, share_name, directory_name, file_name, timeout=None, snapshot=None):
12631446
'''
12641447
Returns all user-defined metadata, standard HTTP properties, and

azure-storage-file/azure/storage/file/models.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,44 @@ def __init__(self):
153153
self.server_encrypted = None
154154

155155

156+
class Handle(object):
157+
"""
158+
Represents a file handle.
159+
160+
:ivar str handle_id:
161+
Used to identify handle.
162+
:ivar str path:
163+
Used to identify the name of the object for which the handle is open.
164+
:ivar str file_id:
165+
Uniquely identifies the file.
166+
This is useful when renames are happening as the file ID does not change.
167+
:ivar str parent_id:
168+
Uniquely identifies the parent directory.
169+
This is useful when renames are happening as the parent ID does not change.
170+
:ivar str session_id:
171+
Session ID in context of which the file handle was opened.
172+
:ivar str client_ip:
173+
Used to identify client that has opened the handle.
174+
The field is included only if client IP is known by the service.
175+
:ivar datetime open_time:
176+
Used to decide if handle may have been leaked.
177+
:ivar datetime last_reconnect_time:
178+
Used to decide if handle was reopened after client/server disconnect due to networking or other faults.
179+
The field is included only if disconnect event occurred and handle was reopened.
180+
"""
181+
182+
def __init__(self, handle_id=None, path=None, file_id=None, parent_id=None, session_id=None,
183+
client_ip=None, open_time=None, last_reconnect_time=None):
184+
self.handle_id = handle_id
185+
self.path = path
186+
self.file_id = file_id
187+
self.parent_id = parent_id
188+
self.session_id = session_id
189+
self.client_ip = client_ip
190+
self.open_time = open_time
191+
self.last_reconnect_time = last_reconnect_time
192+
193+
156194
class ContentSettings(object):
157195
'''
158196
Used to store the content settings of a file.

0 commit comments

Comments
 (0)