Back to index

plone3  3.1.7
DirectoryView.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
00004 #
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE.
00011 #
00012 ##############################################################################
00013 """ Views of filesystem directories as folders.
00014 
00015 $Id: DirectoryView.py 74063 2007-04-09 21:23:43Z tseaver $
00016 """
00017 
00018 import logging
00019 import re
00020 from os import path, listdir, stat
00021 from os.path import abspath
00022 from sys import platform
00023 from warnings import warn
00024 
00025 from AccessControl import ClassSecurityInfo
00026 from Acquisition import aq_inner, aq_parent
00027 from Globals import DevelopmentMode
00028 from Globals import DTMLFile
00029 from Globals import HTMLFile
00030 from Globals import InitializeClass
00031 from Globals import Persistent
00032 from OFS.Folder import Folder
00033 from OFS.ObjectManager import bad_id
00034 from zope.interface import implements
00035 
00036 from FSMetadata import FSMetadata
00037 from FSObject import BadFile
00038 from interfaces import IDirectoryView
00039 from permissions import AccessContentsInformation as ACI
00040 from permissions import ManagePortal
00041 from utils import _dtmldir
00042 from utils import normalize
00043 from utils import getPackageName
00044 from utils import getPackageLocation
00045 from utils import ProductsPath
00046 
00047 logger = logging.getLogger('CMFCore.DirectoryView')
00048 
00049 __reload_module__ = 0
00050 
00051 # Ignore filesystem artifacts
00052 base_ignore = ('.', '..')
00053 # Ignore version control subdirectories
00054 ignore = ('CVS', '.svn')
00055 # Ignore suspected backups and hidden files
00056 ignore_re = re.compile(r'\.|(.*~$)|#')
00057 
00058 # and special names.
00059 def _filtered_listdir(path, ignore):
00060     return [ name
00061              for name
00062              in listdir(path)
00063              if name not in ignore and not ignore_re.match(name) ]
00064 
00065 class _walker:
00066     def __init__(self, ignore):
00067         # make a dict for faster lookup
00068         self.ignore = dict([(x, None) for x in ignore])
00069 
00070     def __call__(self, listdir, dirname, names):
00071         # filter names inplace, so filtered directories don't get visited
00072         names[:] = [ name
00073                      for name
00074                      in names
00075                      if name not in self.ignore and not ignore_re.match(name) ]
00076         # append with stat info
00077         results = [ (name, stat(path.join(dirname,name))[8])
00078                     for name in names ]
00079         listdir.extend(results)
00080 
00081 
00082 def _generateKey(package, subdir):
00083     """Generate a key for a path inside a package.
00084 
00085     The key has the quality that keys for subdirectories can be derived by
00086     simply appending to the key.
00087     """
00088     return ':'.join((package, subdir.replace('\\', '/')))
00089 
00090 def _findProductForPath(path, subdir=None):
00091     # like minimalpath, but raises an error if path is not inside a product
00092     p = abspath(path)
00093     for ppath in ProductsPath:
00094         if p.startswith(ppath):
00095             dirpath = p[len(ppath)+1:]
00096             parts = dirpath.replace('\\', '/').split('/', 1)
00097             parts.append('')
00098             if subdir:
00099                 subdir = '/'.join((parts[1], subdir))
00100                 if subdir.startswith('/'):
00101                     subdir=subdir[1:]
00102             else:
00103                 subdir = parts[1]
00104             return ('Products.' + parts[0], subdir)
00105 
00106     raise ValueError('Path is not inside a product')
00107 
00108 
00109 class DirectoryInformation:
00110     data = None
00111     _v_last_read = 0
00112     _v_last_filelist = [] # Only used on Win32
00113 
00114     def __init__(self, filepath, reg_key, ignore=ignore):
00115         self._filepath = filepath
00116         self._reg_key = reg_key
00117         self.ignore = base_ignore + tuple(ignore)
00118         if platform == 'win32':
00119             self._walker = _walker(self.ignore)
00120         subdirs = []
00121         for entry in _filtered_listdir(self._filepath, ignore=self.ignore):
00122            entry_filepath = path.join(self._filepath, entry)
00123            if path.isdir(entry_filepath):
00124                subdirs.append(entry)
00125         self.subdirs = tuple(subdirs)
00126 
00127     def getSubdirs(self):
00128         return self.subdirs
00129 
00130     def _isAllowableFilename(self, entry):
00131         if entry[-1:] == '~':
00132             return 0
00133         if entry[:1] in ('_', '#'):
00134             return 0
00135         return 1
00136 
00137     def reload(self):
00138         self.data = None
00139 
00140     def _readTypesFile(self):
00141         """ Read the .objects file produced by FSDump.
00142         """
00143         types = {}
00144         try:
00145             f = open( path.join(self._filepath, '.objects'), 'rt' )
00146         except IOError:
00147             pass
00148         else:
00149             lines = f.readlines()
00150             f.close()
00151             for line in lines:
00152                 try:
00153                     obname, meta_type = line.split(':')
00154                 except ValueError:
00155                     pass
00156                 else:
00157                     types[obname.strip()] = meta_type.strip()
00158         return types
00159 
00160     if DevelopmentMode:
00161 
00162         def _changed(self):
00163             mtime=0
00164             filelist=[]
00165             try:
00166                 mtime = stat(self._filepath)[8]
00167                 if platform == 'win32':
00168                     # some Windows directories don't change mtime
00169                     # when a file is added to or deleted from them :-(
00170                     # So keep a list of files as well, and see if that
00171                     # changes
00172                     path.walk(self._filepath, self._walker, filelist)
00173                     filelist.sort()
00174             except:
00175                 logger.exception("Error checking for directory modification")
00176 
00177             if mtime != self._v_last_read or filelist != self._v_last_filelist:
00178                 self._v_last_read = mtime
00179                 self._v_last_filelist = filelist
00180 
00181                 return 1
00182 
00183             return 0
00184 
00185     else:
00186 
00187         def _changed(self):
00188             return 0
00189 
00190     def getContents(self, registry):
00191         changed = self._changed()
00192         if self.data is None or changed:
00193             try:
00194                 self.data, self.objects = self.prepareContents(registry,
00195                     register_subdirs=changed)
00196             except:
00197                 logger.exception("Error during prepareContents")
00198                 self.data = {}
00199                 self.objects = ()
00200 
00201         return self.data, self.objects
00202 
00203     def prepareContents(self, registry, register_subdirs=0):
00204         # Creates objects for each file.
00205         data = {}
00206         objects = []
00207         types = self._readTypesFile()
00208         for entry in _filtered_listdir(self._filepath, ignore=self.ignore):
00209             if not self._isAllowableFilename(entry):
00210                 continue
00211             entry_filepath = path.join(self._filepath, entry)
00212             if path.isdir(entry_filepath):
00213                 # Add a subdirectory only if it was previously registered,
00214                 # unless register_subdirs is set.
00215                 entry_reg_key = '/'.join((self._reg_key, entry))
00216                 info = registry.getDirectoryInfo(entry_reg_key)
00217                 if info is None and register_subdirs:
00218                     # Register unknown subdirs
00219                     registry.registerDirectoryByKey(entry_filepath,
00220                                                     entry_reg_key)
00221                     info = registry.getDirectoryInfo(entry_reg_key)
00222                 if info is not None:
00223                     # Folders on the file system have no extension or
00224                     # meta_type, as a crutch to enable customizing what gets
00225                     # created to represent a filesystem folder in a
00226                     # DirectoryView we use a fake type "FOLDER". That way
00227                     # other implementations can register for that type and
00228                     # circumvent the hardcoded assumption that all filesystem
00229                     # directories will turn into DirectoryViews.
00230                     mt = types.get(entry) or 'FOLDER'
00231                     t = registry.getTypeByMetaType(mt)
00232                     if t is None:
00233                         t = DirectoryView
00234                     metadata = FSMetadata(entry_filepath)
00235                     metadata.read()
00236                     ob = t( entry
00237                           , entry_reg_key
00238                           , properties=metadata.getProperties()
00239                           )
00240                     ob_id = ob.getId()
00241                     data[ob_id] = ob
00242                     objects.append({'id': ob_id, 'meta_type': ob.meta_type})
00243             else:
00244                 pos = entry.rfind('.')
00245                 if pos >= 0:
00246                     name = entry[:pos]
00247                     ext = path.normcase(entry[pos + 1:])
00248                 else:
00249                     name = entry
00250                     ext = ''
00251                 if not name or name == 'REQUEST':
00252                     # Not an allowable id.
00253                     continue
00254                 mo = bad_id(name)
00255                 if mo is not None and mo != -1:  # Both re and regex formats
00256                     # Not an allowable id.
00257                     continue
00258                 t = None
00259                 mt = types.get(entry, None)
00260                 if mt is None:
00261                     mt = types.get(name, None)
00262                 if mt is not None:
00263                     t = registry.getTypeByMetaType(mt)
00264                 if t is None:
00265                     t = registry.getTypeByExtension(ext)
00266 
00267                 if t is not None:
00268                     metadata = FSMetadata(entry_filepath)
00269                     metadata.read()
00270                     try:
00271                         ob = t(name, entry_filepath, fullname=entry,
00272                                properties=metadata.getProperties())
00273                     except:
00274                         import sys
00275                         import traceback
00276                         typ, val, tb = sys.exc_info()
00277                         try:
00278                             logger.exception("prepareContents")
00279 
00280                             exc_lines = traceback.format_exception( typ,
00281                                                                     val,
00282                                                                     tb )
00283                             ob = BadFile( name,
00284                                           entry_filepath,
00285                                           exc_str='\r\n'.join(exc_lines),
00286                                           fullname=entry )
00287                         finally:
00288                             tb = None   # Avoid leaking frame!
00289 
00290                     # FS-based security
00291                     permissions = metadata.getSecurity()
00292                     if permissions is not None:
00293                         for name in permissions.keys():
00294                             acquire, roles = permissions[name]
00295                             try:
00296                                 ob.manage_permission(name,roles,acquire)
00297                             except ValueError:
00298                                 logger.exception("Error setting permissions")
00299 
00300                     # only DTML Methods and Python Scripts can have proxy roles
00301                     if hasattr(ob, '_proxy_roles'):
00302                         try:
00303                             ob._proxy_roles = tuple(metadata.getProxyRoles())
00304                         except:
00305                             logger.exception("Error setting proxy role")
00306 
00307                     ob_id = ob.getId()
00308                     data[ob_id] = ob
00309                     objects.append({'id': ob_id, 'meta_type': ob.meta_type})
00310 
00311         return data, tuple(objects)
00312 
00313 
00314 class DirectoryRegistry:
00315 
00316     def __init__(self):
00317         self._meta_types = {}
00318         self._object_types = {}
00319         self._directories = {}
00320 
00321     def registerFileExtension(self, ext, klass):
00322         self._object_types[ext] = klass
00323 
00324     def registerMetaType(self, mt, klass):
00325         self._meta_types[mt] = klass
00326 
00327     def getTypeByExtension(self, ext):
00328         return self._object_types.get(ext, None)
00329 
00330     def getTypeByMetaType(self, mt):
00331         return self._meta_types.get(mt, None)
00332 
00333     def registerDirectory(self, name, _prefix, subdirs=1, ignore=ignore):
00334         # This what is actually called to register a
00335         # file system directory to become a FSDV.
00336         if not isinstance(_prefix, basestring):
00337             package = getPackageName(_prefix)
00338             filepath = path.join(getPackageLocation(package), name)
00339         else:
00340             warn('registerDirectory() called with deprecated _prefix type. '
00341                  'Support for paths will be removed in CMF 2.3. Please use '
00342                  'globals instead.', DeprecationWarning, stacklevel=2)
00343             filepath = path.join(_prefix, name)
00344             (package, name) = _findProductForPath(_prefix, name)
00345         reg_key = _generateKey(package, name)
00346         self.registerDirectoryByKey(filepath, reg_key, subdirs, ignore)
00347 
00348     def registerDirectoryByKey(self, filepath, reg_key, subdirs=1,
00349                                ignore=ignore):
00350         info = DirectoryInformation(filepath, reg_key, ignore)
00351         self._directories[reg_key] = info
00352         if subdirs:
00353             for entry in info.getSubdirs():
00354                 entry_filepath = path.join(filepath, entry)
00355                 entry_reg_key = '/'.join((reg_key, entry))
00356                 self.registerDirectoryByKey(entry_filepath, entry_reg_key,
00357                                             subdirs, ignore)
00358 
00359     def registerDirectoryByPath(self, filepath, subdirs=1, ignore=ignore):
00360         warn('registerDirectoryByPath() is deprecated and will be removed in '
00361              'CMF 2.3. Please use registerDirectoryByKey() instead.',
00362              DeprecationWarning, stacklevel=2)
00363         (package, subdir) = _findProductForPath(filepath)
00364         reg_key = _generateKey(package, subdir)
00365         self.registerDirectoryByKey(filepath, reg_key, subdirs, ignore)
00366 
00367     def reloadDirectory(self, reg_key):
00368         info = self.getDirectoryInfo(reg_key)
00369         if info is not None:
00370             info.reload()
00371 
00372     def getDirectoryInfo(self, reg_key):
00373         # This is called when we need to get hold of the information
00374         # for a minimal path. Can return None.
00375         return self._directories.get(reg_key, None)
00376 
00377     def listDirectories(self):
00378         dirs = self._directories.keys()
00379         dirs.sort()
00380         return dirs
00381 
00382     def getCurrentKeyFormat(self, reg_key):
00383         # BBB: method will be removed in CMF 2.3
00384 
00385         if reg_key in self._directories:
00386             return reg_key
00387 
00388         # for DirectoryViews created with CMF versions before 2.1
00389         # a path relative to Products/ was used
00390         dirpath = reg_key.replace('\\', '/')
00391         if dirpath.startswith('Products/'):
00392             dirpath = dirpath[9:]
00393         product = ['Products']
00394         dirparts = dirpath.split('/')
00395         while dirparts:
00396             product.append(dirparts[0])
00397             dirparts = dirparts[1:]
00398             possible_key = _generateKey('.'.join(product), '/'.join(dirparts))
00399             if possible_key in self._directories:
00400                 return possible_key
00401 
00402         # for DirectoryViews created with CMF versions before 1.5
00403         # this is basically the old minimalpath() code
00404         dirpath = normalize(reg_key)
00405         index = dirpath.rfind('Products')
00406         if index == -1:
00407             index = dirpath.rfind('products')
00408         if index != -1:
00409             dirpath = dirpath[index+len('products/'):]
00410             product = ['Products']
00411             dirparts = dirpath.split('/')
00412             while dirparts:
00413                 product.append(dirparts[0])
00414                 dirparts = dirparts[1:]
00415                 possible_key = _generateKey('.'.join(product),
00416                                             '/'.join(dirparts))
00417                 if possible_key in self._directories:
00418                     return possible_key
00419 
00420         raise ValueError('Unsupported key given: %s' % reg_key)
00421 
00422 
00423 _dirreg = DirectoryRegistry()
00424 registerDirectory = _dirreg.registerDirectory
00425 registerFileExtension = _dirreg.registerFileExtension
00426 registerMetaType = _dirreg.registerMetaType
00427 
00428 
00429 def listFolderHierarchy(ob, path, rval, adding_meta_type=None):
00430     if not hasattr(ob, 'objectValues'):
00431         return
00432     values = ob.objectValues()
00433     for subob in ob.objectValues():
00434         base = getattr(subob, 'aq_base', subob)
00435         if getattr(base, 'isPrincipiaFolderish', 0):
00436 
00437             if adding_meta_type is not None and hasattr(
00438                 base, 'filtered_meta_types'):
00439                 # Include only if the user is allowed to
00440                 # add the given meta type in this location.
00441                 meta_types = subob.filtered_meta_types()
00442                 found = 0
00443                 for mt in meta_types:
00444                     if mt['name'] == adding_meta_type:
00445                         found = 1
00446                         break
00447                 if not found:
00448                     continue
00449 
00450             if path:
00451                 subpath = path + '/' + subob.getId()
00452             else:
00453                 subpath = subob.getId()
00454             title = getattr(subob, 'title', None)
00455             if title:
00456                 name = '%s (%s)' % (subpath, title)
00457             else:
00458                 name = subpath
00459             rval.append((subpath, name))
00460             listFolderHierarchy(subob, subpath, rval, adding_meta_type)
00461 
00462 
00463 class DirectoryView(Persistent):
00464 
00465     """ Directory views mount filesystem directories.
00466     """
00467 
00468     implements(IDirectoryView)
00469 
00470     meta_type = 'Filesystem Directory View'
00471     _dirpath = None
00472     _properties = None
00473     _objects = ()
00474 
00475     def __init__(self, id, reg_key='', fullname=None, properties=None):
00476         if properties:
00477             # Since props come from the filesystem, this should be
00478             # safe.
00479             self.__dict__.update(properties)
00480 
00481         self.id = id
00482         self._dirpath = reg_key
00483         self._properties = properties
00484 
00485     def __of__(self, parent):
00486         reg_key = self._dirpath
00487         info = _dirreg.getDirectoryInfo(reg_key)
00488         if info is None:
00489             try:
00490                 reg_key = self._dirpath = _dirreg.getCurrentKeyFormat(reg_key)
00491                 info = _dirreg.getDirectoryInfo(reg_key)
00492             except ValueError:
00493                 # During GenericSetup a view will be created with an empty
00494                 # reg_key. This is expected behaviour, so do not warn about it.
00495                 if reg_key:
00496                     warn('DirectoryView %s refers to a non-existing path %r' %
00497                           (self.id, reg_key), UserWarning)
00498         if info is None:
00499             data = {}
00500             objects = ()
00501         else:
00502             data, objects = info.getContents(_dirreg)
00503         s = DirectoryViewSurrogate(self, data, objects)
00504         res = s.__of__(parent)
00505         return res
00506 
00507     def getId(self):
00508         return self.id
00509 
00510 InitializeClass(DirectoryView)
00511 
00512 
00513 class DirectoryViewSurrogate(Folder):
00514 
00515     """ Folderish DirectoryView.
00516     """
00517 
00518     implements(IDirectoryView)
00519 
00520     meta_type = 'Filesystem Directory View'
00521     all_meta_types = ()
00522 
00523     security = ClassSecurityInfo()
00524 
00525     def __init__(self, real, data, objects):
00526         d = self.__dict__
00527         d.update(data)
00528         d.update(real.__dict__)
00529         d['_real'] = real
00530         d['_objects'] = objects
00531 
00532     def __setattr__(self, name, value):
00533         d = self.__dict__
00534         d[name] = value
00535         setattr(d['_real'], name, value)
00536 
00537     def __delattr__(self, name):
00538         d = self.__dict__
00539         del d[name]
00540         delattr(d['_real'], name)
00541 
00542     security.declareProtected(ManagePortal, 'manage_propertiesForm')
00543     manage_propertiesForm = DTMLFile( 'dirview_properties', _dtmldir )
00544 
00545     security.declareProtected(ManagePortal, 'manage_properties')
00546     def manage_properties(self, reg_key, REQUEST=None):
00547         """ Update the directory path of the DirectoryView.
00548         """
00549         self.__dict__['_real']._dirpath = reg_key
00550         if REQUEST is not None:
00551             REQUEST['RESPONSE'].redirect( '%s/manage_propertiesForm'
00552                                         % self.absolute_url() )
00553 
00554     security.declareProtected(ACI, 'getCustomizableObject')
00555     def getCustomizableObject(self):
00556         ob = aq_parent(aq_inner(self))
00557         while ob:
00558             if IDirectoryView.providedBy(ob):
00559                 ob = aq_parent(ob)
00560             elif getattr(ob, '_isDirectoryView', 0):
00561                 # BBB
00562                 warn("The '_isDirectoryView' marker attribute is deprecated, "
00563                      "and will be removed in CMF 2.3.  Please mark the "
00564                      "instance with the 'IDirectoryView' interface instead.",
00565                      DeprecationWarning, stacklevel=2)
00566                 ob = aq_parent(ob)
00567             else:
00568                 break
00569         return ob
00570 
00571     security.declareProtected(ACI, 'listCustFolderPaths')
00572     def listCustFolderPaths(self, adding_meta_type=None):
00573         """ List possible customization folders as key, value pairs.
00574         """
00575         rval = []
00576         ob = self.getCustomizableObject()
00577         listFolderHierarchy(ob, '', rval, adding_meta_type)
00578         rval.sort()
00579         return rval
00580 
00581     security.declareProtected(ACI, 'getDirPath')
00582     def getDirPath(self):
00583         return self.__dict__['_real']._dirpath
00584 
00585     security.declarePublic('getId')
00586     def getId(self):
00587         return self.id
00588 
00589 InitializeClass(DirectoryViewSurrogate)
00590 
00591 
00592 manage_addDirectoryViewForm = HTMLFile('dtml/addFSDirView', globals())
00593 
00594 def createDirectoryView(parent, reg_key, id=None):
00595     """ Add either a DirectoryView or a derivative object.
00596     """
00597     info = _dirreg.getDirectoryInfo(reg_key)
00598     if info is None:
00599         reg_key = _dirreg.getCurrentKeyFormat(reg_key)
00600         info = _dirreg.getDirectoryInfo(reg_key)
00601         warn('createDirectoryView() called with deprecated reg_key format. '
00602              'Support for old key formats will be removed in CMF 2.3. Please '
00603              'use the new key format <product>:<subdir> instead.',
00604              DeprecationWarning, stacklevel=2)
00605     if not id:
00606         id = reg_key.split('/')[-1]
00607     else:
00608         id = str(id)
00609     ob = DirectoryView(id, reg_key)
00610     parent._setObject(id, ob)
00611 
00612 def addDirectoryViews(ob, name, _prefix):
00613     """ Add a directory view for every subdirectory of the given directory.
00614 
00615     Meant to be called by filesystem-based code. Note that registerDirectory()
00616     still needs to be called by product initialization code to satisfy
00617     persistence demands.
00618     """
00619     if not isinstance(_prefix, basestring):
00620         package = getPackageName(_prefix)
00621     else:
00622         warn('addDirectoryViews() called with deprecated _prefix type. '
00623              'Support for paths will be removed in CMF 2.3. Please use '
00624              'globals instead.', DeprecationWarning, stacklevel=2)
00625         (package, name) = _findProductForPath(_prefix, name)
00626     reg_key = _generateKey(package, name)
00627     info = _dirreg.getDirectoryInfo(reg_key)
00628     if info is None:
00629         raise ValueError('Not a registered directory: %s' % reg_key)
00630     for entry in info.getSubdirs():
00631         entry_reg_key = '/'.join((reg_key, entry))
00632         createDirectoryView(ob, entry_reg_key, entry)
00633 
00634 def manage_addDirectoryView(self, reg_key, id=None, REQUEST=None):
00635     """ Add either a DirectoryView or a derivative object.
00636     """
00637     createDirectoryView(self, reg_key, id)
00638     if REQUEST is not None:
00639         return self.manage_main(self, REQUEST)
00640 
00641 def manage_listAvailableDirectories(*args):
00642     """ List registered directories.
00643     """
00644     return list(_dirreg.listDirectories())