diff --git a/README.md b/README.md index 993357a..6beb004 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Create new backup jobs by adding JSON configuration files to the `configs/` dire | `inputs` | List of directories/files to back up | | `ignorePatterns` | List of patterns to exclude from backup | | `archiveType` | Either `"tar"` (default, higher compression) or `"zip"` (faster, better compatibility) | +| `remoteLocation` | Optional. Remote location for backup copy using scp format (e.g., `user@host:~/path`) | Example configuration: @@ -54,7 +55,8 @@ Example configuration: ".git/", "node_modules/" ], - "archiveType": "zip" + "archiveType": "zip", + "remoteLocation": "user@remotehost:~/backups/" } ``` @@ -89,6 +91,20 @@ The exclusion system uses simple string matching: - No regex or glob syntax is supported - For directories, adding a trailing slash (e.g., `.git/`) will exclude the directory and all its contents +## Remote Backup + +With the optional `remoteLocation` setting, Simple-Backup can automatically: +1. Check if the remote server is accessible +2. Ensure the target directory exists on the remote server +3. Copy the backup file via SCP after local backup completes + +This requires: +- SSH key-based authentication set up between your local and remote machines +- The remote server must be accessible via SSH +- You must have write permissions to the specified remote directory + +If the remote copy fails for any reason, the local backup will still be preserved. + ## What's Next? -After creating backups, you'll need to manually copy them to external storage or cloud services like Proton Drive, Google Drive, or Dropbox for off-site backup. \ No newline at end of file +If not using the remote backup feature, you'll need to manually copy your backups to external storage or cloud services like Proton Drive, Google Drive, or Dropbox for off-site backup. \ No newline at end of file diff --git a/backup.py b/backup.py index ee2118c..83fd849 100755 --- a/backup.py +++ b/backup.py @@ -7,6 +7,7 @@ import datetime from functools import partial import time import os +import subprocess def tar_filter(filters, tarinfo): if any([filter_name in tarinfo.name for filter_name in filters]): print(f"\tFiltering: {tarinfo.name}") @@ -42,7 +43,72 @@ def create_tar_archive(archive_path: Path, inputs: list, ignore_patterns: list): print(f"Compressing: {file_or_dir}") f.add(file_or_dir, filter=filter_function) -def local_backup(output_dir: Path, backup_name: str, inputs: list, ignore_patterns: list, archive_type="tar"): +def check_remote_connection(remote_location: str) -> bool: + """Test if we can reach the remote location using ssh.""" + try: + # Extract hostname from remote location (format: user@host:path) + host = remote_location.split('@', 1)[1].split(':', 1)[0] if '@' in remote_location else remote_location.split(':', 1)[0] + + # Test connection with SSH + result = subprocess.run( + ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", host, "echo OK"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=10, + text=True + ) + return result.returncode == 0 + except Exception as e: + print(f"Error checking remote connection: {e}") + return False + +def ensure_remote_directory(remote_location: str) -> bool: + """Ensure the target directory exists on the remote host.""" + try: + # Parse remote location to extract parts + if '@' in remote_location: + host = remote_location.split(':', 1)[0] + path = remote_location.split(':', 1)[1] + else: + host = remote_location.split(':', 1)[0] + path = remote_location.split(':', 1)[1] + + # Create directory on remote host + result = subprocess.run( + ["ssh", host, f"mkdir -p {path}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=10, + text=True + ) + return result.returncode == 0 + except Exception as e: + print(f"Error ensuring remote directory: {e}") + return False + +def copy_to_remote(file_path: Path, remote_location: str) -> bool: + """Copy file to remote location using scp.""" + try: + print(f"Copying {file_path} to {remote_location}...") + result = subprocess.run( + ["scp", str(file_path), remote_location], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=300, # Longer timeout for file transfer + text=True + ) + + if result.returncode == 0: + print(f"Successfully copied to {remote_location}") + return True + else: + print(f"Error copying to remote: {result.stderr}") + return False + except Exception as e: + print(f"Error copying to remote: {e}") + return False + +def local_backup(output_dir: Path, backup_name: str, inputs: list, ignore_patterns: list, archive_type="tar", remote_location=None): current_time = datetime.datetime.now().isoformat() start = time.time() @@ -55,7 +121,27 @@ def local_backup(output_dir: Path, backup_name: str, inputs: list, ignore_patter print(archive_path) create_tar_archive(archive_path, inputs, ignore_patterns) - print(f"Total time: {(time.time() - start) / 60}mins") + print(f"Local backup completed in: {(time.time() - start) / 60:.2f} mins") + + # Handle remote copy if configured + if remote_location: + remote_start = time.time() + print(f"Remote location configured: {remote_location}") + + if check_remote_connection(remote_location): + print("Remote connection successful") + + if ensure_remote_directory(remote_location): + print("Remote directory exists or was created successfully") + + if copy_to_remote(archive_path, remote_location): + print(f"Remote backup completed in: {(time.time() - remote_start) / 60:.2f} mins") + else: + print("Failed to copy backup to remote location") + else: + print("Failed to ensure remote directory exists") + else: + print("Failed to connect to remote location") def backup_all(): @@ -71,6 +157,7 @@ def backup_all(): ignore_patterns = config["ignorePatterns"] backup_name = file.stem archive_type = config.get("archiveType", "tar") - local_backup(output_dir, backup_name, inputs, ignore_patterns, archive_type) + remote_location = config.get("remoteLocation", None) + local_backup(output_dir, backup_name, inputs, ignore_patterns, archive_type, remote_location) if __name__ == "__main__": backup_all() \ No newline at end of file diff --git a/configs/personal.json b/configs/personal.json index 2aa0926..21e1a69 100644 --- a/configs/personal.json +++ b/configs/personal.json @@ -9,5 +9,6 @@ "ignorePatterns": [ ".git/" ], - "archiveType": "zip" + "archiveType": "zip", + "remoteLocation": "craig@serverlocal:~/backups/pc" } \ No newline at end of file