Reorganized repo a bit
This commit is contained in:
343
packetserver/common/__init__.py
Normal file
343
packetserver/common/__init__.py
Normal file
@@ -0,0 +1,343 @@
|
||||
from pe.connect import Connection, ConnectionState
|
||||
from threading import Lock
|
||||
from msgpack import Unpacker
|
||||
from msgpack import packb, unpackb
|
||||
from enum import Enum
|
||||
import bz2
|
||||
from typing import Union, Self
|
||||
import datetime
|
||||
import logging
|
||||
import ax25
|
||||
|
||||
|
||||
class PacketServerConnection(Connection):
|
||||
|
||||
connection_subscribers = []
|
||||
receive_subscribers = []
|
||||
max_send_size = 2000
|
||||
|
||||
def __init__(self, port, call_from, call_to, incoming=False):
|
||||
super().__init__(port, call_from, call_to, incoming=incoming)
|
||||
# Now perform any initialization of your own that you might need
|
||||
self.data = Unpacker()
|
||||
self.data_lock = Lock()
|
||||
self.connection_created = datetime.datetime.now(datetime.UTC)
|
||||
self.connection_last_activity = datetime.datetime.now(datetime.UTC)
|
||||
self.closing = False
|
||||
|
||||
|
||||
@property
|
||||
def local_callsign(self):
|
||||
if self.incoming:
|
||||
return self.call_to
|
||||
else:
|
||||
return self.call_from
|
||||
|
||||
@property
|
||||
def remote_callsign(self):
|
||||
if self.incoming:
|
||||
return self.call_from
|
||||
else:
|
||||
return self.call_to
|
||||
|
||||
def connected(self):
|
||||
logging.debug("connected")
|
||||
logging.debug(f"new connection from {self.call_from} to {self.call_to}")
|
||||
for fn in PacketServerConnection.connection_subscribers:
|
||||
fn(self)
|
||||
|
||||
def disconnected(self):
|
||||
logging.debug(f"connection disconnected: {self.call_from} -> {self.call_to}")
|
||||
|
||||
def data_received(self, pid, data):
|
||||
self.connection_last_activity = datetime.datetime.now(datetime.UTC)
|
||||
logging.debug(f"received data: {data}")
|
||||
with self.data_lock:
|
||||
logging.debug(f"fed received data to unpacker {data}")
|
||||
self.data.feed(data)
|
||||
for fn in PacketServerConnection.receive_subscribers:
|
||||
logging.debug("found function to notify about received data")
|
||||
fn(self)
|
||||
logging.debug("notified function about received data")
|
||||
|
||||
def send_data(self, data: Union[bytes, bytearray]):
|
||||
logging.debug(f"sending data: {data}")
|
||||
self.connection_last_activity = datetime.datetime.now(datetime.UTC)
|
||||
if len(data) > self.max_send_size:
|
||||
logging.debug(f"Large frame detected {len(data)} breaking it up into chunks")
|
||||
index = 0
|
||||
counter = 0
|
||||
while index <= len(data):
|
||||
logging.debug(f"Sending chunk {counter}")
|
||||
if (len(data) - index) < self.max_send_size:
|
||||
super().send_data(data[index:])
|
||||
break
|
||||
super().send_data(data[index:index + self.max_send_size])
|
||||
index = index + self.max_send_size
|
||||
counter = counter + 1
|
||||
else:
|
||||
super().send_data(data)
|
||||
|
||||
@classmethod
|
||||
def query_accept(cls, port, call_from, call_to):
|
||||
return True
|
||||
|
||||
|
||||
class Message:
|
||||
"""Base class for communication encapsulated in msgpack objects."""
|
||||
|
||||
class CompressionType(Enum):
|
||||
NONE = 0
|
||||
BZIP2 = 1
|
||||
GZIP = 2
|
||||
DEFLATE = 3
|
||||
|
||||
class MessageType(Enum):
|
||||
REQUEST = 0
|
||||
RESPONSE = 1
|
||||
|
||||
def __init__(self, msg_type: MessageType, compression: CompressionType, payload: dict):
|
||||
self.type = Message.MessageType(msg_type)
|
||||
self.compression = Message.CompressionType(compression)
|
||||
self.data = payload
|
||||
|
||||
@property
|
||||
def vars(self) -> dict:
|
||||
if 'v' in self.data:
|
||||
if type(self.data['v']) is dict:
|
||||
return self.data['v']
|
||||
return {}
|
||||
|
||||
def get_var(self, key: str):
|
||||
if 'v' not in self.data:
|
||||
raise KeyError(f"Variable '{key}' not found.")
|
||||
if str(key) not in self.data['v']:
|
||||
raise KeyError(f"Variable '{key}' not found.")
|
||||
return self.data['v'][str(key)]
|
||||
|
||||
def set_var(self, key: str, value):
|
||||
if 'v' not in self.data:
|
||||
self.data['v'] = {}
|
||||
self.data['v'][str(key)] = value
|
||||
|
||||
@property
|
||||
def data_bytes(self):
|
||||
return packb(self.data)
|
||||
|
||||
def pack(self) -> bytes:
|
||||
output = {'t': self.type.value, 'c': self.compression.value}
|
||||
data_bytes = self.data_bytes
|
||||
logging.debug("Packing Message")
|
||||
if (self.compression is self.CompressionType.NONE) or (len(data_bytes) < 30):
|
||||
output['d'] = data_bytes
|
||||
output['c'] = self.CompressionType.NONE.value
|
||||
return packb(output)
|
||||
|
||||
if self.compression is self.CompressionType.BZIP2:
|
||||
compressed = bz2.compress(packb(self.data))
|
||||
else:
|
||||
raise NotImplementedError(f"Compression type {self.compression.name} is not implemented yet.")
|
||||
|
||||
if len(compressed) < len(data_bytes):
|
||||
output['d'] = compressed
|
||||
else:
|
||||
output['d'] = data_bytes
|
||||
output['c'] = self.CompressionType.NONE.value
|
||||
return packb(output)
|
||||
|
||||
@property
|
||||
def payload(self):
|
||||
if 'd' in self.data:
|
||||
pl = self.data['d']
|
||||
if type(pl) in (dict, str, bytes, list):
|
||||
return pl
|
||||
else:
|
||||
return str(pl)
|
||||
else:
|
||||
return ""
|
||||
|
||||
@payload.setter
|
||||
def payload(self, payload: Union[str, bytes, dict, list]):
|
||||
logging.debug(f"Setting a message payload: {type(payload)}: {payload}")
|
||||
if type(payload) in (str, bytes, dict, list):
|
||||
logging.debug(f"Payload type is {type(payload)}, conversion to string unnecessary")
|
||||
self.data['d'] = payload
|
||||
else:
|
||||
logging.debug("payload type is not in (str, bytes, dict, list); converting to string")
|
||||
self.data['d'] = str(payload)
|
||||
logging.debug(f"Final payload is: {type(payload)}: {payload}")
|
||||
|
||||
@classmethod
|
||||
def partial_unpack(cls, msg: dict) -> Self:
|
||||
unpacked = msg
|
||||
comp = Message.CompressionType(unpacked['c'])
|
||||
msg_type = Message.MessageType(unpacked['t'])
|
||||
raw_data = unpacked['d']
|
||||
|
||||
if comp is Message.CompressionType.NONE:
|
||||
data = unpackb(raw_data)
|
||||
elif comp is Message.CompressionType.BZIP2:
|
||||
data = unpackb(bz2.decompress(raw_data))
|
||||
else:
|
||||
raise NotImplementedError(f"Compression type {comp.name} is not implemented yet.")
|
||||
|
||||
return Message(msg_type, comp, data)
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, msg_bytes: bytes) -> Self:
|
||||
try:
|
||||
unpacked = unpackb(msg_bytes)
|
||||
except Exception as e:
|
||||
raise ValueError("ERROR: msg_bytes didn't contain a valid msgpack object.\n" + str(e))
|
||||
if type(unpacked) is not dict:
|
||||
raise ValueError("ERROR: unpacked message was not a packetserver message.")
|
||||
for i in ('t', 'c', 'd'):
|
||||
if i not in unpacked:
|
||||
raise ValueError("ERROR: unpacked message was not a packetserver message.")
|
||||
return Message.partial_unpack(unpacked)
|
||||
|
||||
class Request(Message):
|
||||
class Method(Enum):
|
||||
GET = 0
|
||||
POST = 1
|
||||
UPDATE = 2
|
||||
DELETE = 3
|
||||
|
||||
def __init__(self, msg: Message):
|
||||
if msg.type is not Message.MessageType.REQUEST:
|
||||
raise ValueError(f"Can't create a Request Object from a {msg.type} Message object.")
|
||||
|
||||
super().__init__(msg.type, msg.compression, msg.data)
|
||||
|
||||
if ('p' in msg.data) and (type(msg.data['p']) is not str):
|
||||
raise ValueError("Path of Request must be a string.")
|
||||
|
||||
if 'p' in self.data:
|
||||
self.data['p'] = str(self.data['p']).strip().lower()
|
||||
|
||||
if 'm' in msg.data:
|
||||
if type(msg.data['m']) is not bytes:
|
||||
raise ValueError("Method of Request must be bytes.")
|
||||
self.Method(int(self.data['m'][0]))
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if 'p' in self.data:
|
||||
return str(self.data['p']).lower().strip()
|
||||
else:
|
||||
return ""
|
||||
|
||||
@path.setter
|
||||
def path(self, path: str):
|
||||
self.data['p'] = str(path).strip().lower()
|
||||
|
||||
@property
|
||||
def method(self) -> Method:
|
||||
if 'm' in self.data:
|
||||
return self.Method(int(self.data['m'][0]))
|
||||
else:
|
||||
return self.Method.GET
|
||||
|
||||
@method.setter
|
||||
def method(self, meth: Method):
|
||||
meth = self.Method(meth)
|
||||
self.data['m'] = meth.value.to_bytes(1)
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, msg_bytes: bytes) -> Self:
|
||||
msg = super().unpack(msg_bytes)
|
||||
return Request(msg)
|
||||
|
||||
@classmethod
|
||||
def blank(cls) -> Self:
|
||||
msg = Message(Message.MessageType.REQUEST, Message.CompressionType.NONE, {})
|
||||
return Request(msg)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Request: {self.method.name} '{self.path}'>"
|
||||
|
||||
class Response(Message):
|
||||
def __init__(self, msg: Message):
|
||||
if msg.type is not Message.MessageType.RESPONSE:
|
||||
raise ValueError(f"Can't create a Response Object from a {msg.type} Message object.")
|
||||
|
||||
super().__init__(msg.type, msg.compression, msg.data)
|
||||
if 'c' in msg.data:
|
||||
status_bytes = self.data['c']
|
||||
if type(status_bytes) is not bytes:
|
||||
raise ValueError("Invalid Response data")
|
||||
status_code = int.from_bytes(status_bytes)
|
||||
if status_code >= 600:
|
||||
raise ValueError("Invalid status code.")
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, msg_bytes: bytes) -> Self:
|
||||
msg = super().unpack(msg_bytes)
|
||||
return Response(msg)
|
||||
|
||||
@classmethod
|
||||
def blank(cls) -> Self:
|
||||
msg = Message(Message.MessageType.RESPONSE, Message.CompressionType.NONE, {})
|
||||
return Response(msg)
|
||||
|
||||
@property
|
||||
def status_code(self) -> int:
|
||||
if 'c' in self.data:
|
||||
status_bytes = self.data['c']
|
||||
if type(status_bytes) is not bytes:
|
||||
raise ValueError("Invalid Response data")
|
||||
status_code = int.from_bytes(status_bytes)
|
||||
if status_code >= 600:
|
||||
raise ValueError("Invalid status code.")
|
||||
return status_code
|
||||
else:
|
||||
return 200
|
||||
|
||||
@status_code.setter
|
||||
def status_code(self, code: int):
|
||||
if (code <= 0) or (code >= 600):
|
||||
raise ValueError("Status must be a positive integer <= 600")
|
||||
self.data['c'] = code.to_bytes(2)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Response: {self.status_code}>"
|
||||
|
||||
def send_response(conn: PacketServerConnection, response: Response, original_request: Request,
|
||||
compression: Message.CompressionType = Message.CompressionType.BZIP2):
|
||||
if conn.state.name == "CONNECTED" and not conn.closing:
|
||||
|
||||
# figure out compression setting based on request
|
||||
logging.debug("Determining compression of response")
|
||||
comp = compression
|
||||
logging.debug(f"Default comp: {comp}")
|
||||
logging.debug(f"Original vars: {original_request.vars}")
|
||||
if 'C' in original_request.vars:
|
||||
logging.debug(f"Detected compression header in original request: {original_request.vars['C']}")
|
||||
val = original_request.vars['C']
|
||||
for i in Message.CompressionType:
|
||||
logging.debug(f"Checking type: {i}")
|
||||
if str(val).strip().upper() == i.name:
|
||||
comp = i
|
||||
logging.debug(f"matched compression with var to {comp}")
|
||||
break
|
||||
try:
|
||||
if int(val) == i.value:
|
||||
comp = i
|
||||
logging.debug(f"matched compression with var to {comp}")
|
||||
except ValueError:
|
||||
pass
|
||||
response.compression = comp
|
||||
logging.debug(f"Final compression: {response.compression}")
|
||||
|
||||
logging.debug(f"sending response: {response}, {response.compression}, {response.payload}")
|
||||
conn.send_data(response.pack())
|
||||
logging.debug("response sent successfully")
|
||||
else:
|
||||
logging.warning(f"Attempted to send data, but connection state is {conn.state.name}")
|
||||
|
||||
def send_blank_response(conn: PacketServerConnection, original_request: Request, status_code: int = 200,
|
||||
payload: Union[bytes, bytearray, str, dict, list] = ""):
|
||||
response = Response.blank()
|
||||
response.status_code = status_code
|
||||
response.payload = payload
|
||||
send_response(conn, response, original_request)
|
||||
2
packetserver/common/constants.py
Normal file
2
packetserver/common/constants.py
Normal file
@@ -0,0 +1,2 @@
|
||||
no_values = [0, '0', 'n', 'N', 'f', 'F', 'no', 'NO', False]
|
||||
yes_values = [1, '1', 'y', 'Y', 't', 'T', 'yes', 'YES', True]
|
||||
268
packetserver/common/testing.py
Normal file
268
packetserver/common/testing.py
Normal file
@@ -0,0 +1,268 @@
|
||||
import msgpack
|
||||
|
||||
from . import PacketServerConnection
|
||||
from pe.connect import ConnectionState
|
||||
from msgpack import Unpacker
|
||||
from typing import Union, Self, Optional
|
||||
import os.path
|
||||
import logging
|
||||
import ax25
|
||||
|
||||
class DummyPacketServerConnection(PacketServerConnection):
|
||||
|
||||
def __init__(self, call_from: str, call_to: str, incoming=False):
|
||||
super().__init__(0, call_from, call_to, incoming=incoming)
|
||||
self.sent_data = Unpacker()
|
||||
self._state = ConnectionState.CONNECTED
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
def send_data(self, data: Union[bytes, bytearray]):
|
||||
self.sent_data.feed(data)
|
||||
logging.debug(f"Sender added {data} to self.sent_data.feed")
|
||||
|
||||
class DirectoryTestServerConnection(PacketServerConnection):
|
||||
"""Monitors a directory for messages in msgpack format."""
|
||||
def __init__(self, call_from: str, call_to: str, directory: str, incoming=False):
|
||||
super().__init__(0, call_from, call_to, incoming=incoming)
|
||||
self._state = ConnectionState.CONNECTED
|
||||
if not os.path.isdir(directory):
|
||||
raise FileNotFoundError(f"No such directory as {directory}")
|
||||
self._directory = directory
|
||||
self._sent_data = Unpacker()
|
||||
self._pid = 1
|
||||
self.closing = False
|
||||
|
||||
@classmethod
|
||||
def create_directory_connection(cls, self_callsign: str, directory: str) -> Self:
|
||||
|
||||
if not ax25.Address.valid_call(self_callsign):
|
||||
raise ValueError("self_callsign must be a valid callsign.")
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
raise NotADirectoryError(f"{directory} is not a directory or doesn't exist.")
|
||||
|
||||
spl = os.path.basename(directory).split('--')
|
||||
if len(spl) != 2:
|
||||
raise ValueError(f"Directory {directory} has the wrong name to be a connection dir.")
|
||||
|
||||
src = spl[0]
|
||||
dst = spl[1]
|
||||
|
||||
if not ax25.Address.valid_call(src):
|
||||
raise ValueError(f"Directory {directory} has the wrong name to be a connection dir.")
|
||||
|
||||
if not ax25.Address.valid_call(dst):
|
||||
raise ValueError(f"Directory {directory} has the wrong name to be a connection dir.")
|
||||
|
||||
if dst.upper() == self_callsign.upper():
|
||||
incoming = True
|
||||
else:
|
||||
incoming = False
|
||||
|
||||
return DirectoryTestServerConnection(src, dst, directory, incoming=incoming)
|
||||
|
||||
@property
|
||||
def pid(self) -> int:
|
||||
old = self._pid
|
||||
self._pid = self._pid + 1
|
||||
return old
|
||||
|
||||
@property
|
||||
def directory(self) -> str:
|
||||
return self._directory
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def file_path(self) -> str:
|
||||
file_name = f"{self.local_callsign}.msg"
|
||||
file_path = os.path.join(self._directory, file_name)
|
||||
return file_path
|
||||
|
||||
@property
|
||||
def remote_file_path(self) -> str:
|
||||
file_name = f"{self.remote_callsign}.msg"
|
||||
file_path = os.path.join(self._directory, file_name)
|
||||
return file_path
|
||||
|
||||
def check_closed(self):
|
||||
if self.closing:
|
||||
self._state = ConnectionState.DISCONNECTED
|
||||
if self._state is not ConnectionState.CONNECTED:
|
||||
return True
|
||||
if not os.path.isdir(self._directory):
|
||||
self._state = ConnectionState.DISCONNECTED
|
||||
self.disconnected()
|
||||
return True
|
||||
return False
|
||||
|
||||
def write_out(self, data: bytes):
|
||||
if self.check_closed():
|
||||
raise RuntimeError("Connection is closed. Cannot send.")
|
||||
|
||||
if os.path.exists(self.file_path):
|
||||
raise RuntimeError("The outgoing message file already exists. State is wrong for sending.")
|
||||
|
||||
if os.path.exists(self.file_path+".tmp"):
|
||||
os.remove(self.file_path+".tmp")
|
||||
|
||||
open(self.file_path+".tmp", 'wb').write(data)
|
||||
os.rename(self.file_path+".tmp", self.file_path)
|
||||
|
||||
def send_data(self, data: Union[bytes, bytearray]):
|
||||
if self.check_closed():
|
||||
raise RuntimeError("Connection is closed. Cannot send.")
|
||||
self._sent_data.feed(data)
|
||||
logging.debug(f"Sender added {data} to self.sent_data.feed")
|
||||
try:
|
||||
obj = self._sent_data.unpack()
|
||||
self.write_out(msgpack.packb(obj))
|
||||
logging.debug(f"Wrote complete binary message to {self.file_path}")
|
||||
except msgpack.OutOfData as e:
|
||||
pass
|
||||
|
||||
def check_for_data(self):
|
||||
"""Monitors connection directory for data."""
|
||||
if self.closing:
|
||||
self._state = ConnectionState.DISCONNECTED
|
||||
if self.check_closed():
|
||||
return
|
||||
|
||||
if os.path.isfile(self.remote_file_path):
|
||||
logging.debug(f"{self.local_callsign} Found that the remote file path '{self.remote_file_path}' exists now.")
|
||||
data = open(self.remote_file_path, 'rb').read()
|
||||
self.data_received(self.pid, bytearray(data))
|
||||
os.remove(self.remote_file_path)
|
||||
logging.debug(f"{self.local_callsign} detected data from {self.remote_callsign}: {msgpack.unpackb(data)}")
|
||||
|
||||
|
||||
class SimpleDirectoryConnection:
|
||||
def __init__(self, call_from: str, call_to: str, directory: str, incoming=False):
|
||||
self._state = ConnectionState.CONNECTED
|
||||
if not os.path.isdir(directory):
|
||||
raise FileNotFoundError(f"No such directory as {directory}")
|
||||
self._directory = directory
|
||||
self._sent_data = Unpacker()
|
||||
self.data = Unpacker()
|
||||
self._pid = 1
|
||||
self.call_to = call_to
|
||||
self.call_from = call_from
|
||||
self.incoming = incoming
|
||||
self._incoming = incoming
|
||||
self.closing = False
|
||||
if incoming:
|
||||
self.local_callsign = call_to
|
||||
self.remote_callsign = call_from
|
||||
else:
|
||||
self.local_callsign = call_from
|
||||
self.remote_callsign = call_to
|
||||
|
||||
@classmethod
|
||||
def create_directory_connection(cls, self_callsign: str, directory: str) -> Self:
|
||||
|
||||
if not ax25.Address.valid_call(self_callsign):
|
||||
raise ValueError("self_callsign must be a valid callsign.")
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
raise NotADirectoryError(f"{directory} is not a directory or doesn't exist.")
|
||||
|
||||
spl = os.path.basename(directory).split('--')
|
||||
if len(spl) != 2:
|
||||
raise ValueError(f"Directory {directory} has the wrong name to be a connection dir.")
|
||||
|
||||
src = spl[0]
|
||||
dst = spl[1]
|
||||
|
||||
if not ax25.Address.valid_call(src):
|
||||
raise ValueError(f"Directory {directory} has the wrong name to be a connection dir.")
|
||||
|
||||
if not ax25.Address.valid_call(dst):
|
||||
raise ValueError(f"Directory {directory} has the wrong name to be a connection dir.")
|
||||
|
||||
if dst.upper() == self_callsign.upper():
|
||||
incoming = True
|
||||
else:
|
||||
incoming = False
|
||||
|
||||
return SimpleDirectoryConnection(src, dst, directory, incoming=incoming)
|
||||
|
||||
@property
|
||||
def pid(self) -> int:
|
||||
old = self._pid
|
||||
self._pid = self._pid + 1
|
||||
return old
|
||||
|
||||
@property
|
||||
def directory(self) -> str:
|
||||
return self._directory
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def file_path(self) -> str:
|
||||
file_name = f"{self.local_callsign}.msg"
|
||||
file_path = os.path.join(self._directory, file_name)
|
||||
return file_path
|
||||
|
||||
@property
|
||||
def remote_file_path(self) -> str:
|
||||
file_name = f"{self.remote_callsign}.msg"
|
||||
file_path = os.path.join(self._directory, file_name)
|
||||
return file_path
|
||||
|
||||
def check_closed(self):
|
||||
if self.closing:
|
||||
self._state = ConnectionState.DISCONNECTED
|
||||
if self._state is not ConnectionState.CONNECTED:
|
||||
return True
|
||||
if not os.path.isdir(self._directory):
|
||||
self._state = ConnectionState.DISCONNECTED
|
||||
return True
|
||||
return False
|
||||
|
||||
def write_out(self, data: bytes):
|
||||
if self.check_closed():
|
||||
raise RuntimeError("[SIMPLE] Connection is closed. Cannot send.")
|
||||
|
||||
if os.path.exists(self.file_path):
|
||||
raise RuntimeError("[SIMPLE] The outgoing message file already exists. State is wrong for sending.")
|
||||
|
||||
if os.path.exists(self.file_path+".tmp"):
|
||||
os.remove(self.file_path+".tmp")
|
||||
|
||||
open(self.file_path+".tmp", 'wb').write(data)
|
||||
os.rename(self.file_path+".tmp", self.file_path)
|
||||
|
||||
def send_data(self, data: Union[bytes, bytearray]):
|
||||
if self.check_closed():
|
||||
raise RuntimeError("[SIMPLE] Connection is closed. Cannot send.")
|
||||
self._sent_data.feed(data)
|
||||
logging.debug(f"[SIMPLE] Sender added {data} to self.sent_data.feed")
|
||||
try:
|
||||
obj = self._sent_data.unpack()
|
||||
self.write_out(msgpack.packb(obj))
|
||||
logging.debug(f"[SIMPLE] Wrote complete binary message to {self.file_path}")
|
||||
except msgpack.OutOfData as e:
|
||||
pass
|
||||
|
||||
def check_for_data(self) -> bool:
|
||||
"""Monitors connection directory for data."""
|
||||
if self.closing:
|
||||
self._state = ConnectionState.DISCONNECTED
|
||||
if self.check_closed():
|
||||
return False
|
||||
if os.path.isfile(self.remote_file_path):
|
||||
data = open(self.remote_file_path, 'rb').read()
|
||||
os.remove(self.remote_file_path)
|
||||
logging.debug(f"[SIMPLE] {self.local_callsign} detected data from {self.remote_callsign}: {data}")
|
||||
self.data.feed(data)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
151
packetserver/common/util.py
Normal file
151
packetserver/common/util.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import re
|
||||
import datetime
|
||||
import tempfile
|
||||
import tarfile
|
||||
from typing import Union, Iterable, Tuple, Optional, IO
|
||||
import os.path
|
||||
from io import BytesIO, BufferedReader
|
||||
import random
|
||||
import string
|
||||
|
||||
def email_valid(email: str) -> bool:
|
||||
"""Taken from https://www.geeksforgeeks.org/check-if-email-address-valid-or-not-in-python/"""
|
||||
regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b'
|
||||
if re.fullmatch(regex, email):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def to_date_digits(index: datetime.datetime) -> str:
|
||||
return f"{str(index.year).zfill(4)}{str(index.month).zfill(2)}{str(index.day).zfill(2)}{str(index.hour).zfill(2)}{str(index.minute).zfill(2)}{str(index.second).zfill(2)}"
|
||||
|
||||
def from_date_digits(index: str, tz: datetime.timezone = datetime.UTC) -> datetime:
|
||||
ind = str(index)
|
||||
if not ind.isdigit():
|
||||
raise ValueError("Received invalid date digit string, containing non-digit chars.")
|
||||
if len(ind) < 4:
|
||||
raise ValueError("Received invalid date digit string, needs to at least by four digits for a year")
|
||||
year = int(ind[:4])
|
||||
month = 1
|
||||
day = 1
|
||||
hour = 0
|
||||
minute = 0
|
||||
second = 0
|
||||
if len(ind) >= 6:
|
||||
month = int(ind[4:6])
|
||||
|
||||
if len(ind) >= 8:
|
||||
day = int(ind[6:8])
|
||||
|
||||
if len(ind) >= 10:
|
||||
hour = int(ind[8:10])
|
||||
|
||||
if len(ind) >= 12:
|
||||
minute = int(ind[10:12])
|
||||
|
||||
if len(ind) >= 14:
|
||||
second = int(ind[12:14])
|
||||
|
||||
return datetime.datetime(year, month, day ,hour, minute, second, tzinfo=tz)
|
||||
|
||||
def tar_bytes(file: Union[str, Iterable]) -> bytes:
|
||||
"""Creates a tar archive in a temporary file with the specified files at root level.
|
||||
Returns the bytes of the archive."""
|
||||
files = []
|
||||
if type(file) is str:
|
||||
files.append(file)
|
||||
else:
|
||||
for i in file:
|
||||
files.append(str(i))
|
||||
|
||||
with tempfile.TemporaryFile() as temp:
|
||||
tar_obj = tarfile.TarFile(fileobj=temp, mode="w")
|
||||
for i in files:
|
||||
tar_obj.add(i, arcname=os.path.basename(i))
|
||||
tar_obj.close()
|
||||
temp.seek(0)
|
||||
return temp.read()
|
||||
|
||||
def bytes_to_tar_bytes(name: str, data: bytes) -> bytes:
|
||||
"""Creates a tar archive with a single file of name <name> with <data> bytes as the contents"""
|
||||
with tempfile.TemporaryFile() as temp:
|
||||
tar_obj = tarfile.TarFile(fileobj=temp, mode="w")
|
||||
bio = BytesIO(data)
|
||||
tar_info = tarfile.TarInfo(name=name)
|
||||
tar_info.size = len(data)
|
||||
tar_obj.addfile(tar_info, bio)
|
||||
tar_obj.close()
|
||||
temp.seek(0)
|
||||
return temp.read()
|
||||
|
||||
def bytes_tar_has_files(data: Union[bytes, IO]):
|
||||
if type(data) is bytes:
|
||||
bio = BytesIO(data)
|
||||
else:
|
||||
bio = data
|
||||
tar_obj = tarfile.TarFile(fileobj=bio, mode="r")
|
||||
files = [m for m in tar_obj.getmembers() if m.isfile()]
|
||||
if len(files) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def multi_bytes_to_tar_bytes(objects: dict) -> bytes:
|
||||
"""Creates a tar archive with a single file of name <name> with <data> bytes as the contents"""
|
||||
with tempfile.TemporaryFile() as temp:
|
||||
tar_obj = tarfile.TarFile(fileobj=temp, mode="w")
|
||||
for name in objects:
|
||||
data = bytes(objects[name])
|
||||
bio = BytesIO(data)
|
||||
tar_info = tarfile.TarInfo(name=name)
|
||||
tar_info.size = len(data)
|
||||
tar_obj.addfile(tar_info, bio)
|
||||
tar_obj.close()
|
||||
temp.seek(0)
|
||||
return temp.read()
|
||||
|
||||
def extract_tar_bytes(tarfile_bytes: bytes) -> Tuple[str, bytes]:
|
||||
"""Takes the bytes of a tarfile, and returns the name and bytes of the first file in the archive."""
|
||||
out_bytes = b''
|
||||
bio = BytesIO(tarfile_bytes)
|
||||
tar_obj = tarfile.TarFile(fileobj=bio, mode="r")
|
||||
members = tar_obj.getmembers()
|
||||
for i in range(0, len(members)):
|
||||
if members[i].isfile():
|
||||
return members[i].name, tar_obj.extractfile(members[i]).read()
|
||||
raise FileNotFoundError("No files found to extract from archive")
|
||||
|
||||
def random_string(length=8) -> str:
|
||||
rand_str = ''.join(random.choices(string.ascii_letters + string.digits, k=length))
|
||||
return rand_str
|
||||
|
||||
|
||||
class TarFileExtractor(object):
|
||||
"""Generator created from file like object pointing to tar data"""
|
||||
def __init__(self, fileobj: IO):
|
||||
self.fileobj = fileobj
|
||||
try:
|
||||
self.tar_file = tarfile.TarFile(fileobj=self.fileobj)
|
||||
self._raw_members = [m for m in self.tar_file.getmembers() if m.isfile()]
|
||||
except:
|
||||
self._raw_members = []
|
||||
self._count = 0
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
# Python 3 compatibility
|
||||
def __next__(self):
|
||||
return self.next()
|
||||
|
||||
def next(self) -> Tuple[str, IO]:
|
||||
if (self._count + 1) > len(self._raw_members):
|
||||
raise StopIteration()
|
||||
else:
|
||||
member = self._raw_members[self._count]
|
||||
name = member.name
|
||||
if type(name) is bytes:
|
||||
name = name.decode()
|
||||
name = str(name)
|
||||
self._count = self._count + 1
|
||||
return os.path.basename(name), self.tar_file.extractfile(member)
|
||||
Reference in New Issue
Block a user