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 json import loads
from sqlite3 import OperationalError
from sqlite3 import connect as _connect
from aniso8601 import parse_datetime
from twisted.logger import Logger
from twisted.python.filepath import FilePath
from zope.interface import Interface, implementer
from ._json import dumps_utf8
from .schema import get_schema_upgrades, get_schema_version, run_schema_upgrades
from .storage_common import (
get_configured_pass_value,
required_passes,
)
from .validators import greater_than, has_length, is_base64_encoded
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.
"""
Jean-Paul Calderone
committed
# The version number in _zkapauthorizer.api.NAME doesn't match the version
# here because the database is persistent state and we need to be sure to load
# the older version even if we signal an API compatibility break by bumping
# the version number elsewhere. Consider this version number part of a
# different scheme where we're versioning our ability to open the database at
# all. The schema inside the database is versioned by yet another mechanism.
CONFIG_DB_NAME = "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)
try:
conn = connect(
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) 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
)
""",
)
return conn
def with_cursor(f):
"""
Decorate a function so it is automatically passed a cursor with an active
transaction as the first positional argument. If the function returns
normally then the transaction will be committed. Otherwise, the
transaction will be rolled back.
"""
@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):
"""
Always connect to an in-memory SQLite3 database.
"""
return _connect(":memory:", *a, **kw)
# The largest integer SQLite3 can represent in an integer column. Larger than
# this an the representation loses precision as a floating point.
_SQLITE3_INTEGER_MAX = 2 ** 63 - 1
@attr.s(frozen=True)
class VoucherStore(object):
This class implements persistence for vouchers.
:ivar allmydata.node._Config node_config: The Tahoe-LAFS node configuration object for
the node that owns the persisted vouchers.
Loading
Loading full blame...