~piotr-machura/plainsync-common

2ee492cfaf0173e793383a8b883ba96ef28a66a3 — Piotr Machura 1 year, 1 month ago 5004f24
Move the sources
5 files changed, 357 insertions(+), 0 deletions(-)

A __init__.py
A message.py
A request.py
A response.py
A transfer.py
A __init__.py => __init__.py +1 -0
@@ 0,0 1,1 @@
"""Libraries used by the client and the server."""

A message.py => message.py +73 -0
@@ 0,0 1,73 @@
"""Message module.

A message is the basic object transferred over TCP as plain text as means of
client-server communication.
"""
from enum import Enum
import json


class MessageType(str, Enum):
    """Message type enum.

    Indicates the type of transmitted message.
    """
    NONE = 'NONE'
    OK = 'OK'
    ERR = 'ERR'
    AUTH = 'AUTH'
    PUSH = 'PUSH'
    PULL = 'PULL'
    LIST_FILES = 'LIST_FILES'
    NEW_FILE = 'NEW_FILE'
    DELETE_FILE = 'DELETE_FILE'
    NEW_SHARE = 'NEW_SHARE'
    DELETE_SHARE = 'DELETE_SHARE'


class Message():
    """Message class.

    Turned from/into a JSON string when transmitted over TCP as means of
    client-server communication.

    Items:
       type: MessageType enum specifying the type of message.
    """
    def __init__(self, msgType=MessageType.NONE):
        super().__init__()
        self.type = msgType

    def __str__(self):
        return self.toJSON()

    def toJSON(self):
        """Converts the message to a JSON string.

        Returns:
            A valid JSON string representation of the message.
        """
        return json.dumps(self.__dict__)

    @classmethod
    def fromJSON(cls, jsonStr):
        """Recreates the Message form a UTF-8 JSON byte string.

        **Warning:** in order for this to work ALL classes extending Message
        must have a default empty constructor.

        Args:
            jsonStr: valid JSON string obtained  with Message.toJSON.

        Returns:
            A new Message object created form the JSON byte string.
        """
        # Load the json string into a Python dict
        jsonDict = json.loads(jsonStr)
        new = cls()
        # Copy all keys into attributes
        for key, value in jsonDict.items():
            setattr(new, key, value)
        # Turn type into enum from JSON's string
        new.type = MessageType[jsonDict['type']]
        return new

A request.py => request.py +135 -0
@@ 0,0 1,135 @@
"""Request module.

Contains the `Request` classes sent by the client to a server.
"""
import json
from common.message import Message, MessageType


class Request(Message):
    """Base request class.

    Used by the client to request response from a server.
    """
    def __init__(self, **kwargs):
        super().__init__(**kwargs)


class AuthRequest(Request):
    """Authentication request class.

    Used by the client to communicate the authentication credentials. The server
    responds with an `AuthResponse` containing the assigned session ID or
    `ErrResponse`.

    Items:
        user: username of the authenticating user.
        passwd: password of the authenticating user.
    """
    def __init__(self, user=None, passwd=None):
        super().__init__(msgType=MessageType.AUTH, )
        self.user = user
        self.passwd = passwd


class PushRequest(Request):
    """Push request class.

    Used by the client to push local changes to the server. The server responds
    with `OkResponse` if everything went fine or `ErrResponse`.

    Items:
        fileID: the ID of file to be updated.
        content: the contents to update the file with.
    """
    def __init__(self, fileID=None, content=''):
        super().__init__(msgType=MessageType.PUSH, )
        self.content = content
        self.fileID = fileID

    def __str__(self):
        dictionary = self.__dict__.copy()
        del dictionary['content']
        return json.dumps(dictionary)



class PullRequest(Request):
    """Pull request class.

    Used by the client to to pull changes from the server. The server responds
    with `PullResponse` containing the file contents or `ErrResponse`.

    Items:
        fileID: the ID of the file to be pulled.
    """
    def __init__(self, fileID=None):
        super().__init__(msgType=MessageType.PULL, )
        self.fileID = fileID


class FileListRequest(Request):
    """File list request class.

    Used by the client to request listing files the user has access to. The
    server responds with `FileListResponse`, containing a dictionary of (file
    ID, (info)) pairs, where the ID is the unique ID assigned to every file and
    info is a dicitonary that contains information about the owner, file name,
    last edited date and last edited user.
    """
    def __init__(self):
        super().__init__(msgType=MessageType.LIST_FILES)

class NewFileRequest(Request):
    """New file request class.

    Used by the client to request creating a new file for the given user. The
    server responds with `OkResponse` if the action was successful or
    `ErrResponse`.
    """
    def __init__(self, fileName=None):
        super().__init__(msgType=MessageType.NEW_FILE)
        self.fileName = fileName

