From 08a2e6b28b4a6d3a511d55be8cc155ad4df5ade5 Mon Sep 17 00:00:00 2001 From: Q Date: Sun, 22 Oct 2023 10:34:45 +0300 Subject: [PATCH] gpg encryption support --- files/tar-backup | 103 +++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/files/tar-backup b/files/tar-backup index fda5f66..8bb310a 100755 --- a/files/tar-backup +++ b/files/tar-backup @@ -1,12 +1,12 @@ #!/usr/bin/env python3 import datetime -import subprocess -import os -import sys -import shlex -import json import hashlib +import json +import os +import shlex +import subprocess +import sys from argparse import ArgumentParser VERSION = "0.0.9" @@ -23,13 +23,13 @@ class TB: self.ssh_args = options.ssh_args self.delete_older = options.del_older self.keep_n = options.keep_n + self.do_gpg = options.gpg self.delete_disk_percentage = options.del_used self.config_file = os.path.join(self.base_folder, "tar-backup.txt") self.lock_file = os.path.join(self.base_folder, "tar-backup.lock") if not self.backup_source.endswith("/"): self.backup_source += "/" - self.current = os.path.join(self.base_folder, "current") self.folder_format = "backup-%Y%m%d-%H%M%S" self.backup_folder = datetime.datetime.now().strftime(self.folder_format) os.makedirs(self.base_folder, exist_ok=True) @@ -38,8 +38,8 @@ class TB: self.write_config() self.clean_old() success = self.make_backup() - if success: - self.make_softlinks() + + self.make_softlinks(success) self.lock_clean() if not success: @@ -48,9 +48,7 @@ class TB: def diskused(self): """in percents""" - pcent = subprocess.check_output( - ["df", "--output=pcent", self.base_folder] - ).decode("utf8") + pcent = subprocess.check_output(["df", "--output=pcent", self.base_folder]).decode("utf8") try: pcent = int("".join([x for x in pcent if x.isdigit()])) except Exception as e: @@ -70,8 +68,7 @@ class TB: dirs = [ d for d in sorted(os.listdir(self.base_folder)) - if d.startswith("backup-") - and os.path.isdir(os.path.join(self.base_folder, d)) + if d.startswith("backup-") and os.path.isdir(os.path.join(self.base_folder, d)) ] for i, d in enumerate(dirs): if self.delete_older: @@ -89,23 +86,21 @@ class TB: else: not_kept = False - print( - "{}: {} -- old: {}, disk full: {}, keep n: {}".format( - i, d, is_old, is_disk_full, not_kept - ) - ) + print("{}: {} -- old: {}, disk full: {}, keep n: {}".format(i, d, is_old, is_disk_full, not_kept)) if any((is_old, is_disk_full, not_kept)): print("Deleting {}".format(d)) cmd = ["rm", "-rf", os.path.join(self.base_folder, d)] subprocess.call(cmd, shell=False) - def make_softlinks(self): - try: - os.remove(self.current) - except Exception: - pass - if os.path.exists(os.path.join(self.base_folder, self.backup_folder)): - os.symlink(self.backup_folder, self.current) + def make_softlinks(self, success): + target = "success" if success else "failed" + for folder in ("latest", target): + try: + os.remove(os.path.join(self.base_folder, folder)) + except Exception: + pass + if os.path.exists(os.path.join(self.base_folder, self.backup_folder)): + os.symlink(self.backup_folder, os.path.join(self.base_folder, folder)) def make_backup(self): if self.options.no_backup: @@ -123,13 +118,9 @@ class TB: else [] ) - with open( - os.path.join(self.base_folder, self.backup_folder, self.tar_file), "wb" - ) as fp: + with open(os.path.join(self.base_folder, self.backup_folder, self.tar_file), "wb") as fp: with open( - os.path.join( - self.base_folder, self.backup_folder, self.tar_file + ".log" - ), + os.path.join(self.base_folder, self.backup_folder, self.tar_file + ".log"), "w", ) as fp_log: command = [ @@ -141,34 +132,43 @@ class TB: print(command) md5 = hashlib.md5() p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=fp_log) + + if self.do_gpg: + pwpipe_r, pwpipe_w = os.pipe() + os.write(pwpipe_w, sys.stdin.buffer.read()) + os.close(pwpipe_w) + gpg_command = "gpg -o - --passphrase-fd {} --batch --yes --symmetric".format(pwpipe_r) + gnp = subprocess.Popen( + shlex.split(gpg_command), stdin=p.stdout, stdout=subprocess.PIPE, pass_fds=[pwpipe_r] + ) + reader = gnp + else: + reader = p + i = 1 while True: - chunk = p.stdout.read(1048576) + chunk = reader.stdout.read(1048576) if len(chunk) == 0: break print(f"{i} Mb", end="\r") i += 1 md5.update(chunk) fp.write(chunk) - exitcode = p.wait() - success = exitcode in (0, 2) - print( - f"\nWrote {os.path.join(self.base_folder, self.backup_folder,self.tar_file)}" - ) - print( - f"Log file {os.path.join(self.base_folder, self.backup_folder,self.tar_file+'.log')}" - ) + if self.do_gpg: + os.close(pwpipe_r) + exitcode = gnp.wait() + p.wait() + else: + exitcode = p.wait() + success = exitcode in (0,) + print(f"\nWrote {os.path.join(self.base_folder, self.backup_folder,self.tar_file)}") + print(f"Log file {os.path.join(self.base_folder, self.backup_folder,self.tar_file+'.log')}") with open( os.path.join(self.base_folder, self.backup_folder, self.tar_file + ".md5"), "w", ) as fp_md5: fp_md5.write(f"{md5.hexdigest()} {self.tar_file}\n") if not success: - with open( - os.path.join( - self.base_folder, self.backup_folder, self.tar_file + ".log" - ) - ) as f: + with open(os.path.join(self.base_folder, self.backup_folder, self.tar_file + ".log")) as f: print(f.read(), end="") print(f"Exit code: {exitcode}") return success @@ -280,12 +280,17 @@ def get_opts(): default=False, help="Do not actually backup, only clean up.", ) + parser.add_argument( + "--gpg", + dest="gpg", + action="store_true", + default=False, + help="Encrypt with GPG. Read passphrase from stdin. change --tar-file to match, e.g. .gpg suffix. You might want to remove -vv from tar-args.", + ) parser.add_argument("--version", action="version", version=VERSION) - parser.add_argument( - "backup_source", action="store", help="Source folder to backup with tar+ssh" - ) - parser.add_argument("base_folder", action="store", help="Local backup folder") + parser.add_argument("backup_source", action="store", help="Source folder to backup with tar+ssh") + parser.add_argument("base_folder", action="store", help="Local backup folder written to") options = parser.parse_args() if "/" in options.tar_file: parser.error("--tar-file must be a filename")