rucio-clients 37.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rucio-clients might be problematic. Click here for more details.

Files changed (104) hide show
  1. rucio/__init__.py +17 -0
  2. rucio/alembicrevision.py +15 -0
  3. rucio/cli/__init__.py +14 -0
  4. rucio/cli/account.py +216 -0
  5. rucio/cli/bin_legacy/__init__.py +13 -0
  6. rucio/cli/bin_legacy/rucio.py +2825 -0
  7. rucio/cli/bin_legacy/rucio_admin.py +2500 -0
  8. rucio/cli/command.py +272 -0
  9. rucio/cli/config.py +72 -0
  10. rucio/cli/did.py +191 -0
  11. rucio/cli/download.py +128 -0
  12. rucio/cli/lifetime_exception.py +33 -0
  13. rucio/cli/replica.py +162 -0
  14. rucio/cli/rse.py +293 -0
  15. rucio/cli/rule.py +158 -0
  16. rucio/cli/scope.py +40 -0
  17. rucio/cli/subscription.py +73 -0
  18. rucio/cli/upload.py +60 -0
  19. rucio/cli/utils.py +226 -0
  20. rucio/client/__init__.py +15 -0
  21. rucio/client/accountclient.py +432 -0
  22. rucio/client/accountlimitclient.py +183 -0
  23. rucio/client/baseclient.py +983 -0
  24. rucio/client/client.py +120 -0
  25. rucio/client/configclient.py +126 -0
  26. rucio/client/credentialclient.py +59 -0
  27. rucio/client/didclient.py +868 -0
  28. rucio/client/diracclient.py +56 -0
  29. rucio/client/downloadclient.py +1783 -0
  30. rucio/client/exportclient.py +44 -0
  31. rucio/client/fileclient.py +50 -0
  32. rucio/client/importclient.py +42 -0
  33. rucio/client/lifetimeclient.py +90 -0
  34. rucio/client/lockclient.py +109 -0
  35. rucio/client/metaconventionsclient.py +140 -0
  36. rucio/client/pingclient.py +44 -0
  37. rucio/client/replicaclient.py +452 -0
  38. rucio/client/requestclient.py +125 -0
  39. rucio/client/richclient.py +317 -0
  40. rucio/client/rseclient.py +746 -0
  41. rucio/client/ruleclient.py +294 -0
  42. rucio/client/scopeclient.py +90 -0
  43. rucio/client/subscriptionclient.py +173 -0
  44. rucio/client/touchclient.py +82 -0
  45. rucio/client/uploadclient.py +969 -0
  46. rucio/common/__init__.py +13 -0
  47. rucio/common/bittorrent.py +234 -0
  48. rucio/common/cache.py +111 -0
  49. rucio/common/checksum.py +168 -0
  50. rucio/common/client.py +122 -0
  51. rucio/common/config.py +788 -0
  52. rucio/common/constants.py +217 -0
  53. rucio/common/constraints.py +17 -0
  54. rucio/common/didtype.py +237 -0
  55. rucio/common/exception.py +1208 -0
  56. rucio/common/extra.py +31 -0
  57. rucio/common/logging.py +420 -0
  58. rucio/common/pcache.py +1409 -0
  59. rucio/common/plugins.py +185 -0
  60. rucio/common/policy.py +93 -0
  61. rucio/common/schema/__init__.py +200 -0
  62. rucio/common/schema/generic.py +416 -0
  63. rucio/common/schema/generic_multi_vo.py +395 -0
  64. rucio/common/stomp_utils.py +423 -0
  65. rucio/common/stopwatch.py +55 -0
  66. rucio/common/test_rucio_server.py +154 -0
  67. rucio/common/types.py +483 -0
  68. rucio/common/utils.py +1688 -0
  69. rucio/rse/__init__.py +96 -0
  70. rucio/rse/protocols/__init__.py +13 -0
  71. rucio/rse/protocols/bittorrent.py +194 -0
  72. rucio/rse/protocols/cache.py +111 -0
  73. rucio/rse/protocols/dummy.py +100 -0
  74. rucio/rse/protocols/gfal.py +708 -0
  75. rucio/rse/protocols/globus.py +243 -0
  76. rucio/rse/protocols/http_cache.py +82 -0
  77. rucio/rse/protocols/mock.py +123 -0
  78. rucio/rse/protocols/ngarc.py +209 -0
  79. rucio/rse/protocols/posix.py +250 -0
  80. rucio/rse/protocols/protocol.py +361 -0
  81. rucio/rse/protocols/rclone.py +365 -0
  82. rucio/rse/protocols/rfio.py +145 -0
  83. rucio/rse/protocols/srm.py +338 -0
  84. rucio/rse/protocols/ssh.py +414 -0
  85. rucio/rse/protocols/storm.py +195 -0
  86. rucio/rse/protocols/webdav.py +594 -0
  87. rucio/rse/protocols/xrootd.py +302 -0
  88. rucio/rse/rsemanager.py +881 -0
  89. rucio/rse/translation.py +260 -0
  90. rucio/vcsversion.py +11 -0
  91. rucio/version.py +45 -0
  92. rucio_clients-37.0.0rc1.data/data/etc/rse-accounts.cfg.template +25 -0
  93. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.atlas.client.template +43 -0
  94. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.template +241 -0
  95. rucio_clients-37.0.0rc1.data/data/requirements.client.txt +19 -0
  96. rucio_clients-37.0.0rc1.data/data/rucio_client/merge_rucio_configs.py +144 -0
  97. rucio_clients-37.0.0rc1.data/scripts/rucio +133 -0
  98. rucio_clients-37.0.0rc1.data/scripts/rucio-admin +97 -0
  99. rucio_clients-37.0.0rc1.dist-info/METADATA +54 -0
  100. rucio_clients-37.0.0rc1.dist-info/RECORD +104 -0
  101. rucio_clients-37.0.0rc1.dist-info/WHEEL +5 -0
  102. rucio_clients-37.0.0rc1.dist-info/licenses/AUTHORS.rst +100 -0
  103. rucio_clients-37.0.0rc1.dist-info/licenses/LICENSE +201 -0
  104. rucio_clients-37.0.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,250 @@
