X-Git-Url: https://scm.cri.minesparis.psl.eu/git/Photo.git/blobdiff_plain/b0a7e10b4f32cf74864bb53268ca4d3080f23bc0..6c41809185e322ce2d30e98234f71144f78f06c0:/blobbases.py?ds=sidebyside diff --git a/blobbases.py b/blobbases.py deleted file mode 100755 index 964ffe3..0000000 --- a/blobbases.py +++ /dev/null @@ -1,819 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# This module is based on OFS.Image originaly copyrighted as: -# -# Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -"""Image object - -""" - -from cgi import escape -from cStringIO import StringIO -from mimetools import choose_boundary -import struct -from warnings import warn - -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 ZPublisher import HTTPRangeSupport -from ZPublisher.HTTPRequest import FileUpload -from ZPublisher.Iterators import filestream_iterator -from zExceptions import Redirect -from zope.contenttype import guess_content_type -from zope.interface import implementedBy -from zope.interface import implements - -from OFS.Cache import Cacheable -from OFS.PropertyManager import PropertyManager -from OFS.SimpleItem import Item_w__name__ - -from zope.event import notify -from zope.lifecycleevent import ObjectModifiedEvent -from zope.lifecycleevent import ObjectCreatedEvent - -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) - - self=self.this() - self._setObject(id, File(id,title,file,content_type, precondition)) - - newFile = self._getOb(id) - 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(implementedBy(Persistent), - implementedBy(Implicit), - implementedBy(PropertyManager), - implementedBy(RoleManager), - implementedBy(Item_w__name__), - implementedBy(Cacheable), - IWriteLock, - 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') -# 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)) - - - 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() - - 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" - - content_type=self._get_content_type(file, self.__name__, - 'application/octet-stream') - self.update_data(file, content_type) - notify(ObjectModifiedEvent(self)) - - 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 - -manage_addImageForm=DTMLFile('dtml/imageAdd',globals(), - 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)) - - newFile = self._getOb(id) - 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(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 - - -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. - """ - 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 - - -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