duplitab 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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. # selectors
  47. try:
  48. selectors = backup['selectors']
  49. except KeyError:
  50. selectors = []
  51. for selector in selectors:
  52. if selector['option'] in ['include', 'exclude']:
  53. backup_command += ['--{}'.format(selector['option']), selector['shell_pattern']]
  54. else:
  55. raise Exception("unsupported selector option '{}'".format(selector['option']))
  56. # statistics
  57. if no_print_statistics:
  58. backup_command.append('--no-print-statistics')
  59. # source path
  60. backup_command += [backup['source_path']]
  61. # target path
  62. try:
  63. target_mount_path = None
  64. if 'target_via_sshfs' in backup and backup['target_via_sshfs']:
  65. target_mount_path = tempfile.mkdtemp(prefix = 'duplitab-target-sshfs-')
  66. backup_command += ['file://' + target_mount_path]
  67. sshfs_mount(backup['target_url'], target_mount_path)
  68. # set backup name to make archive dir persistent
  69. # (default name: hash of target url)
  70. backup_command += ['--name', hashlib.sha1(backup['target_url'].encode('utf-8')).hexdigest()]
  71. else:
  72. backup_command += [backup['target_url']]
  73. try:
  74. if tab_dry:
  75. print('* {}'.format(command_join(backup_command)))
  76. else:
  77. print('+ {}'.format(command_join(backup_command)))
  78. subprocess.check_call(backup_command)
  79. finally:
  80. if target_mount_path:
  81. try:
  82. sshfs_unmount(target_mount_path)
  83. except subprocess.CalledProcessError:
  84. time.sleep(1)
  85. sshfs_unmount(target_mount_path)
  86. finally:
  87. if target_mount_path:
  88. os.rmdir(target_mount_path)
  89. def run(command, config_path, no_print_config, no_print_statistics, tab_dry):
  90. with open(config_path) as config_file:
  91. config = yaml.load(config_file.read())
  92. if command == 'backup':
  93. backup(
  94. config = config,
  95. no_print_config = no_print_config,
  96. no_print_statistics = no_print_statistics,
  97. tab_dry = tab_dry,
  98. )
  99. def _init_argparser():
  100. import argparse
  101. argparser = argparse.ArgumentParser(description = None)
  102. argparser.add_argument(
  103. '-c',
  104. '--config',
  105. dest = 'config_path',
  106. default = '/etc/duplitab',
  107. )
  108. subparsers = argparser.add_subparsers(
  109. dest = 'command',
  110. )
  111. subparser_backup = subparsers.add_parser('backup')
  112. subparser_backup.add_argument(
  113. '--no-print-config',
  114. action = 'store_true',
  115. )
  116. subparser_backup.add_argument(
  117. '--no-print-statistics',
  118. action = 'store_true',
  119. )
  120. subparser_backup.add_argument(
  121. '--tab-dry',
  122. action = 'store_true',
  123. )
  124. return argparser
  125. def main(argv):
  126. argparser = _init_argparser()
  127. try:
  128. import argcomplete
  129. argcomplete.autocomplete(argparser)
  130. except ImportError:
  131. pass
  132. args = argparser.parse_args(argv)
  133. run(**vars(args))
  134. return 0
  135. if __name__ == "__main__":
  136. import sys
  137. sys.exit(main(sys.argv[1:]))