You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
8.8 KiB

  1. # vim: fileencoding=utf-8
  2. #
  3. import os
  4. from .base_format import Format, FormatError
  5. from calculate.utils.files import join_paths
  6. from pyparsing import Literal, Regex, SkipTo, LineEnd, lineno, LineStart
  7. from calculate.utils.package import PackageAtomParser, Package, PackageNotFound
  8. from ..template_engine import ParametersContainer, Variables
  9. from ...variables.datavars import NamespaceNode
  10. from ...variables.loader import Datavars
  11. from fnmatch import fnmatch
  12. from typing import Union
  13. from glob import iglob
  14. ADD, REMOVE, MOVE = range(0, 3)
  15. class ContentsFormat(Format):
  16. FORMAT = 'contents'
  17. EXECUTABLE = True
  18. _initialized = False
  19. def __new__(cls, *args, **kwargs):
  20. if not cls._initialized:
  21. cls._initialize_parser()
  22. return super().__new__(cls)
  23. def __init__(self, template_text: str,
  24. template_path: str,
  25. parameters: ParametersContainer,
  26. datavars: Union[Datavars, NamespaceNode, Variables]):
  27. self._command_methods = {ADD: self._add_command,
  28. REMOVE: self._remove_command,
  29. MOVE: self._move_command}
  30. self._parse_errors = []
  31. self._commands = self._parse_template(template_text)
  32. if self._parse_errors:
  33. raise FormatError('errors while parsing template:\n{}'.
  34. format('\n'.join(self._parse_errors)))
  35. self._template_path = template_path
  36. self._packages = dict()
  37. self._atom_parser = None
  38. # Предупреждения.
  39. self._warnings: list = []
  40. def execute_format(self, target_path: str, chroot_path='/') -> dict:
  41. '''Метод для запуска работы формата.'''
  42. self._package = dict()
  43. self._atom_parser = PackageAtomParser(chroot_path=chroot_path)
  44. for command in self._commands:
  45. target_package = self._get_package(command['target'], chroot_path)
  46. if 'source' not in command:
  47. source_package = None
  48. else:
  49. source_package = self._get_package(command['source'],
  50. chroot_path)
  51. self._command_methods[command['action']](target_package,
  52. command['path'],
  53. command['lineno'],
  54. source=source_package)
  55. self._save_changes()
  56. return {}
  57. def _add_command(self, target, glob_path, lineno, source=None):
  58. # Если файл уже есть в пакете -- ничего не делаем.
  59. glob_path = join_paths(target.chroot_path, glob_path)
  60. for file_path in iglob(glob_path):
  61. # Если файл уже в пакете -- ничего не делаем.
  62. if file_path in target:
  63. continue
  64. # Проверяем существование файла.
  65. if not os.path.exists(file_path):
  66. raise FormatError(f"Format processing error:{lineno}: file "
  67. f"'{file_path}' does not exist.")
  68. # Проверяем, не принадлежит ли файл какому-нибудь другому пакету.
  69. try:
  70. file_package = self._atom_parser.get_file_package(file_path)
  71. except PackageNotFound:
  72. file_package = None
  73. if file_package is not None:
  74. raise FormatError(f"Format processing error:{lineno}: can not"
  75. f" add file '{file_path}' belonging to the"
  76. f" package {file_package} into the package"
  77. f" '{target}'.")
  78. target.add_file(file_path)
  79. def _remove_command(self, target, glob_path, lineno, source=None):
  80. paths_to_remove = []
  81. if glob_path.endswith('/'):
  82. glob_path = glob_path[0:-1]
  83. for file_path in target.contents_dictionary.keys():
  84. if fnmatch(file_path, glob_path):
  85. paths_to_remove.append(file_path)
  86. for file_path in paths_to_remove:
  87. target.remove_file(file_path)
  88. def _move_command(self, target, glob_path, lineno, source=None):
  89. if source is None:
  90. return
  91. paths_to_move = []
  92. if glob_path.endswith('/'):
  93. glob_path = glob_path[0:-1]
  94. for file_path in source.contents_dictionary.keys():
  95. if fnmatch(file_path, glob_path):
  96. paths_to_move.append(file_path)
  97. for file_path in paths_to_move:
  98. removed = source.remove_file(file_path)
  99. for file_path, file_info in removed.items():
  100. if file_info['type'] == 'dir':
  101. target.add_dir(file_path)
  102. elif file_info['type'] == 'obj':
  103. target.add_obj(file_path,
  104. file_md5=file_info['md5'],
  105. mtime=file_info['mtime'])
  106. elif file_info['type'] == 'sym':
  107. target.add_sym(file_path,
  108. target_path=file_info['target'],
  109. mtime=file_info['mtime'])
  110. target.sort_contents_dictionary()
  111. def _save_changes(self):
  112. for atom_name, package in self._packages.items():
  113. package.write_contents_file()
  114. self._packages = {}
  115. def _get_package(self, atom_name: str, chroot_path: str) -> Package:
  116. atom = self._atom_parser.parse_package_parameter(atom_name)
  117. if atom not in self._packages:
  118. package = Package(atom, chroot_path=chroot_path)
  119. self._packages.update({atom: package})
  120. else:
  121. package = self._packages[atom]
  122. return package
  123. def _parse_template(self, template_text: str) -> list:
  124. for tokens, start, end in self.template_parser.scanString(
  125. template_text):
  126. parse_result = tokens[0]
  127. if parse_result is None:
  128. continue
  129. elif 'error' in parse_result:
  130. error = (f'Parse Error:{parse_result["lineno"]}:'
  131. f' {parse_result["error"]}')
  132. self._parse_errors.append(error)
  133. continue
  134. yield parse_result
  135. @classmethod
  136. def _initialize_parser(cls):
  137. atom = Regex(PackageAtomParser.atom_regex)
  138. remove_symbol = Literal('!')
  139. path = Regex(r'\S+')
  140. add_parser = atom('target') + path('path') + LineEnd().suppress()
  141. add_parser.setParseAction(cls._parse_add)
  142. move_parser = (atom('source') + Literal(',').suppress()
  143. + atom('target') + path('path') + LineEnd().suppress())
  144. move_parser.setParseAction(cls._parse_move)
  145. remove_parser = (remove_symbol.suppress() + atom('target')
  146. + path('path') + LineEnd().suppress())
  147. remove_parser.setParseAction(cls._parse_remove)
  148. empty_line = LineStart().suppress() + LineEnd().suppress()
  149. empty_line.setParseAction(cls._parse_skip)
  150. comment = Literal('#') + SkipTo(LineEnd(), include=True)
  151. comment.setParseAction(cls._parse_skip)
  152. unexpected = SkipTo(LineEnd(), include=True)
  153. unexpected.setParseAction(cls._parse_unexpected)
  154. cls.template_parser = (move_parser | remove_parser | add_parser
  155. | empty_line | comment | unexpected)
  156. @classmethod
  157. def _parse_add(cls, string, location, parse_result):
  158. target = parse_result.asDict()['target']
  159. path = parse_result.asDict()['path']
  160. return {'action': ADD, 'target': target, 'path': path,
  161. 'lineno': lineno(location, string)}
  162. @classmethod
  163. def _parse_remove(cls, string, location, parse_result):
  164. target = parse_result.asDict()['target']
  165. path = parse_result.asDict()['path']
  166. return {'action': REMOVE, 'target': target, 'path': path,
  167. 'lineno': lineno(location, string)}
  168. @classmethod
  169. def _parse_move(cls, string, location, parse_result):
  170. source = parse_result.asDict()['source']
  171. target = parse_result.asDict()['target']
  172. path = parse_result.asDict()['path']
  173. return {'action': MOVE, 'source': source,
  174. 'target': target, 'path': path,
  175. 'lineno': lineno(location, string)}
  176. @classmethod
  177. def _parse_skip(cls, parse_result):
  178. return [None]
  179. @classmethod
  180. def _parse_unexpected(cls, string, location, parse_result):
  181. result = parse_result[0]
  182. if not result:
  183. return [None]
  184. return {'error': result, 'lineno': lineno(location, string)}
  185. @property
  186. def warnings(self):
  187. return self._warnings