""" ExtFile product module """ # -*- coding: latin-1 -*- ############################################################################### # # Copyright (c) 2001 Gregor Heine . All rights reserved. # ExtFile Home: http://www.zope.org/Members/MacGregor/ExtFile/index_html # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission # # Disclaimer # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # In accordance with the license provided for by the software upon # which some of the source code has been derived or used, the following # acknowledgement is hereby provided : # # "This product includes software developed by Digital Creations # for use in the Z Object Publishing Environment # (http://www.zope.org/)." # ############################################################################### __doc__ = """ExtFile product module. The ExtFile-Product works like the Zope File-product, but stores the uploaded file externally in a repository-direcory.""" __version__='1.5.4' from Products.ZCatalog.CatalogPathAwareness import CatalogAware from OFS.SimpleItem import SimpleItem from OFS.PropertyManager import PropertyManager from OFS.Cache import Cacheable from Globals import HTMLFile, MessageDialog, InitializeClass, package_home from AccessControl import ClassSecurityInfo, getSecurityManager from AccessControl import Permissions from Acquisition import aq_acquire from mimetypes import guess_extension from webdav.Lockable import ResourceLockedError from webdav.common import rfc1123_date from DateTime import DateTime import urllib, os, string, types, sha, base64 from os.path import join, isfile from tempfile import TemporaryFile from Products.ExtFile import TM from webdav.WriteLockInterface import WriteLockInterface from IExtFile import IExtFile #from zLOG import * _SUBSYS = 'ExtFile' _debug = 0 #ersetzt zLOG -- Aenderung DW 24.1.2007 from logging import * def LOG(txt,method,txt2): """logging""" info(txt+ txt2) try: import Zope2 except ImportError: ZOPE28 = 0 else: ZOPE28 = 1 try: from zope.contenttype import guess_content_type except ImportError: try: from zope.app.content_types import guess_content_type except ImportError: from OFS.content_types import guess_content_type try: from zExceptions import Redirect except ImportError: Redirect = 'Redirect' try: from ZPublisher.Iterators import IStreamIterator except ImportError: IStreamIterator = None ViewPermission = Permissions.view AccessPermission = Permissions.view_management_screens ChangePermission = 'Change ExtFile/ExtImage' DownloadPermission = 'Download ExtFile/ExtImage' import re copy_of_re = re.compile('(^(copy[0-9]*_of_)+)') from Config import * manage_addExtFileForm = HTMLFile('dtml/extFileAdd', globals()) def manage_addExtFile(self, id='', title='', descr='', file='', content_type='', permission_check=0, redirect_default_view=0, REQUEST=None): """ Add an ExtFile to a folder. """ if not id and getattr(file, 'filename', None) is not None: # generate id from filename and make sure, it has no 'bad' chars id = file.filename id = id[max(string.rfind(id,'/'), string.rfind(id,'\\'), string.rfind(id,':'))+1:] title = title or id id = normalize_id(id) tempExtFile = ExtFile(id, title, descr, permission_check, redirect_default_view) self._setObject(id, tempExtFile) if file != '': self._getOb(id).manage_file_upload(file, content_type) if REQUEST is not None: return self.manage_main(self, REQUEST, update_menu=0) return id class ExtFile(CatalogAware, SimpleItem, PropertyManager, Cacheable): """ The ExtFile-Product works like the Zope File-product, but stores the uploaded file externally in a repository-direcory. """ __implements__ = (IExtFile, WriteLockInterface) # what properties have we? _properties = ( {'id':'title', 'type':'string', 'mode': 'w'}, {'id':'descr', 'type':'text', 'mode': 'w'}, {'id':'content_type', 'type':'string', 'mode': 'w'}, {'id':'use_download_permission_check', 'type':'boolean', 'mode': 'w'}, {'id':'redirect_default_view', 'type':'boolean', 'mode': 'w'}, ) use_download_permission_check = 0 redirect_default_view = 0 # what management options are there? manage_options = (( {'label':'Edit', 'action': 'manage_main' }, {'label':'View', 'action': '' }, {'label':'Upload', 'action': 'manage_uploadForm' },) + PropertyManager.manage_options + SimpleItem.manage_options[1:] + Cacheable.manage_options ) security = ClassSecurityInfo() # what do people think they're adding? meta_type = 'ExtFile' # location of the file-repository _repository = REPOSITORY_PATH # make sure the download permission is available security.setPermissionDefault(DownloadPermission, ('Manager',)) # the above does not work in Zope < 2.8 if not ZOPE28: security.declareProtected(DownloadPermission, '_dummy') # MIME-Type Dictionary. To add a MIME-Type, add a file in the directory # icons/_category_/_subcategory-icon-file_ # example: Icon tifficon.gif for the MIME-Type image/tiff goes to # icons/image/tifficon.gif and the dictionary must be updated like this: # 'image':{'tiff':'tifficon.gif','default':'default.gif'}, ... _types={'image': {'default':'default.gif'}, 'text': {'html':'html.gif', 'xml':'xml.gif', 'default':'default.gif', 'python':'py.gif'}, 'application': {'pdf':'pdf.gif', 'zip':'zip.gif', 'tar':'zip.gif', 'msword':'doc.gif', 'excel':'xls.gif', 'powerpoint':'ppt.gif', 'default':'default.gif'}, 'video': {'default':'default.gif'}, 'audio': {'default':'default.gif'}, 'default':'default.gif' } ################################ # Init method # ################################ def __init__(self, id, title='', descr='', permission_check=0, redirect_default_view=0): """ Initialize a new instance of ExtFile """ self.id = id self.title = title self.descr = descr self.use_download_permission_check = permission_check self.redirect_default_view = redirect_default_view self.__version__ = __version__ self.filename = [] self.content_type = '' ################################ # Public methods # ################################ def __str__(self): return self.index_html() def __len__(self): return 1 def _if_modified_since_request_handler(self, REQUEST): """ 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 = string.split(header, ';')[0] 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: # Set headers for Apache caching last_mod = rfc1123_date(self._p_mtime) REQUEST.RESPONSE.setHeader('Last-Modified', last_mod) REQUEST.RESPONSE.setHeader('Content-Type', self.content_type) # RFC violation. See http://collector.zope.org/Zope/544 #REQUEST.RESPONSE.setHeader('Content-Length', self.get_size()) REQUEST.RESPONSE.setStatus(304) return 1 def _redirect_default_view_request_handler(self, icon, preview, REQUEST): """ redirect_default_view property handling: return True if we can handle this request by returning a 302 response. Patch provided by Oliver Bleutgen. """ if self.redirect_default_view: if self.static_mode() and not icon: static_url = self._static_url(preview=preview) if static_url != self.absolute_url(): REQUEST.RESPONSE.redirect(static_url) return 1 security.declareProtected(ViewPermission, 'index_html') def index_html (self, icon=0, preview=0, width=None, height=None, REQUEST=None): """ Return the file with it's corresponding MIME-type """ if REQUEST is not None: if self._if_modified_since_request_handler(REQUEST): self.ZCacheable_set(None) return '' if self._redirect_default_view_request_handler(icon, preview, REQUEST): return '' filename, content_type, icon, preview = self._get_file_to_serve(icon, preview) filename = self._get_fsname(filename) if _debug > 1: LOG(_SUBSYS, INFO, 'serving %s, %s, %s, %s' %(filename, content_type, icon, preview)) cant_read_exc = "Can't read: " if filename: try: size = os.stat(filename)[6] except: raise cant_read_exc, ("%s (%s)" %(self.id, filename)) else: filename = join(package_home(globals()), 'icons', 'broken.gif') try: size = os.stat(filename)[6] except: raise cant_read_exc, ("%s (%s)" %(self.id, filename)) content_type = 'image/gif' icon = 1 if icon==0 and width is not None and height is not None: data = TemporaryFile() # hold resized image try: from PIL import Image im = Image.open(filename) if im.mode!='RGB': im = im.convert('RGB') filter = Image.BICUBIC if hasattr(Image, 'ANTIALIAS'): # PIL 1.1.3 filter = Image.ANTIALIAS im = im.resize((int(width),int(height)), filter) im.save(data, 'JPEG', quality=85) except: data = open(filename, 'rb') else: data.seek(0,2) size = data.tell() data.seek(0) content_type = 'image/jpeg' else: data = open(filename, 'rb') close_data = 1 try: if REQUEST is not None: last_mod = rfc1123_date(self._p_mtime) REQUEST.RESPONSE.setHeader('Last-Modified', last_mod) REQUEST.RESPONSE.setHeader('Content-Type', content_type) REQUEST.RESPONSE.setHeader('Content-Length', size) self.ZCacheable_set(None) # Support Zope 2.7.1 IStreamIterator if IStreamIterator is not None: close_data = 0 return stream_iterator(data) blocksize = 2<<16 while 1: buffer = data.read(blocksize) REQUEST.RESPONSE.write(buffer) if len(buffer) < blocksize: break return '' else: return data.read() finally: if close_data: data.close() security.declareProtected(ViewPermission, 'view_image_or_file') def view_image_or_file(self): """ The default view of the contents of the File or Image. """ raise Redirect, self.absolute_url() security.declareProtected(ViewPermission, 'link') def link(self, text='', **args): """ Return a HTML link tag to the file """ if text=='': text = self.title_or_id() strg = '' % self._static_url(icon=1) security.declareProtected(ViewPermission, 'icon_html') def icon_html(self): """ Same as icon_tag """ return self.icon_tag() security.declareProtected(ViewPermission, 'is_broken') def is_broken(self): """ Check if external file exists and return true (1) or false (0) """ return not self._get_fsname(self.filename) security.declareProtected(ViewPermission, 'get_size') def get_size(self): """ Returns the size of the file or image """ fn = self._get_fsname(self.filename) if fn: return os.stat(fn)[6] return 0 security.declareProtected(ViewPermission, 'rawsize') def rawsize(self): """ Same as get_size """ return self.get_size() security.declareProtected(ViewPermission, 'getSize') def getSize(self): """ Same as get_size """ return self.get_size() security.declareProtected(ViewPermission, 'size') def size(self): """ Returns a formatted stringified version of the file size """ return self._bytetostring(self.get_size()) security.declareProtected(ViewPermission, 'getContentType') def getContentType(self): """ Returns the content type (MIME type) of a file or image. """ return self.content_type security.declareProtected(ViewPermission, 'getIconPath') def getIconPath(self): """ Depending on the MIME Type of the file/image an icon can be displayed. This function determines which image in the lib/python/Products/ExtFile/icons/... directory shold be used as icon for this file/image """ try: cat, sub = string.split(self.content_type, '/') except ValueError: if getattr(self, 'has_preview', None) is not None: cat, sub = 'image', '' else: cat, sub = '', '' if self._types.has_key(cat): file = self._types[cat]['default'] for item in self._types[cat].keys(): if string.find(sub, item) >= 0: file = self._types[cat][item] break return join('icons', cat, file) return join('icons', self._types['default']) security.declareProtected(ViewPermission, 'static_url') def static_url(self, icon=0, preview=0): """ Returns the static url of the file """ return self._static_url(icon, preview) security.declareProtected(ViewPermission, 'static_mode') def static_mode(self): """ Returns true if serving static urls """ return os.environ.get('EXTFILE_STATIC_PATH') is not None security.declareProtected(AccessPermission, 'get_filename') def get_filename(self): """ Returns the filename as file system path. Used by the ZMI to display the filename. """ return self._fsname(self.filename) security.declareProtected(ViewPermission, 'PrincipiaSearchSource') def PrincipiaSearchSource(self): """ Allow file objects to be searched. """ if self.content_type.startswith('text/'): return str(self) return '' ################################ # Protected management methods # ################################ # Management Interface security.declareProtected(AccessPermission, 'manage_main') manage_main = HTMLFile('dtml/extFileEdit', globals()) security.declareProtected(ChangePermission, 'manage_editExtFile') def manage_editExtFile(self, title='', descr='', REQUEST=None): """ Manage the edited values """ if self.title!=title: self.title = title if self.descr!=descr: self.descr = descr # update ZCatalog self.reindex_object() self.ZCacheable_invalidate() if REQUEST is not None: return self.manage_main(self, REQUEST, manage_tabs_message='Saved changes.') # File upload Interface security.declareProtected(AccessPermission, 'manage_uploadForm') manage_uploadForm = HTMLFile('dtml/extFileUpload', globals()) security.declareProtected(ChangePermission, 'manage_upload') def manage_upload(self, file='', content_type='', REQUEST=None): """ Upload file from file handle or string buffer """ if self.wl_isLocked(): raise ResourceLockedError, "File is locked via WebDAV" if type(file) == types.StringType: temp_file = TemporaryFile() temp_file.write(file) temp_file.seek(0) else: temp_file = file return self.manage_file_upload(temp_file, content_type, REQUEST) security.declareProtected(ChangePermission, 'manage_file_upload') def manage_file_upload(self, file='', content_type='', REQUEST=None): """ Upload file from file handle or local directory """ if self.wl_isLocked(): raise ResourceLockedError, "File is locked via WebDAV" if type(file) == types.StringType: cant_read_exc = "Can't open: " try: file = open(file, 'rb') except: raise cant_read_exc, file if content_type: file = HTTPUpload(file, content_type) self.content_type = self._get_content_type(file, file.read(100), self.id, self.content_type) file.seek(0) self._register() # Register with TM try: new_fn = self._get_ufn(self.filename) self._update_data(file, self._temp_fsname(new_fn)) finally: self._dir__unlock() self.filename = new_fn self._afterUpdate() if REQUEST is not None: return self.manage_main(self, REQUEST, manage_tabs_message='Upload complete.') security.declareProtected(ChangePermission, 'manage_http_upload') def manage_http_upload(self, url, REQUEST=None): """ Upload file from http-server """ if self.wl_isLocked(): raise ResourceLockedError, "File is locked via WebDAV" url = urllib.quote(url,'/:') cant_read_exc = "Can't open: " try: file = urllib.urlopen(url) except: raise cant_read_exc, url file = HTTPUpload(file) self.content_type = self._get_content_type(file, file.read(100), self.id, self.content_type) file.seek(0) self._register() # Register with TM try: new_fn = self._get_ufn(self.filename) self._update_data(file, self._temp_fsname(new_fn)) finally: self._dir__unlock() self.filename = new_fn self._afterUpdate() if REQUEST is not None: return self.manage_main(self, REQUEST, manage_tabs_message='Upload complete.') security.declareProtected(ChangePermission, 'PUT') def PUT(self, REQUEST, RESPONSE): """ Handle HTTP PUT requests """ self.dav__init(REQUEST, RESPONSE) self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1) file = REQUEST['BODYFILE'] content_type = REQUEST.get_header('content-type', None) if content_type: file = HTTPUpload(file, content_type) self.content_type = self._get_content_type(file, file.read(100), self.id, self.content_type) file.seek(0) self._register() # Register with TM try: # Need to pass in the path as webdav.NullResource calls PUT # on an unwrapped object. try: self.aq_parent # This raises AttributeError if no context except AttributeError: path = self._get_zodb_path(REQUEST.PARENTS[0]) else: path = None new_fn = self._get_ufn(self.filename, path=path) self._update_data(file, self._temp_fsname(new_fn)) finally: self._dir__unlock() self.filename = new_fn self._afterUpdate() RESPONSE.setStatus(204) return RESPONSE 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 """ return self.index_html(REQUEST=self.REQUEST) ################################ # Private methods # ################################ def _access_permitted(self, REQUEST=None): """ Check if the user is allowed to download the file """ if REQUEST is None and getattr(self, 'REQUEST', None) is not None: REQUEST = self.REQUEST if getattr(self, 'use_download_permission_check', 0) and \ (REQUEST is None or not getSecurityManager().getUser().has_permission( DownloadPermission, self) ): return 0 else: return 1 def _get_content_type(self, file, body, id, content_type=None): """ Determine the mime-type """ headers = getattr(file, 'headers', None) if headers and headers.has_key('content-type'): content_type = headers['content-type'] else: if type(body) is not type(''): body = body.data content_type, enc = guess_content_type(getattr(file,'filename',id), body, content_type) cutoff = content_type.find(';') if cutoff >= 0: return content_type[:cutoff] return content_type def _update_data(self, infile, outfile): """ Store infile to outfile """ if type(infile) == types.ListType: infile = self._fsname(infile) if type(outfile) == types.ListType: outfile = self._fsname(outfile) try: self._copy(infile, outfile) except: if isfile(outfile): # This is always a .tmp file try: os.remove(outfile) except OSError: pass raise else: self.http__refreshEtag() def _copy(self, infile, outfile): """ Read binary data from infile and write it to outfile infile and outfile may be strings, in which case a file with that name is opened, or filehandles, in which case they are accessed directly. """ if type(infile) is types.StringType: try: instream = open(infile, 'rb') except IOError: raise IOError, ("%s (%s)" %(self.id, infile)) close_in = 1 else: instream = infile close_in = 0 if type(outfile) is types.StringType: umask = os.umask(REPOSITORY_UMASK) try: outstream = open(outfile, 'wb') os.umask(umask) self._dir__unlock() # unlock early except IOError: os.umask(umask) raise IOError, ("%s (%s)" %(self.id, outfile)) close_out = 1 else: outstream = outfile close_out = 0 try: blocksize = 2<<16 block = instream.read(blocksize) outstream.write(block) while len(block)==blocksize: block = instream.read(blocksize) outstream.write(block) except IOError: raise IOError, ("%s (%s)" %(self.id, filename)) try: instream.seek(0) except: pass if close_in: instream.close() if close_out: outstream.close() def _undo(self): """ Restore filename after delete or copy-paste """ fn = self._fsname(self.filename) if not isfile(fn) and isfile(fn+'.undo'): self._register() # Register with TM os.rename(fn+'.undo', self._temp_fsname(self.filename)) def _fsname(self, filename): """ Generates the full filesystem name, incuding directories from self._repository and filename """ path = [INSTANCE_HOME] path.extend(self._repository) if type(filename) == types.ListType: path.extend(filename) elif filename != '': path.append(filename) return apply(join, path) def _temp_fsname(self, filename): """ Generates the full filesystem name of the temporary file """ return '%s.tmp' % self._fsname(filename) def _get_fsname(self, filename): """ Returns the full filesystem name, preferring tmp over main. Also attempts to undo. Returns None if the file is broken. """ tmp_fn = self._temp_fsname(filename) if isfile(tmp_fn): return tmp_fn fn = self._fsname(filename) if isfile(fn): return fn self._undo() if isfile(tmp_fn): return tmp_fn # b/w compatibility def _get_filename(self, filename): """ Deprecated, use _get_fsname """ return self._get_fsname(filename) def _get_ufn(self, filename, path=None, content_type=None, lock=1): """ If no unique filename has been generated, generate one otherwise, return the existing one. """ if UNDO_POLICY==ALWAYS_BACKUP or filename==[]: new_fn = self._get_new_ufn(path=path, content_type=content_type, lock=lock) else: new_fn = filename[:] if filename: old_fn = self._fsname(filename) if UNDO_POLICY==ALWAYS_BACKUP: try: os.rename(old_fn, old_fn+'.undo') except OSError: pass else: try: os.rename(old_fn+'.undo', old_fn) except OSError: pass return new_fn def _get_new_ufn(self, path=None, content_type=None, lock=1): """ Create a new unique filename """ id = self.id # hack so the files are not named copy_of_foo if COPY_OF_PROTECTION: match = copy_of_re.match(id) if match is not None: id = id[len(match.group(1)):] # get name and extension components from id pos = string.rfind(id, '.') if (pos+1): id_name = id[:pos] id_ext = id[pos:] else: id_name = id id_ext = '' if not content_type: content_type = self.content_type if REPOSITORY_EXTENSIONS in (MIMETYPE_APPEND, MIMETYPE_REPLACE): mime_ext = guess_extension(content_type) if mime_ext is not None: if mime_ext in ('.jpeg', '.jpe'): mime_ext = '.jpg' # for IE/Win :-( if mime_ext in ('.obj',): mime_ext = '.exe' # b/w compatibility if mime_ext in ('.tiff',): mime_ext = '.tif' # b/w compatibility # don't change extensions of unknown binaries if not (content_type == 'application/octet-stream' and id_ext): if REPOSITORY_EXTENSIONS == MIMETYPE_APPEND: id_name = id_name + id_ext id_ext = mime_ext # generate directory structure if path is not None: rel_url_list = path else: rel_url_list = self._get_zodb_path() dirs = [] if REPOSITORY == SYNC_ZODB: dirs = rel_url_list elif REPOSITORY in (SLICED, SLICED_REVERSE, SLICED_HASH): if REPOSITORY == SLICED_HASH: # increase distribution by including the path in the hash hashed = ''.join(list(rel_url_list)+[id_name]) temp = base64.encodestring(sha.new(hashed).digest())[:-1] temp = temp.replace('/', '_') temp = temp.replace('+', '_') elif REPOSITORY == SLICED_REVERSE: temp = list(id_name) temp.reverse() temp = ''.join(temp) else: temp = id_name for i in range(SLICE_DEPTH): if len(temp)=0: fileformat = string.replace(fileformat, "%t", "%c") counter = int(DateTime().strftime('%m%d%H%M%S')) else: counter = 0 invalid_format_exc = "Invalid file format: " if string.find(fileformat, "%c")==-1: raise invalid_format_exc, FILE_FORMAT # user (%u) if string.find(fileformat, "%u")>=0: if (getattr(self, 'REQUEST', None) is not None and self.REQUEST.has_key('AUTHENTICATED_USER')): user = getSecurityManager().getUser().getUserName() fileformat = string.replace(fileformat, "%u", user) else: fileformat = string.replace(fileformat, "%u", "") # path (%p) if string.find(fileformat, "%p")>=0: temp = string.joinfields(rel_url_list, "_") fileformat = string.replace(fileformat, "%p", temp) # file and extension (%n and %e) if string.find(fileformat,"%n")>=0 or string.find(fileformat,"%e")>=0: fileformat = string.replace(fileformat, "%n", id_name) fileformat = string.replace(fileformat, "%e", id_ext) # lock the directory if lock: self._dir__lock(dirpath) # search for unique filename if counter: fn = join(dirpath, string.replace(fileformat, "%c", ".%s" % counter)) else: fn = join(dirpath, string.replace(fileformat, "%c", '')) while isfile(fn) or isfile(fn+'.undo') or isfile(fn+'.tmp'): counter = counter + 1 fn = join(dirpath, string.replace(fileformat, "%c", ".%s" % counter)) if counter: fileformat = string.replace(fileformat, "%c", ".%s" % counter) else: fileformat = string.replace(fileformat, "%c", '') dirs.append(fileformat) return dirs def _static_url(self, icon=0, preview=0): """ Return the static url of the file """ static_path = os.environ.get('EXTFILE_STATIC_PATH') if static_path is not None: filename, content_type, icon, preview = \ self._get_file_to_serve(icon, preview) if icon: # cannot serve statically return '%s?icon=1' % self.absolute_url() else: # rewrite to static url static_host = os.environ.get('EXTFILE_STATIC_HOST') host = self.REQUEST.SERVER_URL if static_host is not None: if host[:8] == 'https://': host = 'https://' + static_host else: host = 'http://' + static_host host = host + urllib.quote(static_path) + '/' return host + urllib.quote('/'.join(filename)) else: if icon: return '%s?icon=1' % self.absolute_url() elif preview: return '%s?preview=1' % self.absolute_url() else: return self.absolute_url() def _get_file_to_serve(self, icon=0, preview=0): """ Find out about the file we are going to serve """ if not self._access_permitted(): preview = 1 if preview and not getattr(self, 'has_preview', 0): icon = 1 if icon: filename = join(package_home(globals()), self.getIconPath()) content_type = 'image/gif' elif preview: filename = self.prev_filename content_type = self.prev_content_type else: filename = self.filename content_type = self.content_type return filename, content_type, icon, preview def _get_zodb_path(self, parent=None): """ Returns the ZODB path of the parent object """ # XXX: The Photo product uploads into unwrapped ExtImages. # As we can not reliably guess our parent object we fall back # to the old behavior. This means that Photos will always # use ZODB_PATH = VIRTUAL independent of config settings. try: from Products.Photo.ExtPhotoImage import PhotoImage except ImportError: pass else: if isinstance(self, PhotoImage): path = self.absolute_url(1).split('/')[:-1] return filter(None, path) # XXX: End of hack # For normal operation objects must be wrapped if parent is None: parent = self.aq_parent if ZODB_PATH == VIRTUAL: path = parent.absolute_url(1).split('/') else: path = list(parent.getPhysicalPath()) return filter(None, path) def _bytetostring(self, value): """ Convert an int-value (file-size in bytes) to an String with the file-size in Byte, KB or MB """ bytes = float(value) if bytes>=1000: bytes = bytes/1024 if bytes>=1000: bytes = bytes/1024 typ = ' MB' else: typ = ' KB' else: typ = ' Bytes' strg = '%4.2f'%bytes strg = strg[:4] if strg[3]=='.': strg = strg[:3] strg = strg+typ return strg def _afterUpdate(self): """ Called whenever the file data has been updated. Invokes the manage_afterUpdate() hook. """ self.ZCacheable_invalidate() return self.manage_afterUpdate(self._get_fsname(self.filename), self.content_type, self.get_size()) ################################ # Special management methods # ################################ security.declarePrivate('manage_afterClone') def manage_afterClone(self, item, new_fn=None): """ When a copy of the object is created (zope copy-paste-operation), this function is called by CopySupport.py. A copy of the external file is created and self.filename is changed. """ call_afterUpdate = 0 try: self.aq_parent # This raises AttributeError if no context except AttributeError: self._v_has_been_cloned=1 # This is to make webdav COPY work else: fn = self._get_fsname(self.filename) if fn: self._register() # Register with TM try: new_fn = new_fn or self._get_new_ufn() self._update_data(fn, self._temp_fsname(new_fn)) self.filename = new_fn call_afterUpdate = 1 finally: self._dir__unlock() if call_afterUpdate: self._afterUpdate() return ExtFile.inheritedAttribute("manage_afterClone")(self, item) security.declarePrivate('manage_afterAdd') def manage_afterAdd(self, item, container): """ This method is called, whenever _setObject in ObjectManager gets called. This is the case after a normal add and if the object is a result of cut-paste- or rename-operation. In the first case, the external files doesn't exist yet, otherwise it was renamed to .undo by manage_beforeDelete before and must be restored by _undo(). """ self._undo() if hasattr(self, "_v_has_been_cloned"): delattr(self, "_v_has_been_cloned") self.manage_afterClone(item) return ExtFile.inheritedAttribute("manage_afterAdd")(self, item, container) security.declarePrivate('manage_beforeDelete') def manage_beforeDelete(self, item, container): """ This method is called, when the object is deleted. To support undo-functionality and because this happens too, when the object is moved (cut-paste) or renamed, the external file is not deleted. It is just renamed to filename.undo and remains in the repository, until it is deleted manually. """ tmp_fn = self._temp_fsname(self.filename) fn = self._fsname(self.filename) if isfile(tmp_fn): try: os.rename(tmp_fn, fn+'.undo') except OSError: pass else: try: os.remove(fn) except OSError: pass elif isfile(fn): try: os.rename(fn, fn+'.undo') except OSError: pass return ExtFile.inheritedAttribute("manage_beforeDelete")(self, item, container) security.declarePrivate('manage_afterUpdate') def manage_afterUpdate(self, filename, content_type, size): """ This method is called whenever the file data has been updated. May be overridden by subclasses to perform additional operations. The 'filename' argument contains the path as returned by get_fsname(). """ pass security.declarePrivate('get_fsname') def get_fsname(self): """ Returns the current file system path of the file or image. This path can be used to access the file even while a transaction is in progress (aka Zagy's revenge :-). Returns None if the file does not exist in the repository. """ return self._get_fsname(self.filename) ################################ # Repository locking methods # ################################ def _dir__lock(self, dir): """ Lock a directory """ if hasattr(self, '_v_dir__lock'): raise DirLockError, 'Double lock in thread' self._v_dir__lock = DirLock(dir) def _dir__unlock(self): """ Unlock a previously locked directory """ if hasattr(self, '_v_dir__lock'): self._v_dir__lock.release() delattr(self, '_v_dir__lock') ################################ # Transaction manager methods # ################################ def _register(self): if _debug: LOG(_SUBSYS, INFO, 'registering %s' % TM.contains(self)) TM.register(self) if _debug: LOG(_SUBSYS, INFO, 'registered %s' % TM.contains(self)) def _begin(self): self._v_begin_called = 1 # for tests if _debug: LOG(_SUBSYS, INFO, 'beginning %s' % self.id) def _finish(self): """ Commits the temporary file """ self._v_finish_called = 1 # for tests TM.remove(self) # for tests if self.filename: tmp_fn = self._temp_fsname(self.filename) if _debug: LOG(_SUBSYS, INFO, 'finishing %s' % tmp_fn) if isfile(tmp_fn): if _debug: LOG(_SUBSYS, INFO, 'isfile %s' % tmp_fn) fn = self._fsname(self.filename) try: os.remove(fn) except OSError: pass os.rename(tmp_fn, fn) def _abort(self): """ Deletes the temporary file """ self._v_abort_called = 1 # for tests TM.remove(self) # for tests if self.filename: tmp_fn = self._temp_fsname(self.filename) if _debug: LOG(_SUBSYS, INFO, 'aborting %s' % tmp_fn) if isfile(tmp_fn): if _debug: LOG(_SUBSYS, INFO, 'isfile %s' % tmp_fn) try: os.remove(tmp_fn) except OSError: pass InitializeClass(ExtFile) # Filename to id translation bad_chars = """ ,;:'"()[]{}ÄÅÁÀÂÃäåáàâãÇçÉÈÊËÆéèêëæÍÌÎÏíìîïÑñÖÓÒÔÕØöóòôõøŠšßÜÚÙÛüúùûÝŸýÿŽž""" good_chars = """____________AAAAAAaaaaaaCcEEEEEeeeeeIIIIiiiiNnOOOOOOooooooSssUUUUuuuuYYyyZz""" TRANSMAP = string.maketrans(bad_chars, good_chars) def normalize_id(id): # Support at least utf-8 and latin-1 filenames. # This is lame, but before it was latin-1 only. try: uid = unicode(id, 'utf-8') except UnicodeError, TypeError: try: uid = unicode(id, 'iso-8859-15') except UnicodeError, TypeError: return id id = uid.encode('iso-8859-15', 'ignore') id = string.translate(id, TRANSMAP) return id # FileUpload factory from cgi import FieldStorage from ZPublisher.HTTPRequest import FileUpload def HTTPUpload(fp, content_type=None): """ Create a FileUpload instance from a file handle (and content_type) """ if isinstance(fp, FileUpload): if content_type: fp.headers['content-type'] = content_type return fp else: environ = {'REQUEST_METHOD': 'POST'} if content_type: environ['CONTENT_TYPE'] = content_type elif hasattr(fp, 'headers') and fp.headers.has_key('content-type'): environ['CONTENT_TYPE'] = fp.headers['content-type'] fs = FieldStorage(fp=fp, environ=environ) return FileUpload(fs) # Repository lock import time class DirLockError(OSError): pass class DirLock: """ Manage the lockfile for a directory """ lock_name = '@@@lock' sleep_secs = 1.5 sleep_times = 10 def _mklock(self): f = open(self._lock, 'wt') f.write('ExtFile dir lock. You may want to remove this file.') f.close() def _rmlock(self): os.remove(self._lock) def islocked(self): os.path.isfile(self._lock) def release(self): self._rmlock() def __init__(self, dir): self._lock = os.path.join(dir, self.lock_name) for i in range(self.sleep_times): if self.islocked(): LOG(_SUBSYS, BLATHER, "Waiting for lock '%s'" % self._lock) time.sleep(self.sleep_secs) else: self._mklock() break else: LOG(_SUBSYS, BLATHER, "Failed to get lock '%s'" % self._lock) raise DirLockError, "Failed to get lock '%s'" % self._lock # Stream iterator if IStreamIterator is not None: class stream_iterator: __implements__ = (IStreamIterator,) def __init__(self, stream, blocksize=2<<16): self._stream = stream self._blocksize = blocksize def next(self): data = self._stream.read(self._blocksize) if not data: self._stream.close() self._stream = None raise StopIteration return data def __len__(self): cur_pos = self._stream.tell() self._stream.seek(0, 2) size = self._stream.tell() self._stream.seek(cur_pos, 0) return size def __nonzero__(self): return self.__len__() and 1 or 0