Annotation of ExtFile/ExtFile.py, revision 1.1.1.1
1.1 dwinter 1: """ ExtFile product module """
2: # -*- coding: latin-1 -*-
3: ###############################################################################
4: #
5: # Copyright (c) 2001 Gregor Heine <mac.gregor@gmx.de>. All rights reserved.
6: # ExtFile Home: http://www.zope.org/Members/MacGregor/ExtFile/index_html
7: #
8: # Redistribution and use in source and binary forms, with or without
9: # modification, are permitted provided that the following conditions
10: # are met:
11: #
12: # 1. Redistributions of source code must retain the above copyright
13: # notice, this list of conditions and the following disclaimer.
14: # 2. Redistributions in binary form must reproduce the above copyright
15: # notice, this list of conditions and the following disclaimer in the
16: # documentation and/or other materials provided with the distribution.
17: # 3. The name of the author may not be used to endorse or promote products
18: # derived from this software without specific prior written permission
19: #
20: # Disclaimer
21: #
22: # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
23: # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
24: # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
25: # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
26: # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
27: # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28: # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29: # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30: # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
31: # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32: #
33: # In accordance with the license provided for by the software upon
34: # which some of the source code has been derived or used, the following
35: # acknowledgement is hereby provided :
36: #
37: # "This product includes software developed by Digital Creations
38: # for use in the Z Object Publishing Environment
39: # (http://www.zope.org/)."
40: #
41: ###############################################################################
42:
43: __doc__ = """ExtFile product module.
44: The ExtFile-Product works like the Zope File-product, but stores
45: the uploaded file externally in a repository-direcory."""
46:
47: __version__='1.5.4'
48:
49: from Products.ZCatalog.CatalogPathAwareness import CatalogAware
50: from OFS.SimpleItem import SimpleItem
51: from OFS.PropertyManager import PropertyManager
52: from OFS.Cache import Cacheable
53: from Globals import HTMLFile, MessageDialog, InitializeClass, package_home
54: from AccessControl import ClassSecurityInfo, getSecurityManager
55: from AccessControl import Permissions
56: from Acquisition import aq_acquire
57: from mimetypes import guess_extension
58: from webdav.Lockable import ResourceLockedError
59: from webdav.common import rfc1123_date
60: from DateTime import DateTime
61: import urllib, os, string, types, sha, base64
62: from os.path import join, isfile
63: from tempfile import TemporaryFile
64: from Products.ExtFile import TM
65:
66: from webdav.WriteLockInterface import WriteLockInterface
67: from IExtFile import IExtFile
68:
69: #from zLOG import *
70:
71:
72: _SUBSYS = 'ExtFile'
73: _debug = 0
74:
75: #ersetzt zLOG -- Aenderung DW 24.1.2007
76:
77: from logging import *
78:
79: def LOG(txt,method,txt2):
80: """logging"""
81: info(txt+ txt2)
82:
83:
84: try: import Zope2
85: except ImportError: ZOPE28 = 0
86: else: ZOPE28 = 1
87:
88: try: from zope.contenttype import guess_content_type
89: except ImportError:
90: try: from zope.app.content_types import guess_content_type
91: except ImportError: from OFS.content_types import guess_content_type
92:
93: try: from zExceptions import Redirect
94: except ImportError: Redirect = 'Redirect'
95:
96: try: from ZPublisher.Iterators import IStreamIterator
97: except ImportError: IStreamIterator = None
98:
99: ViewPermission = Permissions.view
100: AccessPermission = Permissions.view_management_screens
101: ChangePermission = 'Change ExtFile/ExtImage'
102: DownloadPermission = 'Download ExtFile/ExtImage'
103:
104: import re
105: copy_of_re = re.compile('(^(copy[0-9]*_of_)+)')
106:
107: from Config import *
108:
109: manage_addExtFileForm = HTMLFile('dtml/extFileAdd', globals())
110:
111:
112: def manage_addExtFile(self, id='', title='', descr='', file='',
113: content_type='', permission_check=0, redirect_default_view=0, REQUEST=None):
114: """ Add an ExtFile to a folder. """
115: if not id and getattr(file, 'filename', None) is not None:
116: # generate id from filename and make sure, it has no 'bad' chars
117: id = file.filename
118: id = id[max(string.rfind(id,'/'),
119: string.rfind(id,'\\'),
120: string.rfind(id,':'))+1:]
121: title = title or id
122: id = normalize_id(id)
123: tempExtFile = ExtFile(id, title, descr, permission_check, redirect_default_view)
124: self._setObject(id, tempExtFile)
125: if file != '':
126: self._getOb(id).manage_file_upload(file, content_type)
127: if REQUEST is not None:
128: return self.manage_main(self, REQUEST, update_menu=0)
129: return id
130:
131:
132:
133: class ExtFile(CatalogAware, SimpleItem, PropertyManager, Cacheable):
134: """ The ExtFile-Product works like the Zope File-product, but stores
135: the uploaded file externally in a repository-direcory. """
136:
137: __implements__ = (IExtFile, WriteLockInterface)
138:
139: # what properties have we?
140: _properties = (
141: {'id':'title', 'type':'string', 'mode': 'w'},
142: {'id':'descr', 'type':'text', 'mode': 'w'},
143: {'id':'content_type', 'type':'string', 'mode': 'w'},
144: {'id':'use_download_permission_check', 'type':'boolean', 'mode': 'w'},
145: {'id':'redirect_default_view', 'type':'boolean', 'mode': 'w'},
146: )
147: use_download_permission_check = 0
148: redirect_default_view = 0
149:
150: # what management options are there?
151: manage_options = ((
152: {'label':'Edit', 'action': 'manage_main' },
153: {'label':'View', 'action': '' },
154: {'label':'Upload', 'action': 'manage_uploadForm' },) +
155: PropertyManager.manage_options +
156: SimpleItem.manage_options[1:] +
157: Cacheable.manage_options
158: )
159:
160: security = ClassSecurityInfo()
161:
162: # what do people think they're adding?
163: meta_type = 'ExtFile'
164:
165: # location of the file-repository
166: _repository = REPOSITORY_PATH
167:
168: # make sure the download permission is available
169: security.setPermissionDefault(DownloadPermission, ('Manager',))
170:
171: # the above does not work in Zope < 2.8
172: if not ZOPE28:
173: security.declareProtected(DownloadPermission, '_dummy')
174:
175: # MIME-Type Dictionary. To add a MIME-Type, add a file in the directory
176: # icons/_category_/_subcategory-icon-file_
177: # example: Icon tifficon.gif for the MIME-Type image/tiff goes to
178: # icons/image/tifficon.gif and the dictionary must be updated like this:
179: # 'image':{'tiff':'tifficon.gif','default':'default.gif'}, ...
180: _types={'image':
181: {'default':'default.gif'},
182: 'text':
183: {'html':'html.gif', 'xml':'xml.gif', 'default':'default.gif',
184: 'python':'py.gif'},
185: 'application':
186: {'pdf':'pdf.gif', 'zip':'zip.gif', 'tar':'zip.gif',
187: 'msword':'doc.gif', 'excel':'xls.gif', 'powerpoint':'ppt.gif',
188: 'default':'default.gif'},
189: 'video':
190: {'default':'default.gif'},
191: 'audio':
192: {'default':'default.gif'},
193: 'default':'default.gif'
194: }
195:
196: ################################
197: # Init method #
198: ################################
199:
200: def __init__(self, id, title='', descr='', permission_check=0, redirect_default_view=0):
201: """ Initialize a new instance of ExtFile """
202: self.id = id
203: self.title = title
204: self.descr = descr
205: self.use_download_permission_check = permission_check
206: self.redirect_default_view = redirect_default_view
207: self.__version__ = __version__
208: self.filename = []
209: self.content_type = ''
210:
211: ################################
212: # Public methods #
213: ################################
214:
215: def __str__(self):
216: return self.index_html()
217:
218: def __len__(self):
219: return 1
220:
221: def _if_modified_since_request_handler(self, REQUEST):
222: """ HTTP If-Modified-Since header handling: return True if
223: we can handle this request by returning a 304 response.
224: """
225: header = REQUEST.get_header('If-Modified-Since', None)
226: if header is not None:
227: header = string.split(header, ';')[0]
228: try: mod_since = long(DateTime(header).timeTime())
229: except: mod_since = None
230: if mod_since is not None:
231: if self._p_mtime:
232: last_mod = long(self._p_mtime)
233: else:
234: last_mod = long(0)
235: if last_mod > 0 and last_mod < mod_since:
236: # Set headers for Apache caching
237: last_mod = rfc1123_date(self._p_mtime)
238: REQUEST.RESPONSE.setHeader('Last-Modified', last_mod)
239: REQUEST.RESPONSE.setHeader('Content-Type', self.content_type)
240: # RFC violation. See http://collector.zope.org/Zope/544
241: #REQUEST.RESPONSE.setHeader('Content-Length', self.get_size())
242: REQUEST.RESPONSE.setStatus(304)
243: return 1
244:
245: def _redirect_default_view_request_handler(self, icon, preview, REQUEST):
246: """ redirect_default_view property handling: return True if
247: we can handle this request by returning a 302 response.
248: Patch provided by Oliver Bleutgen.
249: """
250: if self.redirect_default_view:
251: if self.static_mode() and not icon:
252: static_url = self._static_url(preview=preview)
253: if static_url != self.absolute_url():
254: REQUEST.RESPONSE.redirect(static_url)
255: return 1
256:
257: security.declareProtected(ViewPermission, 'index_html')
258: def index_html (self, icon=0, preview=0, width=None, height=None,
259: REQUEST=None):
260: """ Return the file with it's corresponding MIME-type """
261:
262: if REQUEST is not None:
263: if self._if_modified_since_request_handler(REQUEST):
264: self.ZCacheable_set(None)
265: return ''
266:
267: if self._redirect_default_view_request_handler(icon, preview, REQUEST):
268: return ''
269:
270: filename, content_type, icon, preview = self._get_file_to_serve(icon, preview)
271: filename = self._get_fsname(filename)
272:
273: if _debug > 1: LOG(_SUBSYS, INFO, 'serving %s, %s, %s, %s' %(filename, content_type, icon, preview))
274:
275: cant_read_exc = "Can't read: "
276: if filename:
277: try: size = os.stat(filename)[6]
278: except: raise cant_read_exc, ("%s (%s)" %(self.id, filename))
279: else:
280: filename = join(package_home(globals()), 'icons', 'broken.gif')
281: try: size = os.stat(filename)[6]
282: except: raise cant_read_exc, ("%s (%s)" %(self.id, filename))
283: content_type = 'image/gif'
284: icon = 1
285:
286: if icon==0 and width is not None and height is not None:
287: data = TemporaryFile() # hold resized image
288: try:
289: from PIL import Image
290: im = Image.open(filename)
291: if im.mode!='RGB':
292: im = im.convert('RGB')
293: filter = Image.BICUBIC
294: if hasattr(Image, 'ANTIALIAS'): # PIL 1.1.3
295: filter = Image.ANTIALIAS
296: im = im.resize((int(width),int(height)), filter)
297: im.save(data, 'JPEG', quality=85)
298: except:
299: data = open(filename, 'rb')
300: else:
301: data.seek(0,2)
302: size = data.tell()
303: data.seek(0)
304: content_type = 'image/jpeg'
305: else:
306: data = open(filename, 'rb')
307:
308: close_data = 1
309: try:
310: if REQUEST is not None:
311: last_mod = rfc1123_date(self._p_mtime)
312: REQUEST.RESPONSE.setHeader('Last-Modified', last_mod)
313: REQUEST.RESPONSE.setHeader('Content-Type', content_type)
314: REQUEST.RESPONSE.setHeader('Content-Length', size)
315: self.ZCacheable_set(None)
316:
317: # Support Zope 2.7.1 IStreamIterator
318: if IStreamIterator is not None:
319: close_data = 0
320: return stream_iterator(data)
321:
322: blocksize = 2<<16
323: while 1:
324: buffer = data.read(blocksize)
325: REQUEST.RESPONSE.write(buffer)
326: if len(buffer) < blocksize:
327: break
328: return ''
329: else:
330: return data.read()
331: finally:
332: if close_data: data.close()
333:
334: security.declareProtected(ViewPermission, 'view_image_or_file')
335: def view_image_or_file(self):
336: """ The default view of the contents of the File or Image. """
337: raise Redirect, self.absolute_url()
338:
339: security.declareProtected(ViewPermission, 'link')
340: def link(self, text='', **args):
341: """ Return a HTML link tag to the file """
342: if text=='': text = self.title_or_id()
343: strg = '<a href="%s"' % (self._static_url())
344: for key in args.keys():
345: value = args.get(key)
346: strg = '%s %s="%s"' % (strg, key, value)
347: strg = '%s>%s</a>' % (strg, text)
348: return strg
349:
350: security.declareProtected(ViewPermission, 'icon_gif')
351: def icon_gif(self):
352: """ Return an icon for the file's MIME-Type """
353: raise Redirect, self._static_url(icon=1)
354:
355: security.declareProtected(ViewPermission, 'icon_tag')
356: def icon_tag(self):
357: """ Generate the HTML IMG tag for the icon """
358: return '<img src="%s" border="0" />' % self._static_url(icon=1)
359:
360: security.declareProtected(ViewPermission, 'icon_html')
361: def icon_html(self):
362: """ Same as icon_tag """
363: return self.icon_tag()
364:
365: security.declareProtected(ViewPermission, 'is_broken')
366: def is_broken(self):
367: """ Check if external file exists and return true (1) or false (0) """
368: return not self._get_fsname(self.filename)
369:
370: security.declareProtected(ViewPermission, 'get_size')
371: def get_size(self):
372: """ Returns the size of the file or image """
373: fn = self._get_fsname(self.filename)
374: if fn:
375: return os.stat(fn)[6]
376: return 0
377:
378: security.declareProtected(ViewPermission, 'rawsize')
379: def rawsize(self):
380: """ Same as get_size """
381: return self.get_size()
382:
383: security.declareProtected(ViewPermission, 'getSize')
384: def getSize(self):
385: """ Same as get_size """
386: return self.get_size()
387:
388: security.declareProtected(ViewPermission, 'size')
389: def size(self):
390: """ Returns a formatted stringified version of the file size """
391: return self._bytetostring(self.get_size())
392:
393: security.declareProtected(ViewPermission, 'getContentType')
394: def getContentType(self):
395: """ Returns the content type (MIME type) of a file or image. """
396: return self.content_type
397:
398: security.declareProtected(ViewPermission, 'getIconPath')
399: def getIconPath(self):
400: """ Depending on the MIME Type of the file/image an icon
401: can be displayed. This function determines which
402: image in the lib/python/Products/ExtFile/icons/...
403: directory shold be used as icon for this file/image
404: """
405: try:
406: cat, sub = string.split(self.content_type, '/')
407: except ValueError:
408: if getattr(self, 'has_preview', None) is not None:
409: cat, sub = 'image', ''
410: else:
411: cat, sub = '', ''
412: if self._types.has_key(cat):
413: file = self._types[cat]['default']
414: for item in self._types[cat].keys():
415: if string.find(sub, item) >= 0:
416: file = self._types[cat][item]
417: break
418: return join('icons', cat, file)
419: return join('icons', self._types['default'])
420:
421: security.declareProtected(ViewPermission, 'static_url')
422: def static_url(self, icon=0, preview=0):
423: """ Returns the static url of the file """
424: return self._static_url(icon, preview)
425:
426: security.declareProtected(ViewPermission, 'static_mode')
427: def static_mode(self):
428: """ Returns true if serving static urls """
429: return os.environ.get('EXTFILE_STATIC_PATH') is not None
430:
431: security.declareProtected(AccessPermission, 'get_filename')
432: def get_filename(self):
433: """ Returns the filename as file system path.
434: Used by the ZMI to display the filename.
435: """
436: return self._fsname(self.filename)
437:
438: security.declareProtected(ViewPermission, 'PrincipiaSearchSource')
439: def PrincipiaSearchSource(self):
440: """ Allow file objects to be searched.
441: """
442: if self.content_type.startswith('text/'):
443: return str(self)
444: return ''
445:
446: ################################
447: # Protected management methods #
448: ################################
449:
450: # Management Interface
451: security.declareProtected(AccessPermission, 'manage_main')
452: manage_main = HTMLFile('dtml/extFileEdit', globals())
453:
454: security.declareProtected(ChangePermission, 'manage_editExtFile')
455: def manage_editExtFile(self, title='', descr='', REQUEST=None):
456: """ Manage the edited values """
457: if self.title!=title: self.title = title
458: if self.descr!=descr: self.descr = descr
459: # update ZCatalog
460: self.reindex_object()
461:
462: self.ZCacheable_invalidate()
463:
464: if REQUEST is not None:
465: return self.manage_main(self, REQUEST, manage_tabs_message='Saved changes.')
466:
467: # File upload Interface
468: security.declareProtected(AccessPermission, 'manage_uploadForm')
469: manage_uploadForm = HTMLFile('dtml/extFileUpload', globals())
470:
471: security.declareProtected(ChangePermission, 'manage_upload')
472: def manage_upload(self, file='', content_type='', REQUEST=None):
473: """ Upload file from file handle or string buffer """
474: if self.wl_isLocked():
475: raise ResourceLockedError, "File is locked via WebDAV"
476:
477: if type(file) == types.StringType:
478: temp_file = TemporaryFile()
479: temp_file.write(file)
480: temp_file.seek(0)
481: else:
482: temp_file = file
483: return self.manage_file_upload(temp_file, content_type, REQUEST)
484:
485: security.declareProtected(ChangePermission, 'manage_file_upload')
486: def manage_file_upload(self, file='', content_type='', REQUEST=None):
487: """ Upload file from file handle or local directory """
488: if self.wl_isLocked():
489: raise ResourceLockedError, "File is locked via WebDAV"
490:
491: if type(file) == types.StringType:
492: cant_read_exc = "Can't open: "
493: try: file = open(file, 'rb')
494: except: raise cant_read_exc, file
495: if content_type:
496: file = HTTPUpload(file, content_type)
497: self.content_type = self._get_content_type(file, file.read(100),
498: self.id, self.content_type)
499: file.seek(0)
500: self._register() # Register with TM
501: try:
502: new_fn = self._get_ufn(self.filename)
503: self._update_data(file, self._temp_fsname(new_fn))
504: finally:
505: self._dir__unlock()
506: self.filename = new_fn
507: self._afterUpdate()
508: if REQUEST is not None:
509: return self.manage_main(self, REQUEST, manage_tabs_message='Upload complete.')
510:
511: security.declareProtected(ChangePermission, 'manage_http_upload')
512: def manage_http_upload(self, url, REQUEST=None):
513: """ Upload file from http-server """
514: if self.wl_isLocked():
515: raise ResourceLockedError, "File is locked via WebDAV"
516:
517: url = urllib.quote(url,'/:')
518: cant_read_exc = "Can't open: "
519: try: file = urllib.urlopen(url)
520: except: raise cant_read_exc, url
521: file = HTTPUpload(file)
522: self.content_type = self._get_content_type(file, file.read(100),
523: self.id, self.content_type)
524: file.seek(0)
525: self._register() # Register with TM
526: try:
527: new_fn = self._get_ufn(self.filename)
528: self._update_data(file, self._temp_fsname(new_fn))
529: finally:
530: self._dir__unlock()
531: self.filename = new_fn
532: self._afterUpdate()
533: if REQUEST is not None:
534: return self.manage_main(self, REQUEST, manage_tabs_message='Upload complete.')
535:
536: security.declareProtected(ChangePermission, 'PUT')
537: def PUT(self, REQUEST, RESPONSE):
538: """ Handle HTTP PUT requests """
539: self.dav__init(REQUEST, RESPONSE)
540: self.dav__simpleifhandler(REQUEST, RESPONSE, refresh=1)
541: file = REQUEST['BODYFILE']
542: content_type = REQUEST.get_header('content-type', None)
543: if content_type:
544: file = HTTPUpload(file, content_type)
545: self.content_type = self._get_content_type(file, file.read(100),
546: self.id, self.content_type)
547: file.seek(0)
548: self._register() # Register with TM
549: try:
550: # Need to pass in the path as webdav.NullResource calls PUT
551: # on an unwrapped object.
552: try:
553: self.aq_parent # This raises AttributeError if no context
554: except AttributeError:
555: path = self._get_zodb_path(REQUEST.PARENTS[0])
556: else:
557: path = None
558: new_fn = self._get_ufn(self.filename, path=path)
559: self._update_data(file, self._temp_fsname(new_fn))
560: finally:
561: self._dir__unlock()
562: self.filename = new_fn
563: self._afterUpdate()
564: RESPONSE.setStatus(204)
565: return RESPONSE
566:
567: security.declareProtected('FTP access', 'manage_FTPstat')
568: security.declareProtected('FTP access', 'manage_FTPlist')
569: security.declareProtected('FTP access', 'manage_FTPget')
570: def manage_FTPget(self):
571: """ Return body for FTP """
572: return self.index_html(REQUEST=self.REQUEST)
573:
574: ################################
575: # Private methods #
576: ################################
577:
578: def _access_permitted(self, REQUEST=None):
579: """ Check if the user is allowed to download the file """
580: if REQUEST is None and getattr(self, 'REQUEST', None) is not None:
581: REQUEST = self.REQUEST
582: if getattr(self, 'use_download_permission_check', 0) and \
583: (REQUEST is None or
584: not getSecurityManager().getUser().has_permission(
585: DownloadPermission, self)
586: ):
587: return 0
588: else:
589: return 1
590:
591: def _get_content_type(self, file, body, id, content_type=None):
592: """ Determine the mime-type """
593: headers = getattr(file, 'headers', None)
594: if headers and headers.has_key('content-type'):
595: content_type = headers['content-type']
596: else:
597: if type(body) is not type(''): body = body.data
598: content_type, enc = guess_content_type(getattr(file,'filename',id),
599: body, content_type)
600: cutoff = content_type.find(';')
601: if cutoff >= 0:
602: return content_type[:cutoff]
603: return content_type
604:
605: def _update_data(self, infile, outfile):
606: """ Store infile to outfile """
607: if type(infile) == types.ListType:
608: infile = self._fsname(infile)
609: if type(outfile) == types.ListType:
610: outfile = self._fsname(outfile)
611: try:
612: self._copy(infile, outfile)
613: except:
614: if isfile(outfile): # This is always a .tmp file
615: try: os.remove(outfile)
616: except OSError: pass
617: raise
618: else:
619: self.http__refreshEtag()
620:
621: def _copy(self, infile, outfile):
622: """ Read binary data from infile and write it to outfile
623: infile and outfile may be strings, in which case a file with that
624: name is opened, or filehandles, in which case they are accessed
625: directly.
626: """
627: if type(infile) is types.StringType:
628: try:
629: instream = open(infile, 'rb')
630: except IOError:
631: raise IOError, ("%s (%s)" %(self.id, infile))
632: close_in = 1
633: else:
634: instream = infile
635: close_in = 0
636: if type(outfile) is types.StringType:
637: umask = os.umask(REPOSITORY_UMASK)
638: try:
639: outstream = open(outfile, 'wb')
640: os.umask(umask)
641: self._dir__unlock() # unlock early
642: except IOError:
643: os.umask(umask)
644: raise IOError, ("%s (%s)" %(self.id, outfile))
645: close_out = 1
646: else:
647: outstream = outfile
648: close_out = 0
649: try:
650: blocksize = 2<<16
651: block = instream.read(blocksize)
652: outstream.write(block)
653: while len(block)==blocksize:
654: block = instream.read(blocksize)
655: outstream.write(block)
656: except IOError:
657: raise IOError, ("%s (%s)" %(self.id, filename))
658: try: instream.seek(0)
659: except: pass
660: if close_in: instream.close()
661: if close_out: outstream.close()
662:
663: def _undo(self):
664: """ Restore filename after delete or copy-paste """
665: fn = self._fsname(self.filename)
666: if not isfile(fn) and isfile(fn+'.undo'):
667: self._register() # Register with TM
668: os.rename(fn+'.undo', self._temp_fsname(self.filename))
669:
670: def _fsname(self, filename):
671: """ Generates the full filesystem name, incuding directories from
672: self._repository and filename
673: """
674: path = [INSTANCE_HOME]
675: path.extend(self._repository)
676: if type(filename) == types.ListType:
677: path.extend(filename)
678: elif filename != '':
679: path.append(filename)
680: return apply(join, path)
681:
682: def _temp_fsname(self, filename):
683: """ Generates the full filesystem name of the temporary file """
684: return '%s.tmp' % self._fsname(filename)
685:
686: def _get_fsname(self, filename):
687: """ Returns the full filesystem name, preferring tmp over main.
688: Also attempts to undo. Returns None if the file is broken.
689: """
690: tmp_fn = self._temp_fsname(filename)
691: if isfile(tmp_fn):
692: return tmp_fn
693: fn = self._fsname(filename)
694: if isfile(fn):
695: return fn
696: self._undo()
697: if isfile(tmp_fn):
698: return tmp_fn
699:
700: # b/w compatibility
701: def _get_filename(self, filename):
702: """ Deprecated, use _get_fsname """
703: return self._get_fsname(filename)
704:
705: def _get_ufn(self, filename, path=None, content_type=None, lock=1):
706: """ If no unique filename has been generated, generate one
707: otherwise, return the existing one.
708: """
709: if UNDO_POLICY==ALWAYS_BACKUP or filename==[]:
710: new_fn = self._get_new_ufn(path=path, content_type=content_type, lock=lock)
711: else:
712: new_fn = filename[:]
713: if filename:
714: old_fn = self._fsname(filename)
715: if UNDO_POLICY==ALWAYS_BACKUP:
716: try: os.rename(old_fn, old_fn+'.undo')
717: except OSError: pass
718: else:
719: try: os.rename(old_fn+'.undo', old_fn)
720: except OSError: pass
721: return new_fn
722:
723: def _get_new_ufn(self, path=None, content_type=None, lock=1):
724: """ Create a new unique filename """
725: id = self.id
726:
727: # hack so the files are not named copy_of_foo
728: if COPY_OF_PROTECTION:
729: match = copy_of_re.match(id)
730: if match is not None:
731: id = id[len(match.group(1)):]
732:
733: # get name and extension components from id
734: pos = string.rfind(id, '.')
735: if (pos+1):
736: id_name = id[:pos]
737: id_ext = id[pos:]
738: else:
739: id_name = id
740: id_ext = ''
741:
742: if not content_type:
743: content_type = self.content_type
744:
745: if REPOSITORY_EXTENSIONS in (MIMETYPE_APPEND, MIMETYPE_REPLACE):
746: mime_ext = guess_extension(content_type)
747: if mime_ext is not None:
748: if mime_ext in ('.jpeg', '.jpe'):
749: mime_ext = '.jpg' # for IE/Win :-(
750: if mime_ext in ('.obj',):
751: mime_ext = '.exe' # b/w compatibility
752: if mime_ext in ('.tiff',):
753: mime_ext = '.tif' # b/w compatibility
754: # don't change extensions of unknown binaries
755: if not (content_type == 'application/octet-stream' and id_ext):
756: if REPOSITORY_EXTENSIONS == MIMETYPE_APPEND:
757: id_name = id_name + id_ext
758: id_ext = mime_ext
759:
760: # generate directory structure
761: if path is not None:
762: rel_url_list = path
763: else:
764: rel_url_list = self._get_zodb_path()
765:
766: dirs = []
767: if REPOSITORY == SYNC_ZODB:
768: dirs = rel_url_list
769: elif REPOSITORY in (SLICED, SLICED_REVERSE, SLICED_HASH):
770: if REPOSITORY == SLICED_HASH:
771: # increase distribution by including the path in the hash
772: hashed = ''.join(list(rel_url_list)+[id_name])
773: temp = base64.encodestring(sha.new(hashed).digest())[:-1]
774: temp = temp.replace('/', '_')
775: temp = temp.replace('+', '_')
776: elif REPOSITORY == SLICED_REVERSE:
777: temp = list(id_name)
778: temp.reverse()
779: temp = ''.join(temp)
780: else:
781: temp = id_name
782: for i in range(SLICE_DEPTH):
783: if len(temp)<SLICE_WIDTH*(SLICE_DEPTH-i):
784: dirs.append(SLICE_WIDTH*'_')
785: else:
786: dirs.append(temp[:SLICE_WIDTH])
787: temp=temp[SLICE_WIDTH:]
788: elif REPOSITORY == CUSTOM:
789: method = aq_acquire(self, CUSTOM_METHOD)
790: dirs = method(rel_url_list, id)
791:
792: if NORMALIZE_CASE == NORMALIZE:
793: dirs = [d.lower() for d in dirs]
794:
795: # make directories
796: dirpath = self._fsname(dirs)
797: if not os.path.isdir(dirpath):
798: mkdir_exc = "Can't create directory: "
799: umask = os.umask(REPOSITORY_UMASK)
800: try:
801: os.makedirs(dirpath)
802: os.umask(umask)
803: except:
804: os.umask(umask)
805: raise mkdir_exc, dirpath
806:
807: # generate file name
808: fileformat = FILE_FORMAT
809: # time/counter (%t)
810: if string.find(fileformat, "%t")>=0:
811: fileformat = string.replace(fileformat, "%t", "%c")
812: counter = int(DateTime().strftime('%m%d%H%M%S'))
813: else:
814: counter = 0
815: invalid_format_exc = "Invalid file format: "
816: if string.find(fileformat, "%c")==-1:
817: raise invalid_format_exc, FILE_FORMAT
818: # user (%u)
819: if string.find(fileformat, "%u")>=0:
820: if (getattr(self, 'REQUEST', None) is not None and
821: self.REQUEST.has_key('AUTHENTICATED_USER')):
822: user = getSecurityManager().getUser().getUserName()
823: fileformat = string.replace(fileformat, "%u", user)
824: else:
825: fileformat = string.replace(fileformat, "%u", "")
826: # path (%p)
827: if string.find(fileformat, "%p")>=0:
828: temp = string.joinfields(rel_url_list, "_")
829: fileformat = string.replace(fileformat, "%p", temp)
830: # file and extension (%n and %e)
831: if string.find(fileformat,"%n")>=0 or string.find(fileformat,"%e")>=0:
832: fileformat = string.replace(fileformat, "%n", id_name)
833: fileformat = string.replace(fileformat, "%e", id_ext)
834:
835: # lock the directory
836: if lock: self._dir__lock(dirpath)
837:
838: # search for unique filename
839: if counter:
840: fn = join(dirpath, string.replace(fileformat, "%c", ".%s" % counter))
841: else:
842: fn = join(dirpath, string.replace(fileformat, "%c", ''))
843: while isfile(fn) or isfile(fn+'.undo') or isfile(fn+'.tmp'):
844: counter = counter + 1
845: fn = join(dirpath, string.replace(fileformat, "%c", ".%s" % counter))
846: if counter:
847: fileformat = string.replace(fileformat, "%c", ".%s" % counter)
848: else:
849: fileformat = string.replace(fileformat, "%c", '')
850:
851: dirs.append(fileformat)
852: return dirs
853:
854: def _static_url(self, icon=0, preview=0):
855: """ Return the static url of the file """
856: static_path = os.environ.get('EXTFILE_STATIC_PATH')
857: if static_path is not None:
858: filename, content_type, icon, preview = \
859: self._get_file_to_serve(icon, preview)
860: if icon:
861: # cannot serve statically
862: return '%s?icon=1' % self.absolute_url()
863: else:
864: # rewrite to static url
865: static_host = os.environ.get('EXTFILE_STATIC_HOST')
866: host = self.REQUEST.SERVER_URL
867: if static_host is not None:
868: if host[:8] == 'https://':
869: host = 'https://' + static_host
870: else:
871: host = 'http://' + static_host
872: host = host + urllib.quote(static_path) + '/'
873: return host + urllib.quote('/'.join(filename))
874: else:
875: if icon:
876: return '%s?icon=1' % self.absolute_url()
877: elif preview:
878: return '%s?preview=1' % self.absolute_url()
879: else:
880: return self.absolute_url()
881:
882: def _get_file_to_serve(self, icon=0, preview=0):
883: """ Find out about the file we are going to serve """
884: if not self._access_permitted():
885: preview = 1
886: if preview and not getattr(self, 'has_preview', 0):
887: icon = 1
888:
889: if icon:
890: filename = join(package_home(globals()), self.getIconPath())
891: content_type = 'image/gif'
892: elif preview:
893: filename = self.prev_filename
894: content_type = self.prev_content_type
895: else:
896: filename = self.filename
897: content_type = self.content_type
898:
899: return filename, content_type, icon, preview
900:
901: def _get_zodb_path(self, parent=None):
902: """ Returns the ZODB path of the parent object """
903: # XXX: The Photo product uploads into unwrapped ExtImages.
904: # As we can not reliably guess our parent object we fall back
905: # to the old behavior. This means that Photos will always
906: # use ZODB_PATH = VIRTUAL independent of config settings.
907: try:
908: from Products.Photo.ExtPhotoImage import PhotoImage
909: except ImportError:
910: pass
911: else:
912: if isinstance(self, PhotoImage):
913: path = self.absolute_url(1).split('/')[:-1]
914: return filter(None, path)
915: # XXX: End of hack
916:
917: # For normal operation objects must be wrapped
918: if parent is None:
919: parent = self.aq_parent
920:
921: if ZODB_PATH == VIRTUAL:
922: path = parent.absolute_url(1).split('/')
923: else:
924: path = list(parent.getPhysicalPath())
925: return filter(None, path)
926:
927: def _bytetostring(self, value):
928: """ Convert an int-value (file-size in bytes) to an String
929: with the file-size in Byte, KB or MB
930: """
931: bytes = float(value)
932: if bytes>=1000:
933: bytes = bytes/1024
934: if bytes>=1000:
935: bytes = bytes/1024
936: typ = ' MB'
937: else:
938: typ = ' KB'
939: else:
940: typ = ' Bytes'
941: strg = '%4.2f'%bytes
942: strg = strg[:4]
943: if strg[3]=='.': strg = strg[:3]
944: strg = strg+typ
945: return strg
946:
947: def _afterUpdate(self):
948: """ Called whenever the file data has been updated.
949: Invokes the manage_afterUpdate() hook.
950: """
951: self.ZCacheable_invalidate()
952:
953: return self.manage_afterUpdate(self._get_fsname(self.filename),
954: self.content_type, self.get_size())
955:
956: ################################
957: # Special management methods #
958: ################################
959:
960: security.declarePrivate('manage_afterClone')
961: def manage_afterClone(self, item, new_fn=None):
962: """ When a copy of the object is created (zope copy-paste-operation),
963: this function is called by CopySupport.py. A copy of the external
964: file is created and self.filename is changed.
965: """
966: call_afterUpdate = 0
967: try:
968: self.aq_parent # This raises AttributeError if no context
969: except AttributeError:
970: self._v_has_been_cloned=1 # This is to make webdav COPY work
971: else:
972: fn = self._get_fsname(self.filename)
973: if fn:
974: self._register() # Register with TM
975: try:
976: new_fn = new_fn or self._get_new_ufn()
977: self._update_data(fn, self._temp_fsname(new_fn))
978: self.filename = new_fn
979: call_afterUpdate = 1
980: finally:
981: self._dir__unlock()
982: if call_afterUpdate:
983: self._afterUpdate()
984: return ExtFile.inheritedAttribute("manage_afterClone")(self, item)
985:
986: security.declarePrivate('manage_afterAdd')
987: def manage_afterAdd(self, item, container):
988: """ This method is called, whenever _setObject in ObjectManager gets
989: called. This is the case after a normal add and if the object is a
990: result of cut-paste- or rename-operation. In the first case, the
991: external files doesn't exist yet, otherwise it was renamed to .undo
992: by manage_beforeDelete before and must be restored by _undo().
993: """
994: self._undo()
995: if hasattr(self, "_v_has_been_cloned"):
996: delattr(self, "_v_has_been_cloned")
997: self.manage_afterClone(item)
998: return ExtFile.inheritedAttribute("manage_afterAdd")(self, item, container)
999:
1000: security.declarePrivate('manage_beforeDelete')
1001: def manage_beforeDelete(self, item, container):
1002: """ This method is called, when the object is deleted. To support
1003: undo-functionality and because this happens too, when the object
1004: is moved (cut-paste) or renamed, the external file is not deleted.
1005: It is just renamed to filename.undo and remains in the
1006: repository, until it is deleted manually.
1007: """
1008: tmp_fn = self._temp_fsname(self.filename)
1009: fn = self._fsname(self.filename)
1010: if isfile(tmp_fn):
1011: try: os.rename(tmp_fn, fn+'.undo')
1012: except OSError: pass
1013: else:
1014: try: os.remove(fn)
1015: except OSError: pass
1016: elif isfile(fn):
1017: try: os.rename(fn, fn+'.undo')
1018: except OSError: pass
1019: return ExtFile.inheritedAttribute("manage_beforeDelete")(self, item, container)
1020:
1021: security.declarePrivate('manage_afterUpdate')
1022: def manage_afterUpdate(self, filename, content_type, size):
1023: """ This method is called whenever the file data has been updated.
1024: May be overridden by subclasses to perform additional operations.
1025: The 'filename' argument contains the path as returned by get_fsname().
1026: """
1027: pass
1028:
1029: security.declarePrivate('get_fsname')
1030: def get_fsname(self):
1031: """ Returns the current file system path of the file or image.
1032: This path can be used to access the file even while a
1033: transaction is in progress (aka Zagy's revenge :-).
1034: Returns None if the file does not exist in the repository.
1035: """
1036: return self._get_fsname(self.filename)
1037:
1038: ################################
1039: # Repository locking methods #
1040: ################################
1041:
1042: def _dir__lock(self, dir):
1043: """ Lock a directory """
1044: if hasattr(self, '_v_dir__lock'):
1045: raise DirLockError, 'Double lock in thread'
1046: self._v_dir__lock = DirLock(dir)
1047:
1048: def _dir__unlock(self):
1049: """ Unlock a previously locked directory """
1050: if hasattr(self, '_v_dir__lock'):
1051: self._v_dir__lock.release()
1052: delattr(self, '_v_dir__lock')
1053:
1054: ################################
1055: # Transaction manager methods #
1056: ################################
1057:
1058: def _register(self):
1059: if _debug: LOG(_SUBSYS, INFO, 'registering %s' % TM.contains(self))
1060: TM.register(self)
1061: if _debug: LOG(_SUBSYS, INFO, 'registered %s' % TM.contains(self))
1062:
1063: def _begin(self):
1064: self._v_begin_called = 1 # for tests
1065: if _debug: LOG(_SUBSYS, INFO, 'beginning %s' % self.id)
1066:
1067: def _finish(self):
1068: """ Commits the temporary file """
1069: self._v_finish_called = 1 # for tests
1070: TM.remove(self) # for tests
1071: if self.filename:
1072: tmp_fn = self._temp_fsname(self.filename)
1073: if _debug: LOG(_SUBSYS, INFO, 'finishing %s' % tmp_fn)
1074: if isfile(tmp_fn):
1075: if _debug: LOG(_SUBSYS, INFO, 'isfile %s' % tmp_fn)
1076: fn = self._fsname(self.filename)
1077: try: os.remove(fn)
1078: except OSError: pass
1079: os.rename(tmp_fn, fn)
1080:
1081: def _abort(self):
1082: """ Deletes the temporary file """
1083: self._v_abort_called = 1 # for tests
1084: TM.remove(self) # for tests
1085: if self.filename:
1086: tmp_fn = self._temp_fsname(self.filename)
1087: if _debug: LOG(_SUBSYS, INFO, 'aborting %s' % tmp_fn)
1088: if isfile(tmp_fn):
1089: if _debug: LOG(_SUBSYS, INFO, 'isfile %s' % tmp_fn)
1090: try: os.remove(tmp_fn)
1091: except OSError: pass
1092:
1093: InitializeClass(ExtFile)
1094:
1095:
1096: # Filename to id translation
1097: bad_chars = """ ,;:'"()[]{}ÄÅÁÀÂÃäåáàâãÇçÉÈÊËÆéèêëæÍÌÎÏíìîïÑñÖÓÒÔÕØöóòôõøŠšßÜÚÙÛüúùûÝŸýÿŽž"""
1098: good_chars = """____________AAAAAAaaaaaaCcEEEEEeeeeeIIIIiiiiNnOOOOOOooooooSssUUUUuuuuYYyyZz"""
1099: TRANSMAP = string.maketrans(bad_chars, good_chars)
1100:
1101: def normalize_id(id):
1102: # Support at least utf-8 and latin-1 filenames.
1103: # This is lame, but before it was latin-1 only.
1104: try:
1105: uid = unicode(id, 'utf-8')
1106: except UnicodeError, TypeError:
1107: try:
1108: uid = unicode(id, 'iso-8859-15')
1109: except UnicodeError, TypeError:
1110: return id
1111: id = uid.encode('iso-8859-15', 'ignore')
1112: id = string.translate(id, TRANSMAP)
1113: return id
1114:
1115:
1116: # FileUpload factory
1117: from cgi import FieldStorage
1118: from ZPublisher.HTTPRequest import FileUpload
1119:
1120: def HTTPUpload(fp, content_type=None):
1121: """ Create a FileUpload instance from a file handle (and content_type) """
1122: if isinstance(fp, FileUpload):
1123: if content_type:
1124: fp.headers['content-type'] = content_type
1125: return fp
1126: else:
1127: environ = {'REQUEST_METHOD': 'POST'}
1128: if content_type:
1129: environ['CONTENT_TYPE'] = content_type
1130: elif hasattr(fp, 'headers') and fp.headers.has_key('content-type'):
1131: environ['CONTENT_TYPE'] = fp.headers['content-type']
1132: fs = FieldStorage(fp=fp, environ=environ)
1133: return FileUpload(fs)
1134:
1135:
1136: # Repository lock
1137: import time
1138:
1139: class DirLockError(OSError):
1140: pass
1141:
1142: class DirLock:
1143: """ Manage the lockfile for a directory """
1144:
1145: lock_name = '@@@lock'
1146: sleep_secs = 1.5
1147: sleep_times = 10
1148:
1149: def _mklock(self):
1150: f = open(self._lock, 'wt')
1151: f.write('ExtFile dir lock. You may want to remove this file.')
1152: f.close()
1153:
1154: def _rmlock(self):
1155: os.remove(self._lock)
1156:
1157: def islocked(self):
1158: os.path.isfile(self._lock)
1159:
1160: def release(self):
1161: self._rmlock()
1162:
1163: def __init__(self, dir):
1164: self._lock = os.path.join(dir, self.lock_name)
1165: for i in range(self.sleep_times):
1166: if self.islocked():
1167: LOG(_SUBSYS, BLATHER, "Waiting for lock '%s'" % self._lock)
1168: time.sleep(self.sleep_secs)
1169: else:
1170: self._mklock()
1171: break
1172: else:
1173: LOG(_SUBSYS, BLATHER, "Failed to get lock '%s'" % self._lock)
1174: raise DirLockError, "Failed to get lock '%s'" % self._lock
1175:
1176:
1177: # Stream iterator
1178: if IStreamIterator is not None:
1179:
1180: class stream_iterator:
1181: __implements__ = (IStreamIterator,)
1182:
1183: def __init__(self, stream, blocksize=2<<16):
1184: self._stream = stream
1185: self._blocksize = blocksize
1186:
1187: def next(self):
1188: data = self._stream.read(self._blocksize)
1189: if not data:
1190: self._stream.close()
1191: self._stream = None
1192: raise StopIteration
1193: return data
1194:
1195: def __len__(self):
1196: cur_pos = self._stream.tell()
1197: self._stream.seek(0, 2)
1198: size = self._stream.tell()
1199: self._stream.seek(cur_pos, 0)
1200: return size
1201:
1202: def __nonzero__(self):
1203: return self.__len__() and 1 or 0
1204:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>