A VCS repository archival tool
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

102 rader
3.7 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, metadata: typing.Optional['codearchiver.core.Metadata'] = None):
  14. '''Put a local file and (if provided) its metadata 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, metadata in result.files:
  18. self.put(fn, metadata)
  19. for _, subresult in result.submoduleResults:
  20. self.put_result(subresult)
  21. @abc.abstractmethod
  22. def search_metadata(self, criteria: list[tuple[str, typing.Union[str, tuple[str]]]]) -> collections.abc.Iterator[str]:
  23. '''
  24. Search all metadata in storage by criteria.
  25. Refer to `codearchiver.core.Metadata.matches` for the semantics of `criteria`.
  26. Yields all filenames where all criteria match in lexicographical order.
  27. '''
  28. @abc.abstractmethod
  29. @contextlib.contextmanager
  30. def open_metadata(self, filename: str) -> typing.TextIO:
  31. '''Open the metadata 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, metadata = 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 metadata:
  56. return
  57. metadataFilename = os.path.join(self._directory, f'{filename}_codearchiver_metadata.txt')
  58. # No need to check for existence here thanks to the 'x' mode
  59. _logger.info(f'Writing metadata for {filename} to {metadataFilename}')
  60. with open(metadataFilename, 'x') as fp:
  61. fp.write(metadata.serialise())
  62. def search_metadata(self, criteria):
  63. _logger.info(f'Searching metadata by criteria: {criteria!r}')
  64. # Replace this with `root_dir` when dropping Python 3.9 support
  65. escapedDirPrefix = os.path.join(glob.escape(self._directory), '')
  66. escapedDirPrefixLen = len(escapedDirPrefix)
  67. files = glob.glob(f'{escapedDirPrefix}*_codearchiver_metadata.txt')
  68. files.sort()
  69. for metadataFilename in files:
  70. metadataFilename = metadataFilename[escapedDirPrefixLen:]
  71. _logger.info(f'Searching metadata {metadataFilename}')
  72. with self.open(metadataFilename, 'r') as fp:
  73. idx = codearchiver.core.Metadata.deserialise(fp, validate = False)
  74. if idx.matches(criteria):
  75. _logger.info(f'Found metadata match {metadataFilename}')
  76. yield metadataFilename.rsplit('_', 2)[0]
  77. _logger.info('Done searching metadata')
  78. @contextlib.contextmanager
  79. def open_metadata(self, filename):
  80. with self.open(f'{filename}_codearchiver_metadata.txt', 'r') as fp:
  81. yield fp
  82. @contextlib.contextmanager
  83. def open(self, filename, mode = 'rb'):
  84. with open(os.path.join(self._directory, filename), mode) as fp:
  85. yield fp