duplitab 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/env python3
  2. # PYTHON_ARGCOMPLETE_OK
  3. import hashlib
  4. import os
  5. import shlex
  6. import subprocess
  7. import tempfile
  8. import time
  9. import urllib.parse
  10. import yaml
  11. def command_join(args):
  12. return ' '.join([shlex.quote(a) for a in args])
  13. def sshfs_mount(url, path):
  14. url_attr = urllib.parse.urlparse(url)
  15. assert url_attr.scheme in ['sftp']
  16. mount_command = [
  17. 'sshfs',
  18. '{}:{}'.format(url_attr.netloc, url_attr.path),
  19. path,
  20. ]
  21. print('+ {}'.format(command_join(mount_command)))
  22. subprocess.check_call(mount_command)
  23. def sshfs_unmount(path):
  24. unmount_command = [
  25. 'fusermount',
  26. '-u',
  27. path,
  28. ]
  29. print('+ {}'.format(command_join(unmount_command)))
  30. subprocess.check_call(unmount_command)
  31. def backup(config, no_print_config, no_print_statistics, tab_dry):
  32. for backup in config:
  33. if not no_print_config:
  34. print(yaml.dump({"backup": backup}, default_flow_style = False))
  35. backup_command = ['duplicity']
  36. # encryption
  37. try:
  38. encryption = backup['encryption']
  39. except KeyError:
  40. encryption = True
  41. if 'encryption' in backup and not backup['encryption']:
  42. backup_command += ['--no-encryption']
  43. else:
  44. if 'encrypt_key' in backup:
  45. backup_command += ['--encrypt-key', backup['encrypt_key']]
  46. # determine source
  47. try:
  48. source_type = backup['source_type']
  49. except KeyError:
  50. source_type = 'local'
  51. source_mount_path = None
  52. try:
  53. if source_type == 'local':
  54. local_source_path = backup['source_path']
  55. elif source_type == 'sshfs':
  56. source_mount_path = tempfile.mkdtemp(prefix = 'duplitab-source-sshfs-')
  57. sshfs_mount(
  58. url = 'sftp://{}/{}'.format(backup['source_host'], backup['source_path']),
  59. path = source_mount_path,
  60. )
  61. local_source_path = source_mount_path
  62. backup_command.append('--allow-source-mismatch')
  63. else:
  64. raise Exception("unsupported source type '{}'".format(source_type))
  65. # selectors
  66. try:
  67. selectors = backup['selectors']
  68. except KeyError:
  69. selectors = []
  70. for selector in selectors:
  71. if selector['option'] in ['include', 'exclude']:
  72. shell_pattern = selector['shell_pattern']
  73. if shell_pattern.startswith(backup['source_path']):
  74. shell_pattern = shell_pattern.replace(
  75. backup['source_path'],
  76. local_source_path,
  77. 1,
  78. )
  79. backup_command += ['--{}'.format(selector['option']), shell_pattern]
  80. else:
  81. raise Exception("unsupported selector option '{}'".format(selector['option']))
  82. # statistics
  83. if no_print_statistics:
  84. backup_command.append('--no-print-statistics')
  85. # source path
  86. backup_command.append(local_source_path)
  87. # target
  88. target_mount_path = None
  89. try:
  90. if 'target_via_sshfs' in backup and backup['target_via_sshfs']:
  91. target_mount_path = tempfile.mkdtemp(prefix = 'duplitab-target-sshfs-')
  92. backup_command += ['file://' + target_mount_path]
  93. sshfs_mount(backup['target_url'], target_mount_path)
  94. # set backup name to make archive dir persistent
  95. # (default name: hash of target url)
  96. backup_command += ['--name', hashlib.sha1(backup['target_url'].encode('utf-8')).hexdigest()]
  97. else:
  98. backup_command += [backup['target_url']]
  99. try:
  100. if tab_dry:
  101. print('* {}'.format(command_join(backup_command)))
  102. else:
  103. print('+ {}'.format(command_join(backup_command)))
  104. subprocess.check_call(backup_command)
  105. finally:
  106. if target_mount_path:
  107. try:
  108. sshfs_unmount(target_mount_path)
  109. except subprocess.CalledProcessError:
  110. time.sleep(1)
  111. sshfs_unmount(target_mount_path)
  112. finally:
  113. if target_mount_path:
  114. os.rmdir(target_mount_path)
  115. if source_mount_path:
  116. try:
  117. sshfs_unmount(source_mount_path)
  118. except subprocess.CalledProcessError:
  119. time.sleep(1)
  120. sshfs_unmount(source_mount_path)
  121. finally:
  122. if source_mount_path:
  123. os.rmdir(source_mount_path)
  124. def run(command, config_path, no_print_config, no_print_statistics, tab_dry):
  125. with open(config_path) as config_file:
  126. config = yaml.load(config_file.read())
  127. if command == 'backup':
  128. backup(
  129. config = config,
  130. no_print_config = no_print_config,
  131. no_print_statistics = no_print_statistics,
  132. tab_dry = tab_dry,
  133. )
  134. def _init_argparser():
  135. import argparse
  136. argparser = argparse.ArgumentParser(description = None)
  137. argparser.add_argument(
  138. '-c',
  139. '--config',
  140. dest = 'config_path',
  141. default = '/etc/duplitab',
  142. )
  143. subparsers = argparser.add_subparsers(
  144. dest = 'command',
  145. )
  146. subparser_backup = subparsers.add_parser('backup')
  147. subparser_backup.add_argument(
  148. '--no-print-config',
  149. action = 'store_true',
  150. )
  151. subparser_backup.add_argument(
  152. '--no-print-statistics',
  153. action = 'store_true',
  154. )
  155. subparser_backup.add_argument(
  156. '--tab-dry',
  157. action = 'store_true',
  158. )
  159. return argparser
  160. def main(argv):
  161. argparser = _init_argparser()
  162. try:
  163. import argcomplete
  164. argcomplete.autocomplete(argparser)
  165. except ImportError:
  166. pass
  167. args = argparser.parse_args(argv)
  168. run(**vars(args))
  169. return 0
  170. if __name__ == "__main__":
  171. import sys
  172. sys.exit(main(sys.argv[1:]))