Remove contrib.multistore_file (#589)
Remove dependent modules as well. Resolves #470.
This commit is contained in:
committed by
GitHub
parent
f04d5213d4
commit
0dc30bc033
@@ -1,7 +0,0 @@
|
||||
oauth2client.contrib.locked_file module
|
||||
=======================================
|
||||
|
||||
.. automodule:: oauth2client.contrib.locked_file
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -1,7 +0,0 @@
|
||||
oauth2client.contrib.multistore_file module
|
||||
===========================================
|
||||
|
||||
.. automodule:: oauth2client.contrib.multistore_file
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
@@ -19,9 +19,7 @@ Submodules
|
||||
oauth2client.contrib.flask_util
|
||||
oauth2client.contrib.gce
|
||||
oauth2client.contrib.keyring_storage
|
||||
oauth2client.contrib.locked_file
|
||||
oauth2client.contrib.multiprocess_file_storage
|
||||
oauth2client.contrib.multistore_file
|
||||
oauth2client.contrib.sqlalchemy
|
||||
oauth2client.contrib.xsrfutil
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import errno
|
||||
import fcntl
|
||||
import time
|
||||
|
||||
from oauth2client import util
|
||||
from oauth2client.contrib import locked_file
|
||||
|
||||
|
||||
class _FcntlOpener(locked_file._Opener):
|
||||
"""Open, lock, and unlock a file using fcntl.lockf."""
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
IOError: if the file is a symbolic link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise locked_file.AlreadyLockedException(
|
||||
'File {0} is already locked'.format(self._filename))
|
||||
start_time = time.time()
|
||||
|
||||
util.validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode and
|
||||
# don't lock.
|
||||
if e.errno in (errno.EPERM, errno.EACCES):
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
# We opened in _mode, try to lock the file.
|
||||
while True:
|
||||
try:
|
||||
fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
|
||||
self._locked = True
|
||||
return
|
||||
except IOError as e:
|
||||
# If not retrying, then just pass on the error.
|
||||
if timeout == 0:
|
||||
raise
|
||||
if e.errno != errno.EACCES:
|
||||
raise
|
||||
# We could not acquire the lock. Try again.
|
||||
if (time.time() - start_time) >= timeout:
|
||||
locked_file.logger.warn('Could not lock %s in %s seconds',
|
||||
self._filename, timeout)
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Close and unlock the file using the fcntl.lockf primitive."""
|
||||
if self._locked:
|
||||
fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
|
||||
self._locked = False
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
@@ -1,106 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import errno
|
||||
import time
|
||||
|
||||
import pywintypes
|
||||
import win32con
|
||||
import win32file
|
||||
|
||||
from oauth2client import util
|
||||
from oauth2client.contrib import locked_file
|
||||
|
||||
|
||||
class _Win32Opener(locked_file._Opener):
|
||||
"""Open, lock, and unlock a file using windows primitives."""
|
||||
|
||||
# Error #33:
|
||||
# 'The process cannot access the file because another process'
|
||||
FILE_IN_USE_ERROR = 33
|
||||
|
||||
# Error #158:
|
||||
# 'The segment is already unlocked.'
|
||||
FILE_ALREADY_UNLOCKED_ERROR = 158
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
IOError: if the file is a symbolic link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise locked_file.AlreadyLockedException(
|
||||
'File {0} is already locked'.format(self._filename))
|
||||
start_time = time.time()
|
||||
|
||||
util.validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode
|
||||
# and don't lock.
|
||||
if e.errno == errno.EACCES:
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
# We opened in _mode, try to lock the file.
|
||||
while True:
|
||||
try:
|
||||
hfile = win32file._get_osfhandle(self._fh.fileno())
|
||||
win32file.LockFileEx(
|
||||
hfile,
|
||||
(win32con.LOCKFILE_FAIL_IMMEDIATELY |
|
||||
win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
|
||||
pywintypes.OVERLAPPED())
|
||||
self._locked = True
|
||||
return
|
||||
except pywintypes.error as e:
|
||||
if timeout == 0:
|
||||
raise
|
||||
|
||||
# If the error is not that the file is already
|
||||
# in use, raise.
|
||||
if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
|
||||
raise
|
||||
|
||||
# We could not acquire the lock. Try again.
|
||||
if (time.time() - start_time) >= timeout:
|
||||
locked_file.logger.warn('Could not lock %s in %s seconds',
|
||||
self._filename, timeout)
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Close and unlock the file using the win32 primitive."""
|
||||
if self._locked:
|
||||
try:
|
||||
hfile = win32file._get_osfhandle(self._fh.fileno())
|
||||
win32file.UnlockFileEx(hfile, 0, -0x10000,
|
||||
pywintypes.OVERLAPPED())
|
||||
except pywintypes.error as e:
|
||||
if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
|
||||
raise
|
||||
self._locked = False
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
@@ -1,224 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Locked file interface that should work on Unix and Windows pythons.
|
||||
|
||||
This module first tries to use fcntl locking to ensure serialized access
|
||||
to a file, then falls back on a lock file if that is unavialable.
|
||||
|
||||
Usage::
|
||||
|
||||
f = LockedFile('filename', 'r+b', 'rb')
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
print('Acquired filename with r+b mode')
|
||||
f.file_handle().write('locked data')
|
||||
else:
|
||||
print('Acquired filename with rb mode')
|
||||
f.unlock_and_close()
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
__author__ = 'cache@google.com (David T McWherter)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AlreadyLockedException(Exception):
|
||||
"""Trying to lock a file that has already been locked by the LockedFile."""
|
||||
pass
|
||||
|
||||
|
||||
class _Opener(object):
|
||||
"""Base class for different locking primitives."""
|
||||
|
||||
def __init__(self, filename, mode, fallback_mode):
|
||||
"""Create an Opener.
|
||||
|
||||
Args:
|
||||
filename: string, The pathname of the file.
|
||||
mode: string, The preferred mode to access the file with.
|
||||
fallback_mode: string, The mode to use if locking fails.
|
||||
"""
|
||||
self._locked = False
|
||||
self._filename = filename
|
||||
self._mode = mode
|
||||
self._fallback_mode = fallback_mode
|
||||
self._fh = None
|
||||
self._lock_fd = None
|
||||
|
||||
def is_locked(self):
|
||||
"""Was the file locked."""
|
||||
return self._locked
|
||||
|
||||
def file_handle(self):
|
||||
"""The file handle to the file. Valid only after opened."""
|
||||
return self._fh
|
||||
|
||||
def filename(self):
|
||||
"""The filename that is being locked."""
|
||||
return self._filename
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries.
|
||||
"""
|
||||
pass
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock and close the file."""
|
||||
pass
|
||||
|
||||
|
||||
class _PosixOpener(_Opener):
|
||||
"""Lock files using Posix advisory lock files."""
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Tries to create a .lock file next to the file we're trying to open.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries.
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
IOError: if the file is a symbolic link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise AlreadyLockedException(
|
||||
'File {0} is already locked'.format(self._filename))
|
||||
self._locked = False
|
||||
|
||||
util.validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode and don't lock.
|
||||
if e.errno == errno.EACCES:
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
lock_filename = self._posix_lockfile(self._filename)
|
||||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
self._lock_fd = os.open(lock_filename,
|
||||
os.O_CREAT | os.O_EXCL | os.O_RDWR)
|
||||
self._locked = True
|
||||
break
|
||||
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
if (time.time() - start_time) >= timeout:
|
||||
logger.warn('Could not acquire lock %s in %s seconds',
|
||||
lock_filename, timeout)
|
||||
# Close the file and open in fallback_mode.
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock a file by removing the .lock file, and close the handle."""
|
||||
if self._locked:
|
||||
lock_filename = self._posix_lockfile(self._filename)
|
||||
os.close(self._lock_fd)
|
||||
os.unlink(lock_filename)
|
||||
self._locked = False
|
||||
self._lock_fd = None
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
|
||||
def _posix_lockfile(self, filename):
|
||||
"""The name of the lock file to use for posix locking."""
|
||||
return '{0}.lock'.format(filename)
|
||||
|
||||
|
||||
class LockedFile(object):
|
||||
"""Represent a file that has exclusive access."""
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
|
||||
"""Construct a LockedFile.
|
||||
|
||||
Args:
|
||||
filename: string, The path of the file to open.
|
||||
mode: string, The mode to try to open the file with.
|
||||
fallback_mode: string, The mode to use if locking fails.
|
||||
use_native_locking: bool, Whether or not fcntl/win32 locking is
|
||||
used.
|
||||
"""
|
||||
opener = None
|
||||
if not opener and use_native_locking:
|
||||
try:
|
||||
from oauth2client.contrib._win32_opener import _Win32Opener
|
||||
opener = _Win32Opener(filename, mode, fallback_mode)
|
||||
except ImportError:
|
||||
try:
|
||||
from oauth2client.contrib._fcntl_opener import _FcntlOpener
|
||||
opener = _FcntlOpener(filename, mode, fallback_mode)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if not opener:
|
||||
opener = _PosixOpener(filename, mode, fallback_mode)
|
||||
|
||||
self._opener = opener
|
||||
|
||||
def filename(self):
|
||||
"""Return the filename we were constructed with."""
|
||||
return self._opener._filename
|
||||
|
||||
def file_handle(self):
|
||||
"""Return the file_handle to the opened file."""
|
||||
return self._opener.file_handle()
|
||||
|
||||
def is_locked(self):
|
||||
"""Return whether we successfully locked the file."""
|
||||
return self._opener.is_locked()
|
||||
|
||||
def open_and_lock(self, timeout=0, delay=0.05):
|
||||
"""Open the file, trying to lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, The number of seconds to try to acquire the lock.
|
||||
delay: float, The number of seconds to wait between retry attempts.
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
"""
|
||||
self._opener.open_and_lock(timeout, delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock and close a file."""
|
||||
self._opener.unlock_and_close()
|
||||
@@ -1,505 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Multi-credential file store with lock support.
|
||||
|
||||
This module implements a JSON credential store where multiple
|
||||
credentials can be stored in one file. That file supports locking
|
||||
both in a single process and across processes.
|
||||
|
||||
The credential themselves are keyed off of:
|
||||
|
||||
* client_id
|
||||
* user_agent
|
||||
* scope
|
||||
|
||||
The format of the stored data is like so::
|
||||
|
||||
{
|
||||
'file_version': 1,
|
||||
'data': [
|
||||
{
|
||||
'key': {
|
||||
'clientId': '<client id>',
|
||||
'userAgent': '<user agent>',
|
||||
'scope': '<scope>'
|
||||
},
|
||||
'credential': {
|
||||
# JSON serialized Credentials.
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
import errno
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
from oauth2client.contrib import locked_file
|
||||
|
||||
__author__ = 'jbeda@google.com (Joe Beda)'
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.warning(
|
||||
'The oauth2client.contrib.multistore_file module has been deprecated and '
|
||||
'will be removed in the next release of oauth2client. Please migrate to '
|
||||
'multiprocess_file_storage.')
|
||||
|
||||
# A dict from 'filename'->_MultiStore instances
|
||||
_multistores = {}
|
||||
_multistores_lock = threading.Lock()
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
|
||||
|
||||
class NewerCredentialStoreError(Error):
|
||||
"""The credential store is a newer version than supported."""
|
||||
|
||||
|
||||
def _dict_to_tuple_key(dictionary):
|
||||
"""Converts a dictionary to a tuple that can be used as an immutable key.
|
||||
|
||||
The resulting key is always sorted so that logically equivalent
|
||||
dictionaries always produce an identical tuple for a key.
|
||||
|
||||
Args:
|
||||
dictionary: the dictionary to use as the key.
|
||||
|
||||
Returns:
|
||||
A tuple representing the dictionary in it's naturally sorted ordering.
|
||||
"""
|
||||
return tuple(sorted(dictionary.items()))
|
||||
|
||||
|
||||
@util.positional(4)
|
||||
def get_credential_storage(filename, client_id, user_agent, scope,
|
||||
warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
client_id: The client_id for the credential
|
||||
user_agent: The user agent for the credential
|
||||
scope: string or iterable of strings, Scope(s) being requested
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
# Recreate the legacy key with these specific parameters
|
||||
key = {'clientId': client_id, 'userAgent': user_agent,
|
||||
'scope': util.scopes_to_string(scope)}
|
||||
return get_credential_storage_custom_key(
|
||||
filename, key, warn_on_readonly=warn_on_readonly)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def get_credential_storage_custom_string_key(filename, key_string,
|
||||
warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential using a single string as a key.
|
||||
|
||||
Allows you to provide a string as a custom key that will be used for
|
||||
credential storage and retrieval.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
key_string: A string to use as the key for storing this credential.
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
# Create a key dictionary that can be used
|
||||
key_dict = {'key': key_string}
|
||||
return get_credential_storage_custom_key(
|
||||
filename, key_dict, warn_on_readonly=warn_on_readonly)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def get_credential_storage_custom_key(filename, key_dict,
|
||||
warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential using a dictionary as a key.
|
||||
|
||||
Allows you to provide a dictionary as a custom key that will be used for
|
||||
credential storage and retrieval.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
key_dict: A dictionary to use as the key for storing this credential.
|
||||
There is no ordering of the keys in the dictionary. Logically
|
||||
equivalent dictionaries will produce equivalent storage keys.
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
|
||||
key = _dict_to_tuple_key(key_dict)
|
||||
return multistore._get_storage(key)
|
||||
|
||||
|
||||
@util.positional(1)
|
||||
def get_all_credential_keys(filename, warn_on_readonly=True):
|
||||
"""Gets all the registered credential keys in the given Multistore.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
A list of the credential keys present in the file. They are returned
|
||||
as dictionaries that can be passed into
|
||||
get_credential_storage_custom_key to get the actual credentials.
|
||||
"""
|
||||
multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
|
||||
multistore._lock()
|
||||
try:
|
||||
return multistore._get_all_credential_keys()
|
||||
finally:
|
||||
multistore._unlock()
|
||||
|
||||
|
||||
@util.positional(1)
|
||||
def _get_multistore(filename, warn_on_readonly=True):
|
||||
"""A helper method to initialize the multistore with proper locking.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
A multistore object
|
||||
"""
|
||||
filename = os.path.expanduser(filename)
|
||||
_multistores_lock.acquire()
|
||||
try:
|
||||
multistore = _multistores.setdefault(
|
||||
filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
|
||||
finally:
|
||||
_multistores_lock.release()
|
||||
return multistore
|
||||
|
||||
|
||||
class _MultiStore(object):
|
||||
"""A file backed store for multiple credentials."""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, filename, warn_on_readonly=True):
|
||||
"""Initialize the class.
|
||||
|
||||
This will create the file if necessary.
|
||||
"""
|
||||
self._file = locked_file.LockedFile(filename, 'r+', 'r')
|
||||
self._thread_lock = threading.Lock()
|
||||
self._read_only = False
|
||||
self._warn_on_readonly = warn_on_readonly
|
||||
|
||||
self._create_file_if_needed()
|
||||
|
||||
# Cache of deserialized store. This is only valid after the
|
||||
# _MultiStore is locked or _refresh_data_cache is called. This is
|
||||
# of the form of:
|
||||
#
|
||||
# ((key, value), (key, value)...) -> OAuth2Credential
|
||||
#
|
||||
# If this is None, then the store hasn't been read yet.
|
||||
self._data = None
|
||||
|
||||
class _Storage(client.Storage):
|
||||
"""A Storage object that can read/write a single credential."""
|
||||
|
||||
def __init__(self, multistore, key):
|
||||
self._multistore = multistore
|
||||
self._key = key
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquires any lock necessary to access this Storage.
|
||||
|
||||
This lock is not reentrant.
|
||||
"""
|
||||
self._multistore._lock()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the Storage lock.
|
||||
|
||||
Trying to release a lock that isn't held will result in a
|
||||
RuntimeError.
|
||||
"""
|
||||
self._multistore._unlock()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
"""
|
||||
credential = self._multistore._get_credential(self._key)
|
||||
if credential:
|
||||
credential.set_store(self)
|
||||
return credential
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write a credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
self._multistore._update_credential(self._key, credentials)
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete a credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
self._multistore._delete_credential(self._key)
|
||||
|
||||
def _create_file_if_needed(self):
|
||||
"""Create an empty file if necessary.
|
||||
|
||||
This method will not initialize the file. Instead it implements a
|
||||
simple version of "touch" to ensure the file has been created.
|
||||
"""
|
||||
if not os.path.exists(self._file.filename()):
|
||||
old_umask = os.umask(0o177)
|
||||
try:
|
||||
open(self._file.filename(), 'a+b').close()
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
def _lock(self):
|
||||
"""Lock the entire multistore."""
|
||||
self._thread_lock.acquire()
|
||||
try:
|
||||
self._file.open_and_lock()
|
||||
except (IOError, OSError) as e:
|
||||
if e.errno == errno.ENOSYS:
|
||||
logger.warn('File system does not support locking the '
|
||||
'credentials file.')
|
||||
elif e.errno == errno.ENOLCK:
|
||||
logger.warn('File system is out of resources for writing the '
|
||||
'credentials file (is your disk full?).')
|
||||
elif e.errno == errno.EDEADLK:
|
||||
logger.warn('Lock contention on multistore file, opening '
|
||||
'in read-only mode.')
|
||||
elif e.errno == errno.EACCES:
|
||||
logger.warn('Cannot access credentials file.')
|
||||
else:
|
||||
raise
|
||||
if not self._file.is_locked():
|
||||
self._read_only = True
|
||||
if self._warn_on_readonly:
|
||||
logger.warn('The credentials file (%s) is not writable. '
|
||||
'Opening in read-only mode. Any refreshed '
|
||||
'credentials will only be '
|
||||
'valid for this run.', self._file.filename())
|
||||
|
||||
if os.path.getsize(self._file.filename()) == 0:
|
||||
logger.debug('Initializing empty multistore file')
|
||||
# The multistore is empty so write out an empty file.
|
||||
self._data = {}
|
||||
self._write()
|
||||
elif not self._read_only or self._data is None:
|
||||
# Only refresh the data if we are read/write or we haven't
|
||||
# cached the data yet. If we are readonly, we assume is isn't
|
||||
# changing out from under us and that we only have to read it
|
||||
# once. This prevents us from whacking any new access keys that
|
||||
# we have cached in memory but were unable to write out.
|
||||
self._refresh_data_cache()
|
||||
|
||||
def _unlock(self):
|
||||
"""Release the lock on the multistore."""
|
||||
self._file.unlock_and_close()
|
||||
self._thread_lock.release()
|
||||
|
||||
def _locked_json_read(self):
|
||||
"""Get the raw content of the multistore file.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Returns:
|
||||
The contents of the multistore decoded as JSON.
|
||||
"""
|
||||
assert self._thread_lock.locked()
|
||||
self._file.file_handle().seek(0)
|
||||
return json.load(self._file.file_handle())
|
||||
|
||||
def _locked_json_write(self, data):
|
||||
"""Write a JSON serializable data structure to the multistore.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Args:
|
||||
data: The data to be serialized and written.
|
||||
"""
|
||||
assert self._thread_lock.locked()
|
||||
if self._read_only:
|
||||
return
|
||||
self._file.file_handle().seek(0)
|
||||
json.dump(data, self._file.file_handle(),
|
||||
sort_keys=True, indent=2, separators=(',', ': '))
|
||||
self._file.file_handle().truncate()
|
||||
|
||||
def _refresh_data_cache(self):
|
||||
"""Refresh the contents of the multistore.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Raises:
|
||||
NewerCredentialStoreError: Raised when a newer client has written
|
||||
the store.
|
||||
"""
|
||||
self._data = {}
|
||||
try:
|
||||
raw_data = self._locked_json_read()
|
||||
except Exception:
|
||||
logger.warn('Credential data store could not be loaded. '
|
||||
'Will ignore and overwrite.')
|
||||
return
|
||||
|
||||
version = 0
|
||||
try:
|
||||
version = raw_data['file_version']
|
||||
except Exception:
|
||||
logger.warn('Missing version for credential data store. It may be '
|
||||
'corrupt or an old version. Overwriting.')
|
||||
if version > 1:
|
||||
raise NewerCredentialStoreError(
|
||||
'Credential file has file_version of {0}. '
|
||||
'Only file_version of 1 is supported.'.format(version))
|
||||
|
||||
credentials = []
|
||||
try:
|
||||
credentials = raw_data['data']
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
|
||||
for cred_entry in credentials:
|
||||
try:
|
||||
key, credential = self._decode_credential_from_json(cred_entry)
|
||||
self._data[key] = credential
|
||||
except:
|
||||
# If something goes wrong loading a credential, just ignore it
|
||||
logger.info('Error decoding credential, skipping',
|
||||
exc_info=True)
|
||||
|
||||
def _decode_credential_from_json(self, cred_entry):
|
||||
"""Load a credential from our JSON serialization.
|
||||
|
||||
Args:
|
||||
cred_entry: A dict entry from the data member of our format
|
||||
|
||||
Returns:
|
||||
(key, cred) where the key is the key tuple and the cred is the
|
||||
OAuth2Credential object.
|
||||
"""
|
||||
raw_key = cred_entry['key']
|
||||
key = _dict_to_tuple_key(raw_key)
|
||||
credential = None
|
||||
credential = client.Credentials.new_from_json(
|
||||
json.dumps(cred_entry['credential']))
|
||||
return (key, credential)
|
||||
|
||||
def _write(self):
|
||||
"""Write the cached data back out.
|
||||
|
||||
The multistore must be locked.
|
||||
"""
|
||||
raw_data = {'file_version': 1}
|
||||
raw_creds = []
|
||||
raw_data['data'] = raw_creds
|
||||
for (cred_key, cred) in self._data.items():
|
||||
raw_key = dict(cred_key)
|
||||
raw_cred = json.loads(cred.to_json())
|
||||
raw_creds.append({'key': raw_key, 'credential': raw_cred})
|
||||
self._locked_json_write(raw_data)
|
||||
|
||||
def _get_all_credential_keys(self):
|
||||
"""Gets all the registered credential keys in the multistore.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries corresponding to all the keys currently
|
||||
registered
|
||||
"""
|
||||
return [dict(key) for key in self._data.keys()]
|
||||
|
||||
def _get_credential(self, key):
|
||||
"""Get a credential from the multistore.
|
||||
|
||||
The multistore must be locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
|
||||
Returns:
|
||||
The credential specified or None if not present
|
||||
"""
|
||||
return self._data.get(key, None)
|
||||
|
||||
def _update_credential(self, key, cred):
|
||||
"""Update a credential and write the multistore.
|
||||
|
||||
This must be called when the multistore is locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
cred: The OAuth2Credential to update/set
|
||||
"""
|
||||
self._data[key] = cred
|
||||
self._write()
|
||||
|
||||
def _delete_credential(self, key):
|
||||
"""Delete a credential and write the multistore.
|
||||
|
||||
This must be called when the multistore is locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
"""
|
||||
try:
|
||||
del self._data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
self._write()
|
||||
|
||||
def _get_storage(self, key):
|
||||
"""Get a Storage object to get/set a credential.
|
||||
|
||||
This Storage is a 'view' into the multistore.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
|
||||
Returns:
|
||||
A Storage object that can be used to get/set this cred
|
||||
"""
|
||||
return self._Storage(self, key)
|
||||
@@ -1,244 +0,0 @@
|
||||
# Copyright 2016 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import mock
|
||||
import unittest2
|
||||
|
||||
from oauth2client.contrib import locked_file
|
||||
|
||||
|
||||
class TestOpener(unittest2.TestCase):
|
||||
def _make_one(self):
|
||||
_filehandle, filename = tempfile.mkstemp()
|
||||
os.close(_filehandle)
|
||||
return locked_file._Opener(filename, 'r+', 'r'), filename
|
||||
|
||||
def test_ctor(self):
|
||||
instance, filename = self._make_one()
|
||||
self.assertFalse(instance._locked)
|
||||
self.assertEqual(instance._filename, filename)
|
||||
self.assertEqual(instance._mode, 'r+')
|
||||
self.assertEqual(instance._fallback_mode, 'r')
|
||||
self.assertIsNone(instance._fh)
|
||||
self.assertIsNone(instance._lock_fd)
|
||||
|
||||
def test_is_locked(self):
|
||||
instance, _ = self._make_one()
|
||||
self.assertFalse(instance.is_locked())
|
||||
instance._locked = True
|
||||
self.assertTrue(instance.is_locked())
|
||||
|
||||
def test_file_handle(self):
|
||||
instance, _ = self._make_one()
|
||||
self.assertIsNone(instance.file_handle())
|
||||
fh = mock.Mock()
|
||||
instance._fh = fh
|
||||
self.assertEqual(instance.file_handle(), fh)
|
||||
|
||||
def test_filename(self):
|
||||
instance, filename = self._make_one()
|
||||
self.assertEqual(instance.filename(), filename)
|
||||
|
||||
def test_open_and_lock(self):
|
||||
instance, _ = self._make_one()
|
||||
instance.open_and_lock(1, 1)
|
||||
|
||||
def test_unlock_and_close(self):
|
||||
instance, _ = self._make_one()
|
||||
instance.unlock_and_close()
|
||||
|
||||
|
||||
class TestPosixOpener(TestOpener):
|
||||
def _make_one(self):
|
||||
_filehandle, filename = tempfile.mkstemp()
|
||||
os.close(_filehandle)
|
||||
return locked_file._PosixOpener(filename, 'r+', 'r'), filename
|
||||
|
||||
def test_relock_fail(self):
|
||||
instance, _ = self._make_one()
|
||||
instance.open_and_lock(1, 1)
|
||||
|
||||
self.assertTrue(instance.is_locked())
|
||||
self.assertIsNotNone(instance.file_handle())
|
||||
with self.assertRaises(locked_file.AlreadyLockedException):
|
||||
instance.open_and_lock(1, 1)
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file.open', create=True)
|
||||
def test_lock_access_error_fallback_mode(self, mock_open):
|
||||
# NOTE: This is a bad case. The behavior here should be that the
|
||||
# error gets re-raised, but the module lets the if statement fall
|
||||
# through.
|
||||
instance, _ = self._make_one()
|
||||
mock_open.side_effect = [IOError(errno.ENOENT, '')]
|
||||
instance.open_and_lock(1, 1)
|
||||
|
||||
self.assertIsNone(instance.file_handle())
|
||||
self.assertTrue(instance.is_locked())
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file.open', create=True)
|
||||
def test_lock_non_access_error(self, mock_open):
|
||||
instance, _ = self._make_one()
|
||||
fh_mock = mock.Mock()
|
||||
mock_open.side_effect = [IOError(errno.EACCES, ''), fh_mock]
|
||||
instance.open_and_lock(1, 1)
|
||||
|
||||
self.assertEqual(instance.file_handle(), fh_mock)
|
||||
self.assertFalse(instance.is_locked())
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file.open', create=True)
|
||||
def test_lock_unexpected_error(self, mock_open):
|
||||
instance, _ = self._make_one()
|
||||
|
||||
with mock.patch('os.open') as mock_os_open:
|
||||
mock_os_open.side_effect = [OSError(errno.EPERM, '')]
|
||||
with self.assertRaises(OSError):
|
||||
instance.open_and_lock(1, 1)
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file.open', create=True)
|
||||
@mock.patch('oauth2client.contrib.locked_file.logger')
|
||||
@mock.patch('time.time')
|
||||
def test_lock_timeout_error(self, mock_time, mock_logger, mock_open):
|
||||
instance, _ = self._make_one()
|
||||
# Make it seem like 10 seconds have passed between calls.
|
||||
mock_time.side_effect = [0, 10]
|
||||
|
||||
with mock.patch('os.open') as mock_os_open:
|
||||
# Raising EEXIST should cause it to try to retry locking.
|
||||
mock_os_open.side_effect = [OSError(errno.EEXIST, '')]
|
||||
instance.open_and_lock(1, 1)
|
||||
self.assertFalse(instance.is_locked())
|
||||
self.assertTrue(mock_logger.warn.called)
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file.open', create=True)
|
||||
@mock.patch('oauth2client.contrib.locked_file.logger')
|
||||
@mock.patch('time.time')
|
||||
def test_lock_timeout_error_no_fh(self, mock_time, mock_logger, mock_open):
|
||||
instance, _ = self._make_one()
|
||||
# Make it seem like 10 seconds have passed between calls.
|
||||
mock_time.side_effect = [0, 10]
|
||||
# This will cause the retry loop to enter without a file handle.
|
||||
fh_mock = mock.Mock()
|
||||
mock_open.side_effect = [IOError(errno.ENOENT, ''), fh_mock]
|
||||
|
||||
with mock.patch('os.open') as mock_os_open:
|
||||
# Raising EEXIST should cause it to try to retry locking.
|
||||
mock_os_open.side_effect = [OSError(errno.EEXIST, '')]
|
||||
instance.open_and_lock(1, 1)
|
||||
self.assertFalse(instance.is_locked())
|
||||
self.assertTrue(mock_logger.warn.called)
|
||||
self.assertEqual(instance.file_handle(), fh_mock)
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file.open', create=True)
|
||||
@mock.patch('time.time')
|
||||
@mock.patch('time.sleep')
|
||||
def test_lock_retry_success(self, mock_sleep, mock_time, mock_open):
|
||||
instance, _ = self._make_one()
|
||||
# Make it seem like 1 second has passed between calls. Extra values
|
||||
# are needed by the logging module.
|
||||
mock_time.side_effect = [0, 1]
|
||||
|
||||
with mock.patch('os.open') as mock_os_open:
|
||||
# Raising EEXIST should cause it to try to retry locking.
|
||||
mock_os_open.side_effect = [
|
||||
OSError(errno.EEXIST, ''), mock.Mock()]
|
||||
instance.open_and_lock(10, 1)
|
||||
print(mock_os_open.call_args_list)
|
||||
self.assertTrue(instance.is_locked())
|
||||
mock_sleep.assert_called_with(1)
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file.os')
|
||||
def test_unlock(self, os_mock):
|
||||
instance, _ = self._make_one()
|
||||
instance._locked = True
|
||||
lock_fd_mock = instance._lock_fd = mock.Mock()
|
||||
instance._fh = mock.Mock()
|
||||
|
||||
instance.unlock_and_close()
|
||||
|
||||
self.assertFalse(instance.is_locked())
|
||||
os_mock.close.assert_called_once_with(lock_fd_mock)
|
||||
self.assertTrue(os_mock.unlink.called)
|
||||
self.assertTrue(instance._fh.close.called)
|
||||
|
||||
|
||||
class TestLockedFile(unittest2.TestCase):
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file._PosixOpener')
|
||||
def _make_one(self, opener_ctor_mock):
|
||||
opener_mock = mock.Mock()
|
||||
opener_ctor_mock.return_value = opener_mock
|
||||
return locked_file.LockedFile(
|
||||
'a_file', 'r+', 'r', use_native_locking=False), opener_mock
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file._PosixOpener')
|
||||
def test_ctor_minimal(self, opener_mock):
|
||||
locked_file.LockedFile(
|
||||
'a_file', 'r+', 'r', use_native_locking=False)
|
||||
opener_mock.assert_called_with('a_file', 'r+', 'r')
|
||||
|
||||
@mock.patch.dict('sys.modules', {
|
||||
'oauth2client.contrib._win32_opener': mock.Mock()})
|
||||
def test_ctor_native_win32(self):
|
||||
_win32_opener_mock = sys.modules['oauth2client.contrib._win32_opener']
|
||||
locked_file.LockedFile(
|
||||
'a_file', 'r+', 'r', use_native_locking=True)
|
||||
_win32_opener_mock._Win32Opener.assert_called_with('a_file', 'r+', 'r')
|
||||
|
||||
@mock.patch.dict('sys.modules', {
|
||||
'oauth2client.contrib._win32_opener': None,
|
||||
'oauth2client.contrib._fcntl_opener': mock.Mock()})
|
||||
def test_ctor_native_fcntl(self):
|
||||
_fnctl_opener_mock = sys.modules['oauth2client.contrib._fcntl_opener']
|
||||
locked_file.LockedFile(
|
||||
'a_file', 'r+', 'r', use_native_locking=True)
|
||||
_fnctl_opener_mock._FcntlOpener.assert_called_with('a_file', 'r+', 'r')
|
||||
|
||||
@mock.patch('oauth2client.contrib.locked_file._PosixOpener')
|
||||
@mock.patch.dict('sys.modules', {
|
||||
'oauth2client.contrib._win32_opener': None,
|
||||
'oauth2client.contrib._fcntl_opener': None})
|
||||
def test_ctor_native_posix_fallback(self, opener_mock):
|
||||
locked_file.LockedFile(
|
||||
'a_file', 'r+', 'r', use_native_locking=True)
|
||||
opener_mock.assert_called_with('a_file', 'r+', 'r')
|
||||
|
||||
def test_filename(self):
|
||||
instance, opener = self._make_one()
|
||||
opener._filename = 'some file'
|
||||
self.assertEqual(instance.filename(), 'some file')
|
||||
|
||||
def test_file_handle(self):
|
||||
instance, opener = self._make_one()
|
||||
self.assertEqual(instance.file_handle(), opener.file_handle())
|
||||
self.assertTrue(opener.file_handle.called)
|
||||
|
||||
def test_is_locked(self):
|
||||
instance, opener = self._make_one()
|
||||
self.assertEqual(instance.is_locked(), opener.is_locked())
|
||||
self.assertTrue(opener.is_locked.called)
|
||||
|
||||
def test_open_and_lock(self):
|
||||
instance, opener = self._make_one()
|
||||
instance.open_and_lock()
|
||||
opener.open_and_lock.assert_called_with(0, 0.05)
|
||||
|
||||
def test_unlock_and_close(self):
|
||||
instance, opener = self._make_one()
|
||||
instance.unlock_and_close()
|
||||
opener.unlock_and_close.assert_called_with()
|
||||
@@ -1,381 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Unit tests for oauth2client.multistore_file."""
|
||||
|
||||
import datetime
|
||||
import errno
|
||||
import os
|
||||
import stat
|
||||
import tempfile
|
||||
|
||||
import mock
|
||||
import unittest2
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
from oauth2client.contrib import multistore_file
|
||||
|
||||
_filehandle, FILENAME = tempfile.mkstemp('oauth2client_test.data')
|
||||
os.close(_filehandle)
|
||||
|
||||
|
||||
class _MockLockedFile(object):
|
||||
|
||||
def __init__(self, filename_str, error_class, error_code):
|
||||
self.filename_str = filename_str
|
||||
self.error_class = error_class
|
||||
self.error_code = error_code
|
||||
self.open_and_lock_called = False
|
||||
|
||||
def open_and_lock(self):
|
||||
self.open_and_lock_called = True
|
||||
raise self.error_class(self.error_code, '')
|
||||
|
||||
def is_locked(self):
|
||||
return False
|
||||
|
||||
def filename(self):
|
||||
return self.filename_str
|
||||
|
||||
|
||||
class Test__dict_to_tuple_key(unittest2.TestCase):
|
||||
|
||||
def test_key_conversions(self):
|
||||
key1, val1 = 'somekey', 'some value'
|
||||
key2, val2 = 'another', 'something else'
|
||||
key3, val3 = 'onemore', 'foo'
|
||||
test_dict = {
|
||||
key1: val1,
|
||||
key2: val2,
|
||||
key3: val3,
|
||||
}
|
||||
tuple_key = multistore_file._dict_to_tuple_key(test_dict)
|
||||
|
||||
# the resulting key should be naturally sorted
|
||||
expected_output = (
|
||||
(key2, val2),
|
||||
(key3, val3),
|
||||
(key1, val1),
|
||||
)
|
||||
self.assertTupleEqual(expected_output, tuple_key)
|
||||
# check we get the original dictionary back
|
||||
self.assertDictEqual(test_dict, dict(tuple_key))
|
||||
|
||||
|
||||
class MultistoreFileTests(unittest2.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
os.unlink(FILENAME)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
try:
|
||||
os.unlink(FILENAME)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _create_test_credentials(self, client_id='some_client_id',
|
||||
expiration=None):
|
||||
access_token = 'foo'
|
||||
client_secret = 'cOuDdkfjxxnv+'
|
||||
refresh_token = '1/0/a.df219fjls0'
|
||||
token_expiry = expiration or datetime.datetime.utcnow()
|
||||
token_uri = 'https://www.google.com/accounts/o8/oauth2/token'
|
||||
user_agent = 'refresh_checker/1.0'
|
||||
|
||||
credentials = client.OAuth2Credentials(
|
||||
access_token, client_id, client_secret,
|
||||
refresh_token, token_expiry, token_uri,
|
||||
user_agent)
|
||||
return credentials
|
||||
|
||||
def test_lock_file_raises_ioerror(self):
|
||||
filehandle, filename = tempfile.mkstemp()
|
||||
os.close(filehandle)
|
||||
|
||||
try:
|
||||
for error_code in (errno.EDEADLK, errno.ENOSYS, errno.ENOLCK,
|
||||
errno.EACCES):
|
||||
for error_class in (IOError, OSError):
|
||||
multistore = multistore_file._MultiStore(filename)
|
||||
multistore._file = _MockLockedFile(
|
||||
filename, error_class, error_code)
|
||||
# Should not raise though the underlying file class did.
|
||||
multistore._lock()
|
||||
self.assertTrue(multistore._file.open_and_lock_called)
|
||||
finally:
|
||||
os.unlink(filename)
|
||||
|
||||
def test_lock_file_raise_unexpected_error(self):
|
||||
filehandle, filename = tempfile.mkstemp()
|
||||
os.close(filehandle)
|
||||
|
||||
try:
|
||||
multistore = multistore_file._MultiStore(filename)
|
||||
multistore._file = _MockLockedFile(filename, IOError, errno.EBUSY)
|
||||
with self.assertRaises(IOError):
|
||||
multistore._lock()
|
||||
self.assertTrue(multistore._file.open_and_lock_called)
|
||||
finally:
|
||||
os.unlink(filename)
|
||||
|
||||
def test_read_only_file_fail_lock(self):
|
||||
credentials = self._create_test_credentials()
|
||||
|
||||
open(FILENAME, 'a+b').close()
|
||||
os.chmod(FILENAME, 0o400)
|
||||
|
||||
store = multistore_file.get_credential_storage(
|
||||
FILENAME,
|
||||
credentials.client_id,
|
||||
credentials.user_agent,
|
||||
['some-scope', 'some-other-scope'])
|
||||
|
||||
store.put(credentials)
|
||||
if os.name == 'posix': # pragma: NO COVER
|
||||
self.assertTrue(store._multistore._read_only)
|
||||
os.chmod(FILENAME, 0o600)
|
||||
|
||||
def test_read_only_file_fail_lock_no_warning(self):
|
||||
open(FILENAME, 'a+b').close()
|
||||
os.chmod(FILENAME, 0o400)
|
||||
|
||||
multistore = multistore_file._MultiStore(FILENAME)
|
||||
|
||||
with mock.patch.object(multistore_file.logger, 'warn') as mock_warn:
|
||||
multistore._warn_on_readonly = False
|
||||
multistore._lock()
|
||||
self.assertFalse(mock_warn.called)
|
||||
|
||||
def test_lock_skip_refresh(self):
|
||||
with open(FILENAME, 'w') as f:
|
||||
f.write('123')
|
||||
os.chmod(FILENAME, 0o400)
|
||||
|
||||
multistore = multistore_file._MultiStore(FILENAME)
|
||||
|
||||
refresh_patch = mock.patch.object(
|
||||
multistore, '_refresh_data_cache')
|
||||
|
||||
with refresh_patch as refresh_mock:
|
||||
multistore._data = {}
|
||||
multistore._lock()
|
||||
self.assertFalse(refresh_mock.called)
|
||||
|
||||
@unittest2.skipIf(not hasattr(os, 'symlink'), 'No symlink available')
|
||||
def test_multistore_no_symbolic_link_files(self):
|
||||
SYMFILENAME = FILENAME + 'sym'
|
||||
os.symlink(FILENAME, SYMFILENAME)
|
||||
store = multistore_file.get_credential_storage(
|
||||
SYMFILENAME,
|
||||
'some_client_id',
|
||||
'user-agent/1.0',
|
||||
['some-scope', 'some-other-scope'])
|
||||
try:
|
||||
with self.assertRaises(IOError):
|
||||
store.get()
|
||||
finally:
|
||||
os.unlink(SYMFILENAME)
|
||||
|
||||
def test_multistore_non_existent_file(self):
|
||||
store = multistore_file.get_credential_storage(
|
||||
FILENAME,
|
||||
'some_client_id',
|
||||
'user-agent/1.0',
|
||||
['some-scope', 'some-other-scope'])
|
||||
|
||||
credentials = store.get()
|
||||
self.assertIsNone(credentials)
|
||||
|
||||
def test_multistore_file(self):
|
||||
credentials = self._create_test_credentials()
|
||||
|
||||
store = multistore_file.get_credential_storage(
|
||||
FILENAME,
|
||||
credentials.client_id,
|
||||
credentials.user_agent,
|
||||
['some-scope', 'some-other-scope'])
|
||||
|
||||
# Save credentials
|
||||
store.put(credentials)
|
||||
credentials = store.get()
|
||||
|
||||
self.assertIsNotNone(credentials)
|
||||
self.assertEquals('foo', credentials.access_token)
|
||||
|
||||
# Delete credentials
|
||||
store.delete()
|
||||
credentials = store.get()
|
||||
|
||||
self.assertIsNone(credentials)
|
||||
|
||||
if os.name == 'posix': # pragma: NO COVER
|
||||
self.assertEquals(
|
||||
0o600, stat.S_IMODE(os.stat(FILENAME).st_mode))
|
||||
|
||||
def test_multistore_file_custom_key(self):
|
||||
credentials = self._create_test_credentials()
|
||||
|
||||
custom_key = {'myapp': 'testing', 'clientid': 'some client'}
|
||||
store = multistore_file.get_credential_storage_custom_key(
|
||||
FILENAME, custom_key)
|
||||
|
||||
store.put(credentials)
|
||||
stored_credentials = store.get()
|
||||
|
||||
self.assertIsNotNone(stored_credentials)
|
||||
self.assertEqual(credentials.access_token,
|
||||
stored_credentials.access_token)
|
||||
|
||||
store.delete()
|
||||
stored_credentials = store.get()
|
||||
|
||||
self.assertIsNone(stored_credentials)
|
||||
|
||||
def test_multistore_file_custom_string_key(self):
|
||||
credentials = self._create_test_credentials()
|
||||
|
||||
# store with string key
|
||||
store = multistore_file.get_credential_storage_custom_string_key(
|
||||
FILENAME, 'mykey')
|
||||
|
||||
store.put(credentials)
|
||||
stored_credentials = store.get()
|
||||
|
||||
self.assertIsNotNone(stored_credentials)
|
||||
self.assertEqual(credentials.access_token,
|
||||
stored_credentials.access_token)
|
||||
|
||||
# try retrieving with a dictionary
|
||||
multistore_file.get_credential_storage_custom_string_key(
|
||||
FILENAME, {'key': 'mykey'})
|
||||
stored_credentials = store.get()
|
||||
self.assertIsNotNone(stored_credentials)
|
||||
self.assertEqual(credentials.access_token,
|
||||
stored_credentials.access_token)
|
||||
|
||||
store.delete()
|
||||
stored_credentials = store.get()
|
||||
|
||||
self.assertIsNone(stored_credentials)
|
||||
|
||||
def test_multistore_file_backwards_compatibility(self):
|
||||
credentials = self._create_test_credentials()
|
||||
scopes = ['scope1', 'scope2']
|
||||
|
||||
# store the credentials using the legacy key method
|
||||
store = multistore_file.get_credential_storage(
|
||||
FILENAME, 'client_id', 'user_agent', scopes)
|
||||
store.put(credentials)
|
||||
|
||||
# retrieve the credentials using a custom key that matches the
|
||||
# legacy key
|
||||
key = {'clientId': 'client_id', 'userAgent': 'user_agent',
|
||||
'scope': util.scopes_to_string(scopes)}
|
||||
store = multistore_file.get_credential_storage_custom_key(
|
||||
FILENAME, key)
|
||||
stored_credentials = store.get()
|
||||
|
||||
self.assertEqual(credentials.access_token,
|
||||
stored_credentials.access_token)
|
||||
|
||||
def test_multistore_file_get_all_keys(self):
|
||||
# start with no keys
|
||||
keys = multistore_file.get_all_credential_keys(FILENAME)
|
||||
self.assertEquals([], keys)
|
||||
|
||||
# store credentials
|
||||
credentials = self._create_test_credentials(client_id='client1')
|
||||
custom_key = {'myapp': 'testing', 'clientid': 'client1'}
|
||||
store1 = multistore_file.get_credential_storage_custom_key(
|
||||
FILENAME, custom_key)
|
||||
store1.put(credentials)
|
||||
|
||||
keys = multistore_file.get_all_credential_keys(FILENAME)
|
||||
self.assertEquals([custom_key], keys)
|
||||
|
||||
# store more credentials
|
||||
credentials = self._create_test_credentials(client_id='client2')
|
||||
string_key = 'string_key'
|
||||
store2 = multistore_file.get_credential_storage_custom_string_key(
|
||||
FILENAME, string_key)
|
||||
store2.put(credentials)
|
||||
|
||||
keys = multistore_file.get_all_credential_keys(FILENAME)
|
||||
self.assertEquals(2, len(keys))
|
||||
self.assertTrue(custom_key in keys)
|
||||
self.assertTrue({'key': string_key} in keys)
|
||||
|
||||
# back to no keys
|
||||
store1.delete()
|
||||
store2.delete()
|
||||
keys = multistore_file.get_all_credential_keys(FILENAME)
|
||||
self.assertEquals([], keys)
|
||||
|
||||
def _refresh_data_cache_helper(self):
|
||||
multistore = multistore_file._MultiStore(FILENAME)
|
||||
json_patch = mock.patch.object(multistore, '_locked_json_read')
|
||||
|
||||
return multistore, json_patch
|
||||
|
||||
def test__refresh_data_cache_bad_json(self):
|
||||
multistore, json_patch = self._refresh_data_cache_helper()
|
||||
|
||||
with json_patch as json_mock:
|
||||
json_mock.side_effect = ValueError('')
|
||||
multistore._refresh_data_cache()
|
||||
self.assertTrue(json_mock.called)
|
||||
self.assertEqual(multistore._data, {})
|
||||
|
||||
def test__refresh_data_cache_bad_version(self):
|
||||
multistore, json_patch = self._refresh_data_cache_helper()
|
||||
|
||||
with json_patch as json_mock:
|
||||
json_mock.return_value = {}
|
||||
multistore._refresh_data_cache()
|
||||
self.assertTrue(json_mock.called)
|
||||
self.assertEqual(multistore._data, {})
|
||||
|
||||
def test__refresh_data_cache_newer_version(self):
|
||||
multistore, json_patch = self._refresh_data_cache_helper()
|
||||
|
||||
with json_patch as json_mock:
|
||||
json_mock.return_value = {'file_version': 5}
|
||||
with self.assertRaises(multistore_file.NewerCredentialStoreError):
|
||||
multistore._refresh_data_cache()
|
||||
self.assertTrue(json_mock.called)
|
||||
|
||||
def test__refresh_data_cache_bad_credentials(self):
|
||||
multistore, json_patch = self._refresh_data_cache_helper()
|
||||
|
||||
with json_patch as json_mock:
|
||||
json_mock.return_value = {
|
||||
'file_version': 1,
|
||||
'data': [
|
||||
{'lol': 'this is a bad credential object.'}
|
||||
]}
|
||||
multistore._refresh_data_cache()
|
||||
self.assertTrue(json_mock.called)
|
||||
self.assertEqual(multistore._data, {})
|
||||
|
||||
def test__delete_credential_nonexistent(self):
|
||||
multistore = multistore_file._MultiStore(FILENAME)
|
||||
|
||||
with mock.patch.object(multistore, '_write') as write_mock:
|
||||
multistore._data = {}
|
||||
multistore._delete_credential('nonexistent_key')
|
||||
self.assertTrue(write_mock.called)
|
||||
Reference in New Issue
Block a user