Commit 2cb04972 authored by Antony Chazapis's avatar Antony Chazapis

Merge branch 'permissions'

Conflicts:
	pithos/backends/modular.py
parents da538bc6 0195259e
......@@ -27,7 +27,7 @@ Document Revisions
========================= ================================
Revision Description
========================= ================================
0.8 (Jan 24, 2012) Update allowed versioning values.
0.8 (Feb 9, 2012) Update allowed versioning values.
\ Change policy/meta formatting in JSON/XML replies.
\ Document that all non-ASCII characters in headers should be URL-encoded.
\ Support metadata-based queries when listing objects at the container level.
......@@ -37,6 +37,7 @@ Revision Description
\ Note that ``/login`` will only work if an external authentication system is defined.
\ Include option to ignore Content-Type on ``COPY``/``MOVE``.
\ Use format parameter for conflict (409) and uploaded hash list (container level) replies.
\ Change permissions model.
0.7 (Nov 21, 2011) Suggest upload/download methods using hashmaps.
\ Propose syncing algorithm.
\ Support cross-account object copy and move.
......@@ -863,7 +864,7 @@ The ``X-Object-Sharing`` header may include either a ``read=...`` comma-separate
Return Code Description
============================== ==============================
201 (Created) The object has been created
409 (Conflict) The object can not be created from the provided hashmap, or there are conflicting permissions (a list of missing hashes, or a list of conflicting sharing paths will be included in the reply)
409 (Conflict) The object can not be created from the provided hashmap (a list of missing hashes will be included in the reply)
411 (Length Required) Missing ``Content-Length`` or ``Content-Type`` in the request
413 (Request Entity Too Large) Insufficient quota to complete the request
422 (Unprocessable Entity) The MD5 checksum of the data written to the storage system does not match the (optionally) supplied ETag value
......@@ -913,7 +914,6 @@ X-Object-Version The object's new version
Return Code Description
============================== ==============================
201 (Created) The object has been created
409 (Conflict) There are conflicting permissions (a list of conflicting sharing paths will be included in the reply)
413 (Request Entity Too Large) Insufficient quota to complete the request
============================== ==============================
......@@ -991,7 +991,6 @@ Return Code Description
============================== ==============================
202 (Accepted) The request has been accepted (not a data update)
204 (No Content) The request succeeded (data updated)
409 (Conflict) There are conflicting permissions (a list of conflicting sharing paths will be included in the reply)
411 (Length Required) Missing ``Content-Length`` in the request
413 (Request Entity Too Large) Insufficient quota to complete the request
416 (Range Not Satisfiable) The supplied range is invalid
......@@ -1045,7 +1044,7 @@ Return Code Description
Sharing and Public Objects
^^^^^^^^^^^^^^^^^^^^^^^^^^
Read and write control in Pithos is managed by setting appropriate permissions with the ``X-Object-Sharing`` header. The permissions are applied using prefix-based inheritance. Thus, each set of authorization directives is applied to all objects sharing the same prefix with the object where the corresponding ``X-Object-Sharing`` header is defined. For simplicity, nested/overlapping permissions are not allowed. Setting ``X-Object-Sharing`` will fail, if the object is already "covered", or another object with a longer common-prefix name already has permissions. When retrieving an object, the ``X-Object-Shared-By`` header reports where it gets its permissions from. If not present, the object is the actual source of authorization directives.
Read and write control in Pithos is managed by setting appropriate permissions with the ``X-Object-Sharing`` header. The permissions are applied using directory-based inheritance. A directory is an object with the corresponding content type. The default delimiter is ``/``. Thus, each set of authorization directives is applied to all objects in the directory object where the corresponding ``X-Object-Sharing`` header is defined. If there are nested/overlapping permissions, the closest to the object is applied. When retrieving an object, the ``X-Object-Shared-By`` header reports where it gets its permissions from. If not present, the object is the actual source of authorization directives.
A user may ``GET`` another account or container. The result will include a limited reply, containing only the allowed containers or objects respectively. A top-level request with an authentication token, will return a list of allowed accounts, so the user can easily find out which other users share objects. The ``X-Object-Allowed-To`` header lists the actions allowed on an object, if it does not belong to the requesting user.
......@@ -1093,7 +1092,7 @@ List of differences from the OOS API:
* Time-variant account/container listings via the ``until`` parameter.
* Object versions - parameter ``version`` in ``HEAD``/``GET`` (list versions with ``GET``), ``X-Object-Version-*`` meta in replies, ``X-Source-Version`` in ``PUT``/``COPY``.
* Sharing/publishing with ``X-Object-Sharing``, ``X-Object-Public`` at the object level. Cross-user operations are allowed - controlled by sharing directives. Available actions in cross-user requests are reported with ``X-Object-Allowed-To``. Permissions may include groups defined with ``X-Account-Group-*`` at the account level. These apply to the object - not its versions.
* Support for prefix-based inheritance when enforcing permissions. Parent object carrying the authorization directives is reported in ``X-Object-Shared-By``.
* Support for directory-based inheritance when enforcing permissions. Parent object carrying the authorization directives is reported in ``X-Object-Shared-By``.
* Copy and move between accounts with ``X-Source-Account`` and ``Destination-Account`` headers.
* Large object support with ``X-Object-Manifest``.
* Trace the user that created/modified an object with ``X-Object-Modified-By``.
......
......@@ -557,6 +557,7 @@ def object_list(request, v_account, v_container):
else:
rename_meta_key(meta, 'hash', 'x_object_hash') # Will be replaced by ETag.
rename_meta_key(meta, 'ETag', 'hash')
rename_meta_key(meta, 'type', 'content_type')
rename_meta_key(meta, 'uuid', 'x_object_uuid')
rename_meta_key(meta, 'modified', 'last_modified')
rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
......@@ -797,12 +798,12 @@ def object_write(request, v_account, v_container, v_object):
response['X-Object-Version'] = version_id
return response
meta, permissions, public = get_object_headers(request)
content_type, meta, permissions, public = get_object_headers(request)
content_length = -1
if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
content_length = get_content_length(request)
# Should be BadRequest, but API says otherwise.
if 'Content-Type' not in meta:
if not content_type:
raise LengthRequired('Missing Content-Type header')
if 'hashmap' in request.GET:
......@@ -854,8 +855,8 @@ def object_write(request, v_account, v_container, v_object):
try:
version_id = request.backend.update_object_hashmap(request.user_uniq,
v_account, v_container, v_object, size, hashmap,
'pithos', meta, True, permissions)
v_account, v_container, v_object, size, content_type,
hashmap, 'pithos', meta, True, permissions)
except NotAllowedError:
raise Forbidden('Not allowed')
except IndexError, e:
......@@ -864,8 +865,6 @@ def object_write(request, v_account, v_container, v_object):
raise ItemNotFound('Container does not exist')
except ValueError:
raise BadRequest('Invalid sharing header')
except AttributeError, e:
raise Conflict(simple_list_response(request, e.data))
except QuotaError:
raise RequestEntityTooLarge('Quota exceeded')
if 'ETag' not in meta:
......@@ -905,14 +904,14 @@ def object_write_form(request, v_account, v_container, v_object):
raise BadRequest('Missing X-Object-Data field')
file = request.FILES['X-Object-Data']
content_type = file.content_type
meta = {}
meta['Content-Type'] = file.content_type
meta['ETag'] = file.etag
try:
version_id = request.backend.update_object_hashmap(request.user_uniq,
v_account, v_container, v_object, file.size, file.hashmap,
'pithos', meta, True)
v_account, v_container, v_object, file.size, content_type,
file.hashmap, 'pithos', meta, True)
except NotAllowedError:
raise Forbidden('Not allowed')
except NameError:
......@@ -1008,10 +1007,7 @@ def object_update(request, v_account, v_container, v_object):
# forbidden (403),
# badRequest (400)
meta, permissions, public = get_object_headers(request)
content_type = meta.get('Content-Type')
if content_type:
del(meta['Content-Type']) # Do not allow changing the Content-Type.
content_type, meta, permissions, public = get_object_headers(request)
try:
prev_meta = request.backend.get_object_meta(request.user_uniq, v_account,
......@@ -1025,14 +1021,13 @@ def object_update(request, v_account, v_container, v_object):
if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
validate_matching_preconditions(request, prev_meta)
# If replacing, keep previous values of 'Content-Type' and 'ETag'.
# If replacing, keep previous value of 'ETag'.
replace = True
if 'update' in request.GET:
replace = False
if replace:
for k in ('Content-Type', 'ETag'):
if k in prev_meta:
meta[k] = prev_meta[k]
if 'ETag' in prev_meta:
meta['ETag'] = prev_meta['ETag']
# A Content-Type or X-Source-Object header indicates data updates.
src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
......@@ -1050,8 +1045,6 @@ def object_update(request, v_account, v_container, v_object):
raise ItemNotFound('Object does not exist')
except ValueError:
raise BadRequest('Invalid sharing header')
except AttributeError, e:
raise Conflict(simple_list_response(request, e.data))
if public is not None:
try:
request.backend.update_object_public(request.user_uniq, v_account,
......@@ -1188,16 +1181,14 @@ def object_update(request, v_account, v_container, v_object):
meta.update({'ETag': hashmap_md5(request, hashmap, size)}) # Update ETag.
try:
version_id = request.backend.update_object_hashmap(request.user_uniq,
v_account, v_container, v_object, size, hashmap,
'pithos', meta, replace, permissions)
v_account, v_container, v_object, size, prev_meta['type'],
hashmap, 'pithos', meta, replace, permissions)
except NotAllowedError:
raise Forbidden('Not allowed')
except NameError:
raise ItemNotFound('Container does not exist')
except ValueError:
raise BadRequest('Invalid sharing header')
except AttributeError, e:
raise Conflict(simple_list_response(request, e.data))
except QuotaError:
raise RequestEntityTooLarge('Quota exceeded')
if public is not None:
......
......@@ -172,22 +172,21 @@ def put_container_headers(request, response, meta, policy):
response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
def get_object_headers(request):
content_type = request.META.get('CONTENT_TYPE', None)
meta = get_header_prefix(request, 'X-Object-Meta-')
if request.META.get('CONTENT_TYPE'):
meta['Content-Type'] = request.META['CONTENT_TYPE']
if request.META.get('HTTP_CONTENT_ENCODING'):
meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
if request.META.get('HTTP_CONTENT_DISPOSITION'):
meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
if request.META.get('HTTP_X_OBJECT_MANIFEST'):
meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
return meta, get_sharing(request), get_public(request)
return content_type, meta, get_sharing(request), get_public(request)
def put_object_headers(response, meta, restricted=False):
if 'ETag' in meta:
response['ETag'] = meta['ETag']
response['Content-Length'] = meta['bytes']
response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
response['Content-Type'] = meta.get('type', 'application/octet-stream')
response['Last-Modified'] = http_date(int(meta['modified']))
if not restricted:
response['X-Object-Hash'] = meta['hash']
......@@ -309,25 +308,23 @@ def copy_or_move_object(request, src_account, src_container, src_name, dest_acco
if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
del(request.META['CONTENT_TYPE'])
meta, permissions, public = get_object_headers(request)
content_type, meta, permissions, public = get_object_headers(request)
src_version = request.META.get('HTTP_X_SOURCE_VERSION')
try:
if move:
version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
dest_account, dest_container, dest_name,
'pithos', meta, False, permissions)
content_type, 'pithos', meta, False, permissions)
else:
version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
dest_account, dest_container, dest_name,
'pithos', meta, False, permissions, src_version)
content_type, 'pithos', meta, False, permissions, src_version)
except NotAllowedError:
raise Forbidden('Not allowed')
except (NameError, IndexError):
raise ItemNotFound('Container or object does not exist')
except ValueError:
raise BadRequest('Invalid sharing header')
except AttributeError, e:
raise Conflict(simple_list_response(request, e.data))
except QuotaError:
raise RequestEntityTooLarge('Quota exceeded')
if public is not None:
......
......@@ -328,6 +328,8 @@ class BaseBackend(object):
'bytes': The total data size
'type': The content type
'hash': The hashmap hash
'modified': Last modification timestamp (overall)
......@@ -395,11 +397,6 @@ class BaseBackend(object):
NameError: Container/object does not exist
ValueError: Invalid users/groups in permissions
AttributeError: Can not set permissions, as this object
is already shared/private by another object higher
in the hierarchy, or setting permissions here will
invalidate other permissions deeper in the hierarchy
"""
return
......@@ -438,7 +435,7 @@ class BaseBackend(object):
"""
return 0, []
def update_object_hashmap(self, user, account, container, name, size, hashmap, domain, meta={}, replace_meta=False, permissions=None):
def update_object_hashmap(self, user, account, container, name, size, type, hashmap, domain, meta={}, replace_meta=False, permissions=None):
"""Create/update an object with the specified size and partial hashes and return the new version.
Parameters:
......@@ -457,13 +454,11 @@ class BaseBackend(object):
ValueError: Invalid users/groups in permissions
AttributeError: Can not set permissions
QuotaError: Account or container quota exceeded
"""
return ''
def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, domain, meta={}, replace_meta=False, permissions=None, src_version=None):
def copy_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None, src_version=None):
"""Copy an object's data and metadata and return the new version.
Parameters:
......@@ -486,13 +481,11 @@ class BaseBackend(object):
ValueError: Invalid users/groups in permissions
AttributeError: Can not set permissions
QuotaError: Account or container quota exceeded
"""
return ''
def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, domain, meta={}, replace_meta=False, permissions=None):
def move_object(self, user, src_account, src_container, src_name, dest_account, dest_container, dest_name, type, domain, meta={}, replace_meta=False, permissions=None):
"""Move an object's data and metadata and return the new version.
Parameters:
......@@ -511,8 +504,6 @@ class BaseBackend(object):
ValueError: Invalid users/groups in permissions
AttributeError: Can not set permissions
QuotaError: Account or container quota exceeded
"""
return ''
......
......@@ -32,10 +32,10 @@
# or implied, of GRNET S.A.
from dbwrapper import DBWrapper
from node import Node, ROOTNODE, SERIAL, HASH, SIZE, MTIME, MUSER, UUID, CLUSTER
from node import Node, ROOTNODE, SERIAL, HASH, SIZE, TYPE, MTIME, MUSER, UUID, CLUSTER, MATCH_PREFIX, MATCH_EXACT
from permissions import Permissions, READ, WRITE
__all__ = ["DBWrapper",
"Node", "ROOTNODE", "SERIAL", "HASH", "SIZE", "MTIME", "MUSER", "UUID", "CLUSTER",
"Node", "ROOTNODE", "SERIAL", "HASH", "SIZE", "TYPE", "MTIME", "MUSER", "UUID", "CLUSTER", "MATCH_PREFIX", "MATCH_EXACT",
"Permissions", "READ", "WRITE"]
......@@ -46,7 +46,9 @@ from pithos.lib.filter import parse_filters
ROOTNODE = 0
( SERIAL, NODE, HASH, SIZE, SOURCE, MTIME, MUSER, UUID, CLUSTER ) = range(9)
( SERIAL, NODE, HASH, SIZE, TYPE, SOURCE, MTIME, MUSER, UUID, CLUSTER ) = range(10)
( MATCH_PREFIX, MATCH_EXACT ) = range(2)
inf = float('inf')
......@@ -90,11 +92,12 @@ _propnames = {
'node' : 1,
'hash' : 2,
'size' : 3,
'source' : 4,
'mtime' : 5,
'muser' : 6,
'uuid' : 7,
'cluster' : 8
'type' : 4,
'source' : 5,
'mtime' : 6,
'muser' : 7,
'uuid' : 8,
'cluster' : 9
}
......@@ -157,6 +160,7 @@ class Node(DBWorker):
onupdate='CASCADE')))
columns.append(Column('hash', String(255)))
columns.append(Column('size', BigInteger, nullable=False, default=0))
columns.append(Column('type', String(255), nullable=False, default=''))
columns.append(Column('source', Integer))
columns.append(Column('mtime', DECIMAL(precision=16, scale=6)))
columns.append(Column('muser', String(255), nullable=False, default=''))
......@@ -229,13 +233,14 @@ class Node(DBWorker):
def node_get_versions(self, node, keys=(), propnames=_propnames):
"""Return the properties of all versions at node.
If keys is empty, return all properties in the order
(serial, node, hash, size, source, mtime, muser, uuid, cluster).
(serial, node, hash, size, type, source, mtime, muser, uuid, cluster).
"""
s = select([self.versions.c.serial,
self.versions.c.node,
self.versions.c.hash,
self.versions.c.size,
self.versions.c.type,
self.versions.c.source,
self.versions.c.mtime,
self.versions.c.muser,
......@@ -488,6 +493,7 @@ class Node(DBWorker):
self.versions.c.node,
self.versions.c.hash,
self.versions.c.size,
self.versions.c.type,
self.versions.c.source,
self.versions.c.mtime,
self.versions.c.muser,
......@@ -529,7 +535,7 @@ class Node(DBWorker):
return (0, 0, mtime)
# All children (get size and mtime).
# XXX: This is why the full path is stored.
# This is why the full path is stored.
s = select([func.count(v.c.serial),
func.sum(v.c.size),
func.max(v.c.mtime)])
......@@ -550,13 +556,13 @@ class Node(DBWorker):
mtime = max(mtime, r[2])
return (count, size, mtime)
def version_create(self, node, hash, size, source, muser, uuid, cluster=0):
def version_create(self, node, hash, size, type, source, muser, uuid, cluster=0):
"""Create a new version from the given properties.
Return the (serial, mtime) of the new version.
"""
mtime = time()
s = self.versions.insert().values(node=node, hash=hash, size=size, source=source,
s = self.versions.insert().values(node=node, hash=hash, size=size, type=type, source=source,
mtime=mtime, muser=muser, uuid=uuid, cluster=cluster)
serial = self.conn.execute(s).inserted_primary_key[0]
self.statistics_update_ancestors(node, 1, size, mtime, cluster)
......@@ -565,14 +571,15 @@ class Node(DBWorker):
def version_lookup(self, node, before=inf, cluster=0):
"""Lookup the current version of the given node.
Return a list with its properties:
(serial, node, hash, size, source, mtime, muser, uuid, cluster)
(serial, node, hash, size, type, source, mtime, muser, uuid, cluster)
or None if the current version is not found in the given cluster.
"""
v = self.versions.alias('v')
s = select([v.c.serial, v.c.node, v.c.hash,
v.c.size, v.c.source, v.c.mtime,
v.c.muser, v.c.uuid, v.c.cluster])
v.c.size, v.c.type, v.c.source,
v.c.mtime, v.c.muser, v.c.uuid,
v.c.cluster])
c = select([func.max(self.versions.c.serial)],
self.versions.c.node == node)
if before != inf:
......@@ -590,13 +597,14 @@ class Node(DBWorker):
"""Return a sequence of values for the properties of
the version specified by serial and the keys, in the order given.
If keys is empty, return all properties in the order
(serial, node, hash, size, source, mtime, muser, uuid, cluster).
(serial, node, hash, size, type, source, mtime, muser, uuid, cluster).
"""
v = self.versions.alias()
s = select([v.c.serial, v.c.node, v.c.hash,
v.c.size, v.c.source, v.c.mtime,
v.c.muser, v.c.uuid, v.c.cluster], v.c.serial == serial)
v.c.size, v.c.type, v.c.source,
v.c.mtime, v.c.muser, v.c.uuid,
v.c.cluster], v.c.serial == serial)
rp = self.conn.execute(s)
r = rp.fetchone()
rp.close()
......@@ -748,8 +756,11 @@ class Node(DBWorker):
s = s.where(a.c.domain == domain)
s = s.where(n.c.node == v.c.node)
conj = []
for x in pathq:
conj.append(n.c.path.like(self.escape_like(x) + '%', escape='\\'))
for path, match in pathq:
if match == MATCH_PREFIX:
conj.append(n.c.path.like(self.escape_like(path) + '%', escape='\\'))
elif match == MATCH_EXACT:
conj.append(n.c.path == path)
if conj:
s = s.where(or_(*conj))
rp = self.conn.execute(s)
......@@ -826,8 +837,11 @@ class Node(DBWorker):
s = s.where(n.c.node == v.c.node)
s = s.where(and_(n.c.path > bindparam('start'), n.c.path < nextling))
conj = []
for x in pathq:
conj.append(n.c.path.like(self.escape_like(x) + '%', escape='\\'))
for path, match in pathq:
if match == MATCH_PREFIX:
conj.append(n.c.path.like(self.escape_like(path) + '%', escape='\\'))
elif match == MATCH_EXACT:
conj.append(n.c.path == path)
if conj:
s = s.where(or_(*conj))
......
......@@ -58,17 +58,39 @@ class Permissions(XFeatures, Groups, Public):
if not members:
return
feature = self.xfeature_create(path)
if feature is None:
return
self.feature_setmany(feature, access, members)
def access_set(self, path, permissions):
"""Set permissions for path. The permissions dict
maps 'read', 'write' keys to member lists."""
self.xfeature_destroy(path)
self.access_grant(path, READ, permissions.get('read', []))
self.access_grant(path, WRITE, permissions.get('write', []))
r = permissions.get('read', [])
w = permissions.get('write', [])
if not r and not w:
self.xfeature_destroy(path)
return
feature = self.xfeature_create(path)
if r:
self.feature_clear(feature, READ)
self.feature_setmany(feature, READ, r)
if w:
self.feature_clear(feature, WRITE)
self.feature_setmany(feature, WRITE, w)
def access_get(self, path):
"""Get permissions for path."""
feature = self.xfeature_get(path)
if not feature:
return {}
permissions = self.feature_dict(feature)
if READ in permissions:
permissions['read'] = permissions[READ]
del(permissions[READ])
if WRITE in permissions:
permissions['write'] = permissions[WRITE]
del(permissions[WRITE])
return permissions
def access_clear(self, path):
"""Revoke access to path (both permissions and public)."""
......@@ -79,13 +101,9 @@ class Permissions(XFeatures, Groups, Public):
def access_check(self, path, access, member):
"""Return true if the member has this access to the path."""
if access == READ and self.public_get(path) is not None:
return True
r = self.xfeature_inherit(path)
if not r:
feature = self.xfeature_get(path)
if not feature:
return False
fpath, feature = r
members = self.feature_get(feature, access)
if member in members or '*' in members:
return True
......@@ -95,25 +113,23 @@ class Permissions(XFeatures, Groups, Public):
return False
def access_inherit(self, path):
"""Return the inherited or assigned (path, permissions) pair for path."""
"""Return the paths influencing the access for path."""
r = self.xfeature_inherit(path)
if not r:
return (path, {})
fpath, feature = r
permissions = self.feature_dict(feature)
if READ in permissions:
permissions['read'] = permissions[READ]
del(permissions[READ])
if WRITE in permissions:
permissions['write'] = permissions[WRITE]
del(permissions[WRITE])
return (fpath, permissions)
def access_list(self, path):
"""List all permission paths inherited by or inheriting from path."""
# r = self.xfeature_inherit(path)
# if not r:
# return []
# # Compute valid.
# return [x[0] for x in r if x[0] in valid]
return [x[0] for x in self.xfeature_list(path) if x[0] != path]
# Only keep path components.
parts = path.rstrip('/').split('/')
valid = []
for i in range(1, len(parts)):
subp = '/'.join(parts[:i + 1])
valid.append(subp)
if subp != path:
valid.append(subp + '/')
return [x for x in valid if self.xfeature_get(x)]
def access_list_paths(self, member, prefix=None):
"""Return the list of paths granted to member."""
......
......@@ -67,53 +67,39 @@ class XFeatures(DBWorker):
metadata.create_all(self.engine)
def xfeature_inherit(self, path):
"""Return the (path, feature) inherited by the path, or None."""
s = select([self.xfeatures.c.path, self.xfeatures.c.feature_id])
s = s.where(self.xfeatures.c.path <= path)
s = s.order_by(desc(self.xfeatures.c.path)).limit(1)
r = self.conn.execute(s)
row = r.fetchone()
r.close()
if row and path.startswith(row[0]):
return row
else:
return None
# def xfeature_inherit(self, path):
# """Return the (path, feature) inherited by the path, or None."""
#
# s = select([self.xfeatures.c.path, self.xfeatures.c.feature_id])
# s = s.where(self.xfeatures.c.path <= path)
# #s = s.where(self.xfeatures.c.path.like(self.escape_like(path) + '%', escape='\\')) # XXX: Implement reverse and escape like...
# s = s.order_by(desc(self.xfeatures.c.path))
# r = self.conn.execute(s)
# l = r.fetchall()
# r.close()
# return l
def xfeature_list(self, path):
"""Return the list of the (prefix, feature) pairs matching path.
A prefix matches path if either the prefix includes the path,
or the path includes the prefix.
"""
inherited = self.xfeature_inherit(path)
if inherited:
return [inherited]
def xfeature_get(self, path):
"""Return feature for path."""
s = select([self.xfeatures.c.path, self.xfeatures.c.feature_id])
s = s.where(and_(self.xfeatures.c.path.like(self.escape_like(path) + '%', escape='\\'),
self.xfeatures.c.path != path))
s = select([self.xfeatures.c.feature_id])
s = s.where(self.xfeatures.c.path == path)
s = s.order_by(self.xfeatures.c.path)
r = self.conn.execute(s)
l = r.fetchall()
row = r.fetchone()
r.close()
return l
if row:
return row[0]
return None
def xfeature_create(self, path):
"""Create and return a feature for path.
If the path already inherits a feature or
bestows to paths already inheriting a feature,
create no feature and return None.
If the path has a feature, return it.
"""
prefixes = self.xfeature_list(path)