class DeleteFileRequest(Request):
    """File deletion request.

    Used by the client to request deleting specified file for the given user.
    If the file is owned by the user then it is permanently deleted, if it's
    shared then the share is deleted (user no longer has an access to it, but
    the owner and other shared users still do).

    The server responds with `OkResponse` if the action was successful or
    `ErrResponse`.
    """
    def __init__(self, fileID=None):
        super().__init__(msgType=MessageType.DELETE_FILE)
        self.fileID = fileID

class NewShareRequest(Request):
    """New share request.

    Used by the client to request sharing the file to specified user.

    The server responds with `OkResponse` if the action was successful or
    `ErrResponse`.
    """
    def __init__(self, fileID=None, user=None):
        super().__init__(msgType=MessageType.NEW_SHARE)
        self.fileID = fileID
        self.user = user

class DeleteShareRequest(Request):
    """Request to delete a share.

    Used by the client to request unsharing the file from specified user. Owner
    can delete any share, but every user can delete a share made to him (same
    effect as issuing a DeleteFileRequest).

    The server responds with `OkResponse` if the action was successful or
    `ErrResponse`.
    """
    def __init__(self, fileID=None, user=None):
        super().__init__(msgType=MessageType.DELETE_SHARE)
        self.fileID = fileID
        self.user = user

A response.py => response.py +117 -0
@@ 0,0 1,117 @@
"""Response module.

Contains the `Response` classes sent by the server to a client.
"""
import json
from common.message import Message, MessageType


class Response(Message):
    """Base response class.

    Used by the server to respond to client requests.

    Items:
        description: string describing the response contents.
    """
    def __init__(self, description='', **kwargs):
        super().__init__(**kwargs)
        self.description=description

class OkResponse(Response):
    """OK response class.

    Used by the server to communicate a succesfull outcome of an action which
    does not require any additional data.

    Args:
        action: string describing the succesfull action.
    """
    def __init__(self, action=None):
        super().__init__(
            msgType=MessageType.OK,
            description=f'Action: {action} :: Success.',
        )

class PullResponse(Response):
    """Pull response class.

    Used by the server to send file contents to the client in response to a
    pull request.

    Args:
        content: contents of the specified file.
    """
    def __init__(self, fileID=None, content=None):
        super().__init__(
            msgType=MessageType.PULL,
            description=f'Sending file contents: {fileID}',
        )
        self.content = content

    def __str__(self):
        dictionary = self.__dict__.copy()
        del dictionary['content']
        return json.dumps(dictionary)



class AuthResponse(Response):
    """Authentication response class.

    Used by the server to communicate succesfull authentication.

    Args:
        user: the just authenticated user.

    Items:
        userID: a generated ID of the user authenticating him in this session.
    """
    def __init__(self, sessionID=None, user=''):
        super().__init__(
            msgType=MessageType.AUTH,
            description=f'Authenticated :: {user}',
        )
        self.sessionID=sessionID

class FileListResponse(Response):
    """Response with file names owned by the user.

    Args:
        user: the user for which the file list has been sent.

    Items:
        filelist: a list of files accessible to the user as a dictionary of
            (file ID, (info)) pairs, where the ID is the unique ID assigned to
            every file and info is a dicitonary that contains information about
            the owner, file name, last edited date and last edited user.
    """
    def __init__(self, files=None, user=None):
        super().__init__(
            msgType=MessageType.LIST_FILES,
            description=f'Sending file list: {user}',
        )
        self.files = files
        if self.files is None:
            self.files = dict()

    def __str__(self):
        dictionary = self.__dict__.copy()
        del dictionary['files']
        return json.dumps(dictionary)


class ErrResponse(Response):
    """ Error response class.

    Used by the server to communicate when an error has occured during the
    processing of a request.

    Args:
        err: error description with optional stack trace.
    """
    def __init__(self, err=None):
        super().__init__(
            msgType=MessageType.ERR,
            description=f'Error: {err}.',
        )

A transfer.py => transfer.py +31 -0
@@ 0,0 1,31 @@
"""Transfer module.

This module contains a common tools for sending and recieving messages.
"""


def recieve(sock):
    """Recieve a message as a JSON string from the specified TCP socket.

    Returns:
        JSON representation of a Message, ready to be used in Message.fromJSON.
    Raises:
        ConnectionAbortedError when null is read from the first 8 bytes.
    """
    msgLen = int.from_bytes(sock.recv(2).strip(), byteorder='big')
    if msgLen == 0:
        raise ConnectionAbortedError
    return sock.recv(msgLen).decode('utf-8')


def send(socket, message):
    """
    Sends the message over a TCP connection on the specified socket.

    The message is encoed as a Bytes object where the first 8 bytes represent
    length of the UTF-8 message as uint64 in byteorder 'big', after which the
    message as UTF-8 encoded JSON bytes object follows.
    """
    payload = message.toJSON().encode('utf-8')
    payload = len(payload).to_bytes(2, byteorder='big', signed=False) + payload
    socket.sendall(payload)