diff --git a/swb/config/example.ini b/swb/config/example.ini index 87f1c0b..c4f7782 100644 --- a/swb/config/example.ini +++ b/swb/config/example.ini @@ -21,3 +21,5 @@ compressor = bzip2 -v decompressor = bzip2 -d # The maximum number of local backups to keep max_local_backups = 3 +# Whether to backup the entire site directory in addition to the database +full_backup = no diff --git a/swb/local.py b/swb/local.py index 02a43f7..99f122d 100755 --- a/swb/local.py +++ b/swb/local.py @@ -18,50 +18,11 @@ remote_driver_path = os.path.join(program_dir, 'remote.py') -# Parse command line arguments passed to the local driver -def parse_cli_args(): - - parser = argparse.ArgumentParser() - - parser.add_argument( - '--quiet', - '-q', - action='store_true', - help='silences stdout and stderr') - - parser.add_argument( - 'config_path', - help='the path to a configuration file (.ini)') - - parser.add_argument( - '--restore', - '-r', - help='the path to a compressed backup file from which to restore') - - parser.add_argument( - '--force', - '-f', - action='store_true', - help='bypasses the confirmation prompt when restoring from backup') - - cli_args = parser.parse_args() - return cli_args - - -# Parse configuration files at given paths into object -def parse_config(config_path): - - config = configparser.RawConfigParser() - config.read(config_path) - - return config - - # Create intermediate directories in local backup path if necessary -def create_dir_structure(path): +def create_dir_structure(local_backup_path): try: - os.makedirs(os.path.dirname(path)) + os.makedirs(os.path.dirname(local_backup_path)) except OSError: pass @@ -77,18 +38,18 @@ def quote_arg(arg): if hasattr(shlex, 'quote'): # shlex.quote was introduced in v3.3 - quoted_arg = shlex.quote(arg) + quoted_arg = shlex.quote(str(arg)) else: # pipes.quote is deprecated, but use it if shlex.quote is unavailable - quoted_arg = pipes.quote(arg) + quoted_arg = pipes.quote(str(arg)) quoted_arg = unquote_home_dir(quoted_arg) return quoted_arg # Connect to remote via SSH and execute remote script -def exec_on_remote(ssh_user, ssh_hostname, ssh_port, action, action_args, - *, stdout, stderr): +def exec_on_remote(ssh_user, ssh_hostname, ssh_port, *, + action, action_args, stdout, stderr): # Read remote script so as to pass contents to SSH session with open(remote_driver_path, 'r') as remote_script: @@ -116,8 +77,8 @@ def exec_on_remote(ssh_user, ssh_hostname, ssh_port, action, action_args, # Transfer a file from remote to local (or vice-versa) using SCP -def transfer_file(ssh_user, ssh_hostname, ssh_port, - src_path, dest_path, action, *, stdout, stderr): +def transfer_file(ssh_user, ssh_hostname, ssh_port, *, + src_path, dest_path, action, stdout, stderr): scp_args = ['scp', '-P {}'.format(ssh_port)] @@ -139,34 +100,82 @@ def transfer_file(ssh_user, ssh_hostname, ssh_port, # Execute remote backup script to create remote backup -def create_remote_backup(ssh_user, ssh_hostname, ssh_port, +def create_remote_backup(ssh_user, ssh_hostname, ssh_port, *, wordpress_path, remote_backup_path, - backup_compressor, *, stdout, stderr): + backup_compressor, stdout, stderr): - exec_on_remote(ssh_user, ssh_hostname, ssh_port, 'back-up', [ - wordpress_path, - backup_compressor, - remote_backup_path - ], stdout=stdout, stderr=stderr) + exec_on_remote( + ssh_user=ssh_user, + ssh_hostname=ssh_hostname, + ssh_port=ssh_port, + action='back-up', + action_args=[ + wordpress_path, + backup_compressor, + remote_backup_path + ], + stdout=stdout, stderr=stderr) # Download remote backup to local system -def download_remote_backup(ssh_user, ssh_hostname, ssh_port, +def download_remote_backup(ssh_user, ssh_hostname, ssh_port, *, remote_backup_path, local_backup_path, - *, stdout, stderr): + stdout, stderr): + + transfer_file( + ssh_user=ssh_user, + ssh_hostname=ssh_hostname, + ssh_port=ssh_port, + src_path=remote_backup_path, + dest_path=local_backup_path, + action='download', + stdout=stdout, stderr=stderr) + + +# Uploads the given local backup to the given remote destination +def upload_local_backup(ssh_user, ssh_hostname, ssh_port, *, + local_backup_path, remote_backup_path, + stdout, stderr): + + transfer_file( + ssh_user=ssh_user, + ssh_hostname=ssh_hostname, + ssh_port=ssh_port, + src_path=local_backup_path, + dest_path=remote_backup_path, + action='upload', + stdout=stdout, stderr=stderr) + + +# Restores the local backup after upload to remote +def restore_remote_backup(ssh_user, ssh_hostname, ssh_port, *, + wordpress_path, remote_backup_path, + backup_decompressor, stdout, stderr): - transfer_file(ssh_user, ssh_hostname, ssh_port, - remote_backup_path, local_backup_path, - 'download', stdout=stdout, stderr=stderr) + exec_on_remote( + ssh_user=ssh_user, + ssh_hostname=ssh_hostname, + ssh_port=ssh_port, + action='restore', + action_args=[ + wordpress_path, + remote_backup_path, + backup_decompressor + ], + stdout=stdout, stderr=stderr) # Forcefully remove backup from remote -def purge_remote_backup(ssh_user, ssh_hostname, ssh_port, - remote_backup_path, *, stdout, stderr): +def purge_remote_backup(ssh_user, ssh_hostname, ssh_port, *, + remote_backup_path, stdout, stderr): - exec_on_remote(ssh_user, ssh_hostname, ssh_port, - 'purge-backup', [remote_backup_path], - stdout=stdout, stderr=stderr) + exec_on_remote( + ssh_user=ssh_user, + ssh_hostname=ssh_hostname, + ssh_port=ssh_port, + action='purge-backup', + action_args=[remote_backup_path], + stdout=stdout, stderr=stderr) # Retrieve a file's last modified time in seconds @@ -193,7 +202,7 @@ def purge_empty_dirs(dir_path): # Purge oldest backups to keep number of backups within specified limit -def purge_oldest_backups(local_backup_path, max_local_backups): +def purge_oldest_backups(local_backup_path, *, max_local_backups): # Convert date format sequences to wildcards local_backup_path = re.sub(r'%\-?[A-Za-z]', '*', local_backup_path) @@ -224,66 +233,100 @@ def back_up(config, *, stdout=None, stderr=None): config.get('paths', 'remote_backup')) create_remote_backup( - config.get('ssh', 'user'), - config.get('ssh', 'hostname'), - config.get('ssh', 'port'), - config.get('paths', 'wordpress'), - expanded_remote_backup_path, - config.get('backup', 'compressor'), + ssh_user=config.get('ssh', 'user'), + ssh_hostname=config.get('ssh', 'hostname'), + ssh_port=config.get('ssh', 'port'), + wordpress_path=config.get('paths', 'wordpress'), + remote_backup_path=expanded_remote_backup_path, + backup_compressor=config.get('backup', 'compressor'), stdout=stdout, stderr=stderr) - create_dir_structure(expanded_local_backup_path) + create_dir_structure(local_backup_path=expanded_local_backup_path) download_remote_backup( - config.get('ssh', 'user'), - config.get('ssh', 'hostname'), - config.get('ssh', 'port'), - expanded_remote_backup_path, - expanded_local_backup_path, + ssh_user=config.get('ssh', 'user'), + ssh_hostname=config.get('ssh', 'hostname'), + ssh_port=config.get('ssh', 'port'), + remote_backup_path=expanded_remote_backup_path, + local_backup_path=expanded_local_backup_path, stdout=stdout, stderr=stderr) purge_remote_backup( - config.get('ssh', 'user'), - config.get('ssh', 'hostname'), - config.get('ssh', 'port'), - expanded_remote_backup_path, + ssh_user=config.get('ssh', 'user'), + ssh_hostname=config.get('ssh', 'hostname'), + ssh_port=config.get('ssh', 'port'), + remote_backup_path=expanded_remote_backup_path, stdout=stdout, stderr=stderr) if config.has_option('backup', 'max_local_backups'): purge_oldest_backups( - config.get('paths', 'local_backup'), - config.getint('backup', 'max_local_backups')) + local_backup_path=config.get('paths', 'local_backup'), + max_local_backups=config.getint('backup', 'max_local_backups')) # Restore the chosen database revision to the Wordpress install on remote -def restore(config, local_backup_path, *, stdout=None, stderr=None): +def restore(config, *, local_backup_path, stdout=None, stderr=None): expanded_remote_backup_path = time.strftime( config.get('paths', 'remote_backup')) - # Copy local backup to remote so it can be used for restoration - transfer_file( - config.get('ssh', 'user'), - config.get('ssh', 'hostname'), - config.get('ssh', 'port'), - local_backup_path, - expanded_remote_backup_path, - 'upload', + upload_local_backup( + ssh_user=config.get('ssh', 'user'), + ssh_hostname=config.get('ssh', 'hostname'), + ssh_port=config.get('ssh', 'port'), + local_backup_path=local_backup_path, + remote_backup_path=expanded_remote_backup_path, stdout=stdout, stderr=stderr) - action_args = [ - config.get('paths', 'wordpress'), - expanded_remote_backup_path, - config.get('backup', 'decompressor') - ] - exec_on_remote( - config.get('ssh', 'user'), - config.get('ssh', 'hostname'), - config.get('ssh', 'port'), - 'restore', action_args, + restore_remote_backup( + ssh_user=config.get('ssh', 'user'), + ssh_hostname=config.get('ssh', 'hostname'), + ssh_port=config.get('ssh', 'port'), + wordpress_path=config.get('paths', 'wordpress'), + remote_backup_path=config.get('paths', 'remote_backup'), + backup_decompressor=config.get('backup', 'decompressor'), stdout=stdout, stderr=stderr) +# Parse command line arguments passed to the local driver +def parse_cli_args(): + + parser = argparse.ArgumentParser() + + parser.add_argument( + '--quiet', + '-q', + action='store_true', + help='silences stdout and stderr') + + parser.add_argument( + 'config_path', + help='the path to a configuration file (.ini)') + + parser.add_argument( + '--restore', + '-r', + help='the path to a compressed backup file from which to restore') + + parser.add_argument( + '--force', + '-f', + action='store_true', + help='bypasses the confirmation prompt when restoring from backup') + + cli_args = parser.parse_args() + return cli_args + + +# Parse configuration files at given paths into object +def parse_config(config_path): + + config = configparser.RawConfigParser() + config.read(config_path) + + return config + + def main(): cli_args = parse_cli_args() @@ -304,7 +347,9 @@ def main(): answer = input('Do you want to continue? (y/n) ') if not answer.lower().lstrip().startswith('y'): raise Exception('User canceled. Aborting.') - restore(config, cli_args.restore, stdout=stdout, stderr=stderr) + restore( + config, local_backup_path=cli_args.restore, + stdout=stdout, stderr=stderr) else: back_up(config, stdout=stdout, stderr=stderr) diff --git a/swb/remote.py b/swb/remote.py index 0b8869c..28824c7 100755 --- a/swb/remote.py +++ b/swb/remote.py @@ -43,9 +43,9 @@ def get_db_info(wordpress_path): return db_info -# Dump MySQL database to compressed file on remote -def dump_db(db_name, db_host, db_user, db_password, - backup_compressor, backup_path): +# Dump MySQL database to compressed file +def dump_compressed_db(db_name, db_host, db_user, db_password, + backup_compressor, backup_path): mysqldump = subprocess.Popen([ 'mysqldump', @@ -72,6 +72,7 @@ def dump_db(db_name, db_host, db_user, db_password, def verify_backup_integrity(backup_path): if os.path.getsize(backup_path) < 1024: + os.remove(backup_path) raise OSError('Backup is corrupted (too small). Aborting.') @@ -81,16 +82,20 @@ def purge_downloaded_backup(backup_path): os.remove(backup_path) +# Back up WordPress database or installation def back_up(wordpress_path, backup_compressor, backup_path): backup_path = os.path.expanduser(backup_path) create_dir_structure(backup_path) - db_info = get_db_info(wordpress_path) - dump_db( - db_info['name'], db_info['host'], - db_info['user'], db_info['password'], - backup_compressor, backup_path) + + # backup_path is assumed to refer to SQL database file backup + dump_compressed_db( + db_name=db_info['name'], db_host=db_info['host'], + db_user=db_info['user'], db_password=db_info['password'], + backup_compressor=backup_compressor, + backup_path=backup_path) + verify_backup_integrity(backup_path) @@ -102,12 +107,6 @@ def decompress_backup(backup_path, backup_decompressor): compressor.wait() -# Construct path to decompressed database file from given backup file -def get_db_path(backup_path): - - return re.sub('\.([A-Za-z0-9]+)$', '', backup_path) - - # Replace a WordPress database with the database at the given path def replace_db(db_name, db_host, db_user, db_password, db_path): @@ -130,6 +129,9 @@ def purge_restored_backup(backup_path, db_path): try: os.remove(db_path) + except OSError: + pass + try: os.remove(backup_path) except OSError: pass @@ -138,18 +140,22 @@ def purge_restored_backup(backup_path, db_path): # Restore WordPress database using the given remote backup def restore(wordpress_path, backup_path, backup_decompressor): + wordpress_path = os.path.expanduser(wordpress_path) backup_path = os.path.expanduser(backup_path) verify_backup_integrity(backup_path) - decompress_backup(backup_path, backup_decompressor) + decompress_backup( + backup_path=backup_path, + backup_decompressor=backup_decompressor) db_info = get_db_info(wordpress_path) - db_path = get_db_path(backup_path) + db_path = os.path.splitext(backup_path)[0] replace_db( - db_info['name'], db_info['host'], - db_info['user'], db_info['password'], db_path) + db_name=db_info['name'], db_host=db_info['host'], + db_user=db_info['user'], db_password=db_info['password'], + db_path=db_path) - purge_restored_backup(backup_path, db_path) + purge_restored_backup(backup_path=backup_path, db_path=db_path) def main(): diff --git a/tests/files/config.ini b/tests/files/config.ini index 6f78478..df8308a 100644 --- a/tests/files/config.ini +++ b/tests/files/config.ini @@ -5,10 +5,10 @@ port = 2222 [paths] wordpress = ~/public_html/mysite -remote_backup = ~/backups/mysite.sql.bz2 -local_backup = ~/Backups/mysite.sql.bz2 +remote_backup = ~/backups/%y/%m/%d/mysite.sql.bz2 +local_backup = ~/Backups/%y/%m/%d/mysite.sql.bz2 [backup] -compressor = bzip2 +compressor = bzip2 -v decompressor = bzip2 -d -max_local_backups = 3 +full_backup = no diff --git a/tests/files/mysite/wp-config.php b/tests/files/mysite/wp-config.php new file mode 100644 index 0000000..b579b3c --- /dev/null +++ b/tests/files/mysite/wp-config.php @@ -0,0 +1,8 @@ +