genbt5list 7.32 KB
Newer Older
1
#! /usr/bin/env python3
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
  'product',
72
  'module_component',
73
  'document',
74 75
  'interface',
  'mixin',
76
  'tool_component',
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 124 125
  '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):
126
    self.append((path.encode('utf-8'), sha1(text).digest()))
127 128 129

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


class BusinessTemplate(dict):

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

147 148
  def __init__(self):
    self.revision = BusinessTemplateRevision()
149

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

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

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

230 231 232 233 234
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 '.'
235 236

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

251 252
if __name__ == "__main__":
  main()