Add remote backup capability using SCP

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Craig
2025-04-24 16:49:21 +01:00
parent 3871c3fc8f
commit 3540d48df9
3 changed files with 110 additions and 6 deletions

View File

@@ -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.
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.

View File

@@ -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()

View File

@@ -9,5 +9,6 @@
"ignorePatterns": [
".git/"
],
"archiveType": "zip"
"archiveType": "zip",
"remoteLocation": "craig@serverlocal:~/backups/pc"
}