A VCS repository archival tool
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

storage.py 3.7 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  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