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:
20
README.md
20
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 |
|
| `inputs` | List of directories/files to back up |
|
||||||
| `ignorePatterns` | List of patterns to exclude from backup |
|
| `ignorePatterns` | List of patterns to exclude from backup |
|
||||||
| `archiveType` | Either `"tar"` (default, higher compression) or `"zip"` (faster, better compatibility) |
|
| `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:
|
Example configuration:
|
||||||
|
|
||||||
@@ -54,7 +55,8 @@ Example configuration:
|
|||||||
".git/",
|
".git/",
|
||||||
"node_modules/"
|
"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
|
- No regex or glob syntax is supported
|
||||||
- For directories, adding a trailing slash (e.g., `.git/`) will exclude the directory and all its contents
|
- 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?
|
## 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.
|
||||||
93
backup.py
93
backup.py
@@ -7,6 +7,7 @@ import datetime
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
def tar_filter(filters, tarinfo):
|
def tar_filter(filters, tarinfo):
|
||||||
if any([filter_name in tarinfo.name for filter_name in filters]):
|
if any([filter_name in tarinfo.name for filter_name in filters]):
|
||||||
print(f"\tFiltering: {tarinfo.name}")
|
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}")
|
print(f"Compressing: {file_or_dir}")
|
||||||
f.add(file_or_dir, filter=filter_function)
|
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()
|
current_time = datetime.datetime.now().isoformat()
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
@@ -55,7 +121,27 @@ def local_backup(output_dir: Path, backup_name: str, inputs: list, ignore_patter
|
|||||||
print(archive_path)
|
print(archive_path)
|
||||||
create_tar_archive(archive_path, inputs, ignore_patterns)
|
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():
|
def backup_all():
|
||||||
@@ -71,6 +157,7 @@ def backup_all():
|
|||||||
ignore_patterns = config["ignorePatterns"]
|
ignore_patterns = config["ignorePatterns"]
|
||||||
backup_name = file.stem
|
backup_name = file.stem
|
||||||
archive_type = config.get("archiveType", "tar")
|
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__":
|
if __name__ == "__main__":
|
||||||
backup_all()
|
backup_all()
|
||||||
@@ -9,5 +9,6 @@
|
|||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
".git/"
|
".git/"
|
||||||
],
|
],
|
||||||
"archiveType": "zip"
|
"archiveType": "zip",
|
||||||
|
"remoteLocation": "craig@serverlocal:~/backups/pc"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user