From 158db1209b9a0d0ede5a4605fcba5781c07fa576 Mon Sep 17 00:00:00 2001 From: Ethan Paul <24588726+enpaul@users.noreply.github.com> Date: Sat, 30 Oct 2021 10:59:41 -0400 Subject: [PATCH] Update database schema for new image/alias structure --- kodak/database/__init__.py | 12 +++--- kodak/database/_shared.py | 79 +++++++++++++++++++++++++++++++++++++ kodak/database/access.py | 9 +++++ kodak/database/alias.py | 13 ++++++ kodak/database/image.py | 29 ++++---------- kodak/database/thumbnail.py | 11 ------ 6 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 kodak/database/access.py create mode 100644 kodak/database/alias.py delete mode 100644 kodak/database/thumbnail.py diff --git a/kodak/database/__init__.py b/kodak/database/__init__.py index ffbec22..e336aff 100644 --- a/kodak/database/__init__.py +++ b/kodak/database/__init__.py @@ -4,14 +4,16 @@ from typing import Tuple import peewee from kodak import constants +from kodak import exceptions from kodak.configuration import KodakConfig from kodak.database._shared import INTERFACE as interface from kodak.database._shared import KodakModel +from kodak.database.access import AccessRecord +from kodak.database.alias import AliasRecord from kodak.database.image import ImageRecord -from kodak.database.thumbnail import ThumbnailRecord -MODELS: Tuple[KodakModel, ...] = (ImageRecord, ThumbnailRecord) +MODELS: Tuple[KodakModel, ...] = (ImageRecord, AliasRecord, AccessRecord) def initialize(config: KodakConfig): @@ -26,13 +28,13 @@ def initialize(config: KodakConfig): logger = logging.getLogger(__name__) - if config.database.backend == constants.SupportedDatabaseBackend.SQLITE: + if config.database.backend == constants.DatabaseBackend.SQLITE: logger.debug("Using SQLite database backend") logger.debug(f"Applying SQLite pragmas: {config.database.sqlite.pragmas}") database = peewee.SqliteDatabase( config.database.sqlite.path, pragmas=config.database.sqlite.pragmas ) - elif config.database.backend == constants.SupportedDatabaseBackend.MARIADB: + elif config.database.backend == constants.DatabaseBackend.MARIADB: logger.debug("Using MariaDB database backend") logger.debug( "Configuring MariaDB:" @@ -48,7 +50,7 @@ def initialize(config: KodakConfig): charset="utf8mb4", ) else: - raise ValueError( + raise exceptions.ConfigurationError( f"Invalid storage backend in configuration: {config.database.backend}" ) diff --git a/kodak/database/_shared.py b/kodak/database/_shared.py index df3337a..2968978 100644 --- a/kodak/database/_shared.py +++ b/kodak/database/_shared.py @@ -1,5 +1,9 @@ import datetime +import enum +import hashlib import uuid +from typing import NamedTuple +from typing import Type import peewee @@ -7,7 +11,82 @@ import peewee INTERFACE = peewee.DatabaseProxy() +class Checksum(NamedTuple): + """Checksum data container + + :param algorithm: Hashing algorithm, must be supported by Python's hashlib + :param digest: Hex digest of the hash + """ + + algorithm: str + digest: str + + @classmethod + def from_hash(cls, data: hashlib._hashlib.HASH): # pylint: disable=protected-access + """Construct from a hashlib object""" + return cls(algorithm=data.name, digest=data.hexdigest()) + + +class EnumField(peewee.CharField): + """Custom field for storing enums""" + + def __init__(self, enumeration: Type[enum.Enum], *args, **kwargs): + super().__init__(*args, **kwargs) + self.enumeration = enumeration + + def db_value(self, value: enum.Enum) -> str: + """Convert the enum value to the database string + + :param value: Enum item to store the name of + :raises peewee.IntegrityError: When the item passed to ``value`` is not in the field's enum + :returns: The name of the enum item passed to ``value`` + """ + if not isinstance(value, self.enumeration): + raise peewee.IntegrityError( + f"Enum {self.enumeration.__name__} has no value '{value}'" + ) + return value.name + + def python_value(self, value: str) -> enum.Enum: + """Convert the stored string to the corresponding enum + + :param value: Name of the item from the field's enum to return + :raises peewee.IntegrityError: When the name passed to ``value`` does not correspond to an + item in the field's enum + :returns: The enum item with the name passed to ``value`` + """ + try: + return self.enumeration[value] + except KeyError: + raise peewee.InterfaceError( + f"Enum {self.enumeration.__name__} has no value with name '{value}'" + ) from None + + +class ChecksumField(peewee.CharField): + """Field for storing checksum hashes in the database + + .. note:: The reason for implementing this is to protect against future changes to the hashing + algorithm. Just storing the digest means that if the hashing algorithm is ever + changed (for performance, etc) then any existing records will be invalidated. By + storing the hashing algorithm with the digest we can protect against that possibility. + A custom container needs to be implemented because the builtin hashlib has no way to + recreate a hash object from the algorithm+digest without the original data. + """ + + def db_value(self, value: Checksum) -> str: + """Serialize the checkstum to a database string""" + return f"{value.algorithm}:{value.digest}" + + def python_value(self, value: str) -> Checksum: + """Deserailize a string to a checksum container""" + alg, _, digest = value.partition(":") + return Checksum(algorithm=alg, digest=digest) + + class KodakModel(peewee.Model): + """Base model for defining common fields and attaching database""" + class Meta: # pylint: disable=too-few-public-methods,missing-class-docstring database = INTERFACE diff --git a/kodak/database/access.py b/kodak/database/access.py new file mode 100644 index 0000000..00bdf5b --- /dev/null +++ b/kodak/database/access.py @@ -0,0 +1,9 @@ +import peewee + +from kodak.database._shared import KodakModel + + +class AccessRecord(KodakModel): + """Model for access keys when operating in private mode""" + + password = peewee.CharField(null=False) diff --git a/kodak/database/alias.py b/kodak/database/alias.py new file mode 100644 index 0000000..bbf75dc --- /dev/null +++ b/kodak/database/alias.py @@ -0,0 +1,13 @@ +import peewee + +from kodak.database._shared import ChecksumField +from kodak.database._shared import KodakModel +from kodak.database.image import ImageRecord + + +class AliasRecord(KodakModel): + """Model for manipulated image records""" + + parent = peewee.ForeignKeyField(ImageRecord, null=False) + name = peewee.CharField(null=False) + checksum = ChecksumField(null=False) diff --git a/kodak/database/image.py b/kodak/database/image.py index ad73abc..eb25aa3 100644 --- a/kodak/database/image.py +++ b/kodak/database/image.py @@ -1,30 +1,15 @@ -import json -import uuid -from typing import List - import peewee +from kodak import constants +from kodak.database._shared import ChecksumField +from kodak.database._shared import EnumField from kodak.database._shared import KodakModel class ImageRecord(KodakModel): - """Database record for""" + """Model for source images""" - width = peewee.IntegerField(null=False) - height = peewee.IntegerField(null=False) - format = peewee.CharField(null=False) + name = peewee.Charfield(null=False) + format = EnumField(constants.ImageFormat, null=False) deleted = peewee.BooleanField(null=False, default=False) - public = peewee.BooleanField(null=False, default=False) - owner = peewee.UUIDField(null=False) - sha256 = peewee.CharField(null=False) - _readable = peewee.CharField(null=False, default="[]") - - @property - def readable(self) -> List[uuid.UUID]: - """List of UUIDs corresponding to accounts that can read the file""" - return [uuid.UUID(item) for item in json.loads(self._readable)] - - @readable.setter - def readable(self, value: List[uuid.UUID]): - """Update the list of UUIDs for accounts that can read the file""" - self._readable = json.dumps([str(item) for item in value]) + checksum = ChecksumField(null=False) diff --git a/kodak/database/thumbnail.py b/kodak/database/thumbnail.py deleted file mode 100644 index cfafbee..0000000 --- a/kodak/database/thumbnail.py +++ /dev/null @@ -1,11 +0,0 @@ -import peewee - -from kodak.database._shared import KodakModel -from kodak.database.image import ImageRecord - - -class ThumbnailRecord(KodakModel): - - parent = peewee.ForeignKeyField(ImageRecord) - width = peewee.IntegerField(null=False) - height = peewee.IntegerField(null=False)