convert.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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. def convert_segments(self, segments):
  77. '''convert repository segments from attic to borg
  78. replacement pattern is `s/ATTICSEG/BORG_SEG/` in files in
  79. `$ATTIC_REPO/data/**`.
  80. luckily the segment length didn't change so we can just
  81. replace the 8 first bytes of all regular files in there.'''
  82. for filename in segments:
  83. print("converting segment %s in place" % filename)
  84. with open(filename, 'r+b') as segment:
  85. segment.seek(0)
  86. segment.write(MAGIC)
  87. def find_attic_keyfile(self):
  88. '''find the attic keyfiles
  89. the keyfiles are loaded by `KeyfileKey.find_key_file()`. that
  90. finds the keys with the right identifier for the repo
  91. this is expected to look into $HOME/.attic/keys or
  92. $ATTIC_KEYS_DIR for key files matching the given Borg
  93. repository.
  94. it is expected to raise an exception (KeyfileNotFoundError) if
  95. no key is found. whether that exception is from Borg or Attic
  96. is unclear.
  97. this is split in a separate function in case we want to use
  98. the attic code here directly, instead of our local
  99. implementation.'''
  100. return AtticKeyfileKey.find_key_file(self)
  101. def convert_keyfiles(self, keyfile):
  102. '''convert key files from attic to borg
  103. replacement pattern is `s/ATTIC KEY/BORG_KEY/` in
  104. `get_keys_dir()`, that is `$ATTIC_KEYS_DIR` or
  105. `$HOME/.attic/keys`, and moved to `$BORG_KEYS_DIR` or
  106. `$HOME/.borg/keys`.
  107. no need to decrypt to convert. we need to rewrite the whole
  108. key file because magic number length changed, but that's not a
  109. problem because the keyfiles are small (compared to, say,
  110. all the segments).'''
  111. print("converting keyfile %s" % keyfile)
  112. with open(keyfile, 'r') as f:
  113. data = f.read()
  114. data = data.replace(AtticKeyfileKey.FILE_ID,
  115. KeyfileKey.FILE_ID,
  116. 1)
  117. keyfile = os.path.join(get_keys_dir(),
  118. os.path.basename(keyfile))
  119. print("writing borg keyfile to %s" % keyfile)
  120. with open(keyfile, 'w') as f:
  121. f.write(data)
  122. with open(keyfile, 'r') as f:
  123. data = f.read()
  124. assert data.startswith(KeyfileKey.FILE_ID)
  125. def convert_cache(self):
  126. '''convert caches from attic to borg
  127. those are all hash indexes, so we need to
  128. `s/ATTICIDX/BORG_IDX/` in a few locations:
  129. * the repository index (in `$ATTIC_REPO/index.%d`, where `%d`
  130. is the `Repository.get_index_transaction_id()`), which we
  131. should probably update, with a lock, see
  132. `Repository.open()`, which i'm not sure we should use
  133. because it may write data on `Repository.close()`...
  134. * the `files` and `chunks` cache (in
  135. `$HOME/.cache/attic/<repoid>/`), which we could just drop,
  136. but if we'd want to convert, we could open it with the
  137. `Cache.open()`, edit in place and then `Cache.close()` to
  138. make sure we have locking right
  139. '''
  140. raise NotImplementedException('not implemented')
  141. class AtticKeyfileKey(KeyfileKey):
  142. '''backwards compatible Attick key file parser'''
  143. FILE_ID = 'ATTIC KEY'
  144. # verbatim copy from attic
  145. @staticmethod
  146. def get_keys_dir():
  147. """Determine where to repository keys and cache"""
  148. return os.environ.get('ATTIC_KEYS_DIR',
  149. os.path.join(os.path.expanduser('~'), '.attic', 'keys'))
  150. @classmethod
  151. def find_key_file(cls, repository):
  152. '''copy of attic's `find_key_file`_
  153. this has two small modifications:
  154. 1. it uses the above `get_keys_dir`_ instead of the global one,
  155. assumed to be borg's
  156. 2. it uses `repository.path`_ instead of
  157. `repository._location.canonical_path`_ because we can't
  158. assume the repository has been opened by the archiver yet
  159. '''
  160. get_keys_dir = cls.get_keys_dir
  161. id = hexlify(repository.id).decode('ascii')
  162. keys_dir = get_keys_dir()
  163. for name in os.listdir(keys_dir):
  164. filename = os.path.join(keys_dir, name)
  165. with open(filename, 'r') as fd:
  166. line = fd.readline().strip()
  167. if line and line.startswith(cls.FILE_ID) and line[10:] == id:
  168. return filename
  169. raise KeyfileNotFoundError(repository.path, get_keys_dir())