convert.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. from binascii import hexlify
  2. import os
  3. import pytest
  4. import shutil
  5. import tempfile
  6. import attic.repository
  7. import attic.key
  8. import attic.helpers
  9. from ..helpers import IntegrityError, get_keys_dir
  10. from ..repository import Repository, MAGIC
  11. from ..key import KeyfileKey, KeyfileNotFoundError
  12. from . import BaseTestCase
  13. class NotImplementedException(Exception):
  14. pass
  15. class ConversionTestCase(BaseTestCase):
  16. class MockArgs:
  17. def __init__(self, path):
  18. self.repository = attic.helpers.Location(path)
  19. def open(self, path, repo_type = Repository, create=False):
  20. return repo_type(os.path.join(path, 'repository'), create = create)
  21. def setUp(self):
  22. self.tmppath = tempfile.mkdtemp()
  23. self.attic_repo = self.open(self.tmppath,
  24. repo_type = attic.repository.Repository,
  25. create = True)
  26. # throw some stuff in that repo, copied from `RepositoryTestCase.test1`_
  27. for x in range(100):
  28. self.attic_repo.put(('%-32d' % x).encode('ascii'), b'SOMEDATA')
  29. # we use the repo dir for the created keyfile, because we do
  30. # not want to clutter existing keyfiles
  31. os.environ['ATTIC_KEYS_DIR'] = self.tmppath
  32. # we use the same directory for the converted files, which
  33. # will clutter the previously created one, which we don't care
  34. # about anyways. in real runs, the original key will be retained.
  35. os.environ['BORG_KEYS_DIR'] = self.tmppath
  36. os.environ['ATTIC_PASSPHRASE'] = 'test'
  37. self.key = attic.key.KeyfileKey.create(self.attic_repo, self.MockArgs(self.tmppath))
  38. self.attic_repo.close()
  39. def tearDown(self):
  40. shutil.rmtree(self.tmppath)
  41. def test_convert(self):
  42. self.repository = self.open(self.tmppath)
  43. # check should fail because of magic number
  44. print("this will show an error, it is expected")
  45. assert not self.repository.check() # can't check raises() because check() handles the error
  46. self.repository.close()
  47. print("opening attic repository with borg and converting")
  48. self.open(self.tmppath, repo_type = AtticRepositoryConverter).convert()
  49. # check that the new keyfile is alright
  50. keyfile = os.path.join(get_keys_dir(),
  51. os.path.basename(self.key.path))
  52. with open(keyfile, 'r') as f:
  53. assert f.read().startswith(KeyfileKey.FILE_ID)
  54. self.repository = self.open(self.tmppath)
  55. assert self.repository.check()
  56. self.repository.close()
  57. class AtticRepositoryConverter(Repository):
  58. def convert(self):
  59. '''convert an attic repository to a borg repository
  60. those are the files that need to be converted here, from most
  61. important to least important: segments, key files, and various
  62. caches, the latter being optional, as they will be rebuilt if
  63. missing.'''
  64. print("reading segments from attic repository using borg")
  65. segments = [ filename for i, filename in self.io.segment_iterator() ]
  66. try:
  67. keyfile = self.find_attic_keyfile()
  68. except KeyfileNotFoundError:
  69. print("no key file found for repository")
  70. else:
  71. self.convert_keyfiles(keyfile)
  72. self.close()
  73. self.convert_segments(segments)
  74. with pytest.raises(NotImplementedException):
  75. self.convert_cache()
  76. @staticmethod
  77. def convert_segments(segments):
  78. '''convert repository segments from attic to borg
  79. replacement pattern is `s/ATTICSEG/BORG_SEG/` in files in
  80. `$ATTIC_REPO/data/**`.
  81. luckily the segment length didn't change so we can just
  82. replace the 8 first bytes of all regular files in there.'''
  83. for filename in segments:
  84. print("converting segment %s in place" % filename)
  85. with open(filename, 'r+b') as segment:
  86. segment.seek(0)
  87. segment.write(MAGIC)
  88. def find_attic_keyfile(self):
  89. '''find the attic keyfiles
  90. the keyfiles are loaded by `KeyfileKey.find_key_file()`. that
  91. finds the keys with the right identifier for the repo
  92. this is expected to look into $HOME/.attic/keys or
  93. $ATTIC_KEYS_DIR for key files matching the given Borg
  94. repository.
  95. it is expected to raise an exception (KeyfileNotFoundError) if
  96. no key is found. whether that exception is from Borg or Attic
  97. is unclear.
  98. this is split in a separate function in case we want to use
  99. the attic code here directly, instead of our local
  100. implementation.'''
  101. return AtticKeyfileKey.find_key_file(self)
  102. @staticmethod
  103. def convert_keyfiles(keyfile):
  104. '''convert key files from attic to borg
  105. replacement pattern is `s/ATTIC KEY/BORG_KEY/` in
  106. `get_keys_dir()`, that is `$ATTIC_KEYS_DIR` or
  107. `$HOME/.attic/keys`, and moved to `$BORG_KEYS_DIR` or
  108. `$HOME/.borg/keys`.
  109. no need to decrypt to convert. we need to rewrite the whole
  110. key file because magic number length changed, but that's not a
  111. problem because the keyfiles are small (compared to, say,
  112. all the segments).'''
  113. print("converting keyfile %s" % keyfile)
  114. with open(keyfile, 'r') as f:
  115. data = f.read()
  116. data = data.replace(AtticKeyfileKey.FILE_ID,
  117. KeyfileKey.FILE_ID,
  118. 1)
  119. keyfile = os.path.join(get_keys_dir(),
  120. os.path.basename(keyfile))
  121. print("writing borg keyfile to %s" % keyfile)
  122. with open(keyfile, 'w') as f:
  123. f.write(data)
  124. with open(keyfile, 'r') as f:
  125. data = f.read()
  126. assert data.startswith(KeyfileKey.FILE_ID)
  127. def convert_cache(self):
  128. '''convert caches from attic to borg
  129. those are all hash indexes, so we need to
  130. `s/ATTICIDX/BORG_IDX/` in a few locations:
  131. * the repository index (in `$ATTIC_REPO/index.%d`, where `%d`
  132. is the `Repository.get_index_transaction_id()`), which we
  133. should probably update, with a lock, see
  134. `Repository.open()`, which i'm not sure we should use
  135. because it may write data on `Repository.close()`...
  136. * the `files` and `chunks` cache (in
  137. `$HOME/.cache/attic/<repoid>/`), which we could just drop,
  138. but if we'd want to convert, we could open it with the
  139. `Cache.open()`, edit in place and then `Cache.close()` to
  140. make sure we have locking right
  141. '''
  142. raise NotImplementedException('not implemented')
  143. class AtticKeyfileKey(KeyfileKey):
  144. '''backwards compatible Attick key file parser'''
  145. FILE_ID = 'ATTIC KEY'
  146. # verbatim copy from attic
  147. @staticmethod
  148. def get_keys_dir():
  149. """Determine where to repository keys and cache"""
  150. return os.environ.get('ATTIC_KEYS_DIR',
  151. os.path.join(os.path.expanduser('~'), '.attic', 'keys'))
  152. @classmethod
  153. def find_key_file(cls, repository):
  154. '''copy of attic's `find_key_file`_
  155. this has two small modifications:
  156. 1. it uses the above `get_keys_dir`_ instead of the global one,
  157. assumed to be borg's
  158. 2. it uses `repository.path`_ instead of
  159. `repository._location.canonical_path`_ because we can't
  160. assume the repository has been opened by the archiver yet
  161. '''
  162. get_keys_dir = cls.get_keys_dir
  163. id = hexlify(repository.id).decode('ascii')
  164. keys_dir = get_keys_dir()
  165. for name in os.listdir(keys_dir):
  166. filename = os.path.join(keys_dir, name)
  167. with open(filename, 'r') as fd:
  168. line = fd.readline().strip()
  169. if line and line.startswith(cls.FILE_ID) and line[10:] == id:
  170. return filename
  171. raise KeyfileNotFoundError(repository.path, get_keys_dir())