[fix] fix path unicity process in BytesFileSystemStorage.new_fs_path
The previous implementation was bugged (prefixing the whole path with '_'
instead of the base name). A new version (using number) replace it.
* * *
Improve BytesFileSystemStorage.new_fs_path to use available metadata
This version try to get some hint about how to name the file using metadata.
Using the real file name and extension when available. Keeping the extension
might be usefull for exemple in the case of processing that use filename
extension to detect content-type.
"""custom storages for the system source"""
from os import unlink, path as osp
from yams.schema import role_name
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
def uniquify_path(dirpath, basename):
"""return a unique file name for `basename` in `dirpath`, or None
if all attemps failed.
XXX subject to race condition.
"""
path = osp.join(dirpath, basename)
if not osp.isfile(path):
return path
base, ext = osp.splitext(path)
for i in xrange(1, 256):
path = '%s%s%s' % (base, i, ext)
if not osp.isfile(path):
return path
return None
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):
basename = [str(entity.eid), attr]
# We try to get some hint about how to name the file using attributes
# metadata. Using the real file name and extension when available.
#
# Keeping the extension might be usefull for exemple in the case of PIL
# processing that use filename extension to detect content-type.
name = entity.attr_metadata(attr, 'name')
if name is not None:
basename.append(name.encode(entity._cw.encoding))
fspath = uniquify_path(self.default_directory, '_'.join(basename))
if fspath is None:
msg = entity._cw._('failed to uniquify path (%s, %s)') % (
dirpath, basename)
raise ValidationError(entity.eid, {role_name(attr, 'subject'): msg})
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))