pycfdp 0.2.2__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.
Files changed (53) hide show
  1. cfdp/__init__.py +106 -0
  2. cfdp/checksum.py +94 -0
  3. cfdp/constants.py +153 -0
  4. cfdp/core.py +895 -0
  5. cfdp/event.py +55 -0
  6. cfdp/filestore/__init__.py +2 -0
  7. cfdp/filestore/base.py +99 -0
  8. cfdp/filestore/native.py +162 -0
  9. cfdp/meta/__init__.py +3 -0
  10. cfdp/meta/datafield.py +11 -0
  11. cfdp/meta/filestore.py +127 -0
  12. cfdp/meta/message.py +659 -0
  13. cfdp/pdu/__init__.py +8 -0
  14. cfdp/pdu/ack.py +90 -0
  15. cfdp/pdu/eof.py +123 -0
  16. cfdp/pdu/filedata.py +90 -0
  17. cfdp/pdu/finished.py +99 -0
  18. cfdp/pdu/header.py +159 -0
  19. cfdp/pdu/keep_alive.py +108 -0
  20. cfdp/pdu/metadata.py +211 -0
  21. cfdp/pdu/nak.py +220 -0
  22. cfdp/pdu/prompt.py +75 -0
  23. cfdp/pure/__init__.py +68 -0
  24. cfdp/pure/config.py +141 -0
  25. cfdp/pure/indications.py +130 -0
  26. cfdp/pure/machines/__init__.py +25 -0
  27. cfdp/pure/machines/receiver1.py +433 -0
  28. cfdp/pure/machines/receiver2.py +893 -0
  29. cfdp/pure/machines/sender1.py +400 -0
  30. cfdp/pure/machines/sender2.py +774 -0
  31. cfdp/pure/outputs.py +203 -0
  32. cfdp/pure/query_port.py +48 -0
  33. cfdp/pure/transaction.py +43 -0
  34. cfdp/py.typed +0 -0
  35. cfdp/remote_entity_config.py +12 -0
  36. cfdp/shell/__init__.py +44 -0
  37. cfdp/shell/checksum_service.py +136 -0
  38. cfdp/shell/dispatcher.py +185 -0
  39. cfdp/shell/effect_executor.py +285 -0
  40. cfdp/shell/machine_registry.py +165 -0
  41. cfdp/shell/query_port.py +70 -0
  42. cfdp/shell/timer_service.py +252 -0
  43. cfdp/transaction_handle.py +50 -0
  44. cfdp/transport/__init__.py +0 -0
  45. cfdp/transport/base.py +12 -0
  46. cfdp/transport/spacepacket.py +47 -0
  47. cfdp/transport/udp.py +90 -0
  48. cfdp/transport/zmq.py +54 -0
  49. pycfdp-0.2.2.dist-info/METADATA +76 -0
  50. pycfdp-0.2.2.dist-info/RECORD +53 -0
  51. pycfdp-0.2.2.dist-info/WHEEL +5 -0
  52. pycfdp-0.2.2.dist-info/licenses/LICENSE.txt +19 -0
  53. pycfdp-0.2.2.dist-info/top_level.txt +1 -0
