server/sources/storages.py
author Aurelien Campeas <aurelien.campeas@logilab.fr>
Thu, 08 Apr 2010 17:50:37 +0200
branchstable
changeset 5189 84d4587a92bc
parent 5183 8d66003351f8
child 5218 aebd00a2d316
permissions -rw-r--r--
[doc/book] rql/dbapi cleanup, rip cachekey (prematurely ?)

"""custom storages for the system source"""
from os import unlink, path as osp

from cubicweb import Binary
from cubicweb.server.hook import Operation

def set_attribute_storage(repo, etype, attr, storage):
    repo.system_source.set_storage(etype, attr, storage)

def unset_attribute_storage(repo, etype, attr):
    repo.system_source.unset_storage(etype, attr)

class Storage(object):
    """abstract storage

    * If `source_callback` is true (by default), the callback will be run during
      query result process of fetched attribute's valu and should have the
      following prototype::

        callback(self, source, value)

      where `value` is the value actually stored in the backend. None values
      will be skipped (eg callback won't be called).

    * if `source_callback` is false, the callback will be run during sql
      generation when some attribute with a custom storage is accessed and
      should have the following prototype::

        callback(self, generator, relation, linkedvar)

      where `generator` is the sql generator, `relation` the current rql syntax
      tree relation and linkedvar the principal syntax tree variable holding the
      attribute.
    """
    is_source_callback = True

    def callback(self, *args):
        """see docstring for prototype, which vary according to is_source_callback
        """
        raise NotImplementedError()

    def entity_added(self, entity, attr):
        """an entity using this storage for attr has been added"""
        raise NotImplementedError()
    def entity_updated(self, entity, attr):
        """an entity using this storage for attr has been updatded"""
        raise NotImplementedError()
    def entity_deleted(self, entity, attr):
        """an entity using this storage for attr has been deleted"""
        raise NotImplementedError()

# TODO
# * make it configurable without code
# * better file path attribution
# * handle backup/restore

class BytesFileSystemStorage(Storage):
    """store Bytes attribute value on the file system"""
    def __init__(self, defaultdir):
        self.default_directory = defaultdir

    def callback(self, source, value):
        """sql generator callback when some attribute with a custom storage is
        accessed
        """
        fpath = source.binary_to_str(value)
        try:
            return Binary(file(fpath).read())
        except OSError, ex:
            source.critical("can't open %s: %s", value, ex)
            return None

    def entity_added(self, entity, attr):
        """an entity using this storage for attr has been added"""
        if entity._cw.transaction_data.get('fs_importing'):
            binary = Binary(file(entity[attr].getvalue()).read())
        else:
            binary = entity.pop(attr)
            fpath = self.new_fs_path(entity, attr)
            # bytes storage used to store file's path
            entity[attr] = Binary(fpath)
            file(fpath, 'w').write(binary.getvalue())
            AddFileOp(entity._cw, filepath=fpath)
        return binary

    def entity_updated(self, entity, attr):
        """an entity using this storage for attr has been updatded"""
        if entity._cw.transaction_data.get('fs_importing'):
            oldpath = self.current_fs_path(entity, attr)
            fpath = entity[attr].getvalue()
            if oldpath != fpath:
                DeleteFileOp(entity._cw, filepath=oldpath)
            binary = Binary(file(fpath).read())
        else:
            binary = entity.pop(attr)
            fpath = self.current_fs_path(entity, attr)
            UpdateFileOp(entity._cw, filepath=fpath, filedata=binary.getvalue())
        return binary

    def entity_deleted(self, entity, attr):
        """an entity using this storage for attr has been deleted"""
        DeleteFileOp(entity._cw, filepath=self.current_fs_path(entity, attr))

    def new_fs_path(self, entity, attr):
        fspath = osp.join(self.default_directory, '%s_%s' % (entity.eid, attr))
        while osp.exists(fspath):
            fspath = '_' + fspath
        return fspath

    def current_fs_path(self, entity, attr):
        sysource = entity._cw.pool.source('system')
        cu = sysource.doexec(entity._cw,
                             'SELECT cw_%s FROM cw_%s WHERE cw_eid=%s' % (
                                 attr, entity.__regid__, entity.eid))
        rawvalue = cu.fetchone()[0]
        if rawvalue is None: # no previous value
            return self.new_fs_path(entity, attr)
        return sysource._process_value(rawvalue, cu.description[0],
                                       binarywrap=str)


class AddFileOp(Operation):
    def rollback_event(self):
        try:
            unlink(self.filepath)
        except:
            pass

class DeleteFileOp(Operation):
    def commit_event(self):
        try:
            unlink(self.filepath)
        except:
            pass

class UpdateFileOp(Operation):
    def precommit_event(self):
        try:
            file(self.filepath, 'w').write(self.filedata)
        except Exception, ex:
            self.exception(str(ex))