genbt5list 7.25 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 39 40 41
try:
  from html import escape
except ImportError:
  from cgi import escape # Deprecated since version 3.2
42
from base64 import b64encode
43
from io import BytesIO
44
from hashlib import sha1
45 46 47 48 49 50 51 52 53 54 55
try:
  from urllib.parse import unquote
except ImportError:
  from urllib import unquote

if sys.version_info[0] == 3:
  def iteritems(d):
    return iter(d.items())
else:
  def iteritems(d):
    return d.iteritems()
56 57 58

# Order is important for installation
# We want to have:
59
#  * workflow and portal_type* before ZODB Component {Document,Extension...}
60 61 62 63 64 65 66 67 68 69
#  * 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',
70
  'workflow',
71 72
  'product',
  'document',
73 74
  'interface',
  'mixin',
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 122 123
  '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):
124
    self.append((path.encode('utf-8'), sha1(text).digest()))
125 126 127

  def digest(self):
    self.sort()
128
    return b64encode(sha1(b'\0'.join(h + p for (h, p) in self)).digest())
129 130 131 132 133


class BusinessTemplate(dict):

  property_list = frozenset('''
134 135 136 137 138
title
version
description
license
dependency_list
139
test_dependency_list
140 141
provision_list
copyright_list
142
force_install
143
'''.split())
144

145 146
  def __init__(self):
    self.revision = BusinessTemplateRevision()
147

148
  def _read(self, path, file):
149
    try:
150
      text = file.read()
151
    finally:
152 153 154 155 156 157 158 159 160 161 162 163 164 165
      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()
166
    return iter(sorted(iteritems(self)))
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191

  @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)
192

193
def generateInformation(dir, info=id, err=None):
194 195
  xml = BytesIO()
  xml.write(b'<?xml version="1.0"?>\n<repository>\n')
196 197 198 199
  for name in sorted(os.listdir(dir)):
    path = os.path.join(dir, name)
    if name.endswith('.bt5'):
      info('Reading %s... ' % name)
200
      try:
201
        tar = tarfile.open(path, 'r:gz')
202
      except tarfile.TarError:
203 204 205 206
        if err:
          err('An error happened in %s; skipping\n' % name)
          continue
        raise
207
      try:
208
        property_list = BusinessTemplate.fromTar(tar)
209 210
      finally:
        tar.close()
211
    elif os.path.isfile(os.path.join(path, 'bt', 'title')):
212
      info('Reading Directory %s... ' % name)
213
      property_list = BusinessTemplate.fromDir(path)
214 215
    else:
      continue
216
    xml.write(b'  <template id="%s">\n' % name.encode())
217
    for k, v in property_list:
218 219 220 221 222 223
      if str is not bytes:
        k = k.encode()
      for v in (v,) if type(v) is bytes else v:
        xml.write(b'    <%s>%s</%s>\n' % (k, escape(v) if str is bytes else
                                             escape(v.decode()).encode(), k))
    xml.write(b'  </template>\n')
224
    info('done\n')
225
  xml.write(b'</repository>\n')
226
  return xml
227

228 229 230 231 232
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 '.'
233 234

  for d in dir_list:
235
    bt5list = generateInformation(d, **kw).getvalue()
236
    # add pid in filename to avoid conflicts if several process calls genbt5list
237 238
    destination_path =  os.path.join(d, 'bt5list')
    temporary_path = destination_path + '.new.%i' % os.getpid()
239
    try:
240
      with open(temporary_path, 'wb') as f:
241
        f.write(bt5list)
242
      os.rename(temporary_path, destination_path)
243
    finally:
244
      try:
245
        os.remove(temporary_path)
246 247
      except OSError:
        pass
248

249 250
if __name__ == "__main__":
  main()