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.

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