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.

1207 lines
49 KiB

  1. # vim: fileencoding=utf-8
  2. #
  3. import os
  4. import re
  5. import glob
  6. from collections import OrderedDict
  7. from .files import read_file, read_link, join_paths, FilesError
  8. from typing import (
  9. Generator,
  10. NoReturn,
  11. Optional,
  12. Dict,
  13. Tuple,
  14. Union,
  15. List,
  16. Any
  17. )
  18. from pyparsing import (
  19. Literal,
  20. Regex,
  21. Word,
  22. nums,
  23. alphanums,
  24. LineEnd,
  25. SkipTo,
  26. )
  27. from jinja2 import PackageLoader, Environment
  28. from calculate.utils.tools import Singleton
  29. import hashlib
  30. class PackageError(Exception):
  31. '''Исключение выбрасываемое при ошибках в объектах Package, работающих
  32. с CONTENTS-файлами пакетов.'''
  33. pass
  34. # Коды ошибок ATOM-парсера.
  35. DEFAULT, NOTEXIST, NOTCORRECT = range(3)
  36. class PackageAtomError(Exception):
  37. '''Исключение выбрасываемое при ошибках разбора ATOM-названий.'''
  38. def __init__(self, message: str = 'Package atom error',
  39. errno: int = DEFAULT):
  40. self.message = message
  41. self.errno = errno
  42. class VersionError(Exception):
  43. '''Исключение выбрасываемое объектами Version.'''
  44. pass
  45. class PackageNotFound(Exception):
  46. '''Специальное исключение выбрасываемое, если не удалось найти пакет, к
  47. которому принадлежит файл.'''
  48. pass
  49. class PackageCreator(type):
  50. _instances: Dict["PackageAtomName", "Package"] = {}
  51. def __call__(cls, *args, **kwargs):
  52. autosave = False
  53. if 'autosave' in kwargs:
  54. autosave = kwargs['autosave']
  55. if not autosave or isinstance(args[0], str):
  56. return super().__call__(*args, **kwargs)
  57. if args[0] not in cls._instances:
  58. cls._instances[args[0]] = super().__call__(*args, **kwargs)
  59. return cls._instances[args[0]]
  60. @classmethod
  61. def save_all(cls) -> NoReturn:
  62. for atom, pkg in cls._instances.items():
  63. pkg.remove_empty_directories()
  64. pkg.write_contents_file()
  65. @classmethod
  66. def clear_instances(cls) -> NoReturn:
  67. """Метод для очистки текущего кэша пакетный объектов.
  68. Необходим при тестировании."""
  69. cls._instances.clear()
  70. class Version:
  71. '''Класс объектов представляющих значения версий.'''
  72. _suffix_order = {'alpha': 0, 'beta': 1, 'pre': 2,
  73. 'rc': 3, 'no': 4, 'p': 5}
  74. def __init__(self, version_value: Union["Version", None, int,
  75. float, str] = None):
  76. if version_value is None:
  77. self._string = '-1'
  78. self._value = [-1]
  79. self._literal = None
  80. self._suffix = [(4, 0)]
  81. self._revision = 0
  82. elif isinstance(version_value, Version):
  83. self._string = version_value._string
  84. self._value = version_value._value
  85. self._literal = version_value._literal
  86. self._suffix = version_value._suffix
  87. self._revision = version_value._revision
  88. else:
  89. value = self._get_version_value(version_value)
  90. if value is None:
  91. raise VersionError(
  92. "Can't initialize Version object using '{0}'"
  93. " value with type {1}".format(version_value,
  94. type(version_value)))
  95. self._string = value['string'].strip('-')
  96. self._value = value['value']
  97. self._literal = value['literal']
  98. self._suffix = value['suffix']
  99. self._revision = value['revision']
  100. def _get_version_value(self, version: Union["Version", int, float, str]
  101. ) -> Union[dict, None]:
  102. '''Вспомогательный метод для получения значения версии, представленного
  103. в виде списка.'''
  104. if isinstance(version, Version):
  105. version_value = {'string': version._string,
  106. 'value': version._value,
  107. 'literal': version._literal,
  108. 'suffix': version._suffix,
  109. 'revision': version._revision}
  110. elif isinstance(version, int):
  111. version_value = {'string': str(int),
  112. 'value': [version],
  113. 'literal': '',
  114. 'suffix': [(4, 0)],
  115. 'revision': 0}
  116. elif isinstance(version, float):
  117. version_list = []
  118. version = str(version).split('.')
  119. for version_part in version:
  120. version_list.append(int(version_part.strip()))
  121. version_value = {'string': str(version),
  122. 'value': version_list,
  123. 'literal': '',
  124. 'suffix': (4, 0),
  125. 'revision': 0}
  126. elif isinstance(version, str):
  127. parse_result = PackageAtomParser.version_regex.search(
  128. version.strip('-'))
  129. if not parse_result:
  130. return
  131. result_dict = parse_result.groupdict()
  132. version_value = {'string': version}
  133. version_list = []
  134. for version_part in result_dict['value'].split('.'):
  135. version_list.append(int(version_part.strip('-')))
  136. version_value['value'] = version_list
  137. # Парсим литерал, если он есть.
  138. version_value['literal'] = result_dict['literal'] or ''
  139. # Парсим всю совокупность имеющихся суффиксов.
  140. suffixes = result_dict['suffix']
  141. suffix_list = []
  142. if suffixes is not None:
  143. suffixes = suffixes.strip('_')
  144. suffixes = suffixes.split('_')
  145. for suffix in suffixes:
  146. result = re.search(r'([^\d]+)(\d+)?', suffix)
  147. suffix_list.append((self._suffix_order[result.group(1)],
  148. int(result.group(2) or 0)))
  149. else:
  150. suffix_list = [(self._suffix_order['no'], 0)]
  151. version_value['suffix'] = suffix_list
  152. # Парсим ревизию.
  153. if parse_result['revision'] is not None:
  154. version_value['revision'] = int(
  155. parse_result['revision'].strip('-r'))
  156. else:
  157. version_value['revision'] = 0
  158. else:
  159. return
  160. return version_value
  161. def _compare_lists(self, lversion: list, rversion: list, filler: Any = 0
  162. ) -> int:
  163. '''Метод для сравнения двух списков, даже если если они не одинаковы
  164. по длине. Возвращает 0, если списки равны, 1 если lversion больше, -1
  165. если lversion меньше.'''
  166. if lversion == rversion:
  167. return 0
  168. for index in range(0, max(len(lversion), len(rversion))):
  169. lvalue = lversion[index] if len(lversion) > index else filler
  170. rvalue = rversion[index] if len(rversion) > index else filler
  171. if lvalue == rvalue:
  172. continue
  173. if lvalue > rvalue:
  174. return 1
  175. else:
  176. return -1
  177. return 0
  178. @property
  179. def string(self):
  180. return self._string
  181. def __lt__(self, other: "Version") -> bool:
  182. '''Перегрузка x < y.'''
  183. other_version = self._get_version_value(other)
  184. if other_version is None:
  185. raise VersionError(
  186. "Unable to compare Version object with the '{0}'"
  187. " value of '{1}' type".format(other, type(other)))
  188. cmp_res = self._compare_lists(self._value, other_version['value'])
  189. if cmp_res != 0:
  190. return cmp_res < 0
  191. if self._literal != other_version['literal']:
  192. return self._literal < other_version['literal']
  193. cmp_res = self._compare_lists(self._suffix,
  194. other_version['suffix'],
  195. filler=(4, 0))
  196. if cmp_res != 0:
  197. return cmp_res < 0
  198. return self._revision < other_version['revision']
  199. def __le__(self, other: "Version") -> bool:
  200. '''Перегрузка x <= y.'''
  201. other_version = self._get_version_value(other)
  202. if other_version is None:
  203. raise VersionError(
  204. "Unable to compare Version object with the '{0}'"
  205. " value of '{1}' type".format(other, type(other)))
  206. cmp_res = self._compare_lists(self._value, other_version['value'])
  207. if cmp_res != 0:
  208. return cmp_res < 0
  209. if self._literal != other_version['literal']:
  210. return self._literal < other_version['literal']
  211. cmp_res = self._compare_lists(self._suffix,
  212. other_version['suffix'],
  213. filler=(4, 0))
  214. if cmp_res != 0:
  215. return cmp_res < 0
  216. return self._revision <= other_version['revision']
  217. def __eq__(self, other: "Version", ignore_revision: bool = False) -> bool:
  218. '''Перегрузка x == y.'''
  219. other_version = self._get_version_value(other)
  220. if other_version is None:
  221. raise VersionError(
  222. "Unable to compare Version object with the '{0}'"
  223. " value of '{1}' type".format(other, type(other)))
  224. cmp_res = self._compare_lists(self._value,
  225. other_version['value'])
  226. if cmp_res != 0:
  227. return False
  228. if self._literal != other_version['literal']:
  229. return False
  230. cmp_res = self._compare_lists(self._suffix,
  231. other_version['suffix'],
  232. filler=(4, 0))
  233. if cmp_res != 0:
  234. return False
  235. if ignore_revision:
  236. return True
  237. else:
  238. return self._revision == other_version['revision']
  239. def __ne__(self, other: "Version") -> bool:
  240. '''Перегрузка x != y.'''
  241. return not self.__eq__(other)
  242. def __gt__(self, other: "Version") -> bool:
  243. '''Перегрузка x > y.'''
  244. return not self.__le__(other)
  245. def __ge__(self, other: "Version") -> bool:
  246. '''Перегрузка x >= y.'''
  247. return not self.__lt__(other)
  248. def __hash__(self):
  249. return hash(self._string)
  250. def __repr__(self):
  251. return '<Version: {}>'.format(self._string)
  252. def __str__(self):
  253. return self._string
  254. def __bool__(self):
  255. if self._value == [-1]:
  256. return False
  257. else:
  258. return True
  259. def __rshift__(self, other: tuple) -> bool:
  260. "Проверка нахождения значения переменной в указанном диапазоне."
  261. if (not isinstance(other, tuple) or len(other) != 2
  262. or not isinstance(other[0], str) or not isinstance(other[1], str)):
  263. raise VersionError("Versions range must be tuple of two strings,"
  264. f" not '{type(other)}'")
  265. lequal = other[0].startswith('=')
  266. lversion = Version(other[0].strip('='))
  267. requal = other[0].startswith('=')
  268. rversion = Version(other[1].strip('='))
  269. return (((lequal and self >= lversion)
  270. or (not lequal and self > lversion))
  271. and ((requal and self <= rversion)
  272. or (not requal and self < rversion)))
  273. class ContentsParser(metaclass=Singleton):
  274. def __init__(self):
  275. self._parsers = {'dir': ContentsParser._parse_dir,
  276. 'sym': ContentsParser._parse_sym,
  277. 'obj': ContentsParser._parse_obj}
  278. self._patterns = {'dir': "dir {path}",
  279. 'sym': "sym {path} -> {target} {mtime}",
  280. 'obj': "obj {path} {md5} {mtime}"}
  281. def parse(self, text: str) -> OrderedDict:
  282. output = OrderedDict()
  283. for line in text.split('\n'):
  284. line = line.strip()
  285. if not line:
  286. continue
  287. parts = line.split()
  288. path, value = self._parsers[parts[0]](parts)
  289. output[path] = value
  290. return output
  291. @staticmethod
  292. def _parse_dir(parts: List[str]) -> Tuple[str, dict]:
  293. return parts[1], {'type': 'dir'}
  294. @staticmethod
  295. def _parse_obj(parts: List[str]) -> Tuple[str, dict]:
  296. return parts[1], {'type': 'obj',
  297. 'md5': parts[2].strip(),
  298. 'mtime': parts[3].strip()}
  299. @staticmethod
  300. def _parse_sym(parts: List[str]) -> Tuple[str, dict]:
  301. return parts[1], {'type': 'sym',
  302. 'target': parts[3].strip(),
  303. 'mtime': parts[4].strip()}
  304. def render(self, contents_dictionary: OrderedDict) -> str:
  305. lines = []
  306. for path, value in contents_dictionary.items():
  307. lines.append(
  308. self._patterns[value['type']].format(path=path, **value))
  309. lines.append('')
  310. return "\n".join(lines)
  311. class OldContentsParser(metaclass=Singleton):
  312. def __init__(self):
  313. sym_keyword = Literal('sym')
  314. dir_keyword = Literal('dir')
  315. obj_keyword = Literal('obj')
  316. symlink_arrow = Literal('->')
  317. file_path = Regex(r'\S+')
  318. time_value = Word(nums)
  319. md5 = Word(alphanums)
  320. sym_line = (sym_keyword('type') + file_path('path')
  321. + symlink_arrow.suppress() + file_path('target')
  322. + time_value('mtime') + LineEnd().suppress())
  323. dir_line = (dir_keyword('type') + file_path('path')
  324. + LineEnd().suppress())
  325. obj_line = (obj_keyword('type') + file_path('path')
  326. + md5('md5') + time_value('mtime') + LineEnd().suppress())
  327. unexpected = SkipTo(LineEnd(), include=True)
  328. self._parser = (dir_line | sym_line |
  329. obj_line | unexpected('unexpected')
  330. ).setParseAction(self._parser_method)
  331. def _parser_method(self, parse_result):
  332. if parse_result.getName() == 'unexpected':
  333. return [None]
  334. result_dict = parse_result.asDict()
  335. path = result_dict.pop('path')
  336. value = ({path: result_dict})
  337. return value
  338. def parse(self, contents_text: str) -> OrderedDict:
  339. output_dictionary = OrderedDict()
  340. for tokens, start, end in self._parser.scanString(contents_text):
  341. parse_result = tokens[0]
  342. if parse_result is None:
  343. continue
  344. output_dictionary.update(parse_result)
  345. return output_dictionary
  346. def render(self, contents_dictionary: dict) -> str:
  347. file_loader = PackageLoader('calculate', 'utils')
  348. environment = Environment(loader=file_loader,
  349. trim_blocks=True,
  350. lstrip_blocks=True)
  351. template = environment.get_template('contents_template')
  352. text = template.render(document_dictionary=contents_dictionary)
  353. return text
  354. class PackageAtomName:
  355. '''Класс для хранения результата определения пакета. Для определения пакета
  356. использует путь к его pkg директории.'''
  357. def __init__(self, atom_dictionary: dict):
  358. self._package_directory = atom_dictionary['pkg_path']
  359. self._version = atom_dictionary['version']
  360. if self._package_directory is not None:
  361. self._name = self.fullname[:-len(self._version.string)]
  362. else:
  363. self._name = 'None'
  364. if self._package_directory is not None:
  365. self._with_slot = atom_dictionary.get('with_slot', False)
  366. else:
  367. self._with_slot = False
  368. @property
  369. def name(self) -> str:
  370. return self._name
  371. @property
  372. def fullname(self) -> str:
  373. if self._package_directory is None:
  374. return 'None'
  375. return os.path.basename(self._package_directory)
  376. @property
  377. def category(self) -> str:
  378. if self._package_directory is None:
  379. return 'None'
  380. return os.path.basename(os.path.dirname(self._package_directory))
  381. @property
  382. def atom(self) -> str:
  383. if self._package_directory is None:
  384. return 'None'
  385. return "{}/{}".format(self.category, self.fullname)
  386. @property
  387. def version(self) -> Version:
  388. if self._package_directory is None:
  389. return Version()
  390. return self._version
  391. @property
  392. def contents_path(self) -> str:
  393. if self._package_directory is None:
  394. return 'None'
  395. return os.path.join(self._package_directory, 'CONTENTS')
  396. @property
  397. def use_flags(self) -> list:
  398. if self._package_directory is None:
  399. return []
  400. use_path = os.path.join(self._package_directory, 'USE')
  401. try:
  402. return read_file(use_path).strip('\n').split(' ')
  403. except FilesError:
  404. raise PackageAtomError("could not read use flags for 'package'"
  405. " parameter: {}".format(self.package_atom))
  406. @property
  407. def pkg_path(self) -> str:
  408. return self._package_directory
  409. @property
  410. def slot(self) -> str:
  411. if self._package_directory is None:
  412. return None
  413. slot_path = os.path.join(self._package_directory, 'SLOT')
  414. try:
  415. return read_file(slot_path).strip('\n')
  416. except FilesError:
  417. raise PackageAtomError("could not read slot value for"
  418. " 'package': {}".format(self.package_atom))
  419. @property
  420. def slot_specified(self) -> bool:
  421. return self._with_slot
  422. def __eq__(self, other: Any) -> bool:
  423. if isinstance(other, PackageAtomName):
  424. return self._package_directory == other._package_directory
  425. else:
  426. return False
  427. def __ne__(self, other: Any) -> bool:
  428. if isinstance(other, PackageAtomName):
  429. return self._package_directory != other._package_directory
  430. else:
  431. return False
  432. def __bool__(self) -> bool:
  433. if self._package_directory is None:
  434. return True
  435. return bool(self._package_directory)
  436. def __repr__(self) -> bool:
  437. if self._package_directory is None:
  438. return '<PackageAtomName: None>'
  439. return '<PackageAtomName: {}/{}>'.format(self.category,
  440. self.fullname)
  441. def __hash__(self) -> bool:
  442. return hash(self._package_directory)
  443. def __str__(self) -> str:
  444. category_path, name = os.path.split(self._package_directory)
  445. category = os.path.basename(category_path)
  446. return f'{category}/{name}'
  447. NonePackage = PackageAtomName({'pkg_path': None, 'version': None})
  448. def make_version_pattern() -> str:
  449. _value = r'(?P<value>\d+(\.\d+)*)'
  450. _literal = r'(?P<literal>[a-z])?'
  451. _suffix = r'(?P<suffix>(_(pre|p|beta|alpha|rc)(\d+)?)+)?'
  452. _revision = r'(?P<revision>-r\d+)?'
  453. return _value + _literal + _suffix + _revision
  454. class PackageAtomParser:
  455. '''Класс для парсинга параметра package, его проверки, а также определения
  456. принадлежности файла пакету.'''
  457. _version_pattern: str = make_version_pattern()
  458. package_name_pattern: str =\
  459. fr'(?P<name>\D[\w\d]*(\-\D[\w\d]*)*)(?P<version>-{_version_pattern})?'
  460. atom_name_pattern: str = r'''(?P<condition>[=~><])?
  461. (?P<category>[^\s/]*)/
  462. {0}
  463. (?P<slot>:[^\[\s]*)?
  464. (?P<use_flags>\[\S*(?:\s+\S*)*\])?'''.format(
  465. package_name_pattern)
  466. atom_regex = re.compile(atom_name_pattern, re.VERBOSE)
  467. package_name_regex = re.compile(package_name_pattern)
  468. version_regex = re.compile(_version_pattern)
  469. atom_dict_fields: List[str] = ['category', 'name', 'version', 'slot',
  470. 'use_flags', 'with_slot', 'condition']
  471. def __init__(self, pkg_path: str = '/var/db/pkg',
  472. chroot_path: str = '/'):
  473. self.chroot_path: str = chroot_path
  474. self.pkg_path: str
  475. if chroot_path != '/':
  476. self.pkg_path = join_paths(chroot_path, pkg_path)
  477. else:
  478. self.pkg_path = pkg_path
  479. self.package_atom: str = ''
  480. def parse_package_parameter(self, package_atom: Union[str, dict]
  481. ) -> PackageAtomName:
  482. '''Метод для разбора значения package, после разбора инициирует
  483. проверку полученных значений. Возвращает объект PackageAtomName.'''
  484. self.package_atom = package_atom
  485. print(f'value: {package_atom}')
  486. if isinstance(package_atom, str):
  487. atom_dictionary = self.parse_atom_name(package_atom)
  488. atom_dictionary['package_atom'] = package_atom
  489. print(f'atom_dict: {atom_dictionary}')
  490. elif isinstance(package_atom, dict):
  491. atom_dictionary = package_atom
  492. if 'package_atom' not in atom_dictionary:
  493. atom_dictionary['package_atom'] = (
  494. f'{atom_dictionary["category"]}{atom_dictionary["name"]}')
  495. atom_dictionary = self._check_package_existance(atom_dictionary)
  496. atom_name_object = PackageAtomName(atom_dictionary)
  497. return atom_name_object
  498. def is_package_exists(self, package_atom: Union[str, dict]) -> bool:
  499. try:
  500. self.parse_package_parameter(package_atom)
  501. return True
  502. except PackageAtomError as e:
  503. if e.errno == NOTEXIST:
  504. return False
  505. raise
  506. def _check_package_existance(self, atom_dictionary: dict) -> dict:
  507. '''Метод для проверки существования пакета. Существование пакета
  508. определяется наличием соответствующего CONTENTS файла.'''
  509. # Используем glob-паттерн для поиска.
  510. glob_pattern = r'{0}/{1}/{2}-[0-9]*/CONTENTS'.format(
  511. self.pkg_path,
  512. atom_dictionary['category'],
  513. atom_dictionary['name'])
  514. glob_result = glob.glob(glob_pattern)
  515. if not glob_result:
  516. # Если ничего не нашлось.
  517. raise PackageAtomError("Package '{}' is not found.".format(
  518. atom_dictionary['package_atom']),
  519. errno=NOTEXIST)
  520. if len(glob_result) == 1:
  521. # Если нашелся один пакет.
  522. pkg_path = os.path.dirname(next(iter(glob_result)))
  523. self._check_slot_value(pkg_path, atom_dictionary)
  524. self._check_use_flags_value(pkg_path, atom_dictionary)
  525. atom_name = atom_dictionary['name']
  526. real_name = os.path.basename(pkg_path)
  527. pkg_version = Version(real_name[len(atom_name):])
  528. if atom_dictionary['condition'] is not None:
  529. self._check_version(atom_dictionary, pkg_version)
  530. atom_dictionary['version'] = pkg_version
  531. atom_dictionary['pkg_path'] = pkg_path
  532. else:
  533. packages = dict()
  534. # Если подходящих пакетов много -- проверяем по use-флагам,
  535. # слотам и версии, если таковые заданы.
  536. for contents_path in glob_result:
  537. pkg_path = os.path.dirname(contents_path)
  538. try:
  539. self._check_slot_value(pkg_path, atom_dictionary)
  540. self._check_use_flags_value(pkg_path, atom_dictionary)
  541. atom_name = atom_dictionary['name']
  542. real_name = os.path.basename(pkg_path)
  543. pkg_version = Version(real_name[len(atom_name):])
  544. if atom_dictionary['condition'] is not None:
  545. self._check_version(atom_dictionary, pkg_version)
  546. packages[pkg_path] = pkg_version
  547. except PackageAtomError:
  548. continue
  549. if not packages:
  550. # Если после проверки отсеялись все кандидаты.
  551. raise PackageAtomError(
  552. "Package from 'package' parameter value"
  553. " '{}' does not exist".format(
  554. atom_dictionary['package_atom']),
  555. errno=NOTEXIST)
  556. if len(packages) == 1:
  557. # Если был найден только один кандидат -- выдаем его.
  558. pkg_path = next(iter(packages.keys()))
  559. atom_dictionary['pkg_path'] = pkg_path
  560. atom_dictionary['version'] = packages[pkg_path]
  561. else:
  562. # Если подходящих пакетов много -- берем старшую версию.
  563. pkg_path = sorted(packages.keys(),
  564. key=lambda path: packages[path])[-1]
  565. atom_dictionary['pkg_path'] = pkg_path
  566. atom_dictionary['version'] = packages[pkg_path]
  567. return atom_dictionary
  568. def _check_slot_value(self, pkg_path: str,
  569. atom_dictionary: dict) -> NoReturn:
  570. '''Метод для проверки полученного из параметра package значения slot.
  571. '''
  572. if atom_dictionary['slot']:
  573. slot = self._get_slot_value(pkg_path,
  574. atom_dictionary['package_atom'])
  575. if slot != atom_dictionary['slot']:
  576. raise PackageAtomError("Package '{}' is not found.".format(
  577. atom_dictionary['package_atom']),
  578. errno=NOTEXIST)
  579. def _check_use_flags_value(self, pkg_path: str,
  580. atom_dictionary: dict) -> NoReturn:
  581. '''Метод для проверки полученных из параметра package значений
  582. use-флагов.'''
  583. if atom_dictionary['use_flags']:
  584. use_flags = self._get_use_flags_value(
  585. pkg_path,
  586. atom_dictionary['package_atom'])
  587. for use_flag in atom_dictionary['use_flags']:
  588. if use_flag.startswith('-'):
  589. if use_flag.strip()[1:] not in use_flags:
  590. continue
  591. elif use_flag in use_flags:
  592. continue
  593. raise PackageAtomError("Package '{}' is not found".format(
  594. atom_dictionary['package_atom']),
  595. errno=NOTEXIST)
  596. def _get_slot_value(self, pkg_path: str, package_atom: str) -> str:
  597. '''Метод для получения значения slot из файла SLOT.'''
  598. slot_path = os.path.join(pkg_path, 'SLOT')
  599. try:
  600. return read_file(slot_path).strip('\n')
  601. except FilesError:
  602. raise PackageAtomError("could not read slot value for"
  603. " 'package': {}".format(package_atom))
  604. def _get_use_flags_value(self, pkg_path: str, package_atom: str) -> list:
  605. '''Метод для получения списка значений use-флагов из файла USE.'''
  606. use_path = os.path.join(pkg_path, 'USE')
  607. try:
  608. return read_file(use_path).strip('\n').split(' ')
  609. except FilesError:
  610. raise PackageAtomError("could not read use flags in atom name: {}".
  611. format(package_atom))
  612. def _get_category_packages(self, category: str) -> str:
  613. '''Генератор имен категорий, имеющихся в /var/db/pkg'''
  614. for path in glob.glob('{0}/{1}/*/CONTENTS'.format(self.pkg_path,
  615. category)):
  616. yield path
  617. def _check_version(self, atom_dictionary: dict, pkg_version: Version
  618. ) -> NoReturn:
  619. condition = atom_dictionary['condition']
  620. if condition == '=':
  621. condition_result = (atom_dictionary['version']
  622. == pkg_version)
  623. elif condition == '~':
  624. condition_result = atom_dictionary['version'].__eq__(
  625. pkg_version,
  626. ignore_revision=True)
  627. elif condition == '>':
  628. condition_result = atom_dictionary['version'] < pkg_version
  629. elif condition == '<':
  630. condition_result = atom_dictionary['version'] > pkg_version
  631. else:
  632. condition_result = False
  633. if not condition_result:
  634. raise PackageAtomError("Package '{}' is not found".format(
  635. atom_dictionary['package_atom']),
  636. errno=NOTEXIST)
  637. def get_file_package(self, file_path: str) -> PackageAtomName:
  638. '''Метод для определения пакета, которому принадлежит файл.'''
  639. # Удаляем часть пути соответствующую chroot_path
  640. if self.chroot_path != '/' and file_path.startswith(self.chroot_path):
  641. file_path = file_path[len(self.chroot_path):]
  642. for category in os.listdir(self.pkg_path):
  643. for contents_path in self._get_category_packages(category):
  644. try:
  645. with open(contents_path, 'r') as contents_file:
  646. for file_line in contents_file.readlines():
  647. contents_name = file_line.split(' ')[1].strip()
  648. if contents_name == file_path:
  649. package_path = os.path.dirname(contents_path)
  650. package_name = os.path.basename(package_path)
  651. parsing_result = self.package_name_regex.\
  652. search(package_name)
  653. version = parsing_result.groupdict()['version']
  654. version = Version(version)
  655. package_atom = PackageAtomName(
  656. {'pkg_path': package_path,
  657. 'version': version})
  658. return package_atom
  659. except (OSError, IOError):
  660. continue
  661. else:
  662. raise PackageNotFound("The file does not belong to any package")
  663. @classmethod
  664. def parse_atom_name(cls, atom_name: str) -> dict:
  665. parsing_result = cls.atom_regex.search(atom_name)
  666. if (not parsing_result or parsing_result.string != atom_name or
  667. not parsing_result.groupdict()['category'] or
  668. not parsing_result.groupdict()['name']):
  669. raise PackageAtomError("atom name '{}' is not"
  670. " correct".format(atom_name),
  671. errno=NOTCORRECT)
  672. parsing_result = parsing_result.groupdict()
  673. category = parsing_result['category']
  674. name = parsing_result['name']
  675. if parsing_result['version'] is not None:
  676. version = Version(parsing_result['version'].strip('-'))
  677. else:
  678. version = None
  679. if parsing_result['condition'] is not None:
  680. if parsing_result['version'] is None:
  681. raise PackageAtomError(f"Atom name '{atom_name}' is not"
  682. " correct. Version value is missed",
  683. errno=NOTCORRECT)
  684. elif parsing_result['version'] is not None:
  685. parsing_result['condition'] = '='
  686. if (parsing_result['slot'] is not None
  687. and parsing_result['slot'] != ':'):
  688. slot = parsing_result['slot'].strip(':')
  689. else:
  690. slot = None
  691. if parsing_result['use_flags'] is not None:
  692. use_flags = [use.strip() for use in
  693. parsing_result['use_flags'].strip().
  694. strip('[]').split(' ')]
  695. else:
  696. use_flags = None
  697. atom_dict = {'category': category,
  698. 'name': name,
  699. 'version': version,
  700. 'slot': slot,
  701. 'use_flags': use_flags,
  702. 'with_slot': slot is not None,
  703. 'condition': parsing_result['condition']}
  704. return atom_dict
  705. class Package(metaclass=PackageCreator):
  706. '''Класс для работы с принадлежностью файлов пакетам.'''
  707. re_cfg = re.compile(r'/\._cfg\d{4}_')
  708. def __init__(self, package_atom: Union[str, PackageAtomName],
  709. pkg_path: str = '/var/db/pkg',
  710. chroot_path: str = '/',
  711. autosave: bool = False):
  712. self.chroot_path: str = chroot_path
  713. self.contents_file_path = self._get_contents_path(package_atom)
  714. self.package_name = package_atom
  715. self.parser = ContentsParser()
  716. if (chroot_path != '/' and
  717. not self.contents_file_path.startswith(chroot_path)):
  718. self.contents_file_path = join_paths(chroot_path,
  719. self.contents_file_path)
  720. if not os.path.exists(self.contents_file_path):
  721. raise PackageError("Can not find CONTENTS file in path: {}".format(
  722. self.contents_file_path
  723. ))
  724. self.contents_dictionary = OrderedDict()
  725. self.read_contents_file()
  726. self.autosave: bool = autosave
  727. def _get_contents_path(self, package_atom: Union[str, PackageAtomName]
  728. ) -> str:
  729. '''Метод для получения из ATOM-названия или готового объекта
  730. PackageAtomName пути к файлу CONTENTS.'''
  731. if isinstance(package_atom, str):
  732. package_atom_parser = PackageAtomParser(
  733. chroot_path=self.chroot_path)
  734. atom_name = package_atom_parser.parse_package_parameter(
  735. package_atom)
  736. return os.path.join(atom_name.pkg_path,
  737. 'CONTENTS')
  738. elif isinstance(package_atom, PackageAtomName):
  739. return os.path.join(package_atom.pkg_path,
  740. 'CONTENTS')
  741. else:
  742. raise PackageError(
  743. "Incorrect 'package_atom' value: '{}', type: '{}''".
  744. format(package_atom, type(package_atom)))
  745. def remove_cfg_prefix(self, file_name: str) -> str:
  746. '''Метод для удаления префикса ._cfg????_.'''
  747. return self.re_cfg.sub('/', file_name)
  748. def remove_chroot_path(self, file_name: str) -> str:
  749. '''Метод для удаления из пути файла корневого пути, если он не
  750. является /.'''
  751. if self.chroot_path != '/' and file_name.startswith(self.chroot_path):
  752. return file_name[len(self.chroot_path):]
  753. else:
  754. return file_name
  755. def read_contents_file(self) -> bool:
  756. '''Метод для чтения файла CONTENTS.'''
  757. try:
  758. contents_text = read_file(self.contents_file_path)
  759. except FilesError as error:
  760. raise PackageError(str(error))
  761. if contents_text:
  762. self.contents_dictionary = self.parser.parse(contents_text)
  763. return True
  764. else:
  765. return False
  766. def write_contents_file(self) -> NoReturn:
  767. '''Метод для записи файла CONTENTS.'''
  768. with open(self.contents_file_path, 'w') as contents_file:
  769. contents_text = self.render_contents_file()
  770. contents_file.write(contents_text)
  771. def render_contents_file(self) -> str:
  772. '''Метод для получения текста файла CONTENTS.'''
  773. return self.parser.render(self.contents_dictionary)
  774. @property
  775. def files(self) -> List[str]:
  776. '''Метод для получения списка путей файлов, имеющихся в CONTENTS-файле
  777. пакета.'''
  778. return list(self.contents_dictionary.keys())
  779. def get_file_type(self, file_path: str) -> str:
  780. '''Метод для получения по пути файла типа, указанного для него в
  781. CONTENTS-файле.'''
  782. file_path = self.remove_chroot_path(file_path)
  783. if file_path in self.contents_dictionary:
  784. return self.contents_dictionary[file_path]['type']
  785. return None
  786. def sort_contents_dictionary(self) -> NoReturn:
  787. '''Метод для сортировки словаря, полученного в результате разбора и
  788. изменения CONTENTS-файла.'''
  789. tree = {}
  790. for path in self.contents_dictionary.keys():
  791. path = path.strip('/').split('/')
  792. level = tree
  793. for part in path:
  794. if part not in level:
  795. level[part] = {}
  796. level = level[part]
  797. sorted_contents = OrderedDict()
  798. for path in self._make_paths('/', tree):
  799. sorted_contents[path] = self.contents_dictionary[path]
  800. self.contents_dictionary = sorted_contents
  801. # def _make_paths(self, path: str, level: dict) -> List[str]:
  802. # paths = []
  803. # for part in sorted(level.keys()):
  804. # part_path = os.path.join(path, part)
  805. # paths.append(part_path)
  806. # if level[part]:
  807. # paths.extend(self._make_paths(part_path, level[part]))
  808. # return paths
  809. def _make_paths(self, path: str,
  810. level: dict) -> Generator[str, None, None]:
  811. '''Генератор используемый для преобразования дерева сортировки путей в
  812. последовательность путей.'''
  813. for part in sorted(level.keys()):
  814. part_path = os.path.join(path, part)
  815. yield part_path
  816. if level[part]:
  817. yield from self._make_paths(part_path, level[part])
  818. def add_dir(self, file_name: str) -> NoReturn:
  819. '''Метод для добавления в CONTENTS директорий.'''
  820. file_name = self.remove_chroot_path(file_name)
  821. if (file_name != '/' and
  822. (file_name not in self.contents_dictionary
  823. or self.contents_dictionary[file_name]['type'] != 'dir')):
  824. self.add_dir(os.path.dirname(file_name))
  825. contents_item = OrderedDict({'type': 'dir'})
  826. self.contents_dictionary[file_name] = contents_item
  827. def add_sym(self, file_name: str, target_path: Optional[str] = None,
  828. mtime: Optional[str] = None) -> NoReturn:
  829. '''Метод для добавления в CONTENTS символьных ссылок.'''
  830. real_path = file_name
  831. file_name = self.remove_cfg_prefix(file_name)
  832. file_name = self.remove_chroot_path(file_name)
  833. if not real_path.startswith(self.chroot_path):
  834. real_path = join_paths(self.chroot_path, real_path)
  835. if target_path is None:
  836. target_path = read_link(real_path)
  837. self.add_dir(os.path.dirname(file_name))
  838. if mtime is None:
  839. mtime = str(int(os.lstat(real_path).st_mtime))
  840. try:
  841. contents_item = OrderedDict({'type': 'sym',
  842. 'target': target_path,
  843. 'mtime': mtime})
  844. except FilesError as error:
  845. raise PackageError(str(error))
  846. self.contents_dictionary[file_name] = contents_item
  847. def add_obj(self, file_name: str, file_md5: Optional[str] = None,
  848. mtime: Optional[str] = None) -> NoReturn:
  849. '''Метод для добавления в CONTENTS обычных файлов как obj.'''
  850. real_path = file_name
  851. file_name = self.remove_chroot_path(file_name)
  852. file_name = self.remove_cfg_prefix(file_name)
  853. if real_path == file_name:
  854. real_path = join_paths(self.chroot_path, file_name)
  855. self.add_dir(os.path.dirname(file_name))
  856. if file_md5 is None:
  857. try:
  858. file_text = read_file(real_path, binary=True)
  859. except FilesError as error:
  860. raise PackageError(str(error))
  861. file_md5 = hashlib.md5(file_text).hexdigest()
  862. if mtime is None:
  863. mtime = str(int(os.lstat(real_path).st_mtime))
  864. contents_item = OrderedDict({'type': 'obj',
  865. 'md5': file_md5,
  866. 'mtime': mtime})
  867. self.contents_dictionary[file_name] = contents_item
  868. def add_file(self, file_name: str) -> NoReturn:
  869. '''Метод для добавления в CONTENTS файла любого типа.'''
  870. if file_name != '/':
  871. real_path = file_name
  872. if file_name.startswith(self.chroot_path):
  873. file_name = self.remove_chroot_path(file_name)
  874. else:
  875. real_path = join_paths(self.chroot_path, file_name)
  876. if os.path.isdir(real_path):
  877. self.add_dir(file_name)
  878. elif os.path.islink(real_path):
  879. self.add_sym(file_name)
  880. elif os.path.isfile(real_path):
  881. self.add_obj(file_name)
  882. def remove_obj(self, file_path: str) -> OrderedDict:
  883. '''Метод для удаления файлов и ссылок.'''
  884. file_path = self.remove_chroot_path(file_path)
  885. file_path = self.remove_cfg_prefix(file_path)
  886. removed = OrderedDict()
  887. if file_path in self.contents_dictionary:
  888. removed.update({file_path:
  889. self.contents_dictionary.pop(file_path)})
  890. return removed
  891. def remove_dir(self, file_path: str) -> OrderedDict:
  892. '''Метод для удаления из CONTENTS файлов и директорий находящихся
  893. внутри удаляемой директории и самой директории.'''
  894. directory_path = self.remove_chroot_path(file_path)
  895. paths_to_remove = []
  896. removed = OrderedDict()
  897. for file_path in self.contents_dictionary:
  898. if file_path.startswith(directory_path):
  899. paths_to_remove.append(file_path)
  900. for file_path in paths_to_remove:
  901. removed.update({file_path:
  902. self.contents_dictionary.pop(file_path)})
  903. return removed
  904. def remove_file(self, file_path: str) -> OrderedDict:
  905. file_path = self.remove_chroot_path(file_path)
  906. file_path = self.remove_cfg_prefix(file_path)
  907. removed = OrderedDict()
  908. print(f"REMOVE {file_path} FROM PACKAGE {self.package_name}")
  909. if file_path not in self.contents_dictionary:
  910. return
  911. if self.contents_dictionary[file_path]['type'] == 'dir':
  912. removed.update(self.remove_dir(file_path))
  913. else:
  914. removed.update({file_path:
  915. self.contents_dictionary.pop(file_path)})
  916. return removed
  917. def clear_dir(self, file_path: str) -> OrderedDict:
  918. '''Метод для удаления из CONTENTS файлов и директорий находящихся
  919. внутри очищаемой директории.'''
  920. directory_path = self.remove_chroot_path(file_path)
  921. paths_to_remove = []
  922. removed = OrderedDict()
  923. for file_path in self.contents_dictionary:
  924. if file_path == directory_path:
  925. continue
  926. if file_path.startswith(directory_path):
  927. paths_to_remove.append(file_path)
  928. for file_path in paths_to_remove:
  929. removed.update({file_path:
  930. self.contents_dictionary.pop(file_path)})
  931. return removed
  932. def remove_empty_directories(self) -> OrderedDict:
  933. '''Метод для удаления из CONTENTS директорий, которые после удаления
  934. тех или иных файлов больше не находятся на пути к тем файлам, которые
  935. по-прежнему принадлежат пакету.'''
  936. used_directories = set()
  937. removed = OrderedDict()
  938. not_directory_list = [path for path, value in
  939. self.contents_dictionary.items()
  940. if value['type'] != 'dir']
  941. for filepath in not_directory_list:
  942. file_directory = os.path.dirname(filepath)
  943. while file_directory != '/':
  944. used_directories.add(file_directory)
  945. file_directory = os.path.dirname(file_directory)
  946. paths_to_delete = [file_path for file_path, value in
  947. self.contents_dictionary.items()
  948. if value['type'] == 'dir' and
  949. file_path not in used_directories]
  950. for file_path in paths_to_delete:
  951. removed.update({file_path:
  952. self.contents_dictionary.pop(file_path)})
  953. return removed
  954. def get_md5(self, file_path: str) -> str:
  955. '''Метод для получения md5 хэш-суммы указанного файла.'''
  956. try:
  957. file_text = read_file(file_path).encode()
  958. except FilesError as error:
  959. raise PackageError(str(error))
  960. file_md5 = hashlib.md5(file_text).hexdigest()
  961. return file_md5
  962. def get_link_target(self, link_path: str) -> str:
  963. try:
  964. return read_link(link_path)
  965. except FilesError as error:
  966. raise PackageError(str(error))
  967. def check_contents_data(self, file_path: str, file_md5: str = None,
  968. sym_target: str = None, symlink: bool = False
  969. ) -> bool:
  970. '''Метод для проверки соответствия md5 хэш суммы файла той, что указана
  971. в файле CONTENTS.'''
  972. contents_path = file_path
  973. if self.chroot_path != "/" and contents_path.startswith(
  974. self.chroot_path):
  975. contents_path = contents_path[len(self.chroot_path):]
  976. contents_path = self.remove_cfg_prefix(contents_path)
  977. if (not symlink and
  978. self.contents_dictionary[contents_path]["type"] != "sym"):
  979. if file_md5 is None:
  980. file_md5 = self.get_md5(file_path)
  981. contents_md5 = self.contents_dictionary[contents_path]['md5']
  982. return file_md5 == contents_md5
  983. elif (symlink and
  984. self.contents_dictionary[contents_path]["type"] == "sym"):
  985. if sym_target is None:
  986. sym_target = self.get_link_target(file_path)
  987. return (sym_target ==
  988. self.contents_dictionary[contents_path]["target"])
  989. else:
  990. return False
  991. def __contains__(self, file_path: str) -> bool:
  992. if self.chroot_path != "/":
  993. if file_path.startswith(self.chroot_path):
  994. file_path = file_path[len(self.chroot_path):]
  995. file_path = self.remove_cfg_prefix(file_path)
  996. return file_path in self.contents_dictionary
  997. def __repr__(self) -> str:
  998. return '<Package: {}/{}>'.format(self.package_name.category,
  999. self.package_name.fullname)
  1000. # def __del__(self) -> NoReturn:
  1001. # if self.autosave and os.path.exists(self.contents_file_path):
  1002. # self.remove_empty_directories()
  1003. # self.write_contents_file()