From: BenoƮt Pin Date: Mon, 25 Oct 2010 12:15:44 +0000 (+0200) Subject: recopie de OFS.Image pour remettre le support des blobs sur une base clean. X-Git-Url: https://scm.cri.ensmp.fr/git/Photo.git/commitdiff_plain/8bd9c946d25f740df11c44a67621b1220b6f7696?ds=inline;hp=0981d3aea237de05447f2d40c2e3ea311b64c635 recopie de OFS.Image pour remettre le support des blobs sur une base clean. --- diff --git a/blobbases.py b/blobbases.py index 9d2fb6f..0da0f8b 100755 --- a/blobbases.py +++ b/blobbases.py @@ -14,799 +14,926 @@ ############################################################################## """Image object -$Id: blobbases.py 949 2009-04-30 14:42:24Z pin $ -$URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/blobbases.py $ """ +from cgi import escape +from cStringIO import StringIO +from mimetools import choose_boundary import struct -from warnings import warn -from zope.contenttype import guess_content_type -from Globals import DTMLFile -from Globals import InitializeClass -from OFS.PropertyManager import PropertyManager -from AccessControl import ClassSecurityInfo -from AccessControl.Role import RoleManager + from AccessControl.Permissions import change_images_and_files from AccessControl.Permissions import view_management_screens from AccessControl.Permissions import view as View from AccessControl.Permissions import ftp_access from AccessControl.Permissions import delete_objects +from AccessControl.Role import RoleManager +from AccessControl.SecurityInfo import ClassSecurityInfo +from Acquisition import Implicit +from App.class_init import InitializeClass +from App.special_dtml import DTMLFile +from DateTime.DateTime import DateTime +from Persistence import Persistent from webdav.common import rfc1123_date +from webdav.interfaces import IWriteLock from webdav.Lockable import ResourceLockedError -from webdav.WriteLockInterface import WriteLockInterface -from OFS.SimpleItem import Item_w__name__ -from cStringIO import StringIO -from Globals import Persistent -from Acquisition import Implicit -from DateTime import DateTime -from OFS.Cache import Cacheable -from mimetools import choose_boundary from ZPublisher import HTTPRangeSupport from ZPublisher.HTTPRequest import FileUpload from ZPublisher.Iterators import filestream_iterator from zExceptions import Redirect -from cgi import escape -import transaction -from ZODB.blob import Blob - -CHUNK_SIZE = 1 << 16 - -manage_addFileForm=DTMLFile('dtml/imageAdd', globals(),Kind='File',kind='file') -def manage_addFile(self,id,file='',title='',precondition='', content_type='', - REQUEST=None): - """Add a new File object. - - Creates a new File object 'id' with the contents of 'file'""" - - id=str(id) - title=str(title) - content_type=str(content_type) - precondition=str(precondition) - - id, title = cookId(id, title, file) +from zope.contenttype import guess_content_type +from zope.interface import implementedBy +from zope.interface import implements - self=self.this() - self._setObject(id, File(id,title,file,content_type, precondition)) +from OFS.Cache import Cacheable +from OFS.PropertyManager import PropertyManager +from OFS.SimpleItem import Item_w__name__ - if REQUEST is not None: - REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main') +from zope.event import notify +from zope.lifecycleevent import ObjectModifiedEvent +from zope.lifecycleevent import ObjectCreatedEvent + +manage_addFileForm = DTMLFile('dtml/imageAdd', + globals(), + Kind='File', + kind='file', + ) +def manage_addFile(self, id, file='', title='', precondition='', + content_type='', REQUEST=None): + """Add a new File object. + + Creates a new File object 'id' with the contents of 'file'""" + + id = str(id) + title = str(title) + content_type = str(content_type) + precondition = str(precondition) + + id, title = cookId(id, title, file) + + self=self.this() + + # First, we create the file without data: + self._setObject(id, File(id,title,'',content_type, precondition)) + + newFile = self._getOb(id) + + # Now we "upload" the data. By doing this in two steps, we + # can use a database trick to make the upload more efficient. + if file: + newFile.manage_upload(file) + if content_type: + newFile.content_type=content_type + + notify(ObjectCreatedEvent(newFile)) + + if REQUEST is not None: + REQUEST['RESPONSE'].redirect(self.absolute_url()+'/manage_main') class File(Persistent, Implicit, PropertyManager, - RoleManager, Item_w__name__, Cacheable): - """A File object is a content object for arbitrary files.""" - - __implements__ = (WriteLockInterface, HTTPRangeSupport.HTTPRangeInterface) - meta_type='Blob File' - - security = ClassSecurityInfo() - security.declareObjectProtected(View) - - precondition='' - size=None - - manage_editForm =DTMLFile('dtml/fileEdit',globals(), - Kind='File',kind='file') - manage_editForm._setName('manage_editForm') - - security.declareProtected(view_management_screens, 'manage') - security.declareProtected(view_management_screens, 'manage_main') - manage=manage_main=manage_editForm - manage_uploadForm=manage_editForm - - manage_options=( - ( - {'label':'Edit', 'action':'manage_main', - 'help':('OFSP','File_Edit.stx')}, - {'label':'View', 'action':'', - 'help':('OFSP','File_View.stx')}, - ) - + PropertyManager.manage_options - + RoleManager.manage_options - + Item_w__name__.manage_options - + Cacheable.manage_options - ) - - _properties=({'id':'title', 'type': 'string'}, - {'id':'content_type', 'type':'string'}, - ) - - def __init__(self, id, title, file, content_type='', precondition=''): - self.__name__=id - self.title=title - self.precondition=precondition - self.uploaded_filename = cookId('', '', file)[0] - self.bdata = Blob() - - content_type=self._get_content_type(file, id, content_type) - self.update_data(file, content_type) - - security.declarePrivate('save') - def save(self, file): - bf = self.bdata.open('w') - bf.write(file.read()) - self.size = bf.tell() - bf.close() - - security.declarePrivate('open') - def open(self, mode='r'): - bf = self.bdata.open(mode) - return bf - - security.declarePrivate('updateSize') - def updateSize(self, size=None): - if size is None : - bf = self.open('r') - bf.seek(0,2) - self.size = bf.tell() - bf.close() - else : - self.size = size - - def _getLegacyData(self) : - warn("Accessing 'data' attribute may be inefficient with " - "this blob based file. You should refactor your product " - "by accessing data like: " - "f = self.open('r') " - "data = f.read()", - DeprecationWarning, stacklevel=2) - f = self.open() - data = f.read() - f.close() - return data - - def _setLegacyData(self, data) : - warn("Accessing 'data' attribute may be inefficient with " - "this blob based file. You should refactor your product " - "by accessing data like: " - "f = self.save(data)", - DeprecationWarning, stacklevel=2) - if isinstance(data, str) : - sio = StringIO() - sio.write(data) - sio.seek(0) - data = sio - self.save(data) - - data = property(_getLegacyData, _setLegacyData, - "Data Legacy attribute to ensure compatibility " - "with derived classes that access data by this way.") - - def id(self): - return self.__name__ - - def _if_modified_since_request_handler(self, REQUEST, RESPONSE): - # HTTP If-Modified-Since header handling: return True if - # we can handle this request by returning a 304 response - header=REQUEST.get_header('If-Modified-Since', None) - if header is not None: - header=header.split( ';')[0] - # Some proxies seem to send invalid date strings for this - # header. If the date string is not valid, we ignore it - # rather than raise an error to be generally consistent - # with common servers such as Apache (which can usually - # understand the screwy date string as a lucky side effect - # of the way they parse it). - # This happens to be what RFC2616 tells us to do in the face of an - # invalid date. - try: mod_since=long(DateTime(header).timeTime()) - except: mod_since=None - if mod_since is not None: - if self._p_mtime: - last_mod = long(self._p_mtime) - else: - last_mod = long(0) - if last_mod > 0 and last_mod <= mod_since: - RESPONSE.setHeader('Last-Modified', - rfc1123_date(self._p_mtime)) - RESPONSE.setHeader('Content-Type', self.content_type) - RESPONSE.setHeader('Accept-Ranges', 'bytes') - RESPONSE.setStatus(304) - return True - - def _range_request_handler(self, REQUEST, RESPONSE): - # HTTP Range header handling: return True if we've served a range - # chunk out of our data. - range = REQUEST.get_header('Range', None) - request_range = REQUEST.get_header('Request-Range', None) - if request_range is not None: - # Netscape 2 through 4 and MSIE 3 implement a draft version - # Later on, we need to serve a different mime-type as well. - range = request_range - if_range = REQUEST.get_header('If-Range', None) - if range is not None: - ranges = HTTPRangeSupport.parseRange(range) - - if if_range is not None: - # Only send ranges if the data isn't modified, otherwise send - # the whole object. Support both ETags and Last-Modified dates! - if len(if_range) > 1 and if_range[:2] == 'ts': - # ETag: - if if_range != self.http__etag(): - # Modified, so send a normal response. We delete - # the ranges, which causes us to skip to the 200 - # response. - ranges = None - else: - # Date - date = if_range.split( ';')[0] - try: mod_since=long(DateTime(date).timeTime()) - except: mod_since=None - if mod_since is not None: - if self._p_mtime: - last_mod = long(self._p_mtime) - else: - last_mod = long(0) - if last_mod > mod_since: - # Modified, so send a normal response. We delete - # the ranges, which causes us to skip to the 200 - # response. - ranges = None - - if ranges: - # Search for satisfiable ranges. - satisfiable = 0 - for start, end in ranges: - if start < self.size: - satisfiable = 1 - break - - if not satisfiable: - RESPONSE.setHeader('Content-Range', - 'bytes */%d' % self.size) - RESPONSE.setHeader('Accept-Ranges', 'bytes') - RESPONSE.setHeader('Last-Modified', - rfc1123_date(self._p_mtime)) - RESPONSE.setHeader('Content-Type', self.content_type) - RESPONSE.setHeader('Content-Length', self.size) - RESPONSE.setStatus(416) - return True - - ranges = HTTPRangeSupport.expandRanges(ranges, self.size) - - if len(ranges) == 1: - # Easy case, set extra header and return partial set. - start, end = ranges[0] - size = end - start - - RESPONSE.setHeader('Last-Modified', - rfc1123_date(self._p_mtime)) - RESPONSE.setHeader('Content-Type', self.content_type) - RESPONSE.setHeader('Content-Length', size) - RESPONSE.setHeader('Accept-Ranges', 'bytes') - RESPONSE.setHeader('Content-Range', - 'bytes %d-%d/%d' % (start, end - 1, self.size)) - RESPONSE.setStatus(206) # Partial content - - bf = self.open('r') - bf.seek(start) - RESPONSE.write(bf.read(size)) - bf.close() - return True - - else: - boundary = choose_boundary() - - # Calculate the content length - size = (8 + len(boundary) + # End marker length - len(ranges) * ( # Constant lenght per set - 49 + len(boundary) + len(self.content_type) + - len('%d' % self.size))) - for start, end in ranges: - # Variable length per set - size = (size + len('%d%d' % (start, end - 1)) + - end - start) - - - # Some clients implement an earlier draft of the spec, they - # will only accept x-byteranges. - draftprefix = (request_range is not None) and 'x-' or '' - - RESPONSE.setHeader('Content-Length', size) - RESPONSE.setHeader('Accept-Ranges', 'bytes') - RESPONSE.setHeader('Last-Modified', - rfc1123_date(self._p_mtime)) - RESPONSE.setHeader('Content-Type', - 'multipart/%sbyteranges; boundary=%s' % ( - draftprefix, boundary)) - RESPONSE.setStatus(206) # Partial content - - bf = self.open('r') - - for start, end in ranges: - RESPONSE.write('\r\n--%s\r\n' % boundary) - RESPONSE.write('Content-Type: %s\r\n' % - self.content_type) - RESPONSE.write( - 'Content-Range: bytes %d-%d/%d\r\n\r\n' % ( - start, end - 1, self.size)) - - - size = end - start - bf.seek(start) - RESPONSE.write(bf.read(size)) - - bf.close() - - RESPONSE.write('\r\n--%s--\r\n' % boundary) - return True - - security.declareProtected(View, 'index_html') - def index_html(self, REQUEST, RESPONSE): - """ - The default view of the contents of a File or Image. - - Returns the contents of the file or image. Also, sets the - Content-Type HTTP header to the objects content type. - """ - - if self._if_modified_since_request_handler(REQUEST, RESPONSE): - # we were able to handle this by returning a 304 - # unfortunately, because the HTTP cache manager uses the cache - # API, and because 304 responses are required to carry the Expires - # header for HTTP/1.1, we need to call ZCacheable_set here. - # This is nonsensical for caches other than the HTTP cache manager - # unfortunately. - self.ZCacheable_set(None) - return '' - - if self.precondition and hasattr(self, str(self.precondition)): - # Grab whatever precondition was defined and then - # execute it. The precondition will raise an exception - # if something violates its terms. - c=getattr(self, str(self.precondition)) - if hasattr(c,'isDocTemp') and c.isDocTemp: - c(REQUEST['PARENTS'][1],REQUEST) - else: - c() - - if self._range_request_handler(REQUEST, RESPONSE): - # we served a chunk of content in response to a range request. - return '' - - RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime)) - RESPONSE.setHeader('Content-Type', self.content_type) - RESPONSE.setHeader('Content-Length', self.size) - RESPONSE.setHeader('Accept-Ranges', 'bytes') - - if self.ZCacheable_isCachingEnabled(): - result = self.ZCacheable_get(default=None) - if result is not None: - # We will always get None from RAMCacheManager and HTTP - # Accelerated Cache Manager but we will get - # something implementing the IStreamIterator interface - # from a "FileCacheManager" - return result - - self.ZCacheable_set(None) - - bf = self.open('r') - chunk = bf.read(CHUNK_SIZE) - while chunk : - RESPONSE.write(chunk) - chunk = bf.read(CHUNK_SIZE) - bf.close() - return '' - - security.declareProtected(View, 'view_image_or_file') - def view_image_or_file(self, URL1): - """ - The default view of the contents of the File or Image. - """ - raise Redirect, URL1 - - security.declareProtected(View, 'PrincipiaSearchSource') - def PrincipiaSearchSource(self): - """ Allow file objects to be searched. - """ - if self.content_type.startswith('text/'): - bf = self.open('r') - data = bf.read() - bf.close() - return data - return '' - - security.declarePrivate('update_data') - def update_data(self, file, content_type=None): - if isinstance(file, unicode): - raise TypeError('Data can only be str or file-like. ' - 'Unicode objects are expressly forbidden.') - elif isinstance(file, str) : - sio = StringIO() - sio.write(file) - sio.seek(0) - file = sio - - if content_type is not None: self.content_type=content_type - self.save(file) - self.ZCacheable_invalidate() - self.ZCacheable_set(None) - self.http__refreshEtag() - - security.declareProtected(change_images_and_files, 'manage_edit') - def manage_edit(self, title, content_type, precondition='', - filedata=None, REQUEST=None): - """ - Changes the title and content type attributes of the File or Image. - """ - if self.wl_isLocked(): - raise ResourceLockedError, "File is locked via WebDAV" - - self.title=str(title) - self.content_type=str(content_type) - if precondition: self.precondition=str(precondition) - elif self.precondition: del self.precondition - if filedata is not None: - self.update_data(filedata, content_type) - else: - self.ZCacheable_invalidate() - if REQUEST: - message="Saved changes." - return self.manage_main(self,REQUEST,manage_tabs_message=message) - - security.declareProtected(change_images_and_files, 'manage_upload') - def manage_upload(self,file='',REQUEST=None): - """ - Replaces the current contents of the File or Image object with file. - - The file or images contents are replaced with the contents of 'file'. - """ - if self.wl_isLocked(): - raise ResourceLockedError, "File is locked via WebDAV" - - content_type=self._get_content_type(file, self.__name__, - 'application/octet-stream') - self.update_data(file, content_type) - - if REQUEST: - message="Saved changes." - return self.manage_main(self,REQUEST,manage_tabs_message=message) - - def _get_content_type(self, file, id, content_type=None): - headers=getattr(file, 'headers', None) - if headers and headers.has_key('content-type'): - content_type=headers['content-type'] - else: - name = getattr(file, 'filename', self.uploaded_filename) or id - content_type, enc=guess_content_type(name, '', content_type) - return content_type - - security.declareProtected(delete_objects, 'DELETE') - - security.declareProtected(change_images_and_files, 'PUT') - def PUT(self, REQUEST, RESPONSE): - """Handle HTTP PUT requests""" - self.dav__init(REQUEST, RESPONSE) - self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1) - type=REQUEST.get_header('content-type', None) - - file=REQUEST['BODYFILE'] - - content_type = self._get_content_type(file, self.__name__, - type or self.content_type) - self.update_data(file, content_type) - - RESPONSE.setStatus(204) - return RESPONSE - - security.declareProtected(View, 'get_size') - def get_size(self): - """Get the size of a file or image. - - Returns the size of the file or image. - """ - size=self.size - if size is None : - bf = self.open('r') - bf.seek(0,2) - self.size = size = bf.tell() - bf.close() - return size - - # deprecated; use get_size! - getSize=get_size - - security.declareProtected(View, 'getContentType') - def getContentType(self): - """Get the content type of a file or image. - - Returns the content type (MIME type) of a file or image. - """ - return self.content_type - - - def __str__(self): return str(self.data) - def __len__(self): return 1 - - security.declareProtected(ftp_access, 'manage_FTPstat') - security.declareProtected(ftp_access, 'manage_FTPlist') - - security.declareProtected(ftp_access, 'manage_FTPget') - def manage_FTPget(self): - """Return body for ftp.""" - RESPONSE = self.REQUEST.RESPONSE - - if self.ZCacheable_isCachingEnabled(): - result = self.ZCacheable_get(default=None) - if result is not None: - # We will always get None from RAMCacheManager but we will get - # something implementing the IStreamIterator interface - # from FileCacheManager. - # the content-length is required here by HTTPResponse, even - # though FTP doesn't use it. - RESPONSE.setHeader('Content-Length', self.size) - return result - - bf = self.open('r') - data = bf.read() - bf.close() - RESPONSE.setBase(None) - return data + RoleManager, Item_w__name__, Cacheable): + """A File object is a content object for arbitrary files.""" + + implements(implementedBy(Persistent), + implementedBy(Implicit), + implementedBy(PropertyManager), + implementedBy(RoleManager), + implementedBy(Item_w__name__), + implementedBy(Cacheable), + IWriteLock, + HTTPRangeSupport.HTTPRangeInterface, + ) + meta_type='File' + + security = ClassSecurityInfo() + security.declareObjectProtected(View) + + precondition='' + size=None + + manage_editForm =DTMLFile('dtml/fileEdit',globals(), + Kind='File',kind='file') + manage_editForm._setName('manage_editForm') + + security.declareProtected(view_management_screens, 'manage') + security.declareProtected(view_management_screens, 'manage_main') + manage=manage_main=manage_editForm + manage_uploadForm=manage_editForm + + manage_options=( + ( + {'label':'Edit', 'action':'manage_main', + 'help':('OFSP','File_Edit.stx')}, + {'label':'View', 'action':'', + 'help':('OFSP','File_View.stx')}, + ) + + PropertyManager.manage_options + + RoleManager.manage_options + + Item_w__name__.manage_options + + Cacheable.manage_options + ) + + _properties=({'id':'title', 'type': 'string'}, + {'id':'content_type', 'type':'string'}, + ) + + def __init__(self, id, title, file, content_type='', precondition=''): + self.__name__=id + self.title=title + self.precondition=precondition + + data, size = self._read_data(file) + content_type=self._get_content_type(file, data, id, content_type) + self.update_data(data, content_type, size) + + def id(self): + return self.__name__ + + def _if_modified_since_request_handler(self, REQUEST, RESPONSE): + # HTTP If-Modified-Since header handling: return True if + # we can handle this request by returning a 304 response + header=REQUEST.get_header('If-Modified-Since', None) + if header is not None: + header=header.split( ';')[0] + # Some proxies seem to send invalid date strings for this + # header. If the date string is not valid, we ignore it + # rather than raise an error to be generally consistent + # with common servers such as Apache (which can usually + # understand the screwy date string as a lucky side effect + # of the way they parse it). + # This happens to be what RFC2616 tells us to do in the face of an + # invalid date. + try: mod_since=long(DateTime(header).timeTime()) + except: mod_since=None + if mod_since is not None: + if self._p_mtime: + last_mod = long(self._p_mtime) + else: + last_mod = long(0) + if last_mod > 0 and last_mod <= mod_since: + RESPONSE.setHeader('Last-Modified', + rfc1123_date(self._p_mtime)) + RESPONSE.setHeader('Content-Type', self.content_type) + RESPONSE.setHeader('Accept-Ranges', 'bytes') + RESPONSE.setStatus(304) + return True + + def _range_request_handler(self, REQUEST, RESPONSE): + # HTTP Range header handling: return True if we've served a range + # chunk out of our data. + range = REQUEST.get_header('Range', None) + request_range = REQUEST.get_header('Request-Range', None) + if request_range is not None: + # Netscape 2 through 4 and MSIE 3 implement a draft version + # Later on, we need to serve a different mime-type as well. + range = request_range + if_range = REQUEST.get_header('If-Range', None) + if range is not None: + ranges = HTTPRangeSupport.parseRange(range) + + if if_range is not None: + # Only send ranges if the data isn't modified, otherwise send + # the whole object. Support both ETags and Last-Modified dates! + if len(if_range) > 1 and if_range[:2] == 'ts': + # ETag: + if if_range != self.http__etag(): + # Modified, so send a normal response. We delete + # the ranges, which causes us to skip to the 200 + # response. + ranges = None + else: + # Date + date = if_range.split( ';')[0] + try: mod_since=long(DateTime(date).timeTime()) + except: mod_since=None + if mod_since is not None: + if self._p_mtime: + last_mod = long(self._p_mtime) + else: + last_mod = long(0) + if last_mod > mod_since: + # Modified, so send a normal response. We delete + # the ranges, which causes us to skip to the 200 + # response. + ranges = None + + if ranges: + # Search for satisfiable ranges. + satisfiable = 0 + for start, end in ranges: + if start < self.size: + satisfiable = 1 + break + + if not satisfiable: + RESPONSE.setHeader('Content-Range', + 'bytes */%d' % self.size) + RESPONSE.setHeader('Accept-Ranges', 'bytes') + RESPONSE.setHeader('Last-Modified', + rfc1123_date(self._p_mtime)) + RESPONSE.setHeader('Content-Type', self.content_type) + RESPONSE.setHeader('Content-Length', self.size) + RESPONSE.setStatus(416) + return True + + ranges = HTTPRangeSupport.expandRanges(ranges, self.size) + + if len(ranges) == 1: + # Easy case, set extra header and return partial set. + start, end = ranges[0] + size = end - start + + RESPONSE.setHeader('Last-Modified', + rfc1123_date(self._p_mtime)) + RESPONSE.setHeader('Content-Type', self.content_type) + RESPONSE.setHeader('Content-Length', size) + RESPONSE.setHeader('Accept-Ranges', 'bytes') + RESPONSE.setHeader('Content-Range', + 'bytes %d-%d/%d' % (start, end - 1, self.size)) + RESPONSE.setStatus(206) # Partial content + + data = self.data + if isinstance(data, str): + RESPONSE.write(data[start:end]) + return True + + # Linked Pdata objects. Urgh. + pos = 0 + while data is not None: + l = len(data.data) + pos = pos + l + if pos > start: + # We are within the range + lstart = l - (pos - start) + + if lstart < 0: lstart = 0 + + # find the endpoint + if end <= pos: + lend = l - (pos - end) + + # Send and end transmission + RESPONSE.write(data[lstart:lend]) + break + + # Not yet at the end, transmit what we have. + RESPONSE.write(data[lstart:]) + + data = data.next + + return True + + else: + boundary = choose_boundary() + + # Calculate the content length + size = (8 + len(boundary) + # End marker length + len(ranges) * ( # Constant lenght per set + 49 + len(boundary) + len(self.content_type) + + len('%d' % self.size))) + for start, end in ranges: + # Variable length per set + size = (size + len('%d%d' % (start, end - 1)) + + end - start) + + + # Some clients implement an earlier draft of the spec, they + # will only accept x-byteranges. + draftprefix = (request_range is not None) and 'x-' or '' + + RESPONSE.setHeader('Content-Length', size) + RESPONSE.setHeader('Accept-Ranges', 'bytes') + RESPONSE.setHeader('Last-Modified', + rfc1123_date(self._p_mtime)) + RESPONSE.setHeader('Content-Type', + 'multipart/%sbyteranges; boundary=%s' % ( + draftprefix, boundary)) + RESPONSE.setStatus(206) # Partial content + + data = self.data + # The Pdata map allows us to jump into the Pdata chain + # arbitrarily during out-of-order range searching. + pdata_map = {} + pdata_map[0] = data + + for start, end in ranges: + RESPONSE.write('\r\n--%s\r\n' % boundary) + RESPONSE.write('Content-Type: %s\r\n' % + self.content_type) + RESPONSE.write( + 'Content-Range: bytes %d-%d/%d\r\n\r\n' % ( + start, end - 1, self.size)) + + if isinstance(data, str): + RESPONSE.write(data[start:end]) + + else: + # Yippee. Linked Pdata objects. The following + # calculations allow us to fast-forward through the + # Pdata chain without a lot of dereferencing if we + # did the work already. + first_size = len(pdata_map[0].data) + if start < first_size: + closest_pos = 0 + else: + closest_pos = ( + ((start - first_size) >> 16 << 16) + + first_size) + pos = min(closest_pos, max(pdata_map.keys())) + data = pdata_map[pos] + + while data is not None: + l = len(data.data) + pos = pos + l + if pos > start: + # We are within the range + lstart = l - (pos - start) + + if lstart < 0: lstart = 0 + + # find the endpoint + if end <= pos: + lend = l - (pos - end) + + # Send and loop to next range + RESPONSE.write(data[lstart:lend]) + break + + # Not yet at the end, transmit what we have. + RESPONSE.write(data[lstart:]) + + data = data.next + # Store a reference to a Pdata chain link so we + # don't have to deref during this request again. + pdata_map[pos] = data + + # Do not keep the link references around. + del pdata_map + + RESPONSE.write('\r\n--%s--\r\n' % boundary) + return True + + security.declareProtected(View, 'index_html') + def index_html(self, REQUEST, RESPONSE): + """ + The default view of the contents of a File or Image. + + Returns the contents of the file or image. Also, sets the + Content-Type HTTP header to the objects content type. + """ + + if self._if_modified_since_request_handler(REQUEST, RESPONSE): + # we were able to handle this by returning a 304 + # unfortunately, because the HTTP cache manager uses the cache + # API, and because 304 responses are required to carry the Expires + # header for HTTP/1.1, we need to call ZCacheable_set here. + # This is nonsensical for caches other than the HTTP cache manager + # unfortunately. + self.ZCacheable_set(None) + return '' + + if self.precondition and hasattr(self, str(self.precondition)): + # Grab whatever precondition was defined and then + # execute it. The precondition will raise an exception + # if something violates its terms. + c=getattr(self, str(self.precondition)) + if hasattr(c,'isDocTemp') and c.isDocTemp: + c(REQUEST['PARENTS'][1],REQUEST) + else: + c() + + if self._range_request_handler(REQUEST, RESPONSE): + # we served a chunk of content in response to a range request. + return '' + + RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime)) + RESPONSE.setHeader('Content-Type', self.content_type) + RESPONSE.setHeader('Content-Length', self.size) + RESPONSE.setHeader('Accept-Ranges', 'bytes') + + if self.ZCacheable_isCachingEnabled(): + result = self.ZCacheable_get(default=None) + if result is not None: + # We will always get None from RAMCacheManager and HTTP + # Accelerated Cache Manager but we will get + # something implementing the IStreamIterator interface + # from a "FileCacheManager" + return result + + self.ZCacheable_set(None) + + data=self.data + if isinstance(data, str): + RESPONSE.setBase(None) + return data + + while data is not None: + RESPONSE.write(data.data) + data=data.next + + return '' + + security.declareProtected(View, 'view_image_or_file') + def view_image_or_file(self, URL1): + """ + The default view of the contents of the File or Image. + """ + raise Redirect, URL1 + + security.declareProtected(View, 'PrincipiaSearchSource') + def PrincipiaSearchSource(self): + """ Allow file objects to be searched. + """ + if self.content_type.startswith('text/'): + return str(self.data) + return '' + + security.declarePrivate('update_data') + def update_data(self, data, content_type=None, size=None): + if isinstance(data, unicode): + raise TypeError('Data can only be str or file-like. ' + 'Unicode objects are expressly forbidden.') + + if content_type is not None: self.content_type=content_type + if size is None: size=len(data) + self.size=size + self.data=data + self.ZCacheable_invalidate() + self.ZCacheable_set(None) + self.http__refreshEtag() + + security.declareProtected(change_images_and_files, 'manage_edit') + def manage_edit(self, title, content_type, precondition='', + filedata=None, REQUEST=None): + """ + Changes the title and content type attributes of the File or Image. + """ + if self.wl_isLocked(): + raise ResourceLockedError, "File is locked via WebDAV" + + self.title=str(title) + self.content_type=str(content_type) + if precondition: self.precondition=str(precondition) + elif self.precondition: del self.precondition + if filedata is not None: + self.update_data(filedata, content_type, len(filedata)) + else: + self.ZCacheable_invalidate() + + notify(ObjectModifiedEvent(self)) + + if REQUEST: + message="Saved changes." + return self.manage_main(self,REQUEST,manage_tabs_message=message) + + security.declareProtected(change_images_and_files, 'manage_upload') + def manage_upload(self,file='',REQUEST=None): + """ + Replaces the current contents of the File or Image object with file. + + The file or images contents are replaced with the contents of 'file'. + """ + if self.wl_isLocked(): + raise ResourceLockedError, "File is locked via WebDAV" + + data, size = self._read_data(file) + content_type=self._get_content_type(file, data, self.__name__, + 'application/octet-stream') + self.update_data(data, content_type, size) + + notify(ObjectModifiedEvent(self)) + + if REQUEST: + message="Saved changes." + return self.manage_main(self,REQUEST,manage_tabs_message=message) + + def _get_content_type(self, file, body, id, content_type=None): + headers=getattr(file, 'headers', None) + if headers and headers.has_key('content-type'): + content_type=headers['content-type'] + else: + if not isinstance(body, str): body=body.data + content_type, enc=guess_content_type( + getattr(file, 'filename',id), body, content_type) + return content_type + + def _read_data(self, file): + import transaction + + n=1 << 16 + + if isinstance(file, str): + size=len(file) + if size < n: return file, size + # Big string: cut it into smaller chunks + file = StringIO(file) + + if isinstance(file, FileUpload) and not file: + raise ValueError, 'File not specified' + + if hasattr(file, '__class__') and file.__class__ is Pdata: + size=len(file) + return file, size + + seek=file.seek + read=file.read + + seek(0,2) + size=end=file.tell() + + if size <= 2*n: + seek(0) + if size < n: return read(size), size + return Pdata(read(size)), size + + # Make sure we have an _p_jar, even if we are a new object, by + # doing a sub-transaction commit. + transaction.savepoint(optimistic=True) + + if self._p_jar is None: + # Ugh + seek(0) + return Pdata(read(size)), size + + # Now we're going to build a linked list from back + # to front to minimize the number of database updates + # and to allow us to get things out of memory as soon as + # possible. + next = None + while end > 0: + pos = end-n + if pos < n: + pos = 0 # we always want at least n bytes + seek(pos) + + # Create the object and assign it a next pointer + # in the same transaction, so that there is only + # a single database update for it. + data = Pdata(read(end-pos)) + self._p_jar.add(data) + data.next = next + + # Save the object so that we can release its memory. + transaction.savepoint(optimistic=True) + data._p_deactivate() + # The object should be assigned an oid and be a ghost. + assert data._p_oid is not None + assert data._p_state == -1 + + next = data + end = pos + + return next, size + + security.declareProtected(delete_objects, 'DELETE') + + security.declareProtected(change_images_and_files, 'PUT') + def PUT(self, REQUEST, RESPONSE): + """Handle HTTP PUT requests""" + self.dav__init(REQUEST, RESPONSE) + self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1) + type=REQUEST.get_header('content-type', None) + + file=REQUEST['BODYFILE'] + + data, size = self._read_data(file) + content_type=self._get_content_type(file, data, self.__name__, + type or self.content_type) + self.update_data(data, content_type, size) + + RESPONSE.setStatus(204) + return RESPONSE + + security.declareProtected(View, 'get_size') + def get_size(self): + """Get the size of a file or image. + + Returns the size of the file or image. + """ + size=self.size + if size is None: size=len(self.data) + return size + + # deprecated; use get_size! + getSize=get_size + + security.declareProtected(View, 'getContentType') + def getContentType(self): + """Get the content type of a file or image. + + Returns the content type (MIME type) of a file or image. + """ + return self.content_type + + + def __str__(self): return str(self.data) + def __len__(self): return 1 + + security.declareProtected(ftp_access, 'manage_FTPstat') + security.declareProtected(ftp_access, 'manage_FTPlist') + + security.declareProtected(ftp_access, 'manage_FTPget') + def manage_FTPget(self): + """Return body for ftp.""" + RESPONSE = self.REQUEST.RESPONSE + + if self.ZCacheable_isCachingEnabled(): + result = self.ZCacheable_get(default=None) + if result is not None: + # We will always get None from RAMCacheManager but we will get + # something implementing the IStreamIterator interface + # from FileCacheManager. + # the content-length is required here by HTTPResponse, even + # though FTP doesn't use it. + RESPONSE.setHeader('Content-Length', self.size) + return result + + data = self.data + if isinstance(data, str): + RESPONSE.setBase(None) + return data + + while data is not None: + RESPONSE.write(data.data) + data = data.next + + return '' manage_addImageForm=DTMLFile('dtml/imageAdd',globals(), - Kind='Image',kind='image') + Kind='Image',kind='image') def manage_addImage(self, id, file, title='', precondition='', content_type='', - REQUEST=None): - """ - Add a new Image object. - - Creates a new Image object 'id' with the contents of 'file'. - """ - - id=str(id) - title=str(title) - content_type=str(content_type) - precondition=str(precondition) - - id, title = cookId(id, title, file) - - self=self.this() - self._setObject(id, Image(id,title,file,content_type, precondition)) - - if REQUEST is not None: - try: url=self.DestinationURL() - except: url=REQUEST['URL1'] - REQUEST.RESPONSE.redirect('%s/manage_main' % url) - return id - - -def getImageInfo(file): - height = -1 - width = -1 - content_type = '' - - # handle GIFs - data = file.read(24) - size = len(data) - if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'): - # Check to see if content_type is correct - content_type = 'image/gif' - w, h = struct.unpack("= 24) and (data[:8] == '\211PNG\r\n\032\n') - and (data[12:16] == 'IHDR')): - content_type = 'image/png' - w, h = struct.unpack(">LL", data[16:24]) - width = int(w) - height = int(h) - - # Maybe this is for an older PNG version. - elif (size >= 16) and (data[:8] == '\211PNG\r\n\032\n'): - # Check to see if we have the right content type - content_type = 'image/png' - w, h = struct.unpack(">LL", data[8:16]) - width = int(w) - height = int(h) - - # handle JPEGs - elif (size >= 2) and (data[:2] == '\377\330'): - content_type = 'image/jpeg' - jpeg = file - jpeg.seek(0) - jpeg.read(2) - b = jpeg.read(1) - try: - while (b and ord(b) != 0xDA): - while (ord(b) != 0xFF): b = jpeg.read(1) - while (ord(b) == 0xFF): b = jpeg.read(1) - if (ord(b) >= 0xC0 and ord(b) <= 0xC3): - jpeg.read(3) - h, w = struct.unpack(">HH", jpeg.read(4)) - break - else: - jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2) - b = jpeg.read(1) - width = int(w) - height = int(h) - except: pass - - return content_type, width, height + REQUEST=None): + """ + Add a new Image object. + + Creates a new Image object 'id' with the contents of 'file'. + """ + + id=str(id) + title=str(title) + content_type=str(content_type) + precondition=str(precondition) + + id, title = cookId(id, title, file) + + self=self.this() + + # First, we create the image without data: + self._setObject(id, Image(id,title,'',content_type, precondition)) + + newFile = self._getOb(id) + + # Now we "upload" the data. By doing this in two steps, we + # can use a database trick to make the upload more efficient. + if file: + newFile.manage_upload(file) + if content_type: + newFile.content_type=content_type + + notify(ObjectCreatedEvent(newFile)) + + if REQUEST is not None: + try: url=self.DestinationURL() + except: url=REQUEST['URL1'] + REQUEST.RESPONSE.redirect('%s/manage_main' % url) + return id + + +def getImageInfo(data): + data = str(data) + size = len(data) + height = -1 + width = -1 + content_type = '' + + # handle GIFs + if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'): + # Check to see if content_type is correct + content_type = 'image/gif' + w, h = struct.unpack("= 24) and (data[:8] == '\211PNG\r\n\032\n') + and (data[12:16] == 'IHDR')): + content_type = 'image/png' + w, h = struct.unpack(">LL", data[16:24]) + width = int(w) + height = int(h) + + # Maybe this is for an older PNG version. + elif (size >= 16) and (data[:8] == '\211PNG\r\n\032\n'): + # Check to see if we have the right content type + content_type = 'image/png' + w, h = struct.unpack(">LL", data[8:16]) + width = int(w) + height = int(h) + + # handle JPEGs + elif (size >= 2) and (data[:2] == '\377\330'): + content_type = 'image/jpeg' + jpeg = StringIO(data) + jpeg.read(2) + b = jpeg.read(1) + try: + while (b and ord(b) != 0xDA): + while (ord(b) != 0xFF): b = jpeg.read(1) + while (ord(b) == 0xFF): b = jpeg.read(1) + if (ord(b) >= 0xC0 and ord(b) <= 0xC3): + jpeg.read(3) + h, w = struct.unpack(">HH", jpeg.read(4)) + break + else: + jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0])-2) + b = jpeg.read(1) + width = int(w) + height = int(h) + except: pass + + return content_type, width, height class Image(File): - """Image objects can be GIF, PNG or JPEG and have the same methods - as File objects. Images also have a string representation that - renders an HTML 'IMG' tag. - """ - __implements__ = (WriteLockInterface,) - meta_type='Blob Image' - - security = ClassSecurityInfo() - security.declareObjectProtected(View) - - alt='' - height='' - width='' - - # FIXME: Redundant, already in base class - security.declareProtected(change_images_and_files, 'manage_edit') - security.declareProtected(change_images_and_files, 'manage_upload') - security.declareProtected(change_images_and_files, 'PUT') - security.declareProtected(View, 'index_html') - security.declareProtected(View, 'get_size') - security.declareProtected(View, 'getContentType') - security.declareProtected(ftp_access, 'manage_FTPstat') - security.declareProtected(ftp_access, 'manage_FTPlist') - security.declareProtected(ftp_access, 'manage_FTPget') - security.declareProtected(delete_objects, 'DELETE') - - _properties=({'id':'title', 'type': 'string'}, - {'id':'alt', 'type':'string'}, - {'id':'content_type', 'type':'string','mode':'w'}, - {'id':'height', 'type':'string'}, - {'id':'width', 'type':'string'}, - ) - - manage_options=( - ({'label':'Edit', 'action':'manage_main', - 'help':('OFSP','Image_Edit.stx')}, - {'label':'View', 'action':'view_image_or_file', - 'help':('OFSP','Image_View.stx')},) - + PropertyManager.manage_options - + RoleManager.manage_options - + Item_w__name__.manage_options - + Cacheable.manage_options - ) - - manage_editForm =DTMLFile('dtml/imageEdit',globals(), - Kind='Image',kind='image') - manage_editForm._setName('manage_editForm') - - security.declareProtected(View, 'view_image_or_file') - view_image_or_file =DTMLFile('dtml/imageView',globals()) - - security.declareProtected(view_management_screens, 'manage') - security.declareProtected(view_management_screens, 'manage_main') - manage=manage_main=manage_editForm - manage_uploadForm=manage_editForm - - security.declarePrivate('update_data') - def update_data(self, file, content_type=None): - super(Image, self).update_data(file, content_type) - self.updateFormat(size=self.size, content_type=content_type) - - security.declarePrivate('updateFormat') - def updateFormat(self, size=None, dimensions=None, content_type=None): - self.updateSize(size=size) - - if dimensions is None or content_type is None : - bf = self.open('r') - ct, width, height = getImageInfo(bf) - bf.close() - if ct: - content_type = ct - if width >= 0 and height >= 0: - self.width = width - self.height = height - - # Now we should have the correct content type, or still None - if content_type is not None: self.content_type = content_type - else : - self.width, self.height = dimensions - self.content_type = content_type - - def __str__(self): - return self.tag() - - security.declareProtected(View, 'tag') - def tag(self, height=None, width=None, alt=None, - scale=0, xscale=0, yscale=0, css_class=None, title=None, **args): - """ - Generate an HTML IMG tag for this image, with customization. - Arguments to self.tag() can be any valid attributes of an IMG tag. - 'src' will always be an absolute pathname, to prevent redundant - downloading of images. Defaults are applied intelligently for - 'height', 'width', and 'alt'. If specified, the 'scale', 'xscale', - and 'yscale' keyword arguments will be used to automatically adjust - the output height and width values of the image tag. - - Since 'class' is a Python reserved word, it cannot be passed in - directly in keyword arguments which is a problem if you are - trying to use 'tag()' to include a CSS class. The tag() method - will accept a 'css_class' argument that will be converted to - 'class' in the output tag to work around this. - """ - if height is None: height=self.height - if width is None: width=self.width - - # Auto-scaling support - xdelta = xscale or scale - ydelta = yscale or scale - - if xdelta and width: - width = str(int(round(int(width) * xdelta))) - if ydelta and height: - height = str(int(round(int(height) * ydelta))) - - result='' % result + """Image objects can be GIF, PNG or JPEG and have the same methods + as File objects. Images also have a string representation that + renders an HTML 'IMG' tag. + """ + meta_type='Image' + + security = ClassSecurityInfo() + security.declareObjectProtected(View) + + alt='' + height='' + width='' + + # FIXME: Redundant, already in base class + security.declareProtected(change_images_and_files, 'manage_edit') + security.declareProtected(change_images_and_files, 'manage_upload') + security.declareProtected(change_images_and_files, 'PUT') + security.declareProtected(View, 'index_html') + security.declareProtected(View, 'get_size') + security.declareProtected(View, 'getContentType') + security.declareProtected(ftp_access, 'manage_FTPstat') + security.declareProtected(ftp_access, 'manage_FTPlist') + security.declareProtected(ftp_access, 'manage_FTPget') + security.declareProtected(delete_objects, 'DELETE') + + _properties=({'id':'title', 'type': 'string'}, + {'id':'alt', 'type':'string'}, + {'id':'content_type', 'type':'string','mode':'w'}, + {'id':'height', 'type':'string'}, + {'id':'width', 'type':'string'}, + ) + + manage_options=( + ({'label':'Edit', 'action':'manage_main', + 'help':('OFSP','Image_Edit.stx')}, + {'label':'View', 'action':'view_image_or_file', + 'help':('OFSP','Image_View.stx')},) + + PropertyManager.manage_options + + RoleManager.manage_options + + Item_w__name__.manage_options + + Cacheable.manage_options + ) + + manage_editForm =DTMLFile('dtml/imageEdit',globals(), + Kind='Image',kind='image') + manage_editForm._setName('manage_editForm') + + security.declareProtected(View, 'view_image_or_file') + view_image_or_file =DTMLFile('dtml/imageView',globals()) + + security.declareProtected(view_management_screens, 'manage') + security.declareProtected(view_management_screens, 'manage_main') + manage=manage_main=manage_editForm + manage_uploadForm=manage_editForm + + security.declarePrivate('update_data') + def update_data(self, data, content_type=None, size=None): + if isinstance(data, unicode): + raise TypeError('Data can only be str or file-like. ' + 'Unicode objects are expressly forbidden.') + + if size is None: size=len(data) + + self.size=size + self.data=data + + ct, width, height = getImageInfo(data) + if ct: + content_type = ct + if width >= 0 and height >= 0: + self.width = width + self.height = height + + # Now we should have the correct content type, or still None + if content_type is not None: self.content_type = content_type + + self.ZCacheable_invalidate() + self.ZCacheable_set(None) + self.http__refreshEtag() + + def __str__(self): + return self.tag() + + security.declareProtected(View, 'tag') + def tag(self, height=None, width=None, alt=None, + scale=0, xscale=0, yscale=0, css_class=None, title=None, **args): + """ + Generate an HTML IMG tag for this image, with customization. + Arguments to self.tag() can be any valid attributes of an IMG tag. + 'src' will always be an absolute pathname, to prevent redundant + downloading of images. Defaults are applied intelligently for + 'height', 'width', and 'alt'. If specified, the 'scale', 'xscale', + and 'yscale' keyword arguments will be used to automatically adjust + the output height and width values of the image tag. + + Since 'class' is a Python reserved word, it cannot be passed in + directly in keyword arguments which is a problem if you are + trying to use 'tag()' to include a CSS class. The tag() method + will accept a 'css_class' argument that will be converted to + 'class' in the output tag to work around this. + """ + if height is None: height=self.height + if width is None: width=self.width + + # Auto-scaling support + xdelta = xscale or scale + ydelta = yscale or scale + + if xdelta and width: + width = str(int(round(int(width) * xdelta))) + if ydelta and height: + height = str(int(round(int(height) * ydelta))) + + result='' % result def cookId(id, title, file): - if not id and hasattr(file,'filename'): - filename=file.filename - title=title or filename - id=filename[max(filename.rfind('/'), - filename.rfind('\\'), - filename.rfind(':'), - )+1:] - return id, title - -#class Pdata(Persistent, Implicit): -# # Wrapper for possibly large data -# -# next=None -# -# def __init__(self, data): -# self.data=data -# -# def __getslice__(self, i, j): -# return self.data[i:j] -# -# def __len__(self): -# data = str(self) -# return len(data) -# -# def __str__(self): -# next=self.next -# if next is None: return self.data -# -# r=[self.data] -# while next is not None: -# self=next -# r.append(self.data) -# next=self.next -# -# return ''.join(r) + if not id and hasattr(file,'filename'): + filename=file.filename + title=title or filename + id=filename[max(filename.rfind('/'), + filename.rfind('\\'), + filename.rfind(':'), + )+1:] + return id, title + +class Pdata(Persistent, Implicit): + # Wrapper for possibly large data + + next=None + + def __init__(self, data): + self.data=data + + def __getslice__(self, i, j): + return self.data[i:j] + + def __len__(self): + data = str(self) + return len(data) + + def __str__(self): + next=self.next + if next is None: return self.data + + r=[self.data] + while next is not None: + self=next + r.append(self.data) + next=self.next + + return ''.join(r)