A VCS repository archival tool
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.

96 lines
3.4 KiB

  1. import abc
  2. import codearchiver.core
  3. import collections.abc
  4. import contextlib
  5. import glob
  6. import logging
  7. import os.path
  8. import shutil
  9. import typing
  10. _logger = logging.getLogger(__name__)
  11. class Storage(abc.ABC):
  12. @abc.abstractmethod
  13. def put(self, filename: str, index: typing.Optional['codearchiver.core.Index'] = None):
  14. '''Put a local file and (if provided) its index into storage. If an error occurs, a partial copy may remain in storage. If it completes, the local input file is removed.'''
  15. def put_result(self, result: 'codearchiver.core.Result'):
  16. '''Put a module's Result into storage. The semantics are as for `put`, and the exact behaviour regarding partial copies and leftover files on errors is undefined.'''
  17. for fn, index in result.files:
  18. self.put(fn, index)
  19. for _, subresult in result.submoduleResults:
  20. self.put_result(subresult)
  21. @abc.abstractmethod
  22. def search_indices(self, criteria: list[tuple[str, typing.Union[str, tuple[str]]]]) -> collections.abc.Iterator[str]:
  23. '''
  24. Search all indices in storage by criteria.
  25. Each criterion consists of a key and one or more possible values. A criterion matches if at least one of the specified values is present in a file's index.
  26. Yields all filenames where all criteria match.
  27. '''
  28. @abc.abstractmethod
  29. @contextlib.contextmanager
  30. def open_index(self, filename: str) -> typing.TextIO:
  31. '''Open the index for a file in serialised form.'''
  32. @abc.abstractmethod
  33. @contextlib.contextmanager
  34. def open(self, filename: str, mode: typing.Optional[str] = 'rb') -> typing.Iterator[typing.Union[typing.BinaryIO, typing.TextIO]]:
  35. '''Open a file from storage. The mode must be r or rb.'''
  36. class DirectoryStorage(Storage):
  37. def __init__(self, directory):
  38. super().__init__()
  39. self._directory = directory
  40. def _check_directory(self):
  41. exists = os.path.exists(self._directory)
  42. if exists and not os.path.isdir(self._directory):
  43. raise NotADirectoryError(self._directory)
  44. return exists
  45. def _ensure_directory(self):
  46. if not self._check_directory():
  47. os.makedirs(self._directory)
  48. def put(self, filename, index = None):
  49. self._ensure_directory()
  50. #FIXME: Race condition
  51. if os.path.exists((targetFilename := os.path.join(self._directory, os.path.basename(filename)))):
  52. raise FileExistsError(f'{targetFilename} already exists')
  53. _logger.info(f'Moving {filename} to {self._directory}')
  54. shutil.move(filename, self._directory)
  55. if not index:
  56. return
  57. indexFilename = os.path.join(self._directory, f'{filename}.codearchiver-index')
  58. # No need to check for existence here thanks to the 'x' mode
  59. _logger.info(f'Writing index for {filename} to {indexFilename}')
  60. with open(indexFilename, 'x') as fp:
  61. fp.write(index.serialise())
  62. def search_indices(self, criteria):
  63. _logger.info(f'Searching indices by criteria: {criteria!r}')
  64. for indexFilename in glob.glob('*.codearchiver-index', root_dir = self._directory):
  65. _logger.info(f'Searching index {indexFilename}')
  66. with self.open(indexFilename, 'r') as fp:
  67. idx = codearchiver.core.Index.deserialise(fp, validate = False)
  68. if idx.matches(criteria):
  69. _logger.info(f'Found index match {indexFilename}')
  70. yield indexFilename.rsplit('.', 1)[0]
  71. _logger.info('Done searching indices')
  72. @contextlib.contextmanager
  73. def open_index(self, filename):
  74. with self.open(f'{filename}.codearchiver-index', 'r') as fp:
  75. yield fp
  76. @contextlib.contextmanager
  77. def open(self, filename, mode = 'rb'):
  78. with open(os.path.join(self._directory, filename), mode) as fp:
  79. yield fp