Category: Technology, Web-Applications
Tags: Pyramid, repoze.BFG Zope
When writing applications using Pyramid web framework a simple option to persist data is using the ZODB. It is sort of built in and very easy to ease. Although being a very stable, reliable and fast database, ZODB does not handle binary data very well. But there is a solution for this: Storing binary data (blobs) transparently outside of the database.
ZODB in newer versions has blob support built in but you need to activate it and handle it the rigth way. Here I want to share my findings and some code to make you successfully use blobs in your Pyramid application. Some parts of the code are inspired by z3c.blobfile.
Click here to display full code example
[python]
import cgi
import datetime
from persistent import Persistent
from zope.interface import implements
from ZODB.blob import Blob
import interfaces
from storages import *
MAXCHUNKSIZE = 1 << 16
class FileIterator(object):
chunk_size = 4096
def __init__(self, fileobj, start, stop):
self.filename = fileobj.fileName
self.fileobj = fileobj.openDetached()
if start:
self.fileobj.seek(start)
if stop is not None:
self.length = stop - start
else:
self.length = None
def __iter__(self):
return self
def next(self):
if self.length is not None and self.length <= 0:
raise StopIteration
chunk = self.fileobj.read(self.chunk_size)
if not chunk:
raise StopIteration
if self.length is not None:
self.length -= len(chunk)
if self.length < 0:
chunk = chunk[:self.length]
return chunk
class File(Persistent):
"""A persistent content component storing binary file data."""
implements(interfaces.IBlobFile)
def __init__(self, data='', contentType='',filename=''):
self.contentType = contentType
self.fileName = filename
self._blob = Blob()
f = self._blob.open('w')
f.write('')
f.close()
self._setData(data)
self._modified = datetime.datetime.utcnow()
def open(self, mode='r'):
if mode != 'r' and 'size' in self.__dict__:
del self.__dict__['size']
return self._blob.open(mode)
def openDetached(self):
return open(self._blob.committed(), 'rb')
def _setData(self, data):
if 'size' in self.__dict__:
del self.__dict__['size']
if isinstance(data, cgi.FieldStorage):
data.file.seek(0)
fp = blob.open('w')
block = data.file.read(MAXCHUNKSIZE)
while block:
fp.write(block)
block = data.file.read(MAXCHUNKSIZE)
fp.close()
return
fp = blob.open('w')
fp.write(data)
fp.close()
def _getData(self):
fp = self._blob.open('r')
data = fp.read()
fp.close()
return data
_data = property(_getData, _setData)
data = property(_getData, _setData)
@property
def size(self):
if 'size' in self.__dict__:
return self.__dict__['size']
reader = self._blob.open()
reader.seek(0,2)
size = int(reader.tell())
reader.close()
self.__dict__['size'] = size
return size
def getSize(self):
return self.size
@property
def modified(self):
if hasattr(self, '_modified'):
return self._modified.strftime('%a, %d %b %Y %H:%M:%S GMT')
return ''
def __iter__(self):
return FileIterator(self, None, None)
def app_iter_range(self, start, stop):
return FileIterator(self, start, stop)
[/python]
This code enables you to save binary objects to ZODB. Upon commit the binary data is transparently saved to the file system. This is done by using code like this (assuming that the module containing the code above is called blobfile.py):
[python]
import mimetypes
from blobfile import File
# assuming that we use jQuery Uploadify
data = request.POST.get(‘Filedata’)
filename = request.POST.get(‘Filename’)
obj = app.objectByUid(’123456′)
blob = File(data=data, contentType=mimetypes.guess_type(filename)[0], filename)
setattr(obj, ‘attachment’, blob)
[/python]
The only part missing is the delivery of files upon request. First lets define a route for uploads:
[xml]
path="/attachment/:uid/:fieldname"
name="download-attachment"
view=".core.downloadAttachment"/>
[/xml]
The hard work is done in the downloadAttachment method.
[python]
def downloadAttachment(context, request):
obj = app.objectByUid(request.matchdict.get(‘uid’))
blob = getattr(obj, request.matchdict.get(‘fieldname’))
res = Response(content_type=blob.contentType,
conditional_response=True)
res.headers.add(‘Content-Disposition’, ‘attachment;filename=%s’ % blob.fileName)
res.app_iter = blob # must be blob.file.File
res.content_length = blob.size
res.last_modified = blob.modified
return res
[/python]