genbt5list 6.81 KB
Newer Older
1
#! /usr/bin/env python
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
#                    Yoshinori Okuji <yo@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################


"""Generate repository information on Business Templates.
"""

34
import posixpath
35 36 37
import tarfile
import os
import sys
38
import cgi
39
from base64 import b64encode
40
from cStringIO import StringIO
41 42
from hashlib import sha1
from urllib import unquote
43

44 45 46

# Order is important for installation
# We want to have:
47
#  * workflow and portal_type* before ZODB Component {Document,Extension...}
48 49 50 51 52 53 54 55 56 57
#  * path after module, because path can be module content
#  * path after categories, because path can be categories content
#  * path after portal types roles so that roles in the current bt can be used
#  * path before workflow chain, because path can be a portal type
#         (until chains are set on portal types with categories)
#  * skin after paths, because we can install a custom connection string as
#       path and use it with SQLMethods in a skin.
#    ( and more )
item_name_list = (
  'registered_version_priority_selection',
58
  'workflow',
59 60
  'product',
  'document',
61 62
  'interface',
  'mixin',
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
  'property_sheet',
  'constraint',
  'extension',
  'test',
  'role',
  'tool',
  'message_translation',
  'site_property',
  'portal_type',
  'portal_type_allowed_content_type',
  'portal_type_hidden_content_type',
  'portal_type_property_sheet',
  'portal_type_base_category',
  'category',
  'module',
  'portal_type_roles',
  'path',
  'skin',
  'registered_skin_selection',
  'preference',
  'action',
  'local_roles',
  'portal_type_workflow_chain',
  'catalog_method',
  'catalog_result_key',
  'catalog_related_key',
  'catalog_result_table',
  'catalog_search_key',
  'catalog_keyword_key',
  'catalog_datetime_key',
  'catalog_full_text_key',
  'catalog_request_key',
  'catalog_multivalue_key',
  'catalog_topic_key',
  'catalog_scriptable_key',
  'catalog_role_key',
  'catalog_local_role_key',
  'catalog_security_uid_column',
)

item_set = set(('CatalogDateTimeKey' if x == 'catalog_datetime_key' else
             ''.join(map(str.title, x.split('_')))) + 'TemplateItem'
            for x in item_name_list)
item_set.add('bt')
item_name_list = tuple('_%s_item' % x for x in item_name_list)

class BusinessTemplateRevision(list):

  def hash(self, path, text):
    self.append((path, sha1(text).digest()))

  def digest(self):
    self.sort()
    return b64encode(sha1('\0'.join(h + p for (h, p) in self)).digest())


class BusinessTemplate(dict):

  property_list = frozenset('''
122 123 124 125 126
title
version
description
license
dependency_list
127
test_dependency_list
128 129
provision_list
copyright_list
130
force_install
131
'''.split())
132

133 134
  def __init__(self):
    self.revision = BusinessTemplateRevision()
135

136
  def _read(self, path, file):
137
    try:
138
      text = file.read()
139
    finally:
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
      file.close()
    if path.startswith('bt/'):
      name = path[3:]
      if name in self.property_list:
        if name.endswith('_list'):
          self[name[:-5]] = text.splitlines()
        else:
          self[name] = text
      elif name == 'revision':
        return
    self.revision.hash(unquote(path) if '%' in path else path, text)

  def __iter__(self):
    self['revision'] = self.revision.digest()
    return iter(sorted(self.iteritems()))

  @classmethod
  def fromTar(cls, tar):
    """Read an archived Business Template info"""
    self = cls()
    for info in tar:
      if not info.isdir():
        name = info.name.split('/', 1)[1]
        if name.split('/', 1)[0] in item_set:
          self._read(name, tar.extractfile(info))
    return iter(self)

  @classmethod
  def fromDir(cls, dir):
    """Read Business Template Directory info"""
    self = cls()
    lstrip_len = len(dir + os.sep)
    for root, dirs, files in os.walk(dir):
      if root:
        for path in files:
          path = os.path.join(root, path)
          self._read(posixpath.normpath(path[lstrip_len:]), open(path, 'rb'))
      else:
        dirs[:] = item_set.intersection(dirs)
    return iter(self)
180

181 182 183 184 185 186 187
def generateInformation(dir, info=id, err=None):
  xml = StringIO()
  xml.write('<?xml version="1.0"?>\n<repository>\n')
  for name in sorted(os.listdir(dir)):
    path = os.path.join(dir, name)
    if name.endswith('.bt5'):
      info('Reading %s... ' % name)
188
      try:
189
        tar = tarfile.open(path, 'r:gz')
190
      except tarfile.TarError:
191 192 193 194
        if err:
          err('An error happened in %s; skipping\n' % name)
          continue
        raise
195
      try:
196
        property_list = BusinessTemplate.fromTar(tar)
197 198
      finally:
        tar.close()
199
    elif os.path.isfile(os.path.join(path, 'bt', 'title')):
200
      info('Reading Directory %s... ' % name)
201
      property_list = BusinessTemplate.fromDir(path)
202 203
    else:
      continue
204
    xml.write('  <template id="%s">\n' % name)
205
    for k, v in property_list:
206 207 208
      for v in (v,) if type(v) is str else v:
        xml.write('    <%s>%s</%s>\n' % (k, cgi.escape(v), k))
    xml.write('  </template>\n')
209
    info('done\n')
210 211
  xml.write('</repository>\n')
  return xml
212

213 214 215 216 217
def main(dir_list=None, **kw):
  if dir_list is None:
    kw.setdefault('info', sys.stdout.write)
    kw.setdefault('err', sys.stderr.write)
    dir_list = sys.argv[1:] or '.'
218 219

  for d in dir_list:
220
    bt5list = generateInformation(d, **kw).getvalue()
221
    # add pid in filename to avoid conflicts if several process calls genbt5list
222 223
    destination_path =  os.path.join(d, 'bt5list')
    temporary_path = destination_path + '.new.%i' % os.getpid()
224
    try:
225
      with open(temporary_path, 'wb') as f:
226
        f.write(bt5list)
227
      os.rename(temporary_path, destination_path)
228
    finally:
229
      try:
230
        os.remove(temporary_path)
231 232
      except OSError:
        pass
233

234 235
if __name__ == "__main__":
  main()