# (c) 2009-2011 Martin Wendt and contributors; see WsgiDAV http://wsgidav.googlecode.com/ # Original PyFileServer (c) 2005 Ho Chun Wei. # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php """ Implements two storage providers for `LockManager`. Two alternative lock storage classes are defined here: one in-memory (dict-based), and one persistent low performance variant using shelve. See wsgidav.lock_manager.LockManager See `Developers info`_ for more information about the WsgiDAV architecture. .. _`Developers info`: http://docs.wsgidav.googlecode.com/hg/html/develop.html """ from wsgidav.lock_manager import normalizeLockRoot, lockString,\ generateLockToken, validateLock import os import util import shelve import time from rw_lock import ReadWriteLock __docformat__ = "reStructuredText" _logger = util.getModuleLogger(__name__) # TODO: comment's from Ian Bicking (2005) #@@: Use of shelve means this is only really useful in a threaded environment. # And if you have just a single-process threaded environment, you could get # nearly the same effect with a dictionary of threading.Lock() objects. Of course, # it would be better to move off shelve anyway, probably to a system with # a directory of per-file locks, using the file locking primitives (which, # sadly, are not quite portable). # @@: It would probably be easy to store the properties as pickle objects # in a parallel directory structure to the files you are describing. # Pickle is expedient, but later you could use something more readable # (pickles aren't particularly readable) #=============================================================================== # LockStorageDict #=============================================================================== class LockStorageDict(object): """ An in-memory lock manager storage implementation using a dictionary. R/W access is guarded by a thread.lock object. Also, to make it work with a Shelve dictionary, modifying dictionary members is done by re-assignment and we call a _flush() method. This is obviously not persistent, but should be enough in some cases. For a persistent implementation, see lock_manager.LockStorageShelve(). Notes: expire is stored as expiration date in seconds since epoch (not in seconds until expiration). The dictionary is built like:: { 'URL2TOKEN:/temp/litmus/lockme': ['opaquelocktoken:0x1d7b86...', 'opaquelocktoken:0xd7d4c0...'], 'opaquelocktoken:0x1d7b86...': { 'depth': '0', 'owner': "\\nlitmus test suite\\n", 'principal': 'tester', 'root': '/temp/litmus/lockme', 'scope': 'shared', 'expire': 1261328382.4530001, 'token': 'opaquelocktoken:0x1d7b86...', 'type': 'write', }, 'opaquelocktoken:0xd7d4c0...': { 'depth': '0', 'owner': '\\nlitmus: notowner_sharedlock\\n', 'principal': 'tester', 'root': '/temp/litmus/lockme', 'scope': 'shared', 'expire': 1261328381.6040001, 'token': 'opaquelocktoken:0xd7d4c0...', 'type': 'write' }, } """ LOCK_TIME_OUT_DEFAULT = 604800 # 1 week, in seconds LOCK_TIME_OUT_MAX = 4 * 604800 # 1 month, in seconds def __init__(self): self._dict = None self._lock = ReadWriteLock() def __repr__(self): return self.__class__.__name__ def __del__(self): pass def _flush(self): """Overloaded by Shelve implementation.""" pass def open(self): """Called before first use. May be implemented to initialize a storage. """ assert self._dict is None self._dict = {} def close(self): """Called on shutdown.""" self._dict = None def cleanup(self): """Purge expired locks (optional).""" pass def get(self, token): """Return a lock dictionary for a token. If the lock does not exist or is expired, None is returned. token: lock token Returns: Lock dictionary or Side effect: if lock is expired, it will be purged and None is returned. """ self._lock.acquireRead() try: lock = self._dict.get(token) if lock is None: # Lock not found: purge dangling URL2TOKEN entries _logger.debug("Lock purged dangling: %s" % token) self.delete(token) return None expire = float(lock["expire"]) if expire >= 0 and expire < time.time(): _logger.debug("Lock timed-out(%s): %s" % (expire, lockString(lock))) self.delete(token) return None return lock finally: self._lock.release() def create(self, path, lock): """Create a direct lock for a resource path. path: Normalized path (utf8 encoded string, no trailing '/') lock: lock dictionary, without a token entry Returns: New unique lock token.: - lock['timeout'] may be normalized and shorter than requested - lock['token'] is added """ self._lock.acquireWrite() try: # We expect only a lock definition, not an existing lock assert lock.get("token") is None assert lock.get("expire") is None, "Use timeout instead of expire" assert path and "/" in path # Normalize root: /foo/bar org_path = path path = normalizeLockRoot(path) lock["root"] = path # Normalize timeout from ttl to expire-date timeout = float(lock.get("timeout")) if timeout is None: timeout = LockStorageDict.LOCK_TIME_OUT_DEFAULT elif timeout < 0 or timeout > LockStorageDict.LOCK_TIME_OUT_MAX: timeout = LockStorageDict.LOCK_TIME_OUT_MAX lock["timeout"] = timeout lock["expire"] = time.time() + timeout validateLock(lock) token = generateLockToken() lock["token"] = token # Store lock self._dict[token] = lock # Store locked path reference key = "URL2TOKEN:%s" % path if not key in self._dict: self._dict[key] = [ token ] else: # Note: Shelve dictionary returns copies, so we must reassign values: tokList = self._dict[key] tokList.append(token) self._dict[key] = tokList self._flush() _logger.debug("LockStorageDict.set(%r): %s" % (org_path, lockString(lock))) # print("LockStorageDict.set(%r): %s" % (org_path, lockString(lock))) return lock finally: self._lock.release() def refresh(self, token, timeout): """Modify an existing lock's timeout. token: Valid lock token. timeout: Suggested lifetime in seconds (-1 for infinite). The real expiration time may be shorter than requested! Returns: Lock dictionary. Raises ValueError, if token is invalid. """ assert token in self._dict, "Lock must exist" assert timeout == -1 or timeout > 0 if timeout < 0 or timeout > LockStorageDict.LOCK_TIME_OUT_MAX: timeout = LockStorageDict.LOCK_TIME_OUT_MAX self._lock.acquireWrite() try: # Note: shelve dictionary returns copies, so we must reassign values: lock = self._dict[token] lock["timeout"] = timeout lock["expire"] = time.time() + timeout self._dict[token] = lock self._flush() finally: self._lock.release() return lock def delete(self, token): """Delete lock. Returns True on success. False, if token does not exist, or is expired. """ self._lock.acquireWrite() try: lock = self._dict.get(token) _logger.debug("delete %s" % lockString(lock)) if lock is None: return False # Remove url to lock mapping key = "URL2TOKEN:%s" % lock.get("root") if key in self._dict: # _logger.debug(" delete token %s from url %s" % (token, lock.get("root"))) tokList = self._dict[key] if len(tokList) > 1: # Note: shelve dictionary returns copies, so we must reassign values: tokList.remove(token) self._dict[key] = tokList else: del self._dict[key] # Remove the lock del self._dict[token] self._flush() finally: self._lock.release() return True def getLockList(self, path, includeRoot, includeChildren, tokenOnly): """Return a list of direct locks for . Expired locks are *not* returned (but may be purged). path: Normalized path (utf8 encoded string, no trailing '/') includeRoot: False: don't add lock (only makes sense, when includeChildren is True). includeChildren: True: Also check all sub-paths for existing locks. tokenOnly: True: only a list of token is returned. This may be implemented more efficiently by some providers. Returns: List of valid lock dictionaries (may be empty). """ assert path and path.startswith("/") assert includeRoot or includeChildren def __appendLocks(toklist): # Since we can do this quickly, we use self.get() even if # tokenOnly is set, so expired locks are purged. for token in toklist: lock = self.get(token) if lock: if tokenOnly: lockList.append(lock["token"]) else: lockList.append(lock) path = normalizeLockRoot(path) self._lock.acquireRead() try: key = "URL2TOKEN:%s" % path tokList = self._dict.get(key, []) lockList = [] if includeRoot: __appendLocks(tokList) if includeChildren: for u, ltoks in self._dict.items(): if util.isChildUri(key, u): __appendLocks(ltoks) return lockList finally: self._lock.release() #=============================================================================== # LockStorageShelve #=============================================================================== class LockStorageShelve(LockStorageDict): """ A low performance lock manager implementation using shelve. """ def __init__(self, storagePath): super(LockStorageShelve, self).__init__() self._storagePath = os.path.abspath(storagePath) def __repr__(self): return "LockStorageShelve(%r)" % self._storagePath def _flush(self): """Write persistent dictionary to disc.""" _logger.debug("_flush()") self._lock.acquireWrite() # TODO: read access is enough? try: self._dict.sync() finally: self._lock.release() def open(self): _logger.debug("open(%r)" % self._storagePath) # Open with writeback=False, which is faster, but we have to be # careful to re-assign values to _dict after modifying them self._dict = shelve.open(self._storagePath, writeback=False) # if __debug__ and self._verbose >= 2: ## self._check("After shelve.open()") # self._dump("After shelve.open()") def close(self): _logger.debug("close()") self._lock.acquireWrite() try: if self._dict is not None: self._dict.close() self._dict = None finally: self._lock.release() #=============================================================================== # test #=============================================================================== def test(): # l = ShelveLockManager("wsgidav-locks.shelve") # l._lazyOpen() # l._dump() # l.generateLock("martin", "", lockscope, lockdepth, lockowner, lockroot, timeout) pass if __name__ == "__main__": test()