Skip to content
Snippets Groups Projects
model.py 35.5 KiB
Newer Older
# Copyright 2019 PrivateStorage.io, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This module implements models (in the MVC sense) for the client side of
the storage plugin.
"""

from functools import (
    wraps,
from datetime import (
    datetime,
)
from zope.interface import (
    Interface,
    implementer,
)

from sqlite3 import (
    OperationalError,
    connect as _connect,
from aniso8601 import (
    parse_datetime as _parse_datetime,
from twisted.logger import (
    Logger,
)
from twisted.python.filepath import (
    FilePath,
from ._base64 import (
    urlsafe_b64decode,
)

from .validators import (
    is_base64_encoded,
    has_length,
    greater_than,
)

    pass_value_attribute,
    get_configured_pass_value,
from .schema import (
    get_schema_version,
    get_schema_upgrades,
    run_schema_upgrades,
)

def parse_datetime(s, **kw):
    """
    Like ``aniso8601.parse_datetime`` but accept unicode as well.
    """
    if isinstance(s, unicode):
        s = s.encode("utf-8")
    assert isinstance(s, bytes)
    if "delimiter" in kw and isinstance(kw["delimiter"], unicode):
        kw["delimiter"] = kw["delimiter"].encode("utf-8")
    return _parse_datetime(s, **kw)


class ILeaseMaintenanceObserver(Interface):
    """
    An object which is interested in receiving events related to the progress
    of lease maintenance activity.
    """
    def observe(sizes):
        """
        Observe some shares encountered during lease maintenance.

        :param list[int] sizes: The sizes of the shares encountered.
        """

    def finish():
        """
        Observe that a run of lease maintenance has completed.
        """


class StoreOpenError(Exception):
    """
    There was a problem opening the underlying data store.
    """
    def __init__(self, reason):
        self.reason = reason


class NotEnoughTokens(Exception):
    """
    An attempt to extract tokens failed because the store does not contain as
    many tokens as were requested.
    """


CONFIG_DB_NAME = u"privatestorageio-zkapauthz-v1.sqlite3"
def open_and_initialize(path, connect=None):
    Open a SQLite3 database for use as a voucher store.

    Create the database and populate it with a schema, if it does not already
    exist.

    :param FilePath path: The location of the SQLite3 database file.

    :return: A SQLite3 connection object for the database at the given path.
    """
    if connect is None:
        connect = _connect
    try:
        path.parent().makedirs(ignoreExistingDirectory=True)
    except OSError as e:
        raise StoreOpenError(e)

    dbfile = path.asBytesMode().path
    try:
        conn = connect(
            dbfile,
            isolation_level="IMMEDIATE",
        )
    except OperationalError as e:
        raise StoreOpenError(e)
Jean-Paul Calderone's avatar
Jean-Paul Calderone committed
    # Enforcement of foreign key constraints is off by default.  It must be
    # enabled on a per-connection basis.  This is a helpful feature to ensure
    # consistency so we want it enforced and we use it in our schema.
    conn.execute("PRAGMA foreign_keys = ON")

    with conn:
        cursor = conn.cursor()
        actual_version = get_schema_version(cursor)
        schema_upgrades = list(get_schema_upgrades(actual_version))
        run_schema_upgrades(schema_upgrades, cursor)
Jean-Paul Calderone's avatar
Jean-Paul Calderone committed
    # Create some tables that only exist (along with their contents) for
    # this connection.  These are outside of the schema because they are not
    # persistent.  We can change them any time we like without worrying about
    # upgrade logic because we re-create them on every connection.
    conn.execute(
        """
        -- Track tokens in use by the process holding this connection.
        CREATE TEMPORARY TABLE [in-use] (
            [unblinded-token] text, -- The base64 encoded unblinded token.

            PRIMARY KEY([unblinded-token])
            -- A foreign key on unblinded-token to [unblinded-tokens]([token])
            -- would be alright - however SQLite3 foreign key constraints
            -- can't cross databases (and temporary tables are considered to
            -- be in a different database than normal tables).
        )
        """,
        -- Track tokens that we want to remove from the database.  Mainly just
        -- works around the awkward DB-API interface for dealing with deleting
        -- many rows.
        CREATE TEMPORARY TABLE [to-discard] (
            [unblinded-token] text
        )
        """,
    )
    conn.execute(
        """
        -- Track tokens that we want to remove from the [in-use] set.  Similar
        -- to [to-discard].
        CREATE TEMPORARY TABLE [to-reset] (
            [unblinded-token] text
        )
        """,
    )
Loading
Loading full blame...