2
0

converter.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. from binascii import hexlify
  2. import os
  3. from .helpers import get_keys_dir
  4. from .repository import Repository, MAGIC
  5. from .key import KeyfileKey, KeyfileNotFoundError
  6. class NotImplementedException(Exception):
  7. pass
  8. class AtticRepositoryConverter(Repository):
  9. def convert(self, dryrun=True):
  10. """convert an attic repository to a borg repository
  11. those are the files that need to be converted here, from most
  12. important to least important: segments, key files, and various
  13. caches, the latter being optional, as they will be rebuilt if
  14. missing."""
  15. print("reading segments from attic repository using borg")
  16. segments = [ filename for i, filename in self.io.segment_iterator() ]
  17. try:
  18. keyfile = self.find_attic_keyfile()
  19. except KeyfileNotFoundError:
  20. print("no key file found for repository")
  21. else:
  22. self.convert_keyfiles(keyfile, dryrun)
  23. self.close()
  24. self.convert_segments(segments, dryrun)
  25. self.convert_cache(dryrun)
  26. @staticmethod
  27. def convert_segments(segments, dryrun):
  28. """convert repository segments from attic to borg
  29. replacement pattern is `s/ATTICSEG/BORG_SEG/` in files in
  30. `$ATTIC_REPO/data/**`.
  31. luckily the magic string length didn't change so we can just
  32. replace the 8 first bytes of all regular files in there."""
  33. for filename in segments:
  34. print("converting segment %s in place" % filename)
  35. if not dryrun:
  36. with open(filename, 'r+b') as segment:
  37. segment.seek(0)
  38. segment.write(MAGIC)
  39. def find_attic_keyfile(self):
  40. """find the attic keyfiles
  41. the keyfiles are loaded by `KeyfileKey.find_key_file()`. that
  42. finds the keys with the right identifier for the repo.
  43. this is expected to look into $HOME/.attic/keys or
  44. $ATTIC_KEYS_DIR for key files matching the given Borg
  45. repository.
  46. it is expected to raise an exception (KeyfileNotFoundError) if
  47. no key is found. whether that exception is from Borg or Attic
  48. is unclear.
  49. this is split in a separate function in case we want to use
  50. the attic code here directly, instead of our local
  51. implementation."""
  52. return AtticKeyfileKey.find_key_file(self)
  53. @staticmethod
  54. def convert_keyfiles(keyfile, dryrun):
  55. """convert key files from attic to borg
  56. replacement pattern is `s/ATTIC KEY/BORG_KEY/` in
  57. `get_keys_dir()`, that is `$ATTIC_KEYS_DIR` or
  58. `$HOME/.attic/keys`, and moved to `$BORG_KEYS_DIR` or
  59. `$HOME/.borg/keys`.
  60. no need to decrypt to convert. we need to rewrite the whole
  61. key file because magic string length changed, but that's not a
  62. problem because the keyfiles are small (compared to, say,
  63. all the segments)."""
  64. print("converting keyfile %s" % keyfile)
  65. with open(keyfile, 'r') as f:
  66. data = f.read()
  67. data = data.replace(AtticKeyfileKey.FILE_ID,
  68. KeyfileKey.FILE_ID,
  69. 1)
  70. keyfile = os.path.join(get_keys_dir(),
  71. os.path.basename(keyfile))
  72. print("writing borg keyfile to %s" % keyfile)
  73. if not dryrun:
  74. with open(keyfile, 'w') as f:
  75. f.write(data)
  76. with open(keyfile, 'r') as f:
  77. data = f.read()
  78. assert data.startswith(KeyfileKey.FILE_ID)
  79. def convert_cache(self, dryrun):
  80. """convert caches from attic to borg
  81. those are all hash indexes, so we need to
  82. `s/ATTICIDX/BORG_IDX/` in a few locations:
  83. * the repository index (in `$ATTIC_REPO/index.%d`, where `%d`
  84. is the `Repository.get_index_transaction_id()`), which we
  85. should probably update, with a lock, see
  86. `Repository.open()`, which i'm not sure we should use
  87. because it may write data on `Repository.close()`...
  88. * the `files` and `chunks` cache (in
  89. `$HOME/.cache/attic/<repoid>/`), which we could just drop,
  90. but if we'd want to convert, we could open it with the
  91. `Cache.open()`, edit in place and then `Cache.close()` to
  92. make sure we have locking right
  93. """
  94. raise NotImplementedException('cache conversion not implemented, next borg backup will take longer to rebuild those caches')
  95. class AtticKeyfileKey(KeyfileKey):
  96. """backwards compatible Attick key file parser"""
  97. FILE_ID = 'ATTIC KEY'
  98. # verbatim copy from attic
  99. @staticmethod
  100. def get_keys_dir():
  101. """Determine where to repository keys and cache"""
  102. return os.environ.get('ATTIC_KEYS_DIR',
  103. os.path.join(os.path.expanduser('~'), '.attic', 'keys'))
  104. @classmethod
  105. def find_key_file(cls, repository):
  106. """copy of attic's `find_key_file`_
  107. this has two small modifications:
  108. 1. it uses the above `get_keys_dir`_ instead of the global one,
  109. assumed to be borg's
  110. 2. it uses `repository.path`_ instead of
  111. `repository._location.canonical_path`_ because we can't
  112. assume the repository has been opened by the archiver yet
  113. """
  114. get_keys_dir = cls.get_keys_dir
  115. id = hexlify(repository.id).decode('ascii')
  116. keys_dir = get_keys_dir()
  117. for name in os.listdir(keys_dir):
  118. filename = os.path.join(keys_dir, name)
  119. with open(filename, 'r') as fd:
  120. line = fd.readline().strip()
  121. if line and line.startswith(cls.FILE_ID) and line[10:] == id:
  122. return filename
  123. raise KeyfileNotFoundError(repository.path, get_keys_dir())