duplitab 4.5 KB

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