cfdp/event.py ADDED
@@ -0,0 +1,55 @@
1
+ class Event:
2
+ def __init__(self, transaction, type, pdu=None, checksum=None):
3
+ self.transaction = transaction
4
+ self.type = type
5
+ # Inbound-PDU payload for a PDU-driven (receive-side) machine. The pure
6
+ # machine contract has a single input door, ``update_state(event)``, so
7
+ # everything the machine needs — including the triggering PDU — rides on
8
+ # the event (design "The machine contract").
9
+ self.pdu = pdu
10
+ # Result payload for the async-checksum channel (design R1 / issue #21).
11
+ # A ``RequestChecksum`` effect is computed off the event thread by the
12
+ # shell's checksum service, which injects an ``E42_CHECKSUM_READY`` event
13
+ # carrying the computed value here. The single input door still holds:
14
+ # the machine reads the result off the event, never from a return value.
15
+ self.checksum = checksum
16
+
17
+
18
+ class EventType:
19
+ E0_ENTERED_STATE = "ENTERED_STATE"
20
+ E1_SEND_FILE_DATA = "SEND_FILE_DATA"
21
+ E2_ABANDON_TRANSACTION = "ABANDON_TRANSACTION"
22
+ E3_NOTICE_OF_CANCELLATION = "NOTICE_OF_CANCELLATION"
23
+ E4_NOTICE_OF_SUSPENSION = "NOTICE_OF_SUSPENSION"
24
+ E5_SUSPEND_TIMERS = "SUSPEND_TIMERS"
25
+ E6_RESUME_TIMERS = "RESUME_TIMERS"
26
+
27
+ E10_RECEIVED_METADATA = "RECEIVED_METADATA"
28
+ E11_RECEIVED_FILEDATA = "RECEIVED_FILEDATA"
29
+ E12_RECEIVED_EOF_NO_ERROR = "RECEIVED_EOF_NO_ERROR"
30
+ E13_RECEIVED_EOF_CANCEL = "RECEIVED_EOF_CANCEL"
31
+ E14_RECEIVED_ACK_EOF = "RECEIVED_ACK_EOF"
32
+ E15_RECEIVED_NAK = "RECEIVED_NAK"
33
+ E16_RECEIVED_FINISHED_NO_ERROR = "RECEIVED_FINISHED_NO_ERROR"
34
+ E17_RECEIVED_FINISHED_CANCEL = "RECEIVED_FINISHED_CANCEL"
35
+ E18_RECEIVED_ACK_FINISHED = "RECEIVED_ACK_FINISHED"
36
+
37
+ E25_ACK_TIMEOUT = "ACK_TIMEOUT"
38
+ E26_NAK_TIMEOUT = "NAK_TIMEOUT"
39
+ E27_INACTIVITY_TIMEOUT = "INACTIVITY_TIMEOUT"
40
+
41
+ E30_RECEIVED_PUT_REQUEST = "RECEIVED_PUT_REQUEST"
42
+ E31_RECEIVED_SUSPEND_REQUEST = "RECEIVED_SUSPEND_REQUEST"
43
+ E32_RECEIVED_RESUME_REQUEST = "RECEIVED_RESUME_REQUEST"
44
+ E33_RECEIVED_CANCEL_REQUEST = "RECEIVED_CANCEL_REQUEST"
45
+ E34_RECEIVED_REPORT_REQUEST = "RECEIVED_REPORT_REQUEST"
46
+
47
+ E40_RECEIVED_FREEZE = "RECEIVED_FREEZE"
48
+ E41_RECEIVED_THAW = "RECEIVED_THAW"
49
+
50
+ # Async-checksum result (design R1 / issue #21). Injected by the shell's
51
+ # checksum service once a ``RequestChecksum`` effect has been computed off
52
+ # the event thread; the computed value rides on ``event.checksum``. The old
53
+ # machines never see this event (they compute checksum synchronously via
54
+ # ``Transaction.get_file_checksum``); it exists only on the pure stack.
55
+ E42_CHECKSUM_READY = "CHECKSUM_READY"
@@ -0,0 +1,2 @@
1
+ from .base import FileHandle, VirtualFileStore
2
+ from .native import NativeFileStore
cfdp/filestore/base.py ADDED
@@ -0,0 +1,99 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class FileHandle(ABC):
5
+ """
6
+ Abstract base class for file handle objects returned by VirtualFileStore.open()
7
+ and VirtualFileStore.open_tempfile(). Implementations must provide all listed
8
+ abstract methods.
9
+ """
10
+
11
+ @abstractmethod
12
+ def seek(self, offset, whence=0): ...
13
+
14
+ @abstractmethod
15
+ def tell(self): ...
16
+
17
+ @abstractmethod
18
+ def read(self, n=-1) -> bytes: ...
19
+
20
+ @abstractmethod
21
+ def write(self, data: bytes): ...
22
+
23
+ @abstractmethod
24
+ def close(self): ...
25
+
26
+ @abstractmethod
27
+ def calculate_checksum(self, checksum_type: int) -> int: ...
28
+
29
+
30
+ class VirtualFileStore(ABC):
31
+ """
32
+ The Virtual File Store (VFS) is an abstraction of file system. An
33
+ implementation must provide the methods listed below.
34
+
35
+ An implementation can map the VFS to a platform dependent native file
36
+ system or a packet store for example.
37
+
38
+ The VFS starts at root '/' and uses slashes for subfolders. This is how
39
+ pathes are to be passed to CFDP methods (eg. Put Request, Filestore
40
+ Requests, and Directory Listings.)
41
+
42
+ Example: '/files/a_file.txt'
43
+
44
+ """
45
+
46
+ def __init__(self):
47
+ self.config = None
48
+
49
+ @abstractmethod
50
+ def create_file(self, filepath):
51
+ raise NotImplementedError
52
+
53
+ @abstractmethod
54
+ def delete_file(self, filepath):
55
+ raise NotImplementedError
56
+
57
+ @abstractmethod
58
+ def rename_file(self, filepath1, filepath2):
59
+ raise NotImplementedError
60
+
61
+ @abstractmethod
62
+ def append_file(self, filepath1, filepath2):
63
+ raise NotImplementedError
64
+
65
+ @abstractmethod
66
+ def replace_file(self, filepath1, filepath2):
67
+ raise NotImplementedError
68
+
69
+ @abstractmethod
70
+ def create_directory(self, dirpath):
71
+ raise NotImplementedError
72
+
73
+ @abstractmethod
74
+ def remove_directory(self, dirpath):
75
+ raise NotImplementedError
76
+
77
+ @abstractmethod
78
+ def list_directory(self, dirpath):
79
+ raise NotImplementedError
80
+
81
+ @abstractmethod
82
+ def open(self, filepath, mode="rb"):
83
+ raise NotImplementedError
84
+
85
+ @abstractmethod
86
+ def open_tempfile(self):
87
+ raise NotImplementedError
88
+
89
+ @abstractmethod
90
+ def is_file(self, filepath):
91
+ raise NotImplementedError
92
+
93
+ @abstractmethod
94
+ def get_size(self, filepath):
95
+ raise NotImplementedError
96
+
97
+ @abstractmethod
98
+ def join_path(self, *args):
99
+ raise NotImplementedError
@@ -0,0 +1,162 @@
1
+ import os
2
+ import shutil
3
+ import tempfile
4
+
5
+ from .base import FileHandle, VirtualFileStore
6
+
7
+
8
+ class NativeFileStore(VirtualFileStore):
9
+ """
10
+ The Native File Store implements the Virtual File Store as a platform
11
+ dependent file system, using the Python 'os.path' library.
12
+
13
+ A rootpath is provided as class instantiation. The VFS paths are then
14
+ relative to this root path. For example, if root path is '/home/demo'
15
+ and a Put Request places a file to '/files/small.txt', then this will
16
+ land in '/home/demo/files/small.txt'.
17
+
18
+ """
19
+
20
+ def __init__(self, rootpath):
21
+ super().__init__()
22
+ self.rootpath = os.path.realpath(rootpath)
23
+
24
+ def get_native_path(self, path):
25
+ # convert from virtual filestore path to absolute native path
26
+ path_items = path.split("/")
27
+ path_items = [x for x in path_items if x != ""]
28
+ if path_items:
29
+ joined = os.path.join(*path_items)
30
+ else:
31
+ joined = ""
32
+ native = os.path.realpath(os.path.join(self.rootpath, joined))
33
+ if native != self.rootpath and not native.startswith(self.rootpath + os.sep):
34
+ raise ValueError(
35
+ f"Path traversal detected: {path!r} escapes filestore root"
36
+ )
37
+ return native
38
+
39
+ def get_virtual_path(self, path):
40
+ # convert from native relative path to virtual path
41
+ path = "/".join(os.path.normpath(path).split(os.path.sep))
42
+ if not path.startswith("/"):
43
+ path = "/" + path
44
+ return path
45
+
46
+ def create_file(self, filepath):
47
+ NativeFileHandle(self.get_native_path(filepath), "wb").close()
48
+
49
+ def delete_file(self, filepath):
50
+ os.remove(self.get_native_path(filepath))
51
+
52
+ def rename_file(self, filepath1, filepath2):
53
+ os.rename(self.get_native_path(filepath1), self.get_native_path(filepath2))
54
+
55
+ def append_file(self, filepath1, filepath2):
56
+ fh1 = open(self.get_native_path(filepath1), "ab")
57
+ fh2 = open(self.get_native_path(filepath2), "rb")
58
+ fh1.write(fh2.read())
59
+ fh1.close()
60
+ fh2.close()
61
+
62
+ def replace_file(self, filepath1, filepath2):
63
+ fh1 = open(self.get_native_path(filepath1), "wb")
64
+ fh2 = open(self.get_native_path(filepath2), "rb")
65
+ fh1.write(fh2.read())
66
+ fh1.close()
67
+ fh2.close()
68
+
69
+ def create_directory(self, dirpath):
70
+ os.mkdir(self.get_native_path(dirpath))
71
+
72
+ def remove_directory(self, dirpath):
73
+ shutil.rmtree(self.get_native_path(dirpath))
74
+
75
+ def list_directory(self, dirpath):
76
+ # create directory listing
77
+ dirpath = self.get_native_path(dirpath)
78
+ # get the full path w.r.t to host file system
79
+ dirpath, dirnames, filenames = next(os.walk(dirpath))
80
+
81
+ listing = "type,path,size,timestamp\n"
82
+ for dirname in dirnames:
83
+ fullpath = os.path.join(dirpath, dirname)
84
+ relpath = os.path.relpath(fullpath, self.rootpath)
85
+ line = "{},{},{},{}\n".format(
86
+ "d",
87
+ self.get_virtual_path(relpath),
88
+ os.path.getsize(fullpath),
89
+ os.path.getmtime(fullpath),
90
+ )
91
+ listing += line
92
+ for filename in filenames:
93
+ fullpath = os.path.join(dirpath, filename)
94
+ relpath = os.path.relpath(fullpath, self.rootpath)
95
+ line = "{},{},{},{}\n".format(
96
+ "f",
97
+ self.get_virtual_path(relpath),
98
+ os.path.getsize(fullpath),
99
+ os.path.getmtime(fullpath),
100
+ )
101
+ listing += line
102
+ return listing
103
+
104
+ def open(self, filepath, mode="rb"):
105
+ return NativeFileHandle(self.get_native_path(filepath), mode)
106
+
107
+ def open_tempfile(self):
108
+ return NativeTemporaryFileHandle()
109
+
110
+ def is_file(self, filepath):
111
+ return os.path.isfile(self.get_native_path(filepath))
112
+
113
+ def get_size(self, filepath):
114
+ return os.path.getsize(self.get_native_path(filepath))
115
+
116
+ def join_path(self, *args):
117
+ return "/".join(args)
118
+
119
+
120
+ class NativeFileHandle(FileHandle):
121
+ """
122
+ The NativeFileHandle provides a generic interface to Python's 'open'
123
+ function. It also implements method for calculating the file checksum.
124
+
125
+ """
126
+
127
+ def __init__(self, filepath, mode="rb"):
128
+ self.handle = open(filepath, mode)
129
+
130
+ def seek(self, *args, **kwargs):
131
+ return self.handle.seek(*args, **kwargs)
132
+
133
+ def tell(self, *args, **kwargs):
134
+ return self.handle.tell(*args, **kwargs)
135
+
136
+ def read(self, *args, **kwargs):
137
+ return self.handle.read(*args, **kwargs)
138
+
139
+ def write(self, *args, **kwargs):
140
+ return self.handle.write(*args, **kwargs)
141
+
142
+ def close(self, *args, **kwargs):
143
+ return self.handle.close(*args, **kwargs)
144
+
145
+ def calculate_checksum(self, checksum_type: int) -> int:
146
+ from cfdp.checksum import calculate_checksum as _calc
147
+
148
+ self.handle.seek(0)
149
+
150
+ def _chunks():
151
+ while True:
152
+ chunk = self.handle.read(65536)
153
+ if not chunk:
154
+ break
155
+ yield chunk
156
+
157
+ return _calc(_chunks(), checksum_type)
158
+
159
+
160
+ class NativeTemporaryFileHandle(NativeFileHandle):
161
+ def __init__(self):
162
+ self.handle = tempfile.TemporaryFile()
cfdp/meta/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .datafield import *
2
+ from .filestore import *
3
+ from .message import *
cfdp/meta/datafield.py ADDED
@@ -0,0 +1,11 @@
1
+ class TypeLengthValue:
2
+ def encode(self):
3
+ type = self.type
4
+ length = len(self.value)
5
+ value = self.value
6
+
7
+ databytes = bytearray()
8
+ databytes.append(type)
9
+ databytes.append(length)
10
+ databytes += value
11
+ return databytes
cfdp/meta/filestore.py ADDED
@@ -0,0 +1,127 @@
1
+ from cfdp import logger
2
+ from cfdp.constants import ActionCode, TypeFieldCode
3
+
4
+ from .datafield import TypeLengthValue
5
+
6
+
7
+ class FilestoreRequest(TypeLengthValue):
8
+ def __init__(self, action_code, first_filename, second_filename=None):
9
+ self.type = TypeFieldCode.FILESTORE_REQUEST
10
+
11
+ self.action_code = action_code
12
+ self.first_filename = first_filename
13
+ self.second_filename = second_filename
14
+
15
+ def encode(self):
16
+ data = bytes([(self.action_code << 4)])
17
+
18
+ value = self.first_filename.encode("utf-8")
19
+ length = len(value)
20
+ data += bytes([length])
21
+ data += value
22
+
23
+ if self.second_filename:
24
+ value = self.second_filename.encode("utf-8")
25
+ length = len(value)
26
+ data += bytes([length])
27
+ data += value
28
+
29
+ self.value = data
30
+ return super().encode()
31
+
32
+
33
+ def execute_filestore_requests(kernel, filestore_requests):
34
+ for filestore_request in filestore_requests:
35
+ if filestore_request.action_code == ActionCode.CREATE_FILE:
36
+ filename = filestore_request.first_filename.decode()
37
+ logger.info(f"Create file: <{filename}>")
38
+ try:
39
+ kernel.filestore.create_file(filename)
40
+ except Exception:
41
+ logger.error(f"Failed to create file <{filename}>")
42
+
43
+ elif filestore_request.action_code == ActionCode.DELETE_FILE:
44
+ filename = filestore_request.first_filename.decode()
45
+ logger.info(f"Delete file: <{filename}>")
46
+ try:
47
+ kernel.filestore.delete_file(filename)
48
+ except Exception:
49
+ logger.error(f"Failed to delete file: <{filename}>")
50
+
51
+ elif filestore_request.action_code == ActionCode.RENAME_FILE:
52
+ filename1 = filestore_request.first_filename.decode()
53
+ filename2 = filestore_request.second_filename.decode()
54
+ logger.info(f"Rename file: <{filename1}> to <{filename2}>")
55
+ try:
56
+ kernel.filestore.rename_file(filename1, filename2)
57
+ except Exception:
58
+ logger.error(f"Failed to rename file <{filename1}> to <{filename2}>")
59
+
60
+ elif filestore_request.action_code == ActionCode.APPEND_FILE:
61
+ filename1 = filestore_request.first_filename.decode()
62
+ filename2 = filestore_request.second_filename.decode()
63
+ logger.info(f"Append file <{filename1}> with <{filename2}>")
64
+ try:
65
+ kernel.filestore.append_file(filename1, filename2)
66
+ except Exception:
67
+ logger.error(f"Failed to append file <{filename1}> with <{filename2}>")
68
+
69
+ elif filestore_request.action_code == ActionCode.REPLACE_FILE:
70
+ filename1 = filestore_request.first_filename.decode()
71
+ filename2 = filestore_request.second_filename.decode()
72
+ logger.info(f"Replace content of file <{filename1}> by <{filename2}>")
73
+ try:
74
+ kernel.filestore.replace_file(filename1, filename2)
75
+ except Exception:
76
+ logger.error(
77
+ f"Failed to replace content of <{filename1}> by <{filename2}>"
78
+ )
79
+
80
+ elif filestore_request.action_code == ActionCode.CREATE_DIRECTORY:
81
+ dirname = filestore_request.first_filename.decode()
82
+ logger.info(f"Create directory: <{dirname}>")
83
+ try:
84
+ kernel.filestore.create_directory(dirname)
85
+ except Exception:
86
+ logger.error(f"Failed to create directory: <{dirname}>")
87
+
88
+ elif filestore_request.action_code == ActionCode.REMOVE_DIRECTORY:
89
+ dirname = filestore_request.first_filename.decode()
90
+ logger.info(f"Remove directory: <{dirname}>")
91
+ try:
92
+ kernel.filestore.remove_directory(dirname)
93
+ except Exception:
94
+ logger.error(f"Failed to delete directory: <{dirname}>")
95
+
96
+ elif filestore_request.action_code == ActionCode.DENY_FILE:
97
+ filename = filestore_request.first_filename.decode()
98
+ logger.info(f"Deny file: <{filename}>")
99
+ try:
100
+ kernel.filestore.delete_file(filename)
101
+ except Exception:
102
+ pass
103
+
104
+ elif filestore_request.action_code == ActionCode.DENY_DIRECTORY:
105
+ dirname = filestore_request.first_filename.decode()
106
+ logger.info(f"Deny directory: <{dirname}>")
107
+ try:
108
+ kernel.filestore.remove_directory(dirname)
109
+ except Exception:
110
+ pass
111
+
112
+
113
+ def extract_filestore_request(data):
114
+ action_code = data[0] >> 4
115
+ length_of_first_filename = data[1]
116
+ first_filename = data[2 : 2 + length_of_first_filename]
117
+
118
+ if action_code in [
119
+ ActionCode.RENAME_FILE,
120
+ ActionCode.APPEND_FILE,
121
+ ActionCode.REPLACE_FILE,
122
+ ]:
123
+ second_filename = data[3 + length_of_first_filename :]
124
+ else:
125
+ second_filename = None
126
+
127
+ return FilestoreRequest(action_code, first_filename, second_filename)