From 5e25606880faa2ed367b72ea373240d48ea2965c Mon Sep 17 00:00:00 2001 From: Tiara Rodney Date: Sat, 2 Apr 2022 21:32:15 +0200 Subject: [PATCH] feat(backend/mysql): initialize mysql backend created prototype for backend/mysql module feat(backend/mysql): implement interface tested with MariaDB docs(backend/sql): initialize docs --- docs/guide/backend.rst | 6 ++ src/httpaste/backend/__init__.py | 20 ++++ src/httpaste/backend/mysql/__init__.py | 120 ++++++++++++++++++++++++ src/httpaste/backend/mysql/paste.py | 122 +++++++++++++++++++++++++ src/httpaste/backend/mysql/paste.sql | 9 ++ src/httpaste/backend/mysql/user.py | 101 ++++++++++++++++++++ src/httpaste/backend/mysql/user.sql | 6 ++ 7 files changed, 384 insertions(+) create mode 100644 src/httpaste/backend/mysql/__init__.py create mode 100644 src/httpaste/backend/mysql/paste.py create mode 100644 src/httpaste/backend/mysql/paste.sql create mode 100644 src/httpaste/backend/mysql/user.py create mode 100644 src/httpaste/backend/mysql/user.sql diff --git a/docs/guide/backend.rst b/docs/guide/backend.rst index bc2f9c3..a09c3e9 100644 --- a/docs/guide/backend.rst +++ b/docs/guide/backend.rst @@ -13,4 +13,10 @@ Filesystem ---------- .. autoclass:: httpaste.backend.file.Parameters + :members: + +MySQL +----- + +.. autoclass:: httpaste.backend.mysql.Parameters :members: \ No newline at end of file diff --git a/src/httpaste/backend/__init__.py b/src/httpaste/backend/__init__.py index 48978a0..60800b1 100644 --- a/src/httpaste/backend/__init__.py +++ b/src/httpaste/backend/__init__.py @@ -11,9 +11,13 @@ from .sqlite import Parameters as SqliteParameters from .sqlite import User as SqliteUser from .sqlite import Paste as SqlitePaste from .sqlite import get_connection as get_sqlite_connection +from .mysql import get_connection as get_mysql_connection from .file import Parameters as FileParameters from .file import User as FileUser from .file import Paste as FilePaste +from .mysql import Parameters as MySQLParameters +from .mysql import User as MySQLUser +from .mysql import Paste as MySQLPaste class SQLite(Backend): @@ -46,6 +50,22 @@ class File(Backend): self.paste = FilePaste(parameters, Paste, PasteDataSchema) +class MySQL(Backend): + """MySQL backend interface + """ + + parameter_class = MySQLParameters + user: MySQLUser + paste: MySQLPaste + + def __init__(self, parameters: MySQLParameters): + + parameters = MySQLParameters(*parameters[1:], get_mysql_connection(parameters)) + + self.user = MySQLUser(parameters, User) + self.paste = MySQLPaste(parameters, Paste) + + def get_backend_map() -> Dict[str, Tuple[type, type]]: """get a map of backend ids and their classes """ diff --git a/src/httpaste/backend/mysql/__init__.py b/src/httpaste/backend/mysql/__init__.py new file mode 100644 index 0000000..2a21162 --- /dev/null +++ b/src/httpaste/backend/mysql/__init__.py @@ -0,0 +1,120 @@ +"""MySQL backend +""" +try: + from mysql.connector import connect + from mysql.connector.connection import MySQLConnection +except ImportError as e: + raise ImportError(' '.join(( + '\'mysql-connector-python\' is not installed.', + 'Install it by running', + '\'python3 -m pip install mysql-connector-python\'.' + ))) from e + +from typing import NamedTuple, Optional + +from . import user +from . import paste + + +class Parameters(NamedTuple): + """MySQL parameters + """ + + #: user name + user: str + #: user password + password: str + #: hostname or IP address + host: str + #: database identifier + database: str + #: a mysql.connection.MySQLConnection object (does not apply to config) + connection: Optional[MySQLConnection] = None + + +class User(object): + """MySQL user model backend + """ + + connection: MySQLConnection + + def __init__(self, parameters: Parameters, model_class: type) -> None: + + self.model_class = model_class + + self.connection = get_connection(parameters) + + def load(self, proto: object) -> object: + + return user.load(proto, self.connection, self.model_class) + + def dump(self, model: object) -> None: + + return user.dump(model, self.connection) + + def delete(self, proto: object) -> None: + + return user.delete(proto, self.connection) + + def init(self) -> None: + + return user.init(self.connection) + + def sanitize(self) -> None: + + return user.sanitize(self.connection, self.model_class) + + +class Paste(object): + """MySQL paste model backend + """ + + connection: MySQLConnection + + def __init__(self, parameters: Parameters, model_class: type) -> None: + + self.model_class = model_class + + self.connection = get_connection(parameters) + + def load(self, proto: object) -> object: + + return paste.load(proto, self.connection, self.model_class) + + def dump(self, model: object) -> None: + + return paste.dump(model, self.connection) + + def delete(self, proto: object) -> None: + + return paste.delete(proto, self.connection) + + def init(self) -> None: + + return paste.init(self.connection) + + def sanitize(self) -> None: + + return paste.sanitize(self.connection, self.model_class) + + +def get_connection(parameters: Parameters) -> MySQLConnection: + """get a mysql.connection.MySQLConnection object + """ + + if parameters.connection is not None: + + return parameters.connection + + connection = connect(user=parameters.user, password=parameters.password, + host=parameters.host, + database=parameters.database) + + return connection + + +__all__ = [ + Parameters, + User, + Paste +] \ No newline at end of file diff --git a/src/httpaste/backend/mysql/paste.py b/src/httpaste/backend/mysql/paste.py new file mode 100644 index 0000000..5e4057f --- /dev/null +++ b/src/httpaste/backend/mysql/paste.py @@ -0,0 +1,122 @@ +from os import path +from time import time + +try: + from mysql.connector.connection import MySQLConnection +except ImportError as e: + raise ImportError(' '.join(( + '\'mysql-connector-python\' is not installed.', + 'Install it by running', + '\'python3 -m pip install mysql-connector-python\'.' + ))) from e + + +def load(proto:object, connection: MySQLConnection, model_class: type): + """load a paste model + + :param model: model prototype + :param connection: mysql connector connection object + :param model_class: model class + """ + + cursor = connection.cursor(dictionary=True) + + statement = '''SELECT pid, data, data_hash, sub, expiration, encoding + FROM httpaste_pastes + WHERE pid=%s''' + + cursor.execute(statement, (proto.pid,)) + + row = cursor.fetchone() + + if row is not None: + + return model_class( + row['pid'], + row['sub'], + row['data'], + row['data_hash'], + row['expiration'], + row['encoding']) + + return None + + +def dump(model:object, connection: MySQLConnection): + """dump a paste model + + :param model: model object + :param connection: mysql connector connection object + :param model_class: model class + """ + + cursor = connection.cursor() + + statement = '''REPLACE INTO httpaste_pastes + (pid, data, data_hash, sub, expiration, encoding) + VALUES (%s, %s, %s, %s, %s, %s)''' + + cursor.execute(statement, (model.pid, model.data, model.data_hash, + model.sub, model.expiration, model.encoding)) + + connection.commit() + + return None + + +def delete(proto: object, connection: MySQLConnection): + """delete a paste model + + :param model: model prototype + :param connection: mysql connector connection object + :param model_class: model class + """ + + cursor = connection.cursor() + + statement = '''DELETE FROM httpaste_pastes + WHERE pid=%s''' + + cursor.execute(statement, (proto.pid,)) + + connection.commit() + + return None + + +def init(connection: MySQLConnection): + """initialize paste model table + + :param connection: mysql connector connection object + """ + + cursor = connection.cursor() + + with open(path.join(path.dirname(__file__), 'paste.sql'), 'r') as fh: + + cursor.execute(fh.read()) + + connection.commit() + + return None + + +def sanitize(connection: MySQLConnection, model_class:type): + """sanitize paste model table + + :param connection: mysql connector connection object + """ + + cursor = connection.cursor(dictionary=True) + + statement = '''SELECT pid + FROM httpaste_pastes + WHERE expiration < %s AND expiration > 0''' + + cursor.execute(statement, (time(),)) + + for row in cursor.fetchall(): + + delete(model_class(row['pid'])) + + return None \ No newline at end of file diff --git a/src/httpaste/backend/mysql/paste.sql b/src/httpaste/backend/mysql/paste.sql new file mode 100644 index 0000000..d4c366e --- /dev/null +++ b/src/httpaste/backend/mysql/paste.sql @@ -0,0 +1,9 @@ +CREATE TABLE `httpaste_pastes` ( + `pid` blob NOT NULL, + `data` longblob NOT NULL, + `data_hash` blob NOT NULL, + `sub` blob DEFAULT NULL, + `expiration` int(16) NOT NULL, + `encoding` tinytext DEFAULT NULL, + PRIMARY KEY (`pid`(128)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/src/httpaste/backend/mysql/user.py b/src/httpaste/backend/mysql/user.py new file mode 100644 index 0000000..7efebfc --- /dev/null +++ b/src/httpaste/backend/mysql/user.py @@ -0,0 +1,101 @@ +from os import path +try: + from mysql.connector.connection import MySQLConnection +except ImportError as e: + raise ImportError(' '.join(( + '\'mysql-connector-python\' is not installed.', + 'Install it by running', + '\'python3 -m pip install mysql-connector-python\'.' + ))) from e + + +def load(proto:object, connection: MySQLConnection, model_class: type): + """load a user model + + :param model: model prototype + :param connection: mysql connector connection object + :param model_class: model class + """ + + cursor = connection.cursor(dictionary=True) + + statement = '''SELECT sub, key_hash, paste_index + FROM httpaste_users + WHERE sub=%s''' + + cursor.execute(statement, (proto.sub,)) + + row = cursor.fetchone() + + if row is not None: + + return model_class(row['sub'], row['key_hash'], row['paste_index']) + + return None + + +def dump(model:object, connection: MySQLConnection): + """dump a user model + + :param model: model object + :param connection: mysql connector connection object + :param model_class: model class + """ + + cursor = connection.cursor() + + statement = '''REPLACE INTO httpaste_users + (sub, key_hash, paste_index) + VALUES (%s, %s, %s)''' + + cursor.execute(statement, (model.sub, model.key_hash, model.index)) + + connection.commit() + + return None + + +def delete(proto: object, connection: MySQLConnection): + """delete a user model + + :param model: model prototype + :param connection: mysql connector connection object + :param model_class: model class + """ + + cursor = connection.cursor() + + statement = '''DELETE FROM httpaste_users + WHERE sub=%s''' + + cursor.execute(statement, (proto.sub,)) + + connection.commit() + + return None + + +def init(connection: MySQLConnection): + """initialize user model table + + :param connection: mysql connector connection object + """ + + cursor = connection.cursor() + + with open(path.join(path.dirname(__file__), 'user.sql'), 'r') as fh: + + cursor.execute(fh.read()) + + connection.commit() + + return None + + +def sanitize(connection: MySQLConnection, model_class: type): + """sanitize user model table + + :param connection: mysql connector connection object + """ + + return None \ No newline at end of file diff --git a/src/httpaste/backend/mysql/user.sql b/src/httpaste/backend/mysql/user.sql new file mode 100644 index 0000000..62045a9 --- /dev/null +++ b/src/httpaste/backend/mysql/user.sql @@ -0,0 +1,6 @@ +CREATE TABLE `httpaste_users` ( + `sub` blob NOT NULL, + `key_hash` blob NOT NULL, + `paste_index` blob NOT NULL, + PRIMARY KEY (`sub`(128)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file