1
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import logging
16
+ import os
17
+ import os.path
18
+ import shutil
19
+ from subprocess import call
20
+
21
+ from rucio.common import exception
22
+ from rucio.common.checksum import adler32
23
+ from rucio.rse.protocols import protocol
24
+
25
+
26
+ class Default(protocol.RSEProtocol):
27
+ """ Implementing access to RSEs using the local filesystem."""
28
+
29
+ def exists(self, pfn):
30
+ """
31
+ Checks if the requested file is known by the referred RSE.
32
+
33
+ :param pfn: Physical file name
34
+
35
+ :returns: True if the file exists, False if it doesn't
36
+
37
+ :raises SourceNotFound: if the source file was not found on the referred storage.
38
+ """
39
+ status = ''
40
+ try:
41
+ status = os.path.exists(self.pfn2path(pfn))
42
+ except Exception as e:
43
+ raise exception.ServiceUnavailable(e)
44
+ return status
45
+
46
+ def connect(self):
47
+ """
48
+ Establishes the actual connection to the referred RSE.
49
+
50
+ :param credentials: needed to establish a connection with the storage.
51
+
52
+ :raises RSEAccessDenied: if no connection could be established.
53
+ """
54
+ pass
55
+
56
+ def close(self):
57
+ """ Closes the connection to RSE."""
58
+ pass
59
+
60
+ def get(self, pfn, dest, transfer_timeout=None):
61
+ """ Provides access to files stored inside connected the RSE.
62
+
63
+ :param pfn: Physical file name of requested file
64
+ :param dest: Name and path of the files when stored at the client
65
+ :param transfer_timeout Transfer timeout (in seconds) - dummy
66
+
67
+ :raises DestinationNotAccessible: if the destination storage was not accessible.
68
+ :raises ServiceUnavailable: if some generic error occurred in the library.
69
+ :raises SourceNotFound: if the source file was not found on the referred storage.
70
+ """
71
+ try:
72
+ shutil.copy(self.pfn2path(pfn), dest)
73
+ except OSError as e:
74
+ try: # To check if the error happened local or remote
75
+ with open(dest, 'wb'):
76
+ pass
77
+ call(['rm', '-rf', dest]) # noqa: S607
78
+ except OSError as e:
79
+ if e.errno == 2:
80
+ raise exception.DestinationNotAccessible(e)
81
+ else:
82
+ raise exception.ServiceUnavailable(e)
83
+ if e.errno == 2:
84
+ raise exception.SourceNotFound(e)
85
+ else:
86
+ raise exception.ServiceUnavailable(e)
87
+
88
+ def put(self, source, target, source_dir=None, transfer_timeout=None):
89
+ """
90
+ Allows to store files inside the referred RSE.
91
+
92
+ :param source: path to the source file on the client file system
93
+ :param target: path to the destination file on the storage
94
+ :param source_dir: Path where the to be transferred files are stored in the local file system
95
+ :param transfer_timeout Transfer timeout (in seconds) - dummy
96
+
97
+ :raises DestinationNotAccessible: if the destination storage was not accessible.
98
+ :raises ServiceUnavailable: if some generic error occurred in the library.
99
+ :raises SourceNotFound: if the source file was not found on the referred storage.
100
+ """
101
+ target = self.pfn2path(target)
102
+
103
+ if source_dir:
104
+ sf = source_dir + '/' + source
105
+ else:
106
+ sf = source
107
+ try:
108
+ dirs = os.path.dirname(target)
109
+ if not os.path.exists(dirs):
110
+ os.makedirs(dirs)
111
+ shutil.copy(sf, target)
112
+ except OSError as e:
113
+ if e.errno == 2:
114
+ raise exception.SourceNotFound(e)
115
+ elif not self.exists(self.rse['prefix']):
116
+ path = ''
117
+ for p in self.rse['prefix'].split('/'):
118
+ path += p + '/'
119
+ os.mkdir(path)
120
+ shutil.copy(sf, self.pfn2path(target))
121
+ else:
122
+ raise exception.DestinationNotAccessible(e)
123
+
124
+ def delete(self, pfn):
125
+ """ Deletes a file from the connected RSE.
126
+
127
+ :param pfn: pfn to the to be deleted file
128
+
129
+ :raises ServiceUnavailable: if some generic error occurred in the library.
130
+ :raises SourceNotFound: if the source file was not found on the referred storage.
131
+ """
132
+ try:
133
+ os.remove(self.pfn2path(pfn))
134
+ except OSError as e:
135
+ if e.errno == 2:
136
+ raise exception.SourceNotFound(e)
137
+
138
+ def rename(self, pfn, new_pfn):
139
+ """ Allows to rename a file stored inside the connected RSE.
140
+
141
+ :param path: path to the current file on the storage
142
+ :param new_path: path to the new file on the storage
143
+
144
+ :raises DestinationNotAccessible: if the destination storage was not accessible.
145
+ :raises ServiceUnavailable: if some generic error occurred in the library.
146
+ :raises SourceNotFound: if the source file was not found on the referred storage.
147
+ """
148
+ path = self.pfn2path(pfn)
149
+ new_path = self.pfn2path(new_pfn)
150
+ try:
151
+ if not os.path.exists(os.path.dirname(new_path)):
152
+ os.makedirs(os.path.dirname(new_path))
153
+ os.rename(path, new_path)
154
+ except OSError as e:
155
+ if e.errno == 2:
156
+ if self.exists(self.pfn2path(path)):
157
+ raise exception.SourceNotFound(e)
158
+ else:
159
+ raise exception.DestinationNotAccessible(e)
160
+ else:
161
+ raise exception.ServiceUnavailable(e)
162
+
163
+ def lfns2pfns(self, lfns):
164
+ """ Returns fully qualified PFNs for the file referred by each lfn in
165
+ the lfns list.
166
+
167
+ :param lfns: List of lfns. If lfn['path'] is present it is used as
168
+ the path to the file, otherwise the path is constructed
169
+ deterministically.
170
+
171
+ :returns: Fully qualified PFNs.
172
+ """
173
+ pfns = {}
174
+ prefix = self.attributes['prefix']
175
+
176
+ if not prefix.startswith('/'):
177
+ prefix = ''.join(['/', prefix])
178
+ if not prefix.endswith('/'):
179
+ prefix = ''.join([prefix, '/'])
180
+
181
+ lfns = [lfns] if isinstance(lfns, dict) else lfns
182
+ for lfn in lfns:
183
+ scope, name = str(lfn['scope']), lfn['name']
184
+ if 'path' in lfn and lfn.get('path'):
185
+ pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'],
186
+ '://',
187
+ self.attributes['hostname'],
188
+ prefix,
189
+ lfn['path'] if not lfn['path'].startswith('/') else lfn['path'][1:]
190
+ ])
191
+ else:
192
+ pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'],
193
+ '://',
194
+ self.attributes['hostname'],
195
+ prefix,
196
+ self._get_path(scope=scope, name=name)
197
+ ])
198
+ return pfns
199
+
200
+ def pfn2path(self, pfn):
201
+ tmp = list(self.parse_pfns(pfn).values())[0]
202
+ return '/'.join([tmp['prefix'], tmp['path'], tmp['name']])
203
+
204
+ def stat(self, pfn):
205
+ """ Determines the file size in bytes and checksum (adler32) of the provided file.
206
+
207
+ :param pfn: The PFN the file.
208
+
209
+ :returns: a dict containing the keys filesize and adler32.
210
+ """
211
+ path = self.pfn2path(pfn)
212
+ return {'filesize': os.stat(path)[os.path.stat.ST_SIZE], 'adler32': adler32(path)}
213
+
214
+
215
+ class Symlink(Default):
216
+ """ Implementing access to RSEs using the local filesystem, creating a symlink on a get """
217
+
218
+ def get(self, pfn, dest, transfer_timeout=None):
219
+ """ Provides access to files stored inside connected the RSE.
220
+ A download/get will create a symlink on the local file system pointing to the
221
+ underlying file. Other operations act directly on the remote file.
222
+ :param pfn: Physical file name of requested file
223
+ :param dest: Name and path of the files when stored at the client
224
+ :param transfer_timeout Transfer timeout (in seconds) - dummy
225
+ :raises DestinationNotAccessible: if the destination storage was not accessible.
226
+ :raises ServiceUnavailable: if some generic error occurred in the library.
227
+ :raises SourceNotFound: if the source file was not found on the referred storage.
228
+ """
229
+ path = self.pfn2path(pfn)
230
+ os.symlink(path, dest)
231
+ self.logger(logging.DEBUG,
232
+ 'Symlink {} created for {} from {}'
233
+ .format(dest, path, pfn))
234
+ if not os.lstat(dest):
235
+ # problem in creating the symlink
236
+ self.logger(logging.ERROR, 'Symlink {} could not be created'.format(dest))
237
+ raise exception.DestinationNotAccessible()
238
+ if not os.path.exists(dest):
239
+ # could not find the file following the symlink
240
+ self.logger(logging.ERROR, 'Symlink {} appears to be a broken link to {}'
241
+ .format(dest, path))
242
+ if os.lstat(dest) and os.path.islink(dest):
243
+ os.unlink(dest)
244
+ raise exception.SourceNotFound()
245
+
246
+ def pfn2path(self, pfn):
247
+ # obtain path and sanitise from multiple slashes, etc
248
+ path = os.path.normpath(super().pfn2path(pfn))
249
+ self.logger(logging.DEBUG, 'Extracted path: {} from: {}'.format(path, pfn))
250
+ return path
@@ -0,0 +1,361 @@
1
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ This module defines the base class for implementing a transfer protocol,
17
+ along with some of the default methods for LFN2PFN translations.
18
+ """
19
+ import logging
20
+ from abc import ABC, abstractmethod
21
+ from typing import TYPE_CHECKING, Any, Optional, Union
22
+ from urllib.parse import urlparse
23
+
24
+ from rucio.common import exception
25
+ from rucio.rse import rsemanager
26
+ from rucio.rse.translation import RSEDeterministicTranslation
27
+
28
+ if getattr(rsemanager, 'CLIENT_MODE', None):
29
+ from rucio.client.rseclient import RSEClient
30
+
31
+ if getattr(rsemanager, 'SERVER_MODE', None):
32
+ from rucio.core import replica
33
+ from rucio.core.rse import get_rse_vo
34
+
35
+ if getattr(rsemanager, 'SERVER_MODE', None) or TYPE_CHECKING:
36
+ from rucio.common.types import InternalScope, LFNDict, LoggerFunction, RSESettingsDict
37
+
38
+ if TYPE_CHECKING:
39
+ from collections.abc import Iterable
40
+
41
+ from rucio.common.types import DIDDict
42
+
43
+
44
+ class RSEProtocol(ABC):
45
+ """ This class is virtual and acts as a base to inherit new protocols from. It further provides some common functionality which applies for the majority of the protocols."""
46
+
47
+ def __init__(
48
+ self,
49
+ protocol_attr: dict[str, Any],
50
+ rse_settings: "RSESettingsDict",
51
+ logger: "LoggerFunction" = logging.log
52
+ ):
53
+ """ Initializes the object with information about the referred RSE.
54
+
55
+ :param protocol_attr: Properties of the requested protocol.
56
+ :param rse_settting: The RSE settings.
57
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
58
+ """
59
+
60
+ if 'auth_token' not in protocol_attr:
61
+ raise exception.NoAuthInformation('No authentication token passed for the RSE protocol')
62
+
63
+ self.auth_token = protocol_attr['auth_token']
64
+ protocol_attr.pop('auth_token')
65
+ self.attributes = protocol_attr
66
+ self.translator = None
67
+ self.renaming = True
68
+ self.overwrite = False
69
+ self.rse = rse_settings
70
+ self.logger = logger
71
+ if self.rse['deterministic']:
72
+ self.translator = RSEDeterministicTranslation(self.rse['rse'], rse_settings, self.attributes)
73
+ if getattr(rsemanager, 'CLIENT_MODE', None) and \
74
+ not RSEDeterministicTranslation.supports(self.rse.get('lfn2pfn_algorithm')):
75
+ # Remote server has an algorithm we don't understand; always make the server do the lookup.
76
+ setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
77
+ else:
78
+ if getattr(rsemanager, 'CLIENT_MODE', None):
79
+ setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
80
+ if getattr(rsemanager, 'SERVER_MODE', None):
81
+ setattr(self, '_get_path', self._get_path_nondeterministic_server)
82
+
83
+ def lfns2pfns(
84
+ self,
85
+ lfns: Union[list["LFNDict"], "LFNDict"]
86
+ ) -> dict[str, str]:
87
+ """
88
+ Returns a fully qualified PFN for the file referred by path.
89
+
90
+ :param path: The path to the file.
91
+
92
+ :returns: Fully qualified PFN.
93
+ """
94
+ pfns = {}
95
+ prefix = self.attributes['prefix']
96
+
97
+ if not prefix.startswith('/'):
98
+ prefix = ''.join(['/', prefix])
99
+ if not prefix.endswith('/'):
100
+ prefix = ''.join([prefix, '/'])
101
+
102
+ lfns = [lfns] if isinstance(lfns, dict) else lfns
103
+ for lfn in lfns:
104
+ scope, name = str(lfn['scope']), lfn['name']
105
+ if 'path' in lfn and lfn['path'] is not None:
106
+ pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'],
107
+ '://',
108
+ self.attributes['hostname'],
109
+ ':',
110
+ str(self.attributes['port']),
111
+ prefix,
112
+ lfn['path'] if not lfn['path'].startswith('/') else lfn['path'][1:]
113
+ ])
114
+ else:
115
+ try:
116
+ pfns['%s:%s' % (scope, name)] = ''.join([self.attributes['scheme'],
117
+ '://',
118
+ self.attributes['hostname'],
119
+ ':',
120
+ str(self.attributes['port']),
121
+ prefix,
122
+ self._get_path(scope=scope, name=name)
123
+ ])
124
+ except exception.ReplicaNotFound as e:
125
+ self.logger(logging.WARNING, str(e))
126
+ return pfns
127
+
128
+ def __lfns2pfns_client(
129
+ self,
130
+ lfns: Union[list["DIDDict"], "DIDDict"]
131
+ ) -> dict[str, str]:
132
+ """ Provides the path of a replica for non-deterministic sites. Will be assigned to get path by the __init__ method if necessary.
133
+
134
+ :param scope: list of DIDs
135
+
136
+ :returns: dict with scope:name as keys and PFN as value (in case of errors the Rucio exception si assigned to the key)
137
+ """
138
+ client = RSEClient() # pylint: disable=E0601
139
+
140
+ lfns = [lfns] if isinstance(lfns, dict) else lfns
141
+ lfn_query = ["%s:%s" % (lfn['scope'], lfn['name']) for lfn in lfns]
142
+ return client.lfns2pfns(self.rse['rse'], lfn_query, scheme=self.attributes['scheme'])
143
+
144
+ def _get_path(
145
+ self,
146
+ scope: str,
147
+ name: str):
148
+ """ Transforms the logical file name into a PFN.
149
+ Suitable for sites implementing the RUCIO naming convention.
150
+ This implementation is only invoked if the RSE is deterministic.
151
+
152
+ :param scope: scope
153
+ :param name: filename
154
+
155
+ :returns: RSE specific URI of the physical file
156
+ """
157
+ return self.translator.path(scope, name) # type: ignore (translator could be none)
158
+
159
+ def _get_path_nondeterministic_server( # pylint: disable=invalid-name
160
+ self,
161
+ scope: str,
162
+ name: str
163
+ ) -> str:
164
+ """ Provides the path of a replica for non-deterministic sites. Will be assigned to get path by the __init__ method if necessary. """
165
+ vo = get_rse_vo(self.rse['id']) # pylint: disable=E0601
166
+ internal_scope = InternalScope(scope, vo=vo) # pylint: disable=E0601
167
+ rep = replica.get_replica(scope=internal_scope, name=name, rse_id=self.rse['id']) # pylint: disable=E0601
168
+ if 'path' in rep and rep['path'] is not None:
169
+ path = rep['path']
170
+ elif 'state' in rep and (rep['state'] is None or rep['state'] == 'UNAVAILABLE'):
171
+ raise exception.ReplicaUnAvailable('Missing path information and state is UNAVAILABLE for replica %s:%s on non-deterministic storage named %s' % (internal_scope, name, self.rse['rse']))
172
+ else:
173
+ raise exception.ReplicaNotFound('Missing path information for replica %s:%s on non-deterministic storage named %s' % (internal_scope, name, self.rse['rse']))
174
+ if path.startswith('/'):
175
+ path = path[1:]
176
+ if path.endswith('/'):
177
+ path = path[:-1]
178
+ return path
179
+
180
+ def parse_pfns(
181
+ self,
182
+ pfns: Union['Iterable[str]', str]
183
+ ) -> dict[str, dict[str, str]]:
184
+ """
185
+ Splits the given PFN into the parts known by the protocol. It is also checked if the provided protocol supports the given PFNs.
186
+
187
+ :param pfns: a list of a fully qualified PFNs
188
+
189
+ :returns: dic with PFN as key and a dict with path and name as value
190
+
191
+ :raises RSEFileNameNotSupported: if the provided PFN doesn't match with the protocol settings
192
+ """
193
+ ret = dict()
194
+ pfns = [pfns] if isinstance(pfns, str) else pfns
195
+
196
+ for pfn in pfns:
197
+ parsed = urlparse(pfn)
198
+ scheme = parsed.scheme
199
+ hostname = parsed.netloc.partition(':')[0]
200
+ port = int(parsed.netloc.partition(':')[2]) if parsed.netloc.partition(':')[2] != '' else 0
201
+ while '//' in parsed.path:
202
+ parsed = parsed._replace(path=parsed.path.replace('//', '/'))
203
+ path = parsed.path
204
+ prefix = self.attributes['prefix']
205
+ while '//' in prefix:
206
+ prefix = prefix.replace('//', '/')
207
+
208
+ # Protect against 'lazy' defined prefixes for RSEs in the repository
209
+ if not prefix.startswith('/'):
210
+ prefix = '/' + prefix
211
+ if not prefix.endswith('/'):
212
+ prefix += '/'
213
+
214
+ if self.attributes['hostname'] != hostname:
215
+ if self.attributes['hostname'] != 'localhost': # In the database empty hostnames are replaced with localhost but for some URIs (e.g. file) a hostname is not included
216
+ raise exception.RSEFileNameNotSupported('Invalid hostname: provided \'%s\', expected \'%s\'' % (hostname, self.attributes['hostname']))
217
+
218
+ if self.attributes['port'] != port:
219
+ raise exception.RSEFileNameNotSupported('Invalid port: provided \'%s\', expected \'%s\'' % (port, self.attributes['port']))
220
+
221
+ if not path.startswith(prefix):
222
+ raise exception.RSEFileNameNotSupported('Invalid prefix: provided \'%s\', expected \'%s\'' % ('/'.join(path.split('/')[0:len(prefix.split('/')) - 1]),
223
+ prefix)) # len(...)-1 due to the leading '/
224
+
225
+ # Splitting parsed.path into prefix, path, filename
226
+ path = path.partition(prefix)[2]
227
+ name = path.split('/')[-1]
228
+ path = '/'.join(path.split('/')[:-1])
229
+ if not path.startswith('/'):
230
+ path = '/' + path
231
+ if path != '/' and not path.endswith('/'):
232
+ path = path + '/'
233
+ ret[pfn] = {'path': path, 'name': name, 'scheme': scheme, 'prefix': prefix, 'port': port, 'hostname': hostname, }
234
+
235
+ return ret
236
+
237
+ def exists(
238
+ self,
239
+ path: Optional[str]
240
+ ) -> bool:
241
+ """
242
+ Checks if the requested file is known by the referred RSE.
243
+
244
+ :param path: Physical file name
245
+
246
+ :returns: True if the file exists, False if it doesn't
247
+
248
+ :raises SourceNotFound: if the source file was not found on the referred storage.
249
+ """
250
+ raise NotImplementedError
251
+
252
+ @abstractmethod
253
+ def connect(self) -> None:
254
+ """
255
+ Establishes the actual connection to the referred RSE.
256
+
257
+ :raises RSEAccessDenied: if no connection could be established.
258
+ """
259
+ raise NotImplementedError
260
+
261
+ @abstractmethod
262
+ def close(self) -> None:
263
+ """ Closes the connection to RSE."""
264
+ raise NotImplementedError
265
+
266
+ @abstractmethod
267
+ def get(
268
+ self,
269
+ path: str,
270
+ dest: str,
271
+ transfer_timeout: Optional[int] = None
272
+ ) -> None:
273
+ """
274
+ Provides access to files stored inside connected the RSE.
275
+
276
+ :param path: Physical file name of requested file
277
+ :param dest: Name and path of the files when stored at the client
278
+ :param transfer_timeout: Transfer timeout (in seconds)
279
+
280
+ :raises DestinationNotAccessible: if the destination storage was not accessible.
281
+ :raises ServiceUnavailable: if some generic error occurred in the library.
282
+ :raises SourceNotFound: if the source file was not found on the referred storage.
283
+ """
284
+ raise NotImplementedError
285
+
286
+ @abstractmethod
287
+ def put(
288
+ self,
289
+ source: str,
290
+ target: str,
291
+ source_dir: Optional[str],
292
+ transfer_timeout: Optional[int] = None
293
+ ) -> None:
294
+ """
295
+ Allows to store files inside the referred RSE.
296
+
297
+ :param source: path to the source file on the client file system
298
+ :param target: path to the destination file on the storage
299
+ :param source_dir: Path where the to be transferred files are stored in the local file system
300
+ :param transfer_timeout: Transfer timeout (in seconds)
301
+
302
+ :raises DestinationNotAccessible: if the destination storage was not accessible.
303
+ :raises ServiceUnavailable: if some generic error occurred in the library.
304
+ :raises SourceNotFound: if the source file was not found on the referred storage.
305
+ """
306
+ raise NotImplementedError
307
+
308
+ @abstractmethod
309
+ def delete(
310
+ self,
311
+ path: str
312
+ ) -> None:
313
+ """
314
+ Deletes a file from the connected RSE.
315
+
316
+ :param path: path to the to be deleted file
317
+
318
+ :raises ServiceUnavailable: if some generic error occurred in the library.
319
+ :raises SourceNotFound: if the source file was not found on the referred storage.
320
+ """
321
+ raise NotImplementedError
322
+
323
+ @abstractmethod
324
+ def rename(
325
+ self,
326
+ path: str,
327
+ new_path: str
328
+ ) -> None:
329
+ """ Allows to rename a file stored inside the connected RSE.
330
+
331
+ :param path: path to the current file on the storage
332
+ :param new_path: path to the new file on the storage
333
+
334
+ :raises DestinationNotAccessible: if the destination storage was not accessible.
335
+ :raises ServiceUnavailable: if some generic error occurred in the library.
336
+ :raises SourceNotFound: if the source file was not found on the referred storage.
337
+ """
338
+ raise NotImplementedError
339
+
340
+ def get_space_usage(self) -> tuple[int, int]:
341
+ """
342
+ Get RSE space usage information.
343
+
344
+ :returns: a tuple 'totalsize' and 'unusedsize'
345
+
346
+ :raises ServiceUnavailable: if some generic error occurred in the library.
347
+ """
348
+ raise NotImplementedError
349
+
350
+ def stat(self, path: str) -> dict[str, Any]:
351
+ """
352
+ Returns the stats of a file.
353
+
354
+ :param path: path to file
355
+
356
+ :raises ServiceUnavailable: if some generic error occurred in the library.
357
+ :raises SourceNotFound: if the source file was not found on the referred storage.
358
+
359
+ :returns: a dict with two keys, filesize and adler32 of the file provided in path.
360
+ """
361
+ raise NotImplementedError