|
@@ -1,5 +1,7 @@
|
|
import errno
|
|
import errno
|
|
import fcntl
|
|
import fcntl
|
|
|
|
+import functools
|
|
|
|
+import inspect
|
|
import logging
|
|
import logging
|
|
import os
|
|
import os
|
|
import select
|
|
import select
|
|
@@ -20,8 +22,11 @@ from .helpers import bin_to_hex
|
|
from .helpers import replace_placeholders
|
|
from .helpers import replace_placeholders
|
|
from .helpers import yes
|
|
from .helpers import yes
|
|
from .repository import Repository
|
|
from .repository import Repository
|
|
|
|
+from .version import parse_version, format_version
|
|
|
|
|
|
RPC_PROTOCOL_VERSION = 2
|
|
RPC_PROTOCOL_VERSION = 2
|
|
|
|
+BORG_VERSION = parse_version(__version__)
|
|
|
|
+MSGID, MSG, ARGS, RESULT = b'i', b'm', b'a', b'r'
|
|
|
|
|
|
BUFSIZE = 10 * 1024 * 1024
|
|
BUFSIZE = 10 * 1024 * 1024
|
|
|
|
|
|
@@ -54,6 +59,51 @@ class UnexpectedRPCDataFormatFromServer(Error):
|
|
"""Got unexpected RPC data format from server."""
|
|
"""Got unexpected RPC data format from server."""
|
|
|
|
|
|
|
|
|
|
|
|
+# Protocol compatibility:
|
|
|
|
+# In general the server is responsible for rejecting too old clients and the client it responsible for rejecting
|
|
|
|
+# too old servers. This ensures that the knowledge what is compatible is always held by the newer component.
|
|
|
|
+#
|
|
|
|
+# The server can do checks for the client version in RepositoryServer.negotiate. If the client_data is 2 then
|
|
|
|
+# client is in the version range [0.29.0, 1.0.x] inclusive. For newer clients client_data is a dict which contains
|
|
|
|
+# client_version.
|
|
|
|
+#
|
|
|
|
+# For the client the return of the negotiate method is either 2 if the server is in the version range [0.29.0, 1.0.x]
|
|
|
|
+# inclusive, or it is a dict which includes the server version.
|
|
|
|
+#
|
|
|
|
+# All method calls on the remote repository object must be whitelisted in RepositoryServer.rpc_methods and have api
|
|
|
|
+# stubs in RemoteRepository. The @api decorator on these stubs is used to set server version requirements.
|
|
|
|
+#
|
|
|
|
+# Method parameters are identified only by name and never by position. Unknown parameters are ignored by the server side.
|
|
|
|
+# If a new parameter is important and may not be ignored, on the client a parameter specific version requirement needs
|
|
|
|
+# to be added.
|
|
|
|
+# When parameters are removed, they need to be preserved as defaulted parameters on the client stubs so that older
|
|
|
|
+# servers still get compatible input.
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+compatMap = {
|
|
|
|
+ 'check': ('repair', 'save_space', ),
|
|
|
|
+ 'commit': ('save_space', ),
|
|
|
|
+ 'rollback': (),
|
|
|
|
+ 'destroy': (),
|
|
|
|
+ '__len__': (),
|
|
|
|
+ 'list': ('limit', 'marker', ),
|
|
|
|
+ 'put': ('id', 'data', ),
|
|
|
|
+ 'get': ('id_', ),
|
|
|
|
+ 'delete': ('id', ),
|
|
|
|
+ 'save_key': ('keydata', ),
|
|
|
|
+ 'load_key': (),
|
|
|
|
+ 'break_lock': (),
|
|
|
|
+ 'negotiate': ('client_data', ),
|
|
|
|
+ 'open': ('path', 'create', 'lock_wait', 'lock', 'exclusive', 'append_only', ),
|
|
|
|
+ 'get_free_nonce': (),
|
|
|
|
+ 'commit_nonce_reservation': ('next_unreserved', 'start_nonce', ),
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+def decode_keys(d):
|
|
|
|
+ return {k.decode(): d[k] for k in d}
|
|
|
|
+
|
|
|
|
+
|
|
class RepositoryServer: # pragma: no cover
|
|
class RepositoryServer: # pragma: no cover
|
|
rpc_methods = (
|
|
rpc_methods = (
|
|
'__len__',
|
|
'__len__',
|
|
@@ -79,6 +129,16 @@ class RepositoryServer: # pragma: no cover
|
|
self.repository = None
|
|
self.repository = None
|
|
self.restrict_to_paths = restrict_to_paths
|
|
self.restrict_to_paths = restrict_to_paths
|
|
self.append_only = append_only
|
|
self.append_only = append_only
|
|
|
|
+ self.client_version = parse_version('1.0.8') # fallback version if client is too old to send version information
|
|
|
|
+
|
|
|
|
+ def positional_to_named(self, method, argv):
|
|
|
|
+ """Translate from positional protocol to named protocol."""
|
|
|
|
+ return {name: argv[pos] for pos, name in enumerate(compatMap[method])}
|
|
|
|
+
|
|
|
|
+ def filter_args(self, f, kwargs):
|
|
|
|
+ """Remove unknown named parameters from call, because client did (implicitly) say it's ok."""
|
|
|
|
+ known = set(inspect.signature(f).parameters)
|
|
|
|
+ return {name: kwargs[name] for name in kwargs if name in known}
|
|
|
|
|
|
def serve(self):
|
|
def serve(self):
|
|
stdin_fd = sys.stdin.fileno()
|
|
stdin_fd = sys.stdin.fileno()
|
|
@@ -107,12 +167,20 @@ class RepositoryServer: # pragma: no cover
|
|
return
|
|
return
|
|
unpacker.feed(data)
|
|
unpacker.feed(data)
|
|
for unpacked in unpacker:
|
|
for unpacked in unpacker:
|
|
- if not (isinstance(unpacked, tuple) and len(unpacked) == 4):
|
|
|
|
|
|
+ if isinstance(unpacked, dict):
|
|
|
|
+ dictFormat = True
|
|
|
|
+ msgid = unpacked[MSGID]
|
|
|
|
+ method = unpacked[MSG].decode()
|
|
|
|
+ args = decode_keys(unpacked[ARGS])
|
|
|
|
+ elif isinstance(unpacked, tuple) and len(unpacked) == 4:
|
|
|
|
+ dictFormat = False
|
|
|
|
+ type, msgid, method, args = unpacked
|
|
|
|
+ method = method.decode()
|
|
|
|
+ args = self.positional_to_named(method, args)
|
|
|
|
+ else:
|
|
if self.repository is not None:
|
|
if self.repository is not None:
|
|
self.repository.close()
|
|
self.repository.close()
|
|
raise UnexpectedRPCDataFormatFromClient(__version__)
|
|
raise UnexpectedRPCDataFormatFromClient(__version__)
|
|
- type, msgid, method, args = unpacked
|
|
|
|
- method = method.decode()
|
|
|
|
try:
|
|
try:
|
|
if method not in self.rpc_methods:
|
|
if method not in self.rpc_methods:
|
|
raise InvalidRPCMethod(method)
|
|
raise InvalidRPCMethod(method)
|
|
@@ -120,7 +188,8 @@ class RepositoryServer: # pragma: no cover
|
|
f = getattr(self, method)
|
|
f = getattr(self, method)
|
|
except AttributeError:
|
|
except AttributeError:
|
|
f = getattr(self.repository, method)
|
|
f = getattr(self.repository, method)
|
|
- res = f(*args)
|
|
|
|
|
|
+ args = self.filter_args(f, args)
|
|
|
|
+ res = f(**args)
|
|
except BaseException as e:
|
|
except BaseException as e:
|
|
if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)):
|
|
if isinstance(e, (Repository.DoesNotExist, Repository.AlreadyExists, PathNotAllowed)):
|
|
# These exceptions are reconstructed on the client end in RemoteRepository.call_many(),
|
|
# These exceptions are reconstructed on the client end in RemoteRepository.call_many(),
|
|
@@ -138,18 +207,35 @@ class RepositoryServer: # pragma: no cover
|
|
logging.error(msg)
|
|
logging.error(msg)
|
|
logging.log(tb_log_level, tb)
|
|
logging.log(tb_log_level, tb)
|
|
exc = 'Remote Exception (see remote log for the traceback)'
|
|
exc = 'Remote Exception (see remote log for the traceback)'
|
|
- os.write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc)))
|
|
|
|
|
|
+ if dictFormat:
|
|
|
|
+ os.write(stdout_fd, msgpack.packb({MSGID: msgid, b'exception_class': e.__class__.__name__}))
|
|
|
|
+ else:
|
|
|
|
+ os.write(stdout_fd, msgpack.packb((1, msgid, e.__class__.__name__, exc)))
|
|
else:
|
|
else:
|
|
- os.write(stdout_fd, msgpack.packb((1, msgid, None, res)))
|
|
|
|
|
|
+ if dictFormat:
|
|
|
|
+ os.write(stdout_fd, msgpack.packb({MSGID: msgid, RESULT: res}))
|
|
|
|
+ else:
|
|
|
|
+ os.write(stdout_fd, msgpack.packb((1, msgid, None, res)))
|
|
if es:
|
|
if es:
|
|
self.repository.close()
|
|
self.repository.close()
|
|
return
|
|
return
|
|
|
|
|
|
- def negotiate(self, versions):
|
|
|
|
- return RPC_PROTOCOL_VERSION
|
|
|
|
|
|
+ def negotiate(self, client_data):
|
|
|
|
+ # old format used in 1.0.x
|
|
|
|
+ if client_data == RPC_PROTOCOL_VERSION:
|
|
|
|
+ return RPC_PROTOCOL_VERSION
|
|
|
|
+ # clients since 1.1.0b3 use a dict as client_data
|
|
|
|
+ if isinstance(client_data, dict):
|
|
|
|
+ self.client_version = client_data[b'client_version']
|
|
|
|
+ else:
|
|
|
|
+ self.client_version = BORG_VERSION # seems to be newer than current version (no known old format)
|
|
|
|
+
|
|
|
|
+ # not a known old format, send newest negotiate this version knows
|
|
|
|
+ return {'server_version': BORG_VERSION}
|
|
|
|
|
|
def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False):
|
|
def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False):
|
|
- path = os.fsdecode(path)
|
|
|
|
|
|
+ if isinstance(path, bytes):
|
|
|
|
+ path = os.fsdecode(path)
|
|
if path.startswith('/~'): # /~/x = path x relative to home dir, /~username/x = relative to "user" home dir
|
|
if path.startswith('/~'): # /~/x = path x relative to home dir, /~username/x = relative to "user" home dir
|
|
path = os.path.join(get_home_dir(), path[2:]) # XXX check this (see also 1.0-maint), is it correct for ~u?
|
|
path = os.path.join(get_home_dir(), path[2:]) # XXX check this (see also 1.0-maint), is it correct for ~u?
|
|
elif path.startswith('/./'): # /./x = path x relative to cwd
|
|
elif path.startswith('/./'): # /./x = path x relative to cwd
|
|
@@ -203,6 +289,54 @@ class SleepingBandwidthLimiter:
|
|
return written
|
|
return written
|
|
|
|
|
|
|
|
|
|
|
|
+def api(*, since, **kwargs_decorator):
|
|
|
|
+ """Check version requirements and use self.call to do the remote method call.
|
|
|
|
+
|
|
|
|
+ <since> specifies the version in which borg introduced this method,
|
|
|
|
+ calling this method when connected to an older version will fail without transmiting
|
|
|
|
+ anything to the server.
|
|
|
|
+
|
|
|
|
+ Further kwargs can be used to encode version specific restrictions.
|
|
|
|
+ If a previous hardcoded behaviour is parameterized in a version, this allows calls that
|
|
|
|
+ use the previously hardcoded behaviour to pass through and generates an error if another
|
|
|
|
+ behaviour is requested by the client.
|
|
|
|
+
|
|
|
|
+ e.g. when 'append_only' was introduced in 1.0.7 the previous behaviour was what now is append_only=False.
|
|
|
|
+ Thus @api(..., append_only={'since': parse_version('1.0.7'), 'previously': False}) allows calls
|
|
|
|
+ with append_only=False for all version but rejects calls using append_only=True on versions older than 1.0.7.
|
|
|
|
+ """
|
|
|
|
+ def decorator(f):
|
|
|
|
+ @functools.wraps(f)
|
|
|
|
+ def do_rpc(self, *args, **kwargs):
|
|
|
|
+ sig = inspect.signature(f)
|
|
|
|
+ bound_args = sig.bind(self, *args, **kwargs)
|
|
|
|
+ named = {}
|
|
|
|
+ for name, param in sig.parameters.items():
|
|
|
|
+ if name == 'self':
|
|
|
|
+ continue
|
|
|
|
+ if name in bound_args.arguments:
|
|
|
|
+ named[name] = bound_args.arguments[name]
|
|
|
|
+ else:
|
|
|
|
+ if param.default is not param.empty:
|
|
|
|
+ named[name] = param.default
|
|
|
|
+
|
|
|
|
+ if self.server_version < since:
|
|
|
|
+ raise self.RPCServerOutdated(f.__name__, format_version(since))
|
|
|
|
+
|
|
|
|
+ for name, restriction in kwargs_decorator.items():
|
|
|
|
+ if restriction['since'] <= self.server_version:
|
|
|
|
+ continue
|
|
|
|
+ if 'previously' in restriction and named[name] == restriction['previously']:
|
|
|
|
+ continue
|
|
|
|
+
|
|
|
|
+ raise self.RPCServerOutdated("{0} {1}={2!s}".format(f.__name__, name, named[name]),
|
|
|
|
+ format_version(restriction['since']))
|
|
|
|
+
|
|
|
|
+ return self.call(f.__name__, named)
|
|
|
|
+ return do_rpc
|
|
|
|
+ return decorator
|
|
|
|
+
|
|
|
|
+
|
|
class RemoteRepository:
|
|
class RemoteRepository:
|
|
extra_test_args = []
|
|
extra_test_args = []
|
|
|
|
|
|
@@ -214,6 +348,17 @@ class RemoteRepository:
|
|
class NoAppendOnlyOnServer(Error):
|
|
class NoAppendOnlyOnServer(Error):
|
|
"""Server does not support --append-only."""
|
|
"""Server does not support --append-only."""
|
|
|
|
|
|
|
|
+ class RPCServerOutdated(Error):
|
|
|
|
+ """Borg server is too old for {}. Required version {}"""
|
|
|
|
+
|
|
|
|
+ @property
|
|
|
|
+ def method(self):
|
|
|
|
+ return self.args[0]
|
|
|
|
+
|
|
|
|
+ @property
|
|
|
|
+ def required_version(self):
|
|
|
|
+ return self.args[1]
|
|
|
|
+
|
|
def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False, args=None):
|
|
def __init__(self, location, create=False, exclusive=False, lock_wait=None, lock=True, append_only=False, args=None):
|
|
self.location = self._location = location
|
|
self.location = self._location = location
|
|
self.preload_ids = []
|
|
self.preload_ids = []
|
|
@@ -225,6 +370,8 @@ class RemoteRepository:
|
|
self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0)
|
|
self.ratelimit = SleepingBandwidthLimiter(args.remote_ratelimit * 1024 if args and args.remote_ratelimit else 0)
|
|
|
|
|
|
self.unpacker = msgpack.Unpacker(use_list=False)
|
|
self.unpacker = msgpack.Unpacker(use_list=False)
|
|
|
|
+ self.dictFormat = False
|
|
|
|
+ self.server_version = parse_version('1.0.8') # fallback version if server is too old to send version information
|
|
self.p = None
|
|
self.p = None
|
|
testing = location.host == '__testsuite__'
|
|
testing = location.host == '__testsuite__'
|
|
borg_cmd = self.borg_cmd(args, testing)
|
|
borg_cmd = self.borg_cmd(args, testing)
|
|
@@ -254,15 +401,22 @@ class RemoteRepository:
|
|
|
|
|
|
try:
|
|
try:
|
|
try:
|
|
try:
|
|
- version = self.call('negotiate', RPC_PROTOCOL_VERSION)
|
|
|
|
|
|
+ version = self.call('negotiate', {'client_data': {b'client_version': BORG_VERSION}})
|
|
except ConnectionClosed:
|
|
except ConnectionClosed:
|
|
raise ConnectionClosedWithHint('Is borg working on the server?') from None
|
|
raise ConnectionClosedWithHint('Is borg working on the server?') from None
|
|
- if version != RPC_PROTOCOL_VERSION:
|
|
|
|
- raise Exception('Server insisted on using unsupported protocol version %d' % version)
|
|
|
|
|
|
+ if version == RPC_PROTOCOL_VERSION:
|
|
|
|
+ self.dictFormat = False
|
|
|
|
+ elif isinstance(version, dict) and b'server_version' in version:
|
|
|
|
+ self.dictFormat = True
|
|
|
|
+ self.server_version = version[b'server_version']
|
|
|
|
+ else:
|
|
|
|
+ raise Exception('Server insisted on using unsupported protocol version %s' % version)
|
|
|
|
+
|
|
try:
|
|
try:
|
|
- self.id = self.call('open', self.location.path, create, lock_wait, lock, exclusive, append_only)
|
|
|
|
|
|
+ self.id = self.call('open', {'path': self.location.path, 'create': create, 'lock_wait': lock_wait,
|
|
|
|
+ 'lock': lock, 'exclusive': exclusive, 'append_only': append_only})
|
|
except self.RPCError as err:
|
|
except self.RPCError as err:
|
|
- if err.remote_type != 'TypeError':
|
|
|
|
|
|
+ if self.dictFormat or err.remote_type != 'TypeError':
|
|
raise
|
|
raise
|
|
msg = """\
|
|
msg = """\
|
|
Please note:
|
|
Please note:
|
|
@@ -276,7 +430,9 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
|
|
sys.stderr.write(msg)
|
|
sys.stderr.write(msg)
|
|
if append_only:
|
|
if append_only:
|
|
raise self.NoAppendOnlyOnServer()
|
|
raise self.NoAppendOnlyOnServer()
|
|
- self.id = self.call('open', self.location.path, create, lock_wait, lock)
|
|
|
|
|
|
+ compatMap['open'] = ('path', 'create', 'lock_wait', 'lock', )
|
|
|
|
+ self.id = self.call('open', {'path': self.location.path, 'create': create, 'lock_wait': lock_wait,
|
|
|
|
+ 'lock': lock, 'exclusive': exclusive, 'append_only': append_only})
|
|
except Exception:
|
|
except Exception:
|
|
self.close()
|
|
self.close()
|
|
raise
|
|
raise
|
|
@@ -348,7 +504,10 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
|
|
args.append('%s' % location.host)
|
|
args.append('%s' % location.host)
|
|
return args
|
|
return args
|
|
|
|
|
|
- def call(self, cmd, *args, **kw):
|
|
|
|
|
|
+ def named_to_positional(self, method, kwargs):
|
|
|
|
+ return [kwargs[name] for name in compatMap[method]]
|
|
|
|
+
|
|
|
|
+ def call(self, cmd, args, **kw):
|
|
for resp in self.call_many(cmd, [args], **kw):
|
|
for resp in self.call_many(cmd, [args], **kw):
|
|
return resp
|
|
return resp
|
|
|
|
|
|
@@ -386,12 +545,12 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
|
|
while wait or calls:
|
|
while wait or calls:
|
|
while waiting_for:
|
|
while waiting_for:
|
|
try:
|
|
try:
|
|
- error, res = self.responses.pop(waiting_for[0])
|
|
|
|
|
|
+ unpacked = self.responses.pop(waiting_for[0])
|
|
waiting_for.pop(0)
|
|
waiting_for.pop(0)
|
|
- if error:
|
|
|
|
- handle_error(error, res)
|
|
|
|
|
|
+ if b'exception_class' in unpacked:
|
|
|
|
+ handle_error(unpacked[b'exception_class'], None)
|
|
else:
|
|
else:
|
|
- yield res
|
|
|
|
|
|
+ yield unpacked[RESULT]
|
|
if not waiting_for and not calls:
|
|
if not waiting_for and not calls:
|
|
return
|
|
return
|
|
except KeyError:
|
|
except KeyError:
|
|
@@ -410,15 +569,22 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
|
|
raise ConnectionClosed()
|
|
raise ConnectionClosed()
|
|
self.unpacker.feed(data)
|
|
self.unpacker.feed(data)
|
|
for unpacked in self.unpacker:
|
|
for unpacked in self.unpacker:
|
|
- if not (isinstance(unpacked, tuple) and len(unpacked) == 4):
|
|
|
|
|
|
+ if isinstance(unpacked, dict):
|
|
|
|
+ msgid = unpacked[MSGID]
|
|
|
|
+ elif isinstance(unpacked, tuple) and len(unpacked) == 4:
|
|
|
|
+ type, msgid, error, res = unpacked
|
|
|
|
+ if error:
|
|
|
|
+ unpacked = {MSGID: msgid, b'exception_class': error}
|
|
|
|
+ else:
|
|
|
|
+ unpacked = {MSGID: msgid, RESULT: res}
|
|
|
|
+ else:
|
|
raise UnexpectedRPCDataFormatFromServer()
|
|
raise UnexpectedRPCDataFormatFromServer()
|
|
- type, msgid, error, res = unpacked
|
|
|
|
if msgid in self.ignore_responses:
|
|
if msgid in self.ignore_responses:
|
|
self.ignore_responses.remove(msgid)
|
|
self.ignore_responses.remove(msgid)
|
|
- if error:
|
|
|
|
- handle_error(error, res)
|
|
|
|
|
|
+ if b'exception_class' in unpacked:
|
|
|
|
+ handle_error(unpacked[b'exception_class'], None)
|
|
else:
|
|
else:
|
|
- self.responses[msgid] = error, res
|
|
|
|
|
|
+ self.responses[msgid] = unpacked
|
|
elif fd is self.stderr_fd:
|
|
elif fd is self.stderr_fd:
|
|
data = os.read(fd, 32768)
|
|
data = os.read(fd, 32768)
|
|
if not data:
|
|
if not data:
|
|
@@ -431,22 +597,28 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
|
|
if calls:
|
|
if calls:
|
|
if is_preloaded:
|
|
if is_preloaded:
|
|
assert cmd == 'get', "is_preload is only supported for 'get'"
|
|
assert cmd == 'get', "is_preload is only supported for 'get'"
|
|
- if calls[0][0] in self.chunkid_to_msgids:
|
|
|
|
- waiting_for.append(pop_preload_msgid(calls.pop(0)[0]))
|
|
|
|
|
|
+ if calls[0]['id_'] in self.chunkid_to_msgids:
|
|
|
|
+ waiting_for.append(pop_preload_msgid(calls.pop(0)['id_']))
|
|
else:
|
|
else:
|
|
args = calls.pop(0)
|
|
args = calls.pop(0)
|
|
- if cmd == 'get' and args[0] in self.chunkid_to_msgids:
|
|
|
|
- waiting_for.append(pop_preload_msgid(args[0]))
|
|
|
|
|
|
+ if cmd == 'get' and args['id_'] in self.chunkid_to_msgids:
|
|
|
|
+ waiting_for.append(pop_preload_msgid(args['id_']))
|
|
else:
|
|
else:
|
|
self.msgid += 1
|
|
self.msgid += 1
|
|
waiting_for.append(self.msgid)
|
|
waiting_for.append(self.msgid)
|
|
- self.to_send = msgpack.packb((1, self.msgid, cmd, args))
|
|
|
|
|
|
+ if self.dictFormat:
|
|
|
|
+ self.to_send = msgpack.packb({MSGID: self.msgid, MSG: cmd, ARGS: args})
|
|
|
|
+ else:
|
|
|
|
+ self.to_send = msgpack.packb((1, self.msgid, cmd, self.named_to_positional(cmd, args)))
|
|
if not self.to_send and self.preload_ids:
|
|
if not self.to_send and self.preload_ids:
|
|
chunk_id = self.preload_ids.pop(0)
|
|
chunk_id = self.preload_ids.pop(0)
|
|
- args = (chunk_id,)
|
|
|
|
|
|
+ args = {'id_': chunk_id}
|
|
self.msgid += 1
|
|
self.msgid += 1
|
|
self.chunkid_to_msgids.setdefault(chunk_id, []).append(self.msgid)
|
|
self.chunkid_to_msgids.setdefault(chunk_id, []).append(self.msgid)
|
|
- self.to_send = msgpack.packb((1, self.msgid, 'get', args))
|
|
|
|
|
|
+ if self.dictFormat:
|
|
|
|
+ self.to_send = msgpack.packb({MSGID: self.msgid, MSG: 'get', ARGS: args})
|
|
|
|
+ else:
|
|
|
|
+ self.to_send = msgpack.packb((1, self.msgid, 'get', self.named_to_positional(cmd, args)))
|
|
|
|
|
|
if self.to_send:
|
|
if self.to_send:
|
|
try:
|
|
try:
|
|
@@ -458,55 +630,69 @@ This problem will go away as soon as the server has been upgraded to 1.0.7+.
|
|
raise
|
|
raise
|
|
self.ignore_responses |= set(waiting_for)
|
|
self.ignore_responses |= set(waiting_for)
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def check(self, repair=False, save_space=False):
|
|
def check(self, repair=False, save_space=False):
|
|
- return self.call('check', repair, save_space)
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def commit(self, save_space=False):
|
|
def commit(self, save_space=False):
|
|
- return self.call('commit', save_space)
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
- def rollback(self, *args):
|
|
|
|
- return self.call('rollback')
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
|
|
+ def rollback(self):
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def destroy(self):
|
|
def destroy(self):
|
|
- return self.call('destroy')
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def __len__(self):
|
|
def __len__(self):
|
|
- return self.call('__len__')
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def list(self, limit=None, marker=None):
|
|
def list(self, limit=None, marker=None):
|
|
- return self.call('list', limit, marker)
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.1.0b3'))
|
|
def scan(self, limit=None, marker=None):
|
|
def scan(self, limit=None, marker=None):
|
|
- return self.call('scan', limit, marker)
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
def get(self, id_):
|
|
def get(self, id_):
|
|
for resp in self.get_many([id_]):
|
|
for resp in self.get_many([id_]):
|
|
return resp
|
|
return resp
|
|
|
|
|
|
def get_many(self, ids, is_preloaded=False):
|
|
def get_many(self, ids, is_preloaded=False):
|
|
- for resp in self.call_many('get', [(id_,) for id_ in ids], is_preloaded=is_preloaded):
|
|
|
|
|
|
+ for resp in self.call_many('get', [{'id_': id_} for id_ in ids], is_preloaded=is_preloaded):
|
|
yield resp
|
|
yield resp
|
|
|
|
|
|
- def put(self, id_, data, wait=True):
|
|
|
|
- return self.call('put', id_, data, wait=wait)
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
|
|
+ def put(self, id, data, wait=True):
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
- def delete(self, id_, wait=True):
|
|
|
|
- return self.call('delete', id_, wait=wait)
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
|
|
+ def delete(self, id, wait=True):
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def save_key(self, keydata):
|
|
def save_key(self, keydata):
|
|
- return self.call('save_key', keydata)
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def load_key(self):
|
|
def load_key(self):
|
|
- return self.call('load_key')
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def get_free_nonce(self):
|
|
def get_free_nonce(self):
|
|
- return self.call('get_free_nonce')
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def commit_nonce_reservation(self, next_unreserved, start_nonce):
|
|
def commit_nonce_reservation(self, next_unreserved, start_nonce):
|
|
- return self.call('commit_nonce_reservation', next_unreserved, start_nonce)
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
|
|
+ @api(since=parse_version('1.0.0'))
|
|
def break_lock(self):
|
|
def break_lock(self):
|
|
- return self.call('break_lock')
|
|
|
|
|
|
+ """actual remoting is done via self.call in the @api decorator"""
|
|
|
|
|
|
def close(self):
|
|
def close(self):
|
|
if self.p:
|
|
if self.p:
|