|
14 | 14 | # You should have received a copy of the GNU General Public License |
15 | 15 | # along with this program. If not, see <https://www.gnu.org/licenses/>. |
16 | 16 |
|
| 17 | +import datetime |
17 | 18 | import logging |
18 | 19 |
|
19 | 20 | from collections.abc import Iterable |
| 21 | +import os |
| 22 | +from pathlib import Path |
| 23 | +import random |
20 | 24 | from typing_extensions import Callable, override |
21 | 25 |
|
22 | 26 | from icspacket.core.connection import connection |
23 | 27 | from icspacket.proto.mms._mms import ( |
| 28 | + ApplicationReference, |
| 29 | + Confirmed_ResponsePDU, |
| 30 | + FileClose_Response, |
| 31 | + FileOpen_Response, |
| 32 | + FileRead_Response, |
24 | 33 | GetNamedVariableListAttributes_Request, |
25 | 34 | GetNamedVariableListAttributes_Response, |
| 35 | + ObtainFile_Request, |
26 | 36 | Unconfirmed_PDU, |
27 | 37 | UnconfirmedService, |
28 | 38 | ) |
@@ -933,6 +943,166 @@ def variable_list_attributes( |
933 | 943 | response = self.service_request(service) |
934 | 944 | return response.getNamedVariableListAttributes |
935 | 945 |
|
| 946 | + # --------------------------------------------------------------------------- |
| 947 | + # Annex C - File Access service |
| 948 | + # --------------------------------------------------------------------------- |
| 949 | + def obtain_file( |
| 950 | + self, |
| 951 | + source: str | Path, |
| 952 | + destination: str | Path, |
| 953 | + remote: ApplicationReference | None = None, |
| 954 | + ) -> None: |
| 955 | + """ |
| 956 | + Perform the MMS :term:`ObtainFile` service to transfer a file |
| 957 | + between the client and a remote MMS server. |
| 958 | +
|
| 959 | + The ObtainFile service may be used by an MMS client to instruct an |
| 960 | + MMS server to obtain a specified file from a file server. Depending |
| 961 | + on the usage, this can either be a request to *pull* a file from |
| 962 | + a remote source or to *serve* a local file to the requesting MMS |
| 963 | + peer. |
| 964 | +
|
| 965 | + The method implements the complete reques-response sequence defined |
| 966 | + in IEC 61850 Annex C, including ``FileOpen``, ``FileRead``, ``FileClose``, |
| 967 | + and the final ``ObtainFile`` confirmation. |
| 968 | +
|
| 969 | + :param source: |
| 970 | + Path to the source file. If ``remote`` is provided, this denotes the |
| 971 | + file path to be retrieved from the remote server. Otherwise, this |
| 972 | + must point to a local file which will be transferred. |
| 973 | + :type source: str | Path |
| 974 | +
|
| 975 | + :param destination: |
| 976 | + Path where the file should be stored on the remote server. |
| 977 | + For local serving, this is the name advertised to the requesting peer. |
| 978 | + :type destination: str | Path |
| 979 | +
|
| 980 | + :param remote: |
| 981 | + Optional reference to an external application server from which |
| 982 | + the file should be obtained. If ``None``, this MMS connection |
| 983 | + instance will act as the file source. |
| 984 | + :type remote: ApplicationReference | None |
| 985 | +
|
| 986 | + :raises MMSServiceError: |
| 987 | + If the MMS server responds with an unexpected PDU type or an error |
| 988 | + occurs during the transfer sequence. |
| 989 | +
|
| 990 | + :raises OSError: |
| 991 | + If the local file system access fails (e.g., file not found or |
| 992 | + permission denied). |
| 993 | +
|
| 994 | + .. versionadded:: 0.2.4 |
| 995 | + """ |
| 996 | + request = ObtainFile_Request( |
| 997 | + sourceFile=FileName([os.path.basename(str(source))]), |
| 998 | + destinationFile=FileName([str(destination)]), |
| 999 | + ) |
| 1000 | + if remote: |
| 1001 | + request.sourceFileServer = remote |
| 1002 | + |
| 1003 | + obtain_service = ConfirmedServiceRequest(obtainFile=request) |
| 1004 | + pdu = Confirmed_RequestPDU() |
| 1005 | + pdu.invokeID = self.next_invoke_id |
| 1006 | + pdu.service = obtain_service |
| 1007 | + |
| 1008 | + self.send_mms_data(MMSpdu(confirmed_RequestPDU=pdu)) |
| 1009 | + response = self.recv_mms_data() |
| 1010 | + error = self._error_from_service_response( |
| 1011 | + obtain_service, response, need_response=remote is not None |
| 1012 | + ) |
| 1013 | + if error is not None: |
| 1014 | + raise error |
| 1015 | + |
| 1016 | + if remote: |
| 1017 | + return |
| 1018 | + |
| 1019 | + if response.present != MMSpdu.PRESENT.PR_confirmed_RequestPDU: |
| 1020 | + raise MMSServiceError( |
| 1021 | + f"Failed ObtainFile request with unexpected response: {response.present!r} - Expected FileOpen" |
| 1022 | + ) |
| 1023 | + |
| 1024 | + service = response.confirmed_RequestPDU.service |
| 1025 | + if service.present != ConfirmedServiceRequest.PRESENT.PR_fileOpen: |
| 1026 | + raise MMSServiceError( |
| 1027 | + f"Failed ObtainFile request with unexpected response: {service.present!r} - Expected FileOpen" |
| 1028 | + ) |
| 1029 | + |
| 1030 | + invoke_id = response.confirmed_RequestPDU.invokeID.value |
| 1031 | + # We simply generate a new handle |
| 1032 | + handle = random.randint(0, 0xFFFF) |
| 1033 | + pdu = ConfirmedServiceResponse() |
| 1034 | + |
| 1035 | + source_path = Path(source) |
| 1036 | + source_stat = source_path.stat() |
| 1037 | + mtime = datetime.datetime.fromtimestamp(source_stat.st_mtime) |
| 1038 | + |
| 1039 | + file_open = FileOpen_Response(frsmID=handle) |
| 1040 | + file_open.fileAttributes.sizeOfFile = source_stat.st_size |
| 1041 | + file_open.fileAttributes.lastModified = mtime.strftime("%Y%m%d%H%M%S.%fZ") |
| 1042 | + pdu.fileOpen = file_open |
| 1043 | + |
| 1044 | + response = Confirmed_ResponsePDU() |
| 1045 | + response.invokeID = invoke_id |
| 1046 | + response.service = pdu |
| 1047 | + self.send_mms_data(MMSpdu(confirmed_ResponsePDU=response)) |
| 1048 | + |
| 1049 | + read_req = self.recv_mms_data() |
| 1050 | + if read_req.present != MMSpdu.PRESENT.PR_confirmed_RequestPDU: |
| 1051 | + raise MMSServiceError( |
| 1052 | + f"Failed ObtainFile request with unexpected response: {read_req.present!r} - Expected FileRead" |
| 1053 | + ) |
| 1054 | + |
| 1055 | + request = read_req.confirmed_RequestPDU |
| 1056 | + if request.service.present != ConfirmedServiceRequest.PRESENT.PR_fileRead: |
| 1057 | + raise MMSServiceError( |
| 1058 | + f"Failed ObtainFile request with unexpected response: {request.service.present!r} - Expected FileRead" |
| 1059 | + ) |
| 1060 | + |
| 1061 | + invoke_id = request.invokeID.value |
| 1062 | + pdu = ConfirmedServiceResponse() |
| 1063 | + |
| 1064 | + file_read = FileRead_Response() |
| 1065 | + file_read.fileData = source_path.read_bytes() |
| 1066 | + file_read.moreFollows = False |
| 1067 | + |
| 1068 | + pdu.fileRead = file_read |
| 1069 | + response = Confirmed_ResponsePDU() |
| 1070 | + response.invokeID = invoke_id |
| 1071 | + response.service = pdu |
| 1072 | + self.send_mms_data(MMSpdu(confirmed_ResponsePDU=response)) |
| 1073 | + |
| 1074 | + # expect fileClose and obtainFile response |
| 1075 | + close_req = self.recv_mms_data() |
| 1076 | + if close_req.present != MMSpdu.PRESENT.PR_confirmed_RequestPDU: |
| 1077 | + raise MMSServiceError( |
| 1078 | + f"Failed ObtainFile request with unexpected response: {close_req.present!r} - Expected FileClose" |
| 1079 | + ) |
| 1080 | + |
| 1081 | + close_req = close_req.confirmed_RequestPDU |
| 1082 | + if close_req.service.present != ConfirmedServiceRequest.PRESENT.PR_fileClose: |
| 1083 | + raise MMSServiceError( |
| 1084 | + f"Failed ObtainFile request with unexpected response: {close_req.service.present!r} - Expected FileClose" |
| 1085 | + ) |
| 1086 | + |
| 1087 | + invoke_id = close_req.invokeID.value |
| 1088 | + pdu = ConfirmedServiceResponse() |
| 1089 | + pdu.fileClose = FileClose_Response(value=None) |
| 1090 | + response = Confirmed_ResponsePDU() |
| 1091 | + response.invokeID = invoke_id |
| 1092 | + response.service = pdu |
| 1093 | + self.send_mms_data(MMSpdu(confirmed_ResponsePDU=response)) |
| 1094 | + |
| 1095 | + obtain_response = self.recv_mms_data() |
| 1096 | + error = self._error_from_service_response(obtain_service, obtain_response, True) |
| 1097 | + if error is not None: |
| 1098 | + raise error |
| 1099 | + |
| 1100 | + # alias to match file_XXX naming convention |
| 1101 | + file_transfer = obtain_file |
| 1102 | + """ |
| 1103 | + .. versionadded:: 0.2.4 |
| 1104 | + """ |
| 1105 | + |
936 | 1106 | # --------------------------------------------------------------------------- |
937 | 1107 | # Annex D - File Management |
938 | 1108 | # --------------------------------------------------------------------------- |
|
0 commit comments