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 json import (
loads,
dumps,
)
from datetime import (
datetime,
)
from zope.interface import (
Interface,
implementer,
)
from sqlite3 import (
from aniso8601 import (
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,
)
from .storage_common import (
pass_value_attribute,
get_configured_pass_value,
required_passes,
)
from .schema import (
get_schema_version,
get_schema_upgrades,
run_schema_upgrades,
)
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)
# 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)
# Create some tables that only exist (along with their contents) only 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
)
""",
)
def with_cursor(f):
@wraps(f)
def with_cursor(self, *a, **kw):
with self._connection:
cursor = self._connection.cursor()
cursor.execute("BEGIN IMMEDIATE TRANSACTION")
return f(self, cursor, *a, **kw)
return with_cursor
def memory_connect(path, *a, **kw):
"""
Loading
Loading full blame...