Compare commits
4 Commits
4efa21d462
...
5828140a35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5828140a35 | ||
|
|
d6c86044eb | ||
|
|
84b1ad10f6 | ||
|
|
0367c3f7e6 |
46
.github/copilot-instructions.md
vendored
Normal file
46
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
|
||||
- [x] Verify that the copilot-instructions.md file in the .github directory is created.
|
||||
|
||||
- [x] Clarify Project Requirements
|
||||
Project: Linux backup system with GUI button for rebooting and cloning internal HDD to external M.2 SSD
|
||||
Language: Python with tkinter for GUI
|
||||
Requirements: Backup script, GUI interface, systemd service for reboot integration
|
||||
|
||||
- [x] Scaffold the Project
|
||||
Project structure created with:
|
||||
- backup_manager.py: GUI application with tkinter
|
||||
- backup_script.sh: Command-line backup script
|
||||
- install.sh: Installation and setup script
|
||||
- README.md: Comprehensive documentation
|
||||
|
||||
- [x] Customize the Project
|
||||
Customized with backup-specific features:
|
||||
- Drive detection and validation
|
||||
- GUI with progress monitoring
|
||||
- Reboot integration capabilities
|
||||
- Safety checks and confirmations
|
||||
- Desktop integration support
|
||||
|
||||
- [x] Install Required Extensions
|
||||
No VS Code extensions required for this Python/Bash project
|
||||
|
||||
- [x] Compile the Project
|
||||
Dependencies installed and scripts tested:
|
||||
- python3-tk for GUI interface
|
||||
- pv for progress monitoring
|
||||
- All scripts executable and functional
|
||||
|
||||
- [x] Create and Run Task
|
||||
No build task needed - Python/Bash scripts are ready to run
|
||||
|
||||
- [x] Launch the Project
|
||||
Project successfully launches:
|
||||
- GUI: python3 backup_manager.py
|
||||
- CLI: ./backup_script.sh --help
|
||||
- Installation: ./install.sh
|
||||
|
||||
- [x] Ensure Documentation is Complete
|
||||
Documentation completed:
|
||||
- README.md with comprehensive setup and usage instructions
|
||||
- copilot-instructions.md properly configured
|
||||
- All safety warnings and technical details included
|
||||
391
README.md
Normal file
391
README.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# System Backup to External M.2 SSD
|
||||
|
||||
A comprehensive backup solution for Linux systems that provides both GUI and command-line interfaces for cloning your internal drive to an external M.2 SSD.
|
||||
|
||||
## Features
|
||||
|
||||
## Features
|
||||
|
||||
- **GUI Interface**: User-friendly graphical interface built with Python Tkinter
|
||||
- **Command Line Interface**: Full CLI support for automated and scripted operations
|
||||
- **Smart Drive Detection**: Automatically detects internal drive and external M.2 SSDs
|
||||
- **Full System Backup**: Complete drive cloning with dd for exact system replication
|
||||
- **Smart Sync Backup**: ⚡ NEW! Fast incremental backups using rsync for minor changes
|
||||
- **Change Analysis**: Analyze filesystem changes to recommend sync vs full backup
|
||||
- **Restore Functionality**: Complete system restore from external drive
|
||||
- **Portable Tools**: Backup tools survive on external drive and remain accessible after cloning
|
||||
- **Reboot Integration**: Optional reboot before backup/restore operations
|
||||
- **Progress Monitoring**: Real-time progress display and logging
|
||||
- **Safety Features**: Multiple confirmations and drive validation
|
||||
- **Desktop Integration**: Create desktop shortcuts for easy access
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux system (tested on Ubuntu/Debian)
|
||||
- Python 3.6+ with tkinter
|
||||
- External M.2 SSD in USB enclosure
|
||||
- sudo privileges for drive operations
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone or download this repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd backup_to_external_m.2
|
||||
```
|
||||
|
||||
2. Make scripts executable:
|
||||
```bash
|
||||
chmod +x backup_script.sh
|
||||
chmod +x backup_manager.py
|
||||
```
|
||||
|
||||
3. Install dependencies (if needed):
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install python3-tk pv parted
|
||||
```
|
||||
|
||||
4. **Optional**: Set up portable tools on external M.2 SSD:
|
||||
```bash
|
||||
./setup_portable_tools.sh
|
||||
```
|
||||
This creates a separate partition on your external drive that preserves backup tools even after cloning.
|
||||
|
||||
## Usage
|
||||
|
||||
### GUI Application
|
||||
|
||||
Launch the graphical backup manager:
|
||||
|
||||
```bash
|
||||
python3 backup_manager.py
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Automatic drive detection and classification
|
||||
- Source and target drive selection with smart defaults
|
||||
- Real-time progress monitoring
|
||||
- **Backup Modes**:
|
||||
- **Smart Sync Backup**: ⚡ Fast incremental backup using rsync (requires existing backup)
|
||||
- **Analyze Changes**: Analyze what has changed since last backup
|
||||
- **Start Backup**: Full drive clone (immediate backup while system running)
|
||||
- **Reboot & Backup**: Reboot system then full backup (recommended for first backup)
|
||||
- **Restore Modes**:
|
||||
- **Restore from External**: Immediate restore from external to internal
|
||||
- **Reboot & Restore**: Reboot system then restore (recommended)
|
||||
- **Drive Swap Button**: Easily swap source and target for restore operations
|
||||
|
||||
### Command Line Script
|
||||
|
||||
For command-line usage:
|
||||
|
||||
```bash
|
||||
# List available drives
|
||||
./backup_script.sh --list
|
||||
|
||||
# Analyze changes without performing backup
|
||||
./backup_script.sh --analyze --target /dev/sdb
|
||||
|
||||
# Smart sync backup (fast incremental update)
|
||||
sudo ./backup_script.sh --sync --target /dev/sdb
|
||||
|
||||
# Perform full backup with specific drives
|
||||
sudo ./backup_script.sh --source /dev/nvme0n1 --target /dev/sda
|
||||
|
||||
# Restore from external to internal (note the restore flag)
|
||||
sudo ./backup_script.sh --restore --source /dev/sda --target /dev/nvme0n1
|
||||
|
||||
# Or more simply in restore mode (source/target are swapped automatically)
|
||||
sudo ./backup_script.sh --restore --source /dev/nvme0n1 --target /dev/sda
|
||||
|
||||
# Create desktop entry
|
||||
./backup_script.sh --desktop
|
||||
|
||||
# Launch GUI from script
|
||||
./backup_script.sh --gui
|
||||
```
|
||||
|
||||
### Desktop Integration
|
||||
|
||||
Create a desktop shortcut:
|
||||
|
||||
```bash
|
||||
./backup_script.sh --desktop
|
||||
```
|
||||
|
||||
This creates a clickable icon on your desktop that launches the backup tool.
|
||||
|
||||
## Restore Operations
|
||||
|
||||
### When to Restore
|
||||
- **System Failure**: Internal drive crashed or corrupted
|
||||
- **System Migration**: Moving to new hardware
|
||||
- **Rollback**: Reverting to previous system state
|
||||
- **Testing**: Restoring test environment
|
||||
|
||||
### How to Restore
|
||||
|
||||
#### GUI Method
|
||||
1. **Connect External Drive**: Plug in your M.2 SSD with backup
|
||||
2. **Launch GUI**: `python3 backup_manager.py`
|
||||
3. **Check Drive Selection**:
|
||||
- Source should be external drive (your backup)
|
||||
- Target should be internal drive (will be overwritten)
|
||||
4. **Use Swap Button**: If needed, click "Swap Source↔Target"
|
||||
5. **Choose Restore Mode**:
|
||||
- **"Restore from External"**: Immediate restore
|
||||
- **"Reboot & Restore"**: Reboot then restore (recommended)
|
||||
|
||||
#### Command Line Method
|
||||
```bash
|
||||
# Restore with automatic drive detection
|
||||
sudo ./backup_script.sh --restore
|
||||
|
||||
# Restore with specific drives
|
||||
sudo ./backup_script.sh --restore --source /dev/sda --target /dev/nvme0n1
|
||||
```
|
||||
|
||||
### ⚠️ Restore Safety Warnings
|
||||
- **Data Loss**: Target drive is completely overwritten
|
||||
- **Irreversible**: No undo after restore starts
|
||||
- **Multiple Confirmations**: System requires explicit confirmation
|
||||
- **Drive Verification**: Double-check source and target drives
|
||||
- **Boot Issues**: Ensure external drive contains valid system backup
|
||||
|
||||
## Portable Tools (Boot from External M.2)
|
||||
|
||||
### Setup Portable Tools
|
||||
Run this once to set up backup tools on your external M.2 SSD:
|
||||
|
||||
```bash
|
||||
./setup_portable_tools.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Create a 512MB tools partition on your external drive
|
||||
- Install backup tools that survive cloning operations
|
||||
- Set up automatic tool restoration after each backup
|
||||
|
||||
### Using Portable Tools
|
||||
|
||||
#### When Booted from External M.2 SSD:
|
||||
|
||||
1. **Access Tools**:
|
||||
```bash
|
||||
# Easy access helper
|
||||
./access_tools.sh
|
||||
|
||||
# Or manually:
|
||||
sudo mount LABEL=BACKUP_TOOLS /mnt/tools
|
||||
cd /mnt/tools/backup_system
|
||||
./launch_backup_tools.sh
|
||||
```
|
||||
|
||||
2. **Restore Internal Drive**:
|
||||
- Launch backup tools from external drive
|
||||
- External drive (source) → Internal drive (target)
|
||||
- Click "Reboot & Restore" for safest operation
|
||||
|
||||
3. **Create Desktop Entry**:
|
||||
```bash
|
||||
cd /mnt/tools/backup_system
|
||||
./create_desktop_entry.sh
|
||||
```
|
||||
|
||||
### Disaster Recovery Workflow
|
||||
|
||||
1. **Normal Operation**: Internal drive fails
|
||||
2. **Boot from External**: Use M.2 SSD as boot drive
|
||||
3. **Access Tools**: Run `./access_tools.sh`
|
||||
4. **Restore System**: Use backup tools to restore to new internal drive
|
||||
5. **Back to Normal**: Boot from restored internal drive
|
||||
|
||||
## Safety Features
|
||||
|
||||
- **Drive Validation**: Prevents accidental overwriting of wrong drives
|
||||
- **Size Checking**: Ensures target drive is large enough
|
||||
- **Confirmation Prompts**: Multiple confirmation steps before destructive operations
|
||||
- **Mount Detection**: Automatically unmounts target drives before backup
|
||||
- **Progress Monitoring**: Real-time feedback during backup operations
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
backup_to_external_m.2/
|
||||
├── backup_manager.py # GUI application
|
||||
├── backup_script.sh # Command-line script
|
||||
├── install.sh # Installation script
|
||||
├── systemd/ # Systemd service files
|
||||
│ └── backup-service.service
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Smart Sync Technology ⚡
|
||||
|
||||
The backup system now includes advanced **Smart Sync** functionality that dramatically reduces backup time for incremental updates:
|
||||
|
||||
### How Smart Sync Works
|
||||
|
||||
1. **Analysis Phase**: Compares source and target filesystems to determine changes
|
||||
2. **Decision Engine**: Recommends sync vs full clone based on amount of changes:
|
||||
- **< 2GB changes**: Smart sync recommended (much faster)
|
||||
- **2-10GB changes**: Smart sync beneficial
|
||||
- **> 10GB changes**: Full clone may be more appropriate
|
||||
3. **Sync Operation**: Uses rsync to transfer only changed files and metadata
|
||||
|
||||
### Smart Sync Benefits
|
||||
|
||||
- **Speed**: 10-100x faster than full clone for minor changes
|
||||
- **No Downtime**: System remains usable during sync operation
|
||||
- **Efficiency**: Only transfers changed data, preserving bandwidth and storage wear
|
||||
- **Safety**: Preserves backup tools and maintains full system consistency
|
||||
|
||||
### When to Use Smart Sync vs Full Clone
|
||||
|
||||
**Use Smart Sync when:**
|
||||
- You have an existing backup on the target drive
|
||||
- Regular incremental updates (daily/weekly backups)
|
||||
- Minimal system changes since last backup
|
||||
- You want faster backup with minimal downtime
|
||||
|
||||
**Use Full Clone when:**
|
||||
- First-time backup to a new drive
|
||||
- Major system changes (OS upgrade, large software installations)
|
||||
- Corrupted or incomplete previous backup
|
||||
- Maximum compatibility and reliability needed
|
||||
|
||||
### Smart Sync Usage
|
||||
|
||||
**GUI Method:**
|
||||
1. Click "Analyze Changes" to see what has changed
|
||||
2. Review the recommendation and estimated time savings
|
||||
3. Click "Smart Sync Backup" to perform incremental update
|
||||
|
||||
**Command Line:**
|
||||
```bash
|
||||
# Analyze changes first
|
||||
./backup_script.sh --analyze --target /dev/sdb
|
||||
|
||||
# Perform smart sync
|
||||
sudo ./backup_script.sh --sync --target /dev/sdb
|
||||
```
|
||||
|
||||
## Traditional Full Backup
|
||||
|
||||
For comprehensive system backup, the system uses proven `dd` cloning technology:
|
||||
|
||||
### Backup Process
|
||||
1. **Drive Detection**: Automatically scans for available drives
|
||||
2. **Auto-Selection**: Internal drive as source, external as target
|
||||
3. **Operation Selection**: Choose backup or restore mode
|
||||
4. **Validation**: Confirms drives exist and are different
|
||||
5. **Execution**: Uses `dd` command to clone entire drive
|
||||
6. **Progress**: Shows real-time progress and logging
|
||||
|
||||
### Backup vs Restore
|
||||
- **Backup**: Internal → External (preserves your system on external drive)
|
||||
- **Restore**: External → Internal (overwrites internal with backup data)
|
||||
- **Swap Button**: Easily switch source/target for restore operations
|
||||
|
||||
### Reboot vs Immediate Operations
|
||||
- **Immediate**: Faster start, but system is running (potential file locks)
|
||||
- **Reboot Mode**: System restarts first, then operates (cleaner, more reliable)
|
||||
- **Recommendation**: Use reboot mode for critical operations
|
||||
|
||||
### Reboot & Backup Mode
|
||||
1. **Script Creation**: Creates a backup script for post-reboot execution
|
||||
2. **Reboot Scheduling**: Schedules system reboot
|
||||
3. **Auto-Execution**: Backup starts automatically after reboot
|
||||
4. **Notification**: Shows completion status
|
||||
|
||||
### Command Line Mode
|
||||
- Direct `dd` cloning with progress monitoring
|
||||
- Comprehensive logging to `/var/log/system_backup.log`
|
||||
- Drive size validation and safety checks
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Backup Method
|
||||
- Uses `dd` command for bit-perfect drive cloning
|
||||
- Block size optimized at 4MB for performance
|
||||
- `fdatasync` ensures all data is written to disk
|
||||
|
||||
### Drive Detection
|
||||
- Uses `lsblk` to enumerate block devices
|
||||
- Filters for actual disk drives
|
||||
- Shows drive sizes for easy identification
|
||||
|
||||
### Safety Mechanisms
|
||||
- Root privilege verification
|
||||
- Block device validation
|
||||
- Source/target drive comparison
|
||||
- Mount status checking
|
||||
- Size compatibility verification
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Permission Denied**
|
||||
```bash
|
||||
# Run with sudo for drive operations
|
||||
sudo python3 backup_manager.py
|
||||
```
|
||||
|
||||
2. **Drive Not Detected**
|
||||
```bash
|
||||
# Check if drive is connected and recognized
|
||||
lsblk
|
||||
dmesg | tail
|
||||
```
|
||||
|
||||
3. **Backup Fails**
|
||||
```bash
|
||||
# Check system logs
|
||||
sudo journalctl -f
|
||||
tail -f /var/log/system_backup.log
|
||||
```
|
||||
|
||||
### Performance Tips
|
||||
|
||||
- Use USB 3.0+ connection for external M.2 SSD
|
||||
- Ensure sufficient power supply for external enclosure
|
||||
- Close unnecessary applications during backup
|
||||
- Use SSD for better performance than traditional HDD
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Data Destruction**: Target drive data is completely overwritten
|
||||
- **Root Access**: Scripts require elevated privileges
|
||||
- **Verification**: Always verify backup integrity after completion
|
||||
- **Testing**: Test restore process with non-critical data first
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only works with block devices (entire drives)
|
||||
- Cannot backup to smaller drives
|
||||
- Requires manual drive selection for safety
|
||||
- No incremental backup support (full clone only)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This project is open source. Use at your own risk.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**WARNING**: This tool performs low-level disk operations that can result in data loss. Always:
|
||||
- Verify drive selections carefully
|
||||
- Test with non-critical data first
|
||||
- Maintain separate backups of important data
|
||||
- Understand the risks involved
|
||||
|
||||
The authors are not responsible for any data loss or system damage.
|
||||
BIN
__pycache__/backup_manager.cpython-312.pyc
Normal file
BIN
__pycache__/backup_manager.cpython-312.pyc
Normal file
Binary file not shown.
113
access_tools.sh
Executable file
113
access_tools.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
# Mount backup tools partition and provide easy access
|
||||
# Use this when booted from the external M.2 SSD
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Backup Tools Access Helper"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Try to find backup tools partition
|
||||
TOOLS_PARTITION=$(blkid -L "BACKUP_TOOLS" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$TOOLS_PARTITION" ]]; then
|
||||
print_warning "Backup tools partition not found by label."
|
||||
print_info "Searching for tools partition..."
|
||||
|
||||
# Look for small partitions that might be our tools partition
|
||||
while IFS= read -r line; do
|
||||
partition=$(echo "$line" | awk '{print $1}')
|
||||
size=$(echo "$line" | awk '{print $4}')
|
||||
|
||||
# Check if it's a small partition (likely tools)
|
||||
if [[ "$size" == *M ]] && [[ ${size%M} -le 1024 ]]; then
|
||||
print_info "Found potential tools partition: $partition ($size)"
|
||||
TOOLS_PARTITION="/dev/$partition"
|
||||
break
|
||||
fi
|
||||
done < <(lsblk -n -o NAME,SIZE | grep -E "sd|nvme.*p")
|
||||
fi
|
||||
|
||||
if [[ -z "$TOOLS_PARTITION" ]]; then
|
||||
echo "❌ Could not find backup tools partition"
|
||||
echo ""
|
||||
echo "Available partitions:"
|
||||
lsblk
|
||||
echo ""
|
||||
echo "To manually mount tools:"
|
||||
echo "1. Identify the tools partition from the list above"
|
||||
echo "2. sudo mkdir -p /mnt/backup_tools"
|
||||
echo "3. sudo mount /dev/[partition] /mnt/backup_tools"
|
||||
echo "4. cd /mnt/backup_tools/backup_system"
|
||||
echo "5. ./launch_backup_tools.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create mount point
|
||||
MOUNT_POINT="/mnt/backup_tools"
|
||||
print_info "Creating mount point: $MOUNT_POINT"
|
||||
sudo mkdir -p "$MOUNT_POINT"
|
||||
|
||||
# Mount tools partition
|
||||
print_info "Mounting tools partition: $TOOLS_PARTITION"
|
||||
if sudo mount "$TOOLS_PARTITION" "$MOUNT_POINT"; then
|
||||
print_success "Tools partition mounted successfully"
|
||||
else
|
||||
echo "❌ Failed to mount tools partition"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if backup system exists
|
||||
if [[ -d "$MOUNT_POINT/backup_system" ]]; then
|
||||
print_success "Backup tools found!"
|
||||
|
||||
cd "$MOUNT_POINT/backup_system"
|
||||
|
||||
echo ""
|
||||
echo "📁 Backup tools are now available at:"
|
||||
echo " $MOUNT_POINT/backup_system"
|
||||
echo ""
|
||||
echo "🚀 Available commands:"
|
||||
echo " ./launch_backup_tools.sh - Interactive menu"
|
||||
echo " ./backup_script.sh --help - Command line help"
|
||||
echo " python3 backup_manager.py - GUI (if available)"
|
||||
echo " ./create_desktop_entry.sh - Create desktop shortcut"
|
||||
echo ""
|
||||
|
||||
# Ask what to do
|
||||
read -p "Launch backup tools now? (y/n): " launch
|
||||
if [[ "$launch" =~ ^[Yy] ]]; then
|
||||
./launch_backup_tools.sh
|
||||
else
|
||||
echo ""
|
||||
echo "Tools are ready to use. Change to the tools directory:"
|
||||
echo "cd $MOUNT_POINT/backup_system"
|
||||
fi
|
||||
|
||||
else
|
||||
print_warning "Backup system not found in tools partition"
|
||||
echo ""
|
||||
echo "Contents of tools partition:"
|
||||
ls -la "$MOUNT_POINT"
|
||||
fi
|
||||
986
backup_manager.py
Executable file
986
backup_manager.py
Executable file
@@ -0,0 +1,986 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Linux System Backup Tool with GUI
|
||||
A tool for creating full system backups to external M.2 SSD with reboot functionality.
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, scrolledtext
|
||||
import subprocess
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
class BackupManager:
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("System Backup Manager")
|
||||
self.root.geometry("600x500")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
# Variables
|
||||
self.source_drive = tk.StringVar() # Will be auto-detected
|
||||
self.target_drive = tk.StringVar()
|
||||
self.operation_running = False
|
||||
self.operation_type = "backup" # "backup" or "restore"
|
||||
self.sync_mode = "full" # "full", "sync", or "auto"
|
||||
|
||||
self.setup_ui()
|
||||
self.detect_drives()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Setup the user interface"""
|
||||
# Main frame
|
||||
main_frame = ttk.Frame(self.root, padding="10")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Configure grid weights
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Title
|
||||
title_label = ttk.Label(main_frame, text="System Backup Manager",
|
||||
font=("Arial", 16, "bold"))
|
||||
title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20))
|
||||
|
||||
# Source drive selection
|
||||
ttk.Label(main_frame, text="Source Drive (Internal):").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
source_combo = ttk.Combobox(main_frame, textvariable=self.source_drive, width=40)
|
||||
source_combo.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=5, padx=(10, 0))
|
||||
|
||||
# Target drive selection
|
||||
ttk.Label(main_frame, text="Target Drive (External M.2):").grid(row=2, column=0, sticky=tk.W, pady=5)
|
||||
target_combo = ttk.Combobox(main_frame, textvariable=self.target_drive, width=40)
|
||||
target_combo.grid(row=2, column=1, sticky=(tk.W, tk.E), pady=5, padx=(10, 0))
|
||||
|
||||
# Refresh drives button
|
||||
refresh_btn = ttk.Button(main_frame, text="Refresh Drives", command=self.detect_drives)
|
||||
refresh_btn.grid(row=3, column=1, sticky=tk.E, pady=10, padx=(10, 0))
|
||||
|
||||
# Status frame
|
||||
status_frame = ttk.LabelFrame(main_frame, text="Status", padding="10")
|
||||
status_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
|
||||
status_frame.columnconfigure(0, weight=1)
|
||||
status_frame.rowconfigure(0, weight=1)
|
||||
|
||||
# Log area
|
||||
self.log_text = scrolledtext.ScrolledText(status_frame, height=15, width=60)
|
||||
self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Button frame
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.grid(row=5, column=0, columnspan=2, pady=20)
|
||||
|
||||
# Backup buttons
|
||||
backup_frame = ttk.LabelFrame(button_frame, text="Backup Operations", padding="10")
|
||||
backup_frame.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.sync_backup_btn = ttk.Button(backup_frame, text="Smart Sync Backup",
|
||||
command=self.smart_sync_backup, style="Accent.TButton")
|
||||
self.sync_backup_btn.pack(side=tk.TOP, pady=2)
|
||||
|
||||
self.backup_btn = ttk.Button(backup_frame, text="Full Clone Backup",
|
||||
command=self.start_backup)
|
||||
self.backup_btn.pack(side=tk.TOP, pady=2)
|
||||
|
||||
self.reboot_backup_btn = ttk.Button(backup_frame, text="Reboot & Full Clone",
|
||||
command=self.reboot_and_backup)
|
||||
self.reboot_backup_btn.pack(side=tk.TOP, pady=2)
|
||||
|
||||
# Restore buttons
|
||||
restore_frame = ttk.LabelFrame(button_frame, text="Restore Operations", padding="10")
|
||||
restore_frame.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.restore_btn = ttk.Button(restore_frame, text="Restore from External",
|
||||
command=self.start_restore)
|
||||
self.restore_btn.pack(side=tk.TOP, pady=2)
|
||||
|
||||
self.reboot_restore_btn = ttk.Button(restore_frame, text="Reboot & Restore",
|
||||
command=self.reboot_and_restore)
|
||||
self.reboot_restore_btn.pack(side=tk.TOP, pady=2)
|
||||
|
||||
# Control buttons
|
||||
control_frame = ttk.Frame(button_frame)
|
||||
control_frame.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.stop_btn = ttk.Button(control_frame, text="Stop", command=self.stop_operation, state="disabled")
|
||||
self.stop_btn.pack(side=tk.TOP, pady=2)
|
||||
|
||||
self.swap_btn = ttk.Button(control_frame, text="Swap Source↔Target", command=self.swap_drives)
|
||||
self.swap_btn.pack(side=tk.TOP, pady=2)
|
||||
|
||||
self.analyze_btn = ttk.Button(control_frame, text="Analyze Changes", command=self.analyze_changes)
|
||||
self.analyze_btn.pack(side=tk.TOP, pady=2)
|
||||
|
||||
# Progress bar
|
||||
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
|
||||
self.progress.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
|
||||
|
||||
# Store combo references for updating
|
||||
self.source_combo = source_combo
|
||||
self.target_combo = target_combo
|
||||
|
||||
# Add initial log message
|
||||
self.log("System Backup Manager initialized")
|
||||
self.log("Select source and target drives, then click 'Start Backup' or 'Reboot & Backup'")
|
||||
|
||||
def log(self, message):
|
||||
"""Add message to log with timestamp"""
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
|
||||
self.log_text.see(tk.END)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def get_root_drive(self):
|
||||
"""Get the drive containing the root filesystem"""
|
||||
try:
|
||||
# Find the device containing the root filesystem
|
||||
result = subprocess.run(['df', '/'], capture_output=True, text=True)
|
||||
lines = result.stdout.strip().split('\n')
|
||||
if len(lines) > 1:
|
||||
device = lines[1].split()[0]
|
||||
# Remove partition number to get base device
|
||||
import re
|
||||
base_device = re.sub(r'[0-9]+$', '', device)
|
||||
# Handle nvme drives (e.g., /dev/nvme0n1p1 -> /dev/nvme0n1)
|
||||
base_device = re.sub(r'p[0-9]+$', '', base_device)
|
||||
return base_device
|
||||
except Exception as e:
|
||||
self.log(f"Error detecting root drive: {e}")
|
||||
return None
|
||||
|
||||
def detect_drives(self):
|
||||
"""Detect available drives"""
|
||||
try:
|
||||
self.log("Detecting available drives...")
|
||||
|
||||
# First, detect the root filesystem drive
|
||||
root_drive = self.get_root_drive()
|
||||
if root_drive:
|
||||
self.log(f"Detected root filesystem on: {root_drive}")
|
||||
|
||||
# Get block devices with more information
|
||||
result = subprocess.run(['lsblk', '-d', '-n', '-o', 'NAME,SIZE,TYPE,TRAN,HOTPLUG'],
|
||||
capture_output=True, text=True)
|
||||
|
||||
internal_drives = []
|
||||
external_drives = []
|
||||
all_drives = []
|
||||
root_drive_info = None
|
||||
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line and 'disk' in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 3:
|
||||
name = f"/dev/{parts[0]}"
|
||||
size = parts[1]
|
||||
transport = parts[3] if len(parts) > 3 else ""
|
||||
hotplug = parts[4] if len(parts) > 4 else "0"
|
||||
|
||||
drive_info = f"{name} ({size})"
|
||||
all_drives.append(drive_info)
|
||||
|
||||
# Check if this is the root drive and mark it
|
||||
if root_drive and name == root_drive:
|
||||
drive_info = f"{name} ({size}) [SYSTEM]"
|
||||
root_drive_info = drive_info
|
||||
self.log(f"Root drive found: {drive_info}")
|
||||
|
||||
# Classify drives
|
||||
if transport in ['usb', 'uas'] or hotplug == "1":
|
||||
external_drives.append(drive_info)
|
||||
self.log(f"External drive detected: {drive_info}")
|
||||
else:
|
||||
internal_drives.append(drive_info)
|
||||
self.log(f"Internal drive detected: {drive_info}")
|
||||
|
||||
# Auto-select root drive as source if found, otherwise first internal
|
||||
if root_drive_info:
|
||||
self.source_drive.set(root_drive_info)
|
||||
self.log(f"Auto-selected root drive as source: {root_drive_info}")
|
||||
elif internal_drives:
|
||||
self.source_drive.set(internal_drives[0])
|
||||
self.log(f"Auto-selected internal drive as source: {internal_drives[0]}")
|
||||
|
||||
# Update combo boxes - put internal drives first for source
|
||||
self.source_combo['values'] = internal_drives + external_drives
|
||||
self.target_combo['values'] = external_drives + internal_drives # Prefer external for target
|
||||
|
||||
# If there's an external drive, auto-select it as target
|
||||
if external_drives:
|
||||
self.target_drive.set(external_drives[0])
|
||||
self.log(f"Auto-selected external drive as target: {external_drives[0]}")
|
||||
|
||||
self.log(f"Found {len(internal_drives)} internal and {len(external_drives)} external drives")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error detecting drives: {e}")
|
||||
|
||||
def validate_selection(self):
|
||||
"""Validate drive selection"""
|
||||
source = self.source_drive.get().split()[0] if self.source_drive.get() else ""
|
||||
target = self.target_drive.get().split()[0] if self.target_drive.get() else ""
|
||||
|
||||
if not source:
|
||||
messagebox.showerror("Error", "Please select a source drive")
|
||||
return False
|
||||
|
||||
if not target:
|
||||
messagebox.showerror("Error", "Please select a target drive")
|
||||
return False
|
||||
|
||||
if source == target:
|
||||
messagebox.showerror("Error", "Source and target drives cannot be the same")
|
||||
return False
|
||||
|
||||
# Check if drives exist
|
||||
if not os.path.exists(source):
|
||||
messagebox.showerror("Error", f"Source drive {source} does not exist")
|
||||
return False
|
||||
|
||||
if not os.path.exists(target):
|
||||
messagebox.showerror("Error", f"Target drive {target} does not exist")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def analyze_changes(self):
|
||||
"""Analyze changes between source and target drives"""
|
||||
if not self.validate_selection():
|
||||
return
|
||||
|
||||
# Get drive paths
|
||||
source = self.source_var.get().split()[0]
|
||||
target = self.target_var.get().split()[0]
|
||||
|
||||
self.run_backup_script("analyze", source, target)
|
||||
|
||||
def run_change_analysis(self, source, target):
|
||||
"""Run change analysis in background"""
|
||||
try:
|
||||
# Check if target has existing backup
|
||||
backup_info = self.check_existing_backup(target)
|
||||
|
||||
if not backup_info['has_backup']:
|
||||
self.log("No existing backup found. Full clone required.")
|
||||
return
|
||||
|
||||
self.log(f"Found existing backup from: {backup_info['backup_date']}")
|
||||
|
||||
# Mount both filesystems to compare
|
||||
changes = self.compare_filesystems(source, target)
|
||||
|
||||
self.log(f"Analysis complete:")
|
||||
self.log(f" Files changed: {changes['files_changed']}")
|
||||
self.log(f" Files added: {changes['files_added']}")
|
||||
self.log(f" Files deleted: {changes['files_deleted']}")
|
||||
self.log(f" Total size changed: {changes['size_changed_mb']:.1f} MB")
|
||||
self.log(f" Recommended action: {changes['recommendation']}")
|
||||
|
||||
# Show recommendation
|
||||
if changes['recommendation'] == 'sync':
|
||||
messagebox.showinfo("Analysis Complete",
|
||||
f"Smart Sync Recommended\n\n"
|
||||
f"Changes detected: {changes['files_changed']} files\n"
|
||||
f"Size to sync: {changes['size_changed_mb']:.1f} MB\n"
|
||||
f"Estimated time: {changes['estimated_time_min']:.1f} minutes\n\n"
|
||||
f"This is much faster than full clone!")
|
||||
else:
|
||||
messagebox.showinfo("Analysis Complete",
|
||||
f"Full Clone Recommended\n\n"
|
||||
f"Reason: {changes['reason']}\n"
|
||||
f"Use 'Full Clone Backup' for best results.")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error during analysis: {e}")
|
||||
messagebox.showerror("Analysis Error", f"Could not analyze changes: {e}")
|
||||
|
||||
def smart_sync_backup(self):
|
||||
"""Start smart sync backup operation"""
|
||||
if not self.validate_selection():
|
||||
return
|
||||
|
||||
# Get drive paths
|
||||
source = self.source_var.get().split()[0]
|
||||
target = self.target_var.get().split()[0]
|
||||
|
||||
# Confirm operation
|
||||
result = messagebox.askyesno(
|
||||
"Confirm Smart Sync Backup",
|
||||
f"Perform smart sync backup?\n\n"
|
||||
f"Source: {source}\n"
|
||||
f"Target: {target}\n\n"
|
||||
f"This will quickly update the target drive with changes from the source.\n"
|
||||
f"The operation is much faster than a full backup but requires an existing backup on the target."
|
||||
)
|
||||
|
||||
if result:
|
||||
self.run_backup_script("sync", source, target)
|
||||
|
||||
def run_backup_script(self, mode, source, target):
|
||||
"""Run the backup script with specified mode"""
|
||||
try:
|
||||
# Clear previous output
|
||||
self.output_text.delete(1.0, tk.END)
|
||||
|
||||
# Determine command arguments
|
||||
if mode == "analyze":
|
||||
cmd = ['sudo', './backup_script.sh', '--analyze', '--source', source, '--target', target]
|
||||
self.log_message("🔍 Analyzing changes between drives...")
|
||||
elif mode == "sync":
|
||||
cmd = ['sudo', './backup_script.sh', '--sync', '--source', source, '--target', target]
|
||||
self.log_message("⚡ Starting smart sync backup...")
|
||||
elif mode == "backup":
|
||||
cmd = ['sudo', './backup_script.sh', '--source', source, '--target', target]
|
||||
self.log_message("🔄 Starting full backup...")
|
||||
elif mode == "restore":
|
||||
cmd = ['sudo', './backup_script.sh', '--restore', '--source', source, '--target', target]
|
||||
self.log_message("🔧 Starting restore operation...")
|
||||
else:
|
||||
raise ValueError(f"Unknown mode: {mode}")
|
||||
|
||||
# Change to script directory
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Run the command
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=script_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor progress in real-time
|
||||
while True:
|
||||
output = process.stdout.readline()
|
||||
if output == '' and process.poll() is not None:
|
||||
break
|
||||
if output:
|
||||
# Update GUI in real-time
|
||||
self.output_text.insert(tk.END, output)
|
||||
self.output_text.see(tk.END)
|
||||
self.root.update()
|
||||
|
||||
# Get final result
|
||||
return_code = process.poll()
|
||||
|
||||
if return_code == 0:
|
||||
if mode == "analyze":
|
||||
self.log_message("✅ Analysis completed successfully!")
|
||||
messagebox.showinfo("Analysis Complete", "Drive analysis completed. Check the output for recommendations.")
|
||||
elif mode == "sync":
|
||||
self.log_message("✅ Smart sync completed successfully!")
|
||||
messagebox.showinfo("Success", "Smart sync backup completed successfully!")
|
||||
elif mode == "backup":
|
||||
self.log_message("✅ Backup completed successfully!")
|
||||
messagebox.showinfo("Success", "Full backup completed successfully!")
|
||||
elif mode == "restore":
|
||||
self.log_message("✅ Restore completed successfully!")
|
||||
messagebox.showinfo("Success", "System restore completed successfully!")
|
||||
else:
|
||||
self.log_message(f"❌ {mode.title()} operation failed!")
|
||||
messagebox.showerror("Error", f"{mode.title()} operation failed. Check the output for details.")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error running {mode} operation: {str(e)}"
|
||||
self.log_message(f"❌ {error_msg}")
|
||||
messagebox.showerror("Error", error_msg)
|
||||
|
||||
def check_existing_backup(self, target_drive):
|
||||
"""Check if target drive has existing backup and get info"""
|
||||
try:
|
||||
# Try to mount the target drive temporarily
|
||||
temp_mount = f"/tmp/backup_check_{os.getpid()}"
|
||||
os.makedirs(temp_mount, exist_ok=True)
|
||||
|
||||
# Find the main partition (usually partition 1)
|
||||
partitions = subprocess.run(['lsblk', '-n', '-o', 'NAME', target_drive],
|
||||
capture_output=True, text=True).stdout.strip().split('\n')
|
||||
|
||||
main_partition = None
|
||||
for partition in partitions:
|
||||
if partition.strip() and partition.strip() != os.path.basename(target_drive):
|
||||
main_partition = f"/dev/{partition.strip()}"
|
||||
break
|
||||
|
||||
if not main_partition:
|
||||
return {'has_backup': False, 'reason': 'No partitions found'}
|
||||
|
||||
# Try to mount and check
|
||||
try:
|
||||
subprocess.run(['sudo', 'mount', '-o', 'ro', main_partition, temp_mount],
|
||||
check=True, capture_output=True)
|
||||
|
||||
# Check if it looks like a Linux system
|
||||
has_backup = (os.path.exists(os.path.join(temp_mount, 'etc')) and
|
||||
os.path.exists(os.path.join(temp_mount, 'home')) and
|
||||
os.path.exists(os.path.join(temp_mount, 'usr')))
|
||||
|
||||
backup_date = "Unknown"
|
||||
if has_backup:
|
||||
# Try to get last modification time of /etc
|
||||
try:
|
||||
etc_stat = os.stat(os.path.join(temp_mount, 'etc'))
|
||||
backup_date = time.strftime('%Y-%m-%d %H:%M', time.localtime(etc_stat.st_mtime))
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'has_backup': has_backup,
|
||||
'backup_date': backup_date,
|
||||
'main_partition': main_partition
|
||||
}
|
||||
|
||||
finally:
|
||||
subprocess.run(['sudo', 'umount', temp_mount], capture_output=True)
|
||||
os.rmdir(temp_mount)
|
||||
|
||||
except Exception as e:
|
||||
return {'has_backup': False, 'reason': f'Mount error: {e}'}
|
||||
|
||||
def compare_filesystems(self, source_drive, target_drive):
|
||||
"""Compare filesystems to determine sync requirements"""
|
||||
try:
|
||||
# Get basic change information using filesystem comparison
|
||||
# This is a simplified analysis - in practice you'd want more sophisticated comparison
|
||||
|
||||
# Check filesystem sizes
|
||||
source_size = self.get_filesystem_usage(source_drive)
|
||||
target_info = self.check_existing_backup(target_drive)
|
||||
|
||||
if not target_info['has_backup']:
|
||||
return {
|
||||
'recommendation': 'full',
|
||||
'reason': 'No existing backup',
|
||||
'files_changed': 0,
|
||||
'files_added': 0,
|
||||
'files_deleted': 0,
|
||||
'size_changed_mb': 0,
|
||||
'estimated_time_min': 0,
|
||||
'full_clone_time_min': source_size['total_gb'] * 2 # Rough estimate
|
||||
}
|
||||
|
||||
target_size = self.get_filesystem_usage(target_drive)
|
||||
|
||||
# Simple heuristic based on size difference
|
||||
size_diff_gb = abs(source_size['used_gb'] - target_size['used_gb'])
|
||||
size_change_percent = (size_diff_gb / max(source_size['used_gb'], 0.1)) * 100
|
||||
|
||||
# Estimate file changes (rough approximation)
|
||||
estimated_files_changed = int(size_diff_gb * 1000) # Assume 1MB per file average
|
||||
estimated_sync_time = size_diff_gb * 1.5 # 1.5 minutes per GB for sync
|
||||
estimated_full_time = source_size['total_gb'] * 2 # 2 minutes per GB for full clone
|
||||
|
||||
# Decision logic
|
||||
if size_change_percent < 5 and size_diff_gb < 2:
|
||||
recommendation = 'sync'
|
||||
reason = 'Minor changes detected'
|
||||
elif size_change_percent < 15 and size_diff_gb < 10:
|
||||
recommendation = 'sync'
|
||||
reason = 'Moderate changes, sync beneficial'
|
||||
else:
|
||||
recommendation = 'full'
|
||||
reason = 'Major changes detected, full clone safer'
|
||||
|
||||
return {
|
||||
'recommendation': recommendation,
|
||||
'reason': reason,
|
||||
'files_changed': estimated_files_changed,
|
||||
'files_added': max(0, estimated_files_changed // 2),
|
||||
'files_deleted': max(0, estimated_files_changed // 4),
|
||||
'size_changed_mb': size_diff_gb * 1024,
|
||||
'estimated_time_min': estimated_sync_time,
|
||||
'full_clone_time_min': estimated_full_time
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'recommendation': 'full',
|
||||
'reason': f'Analysis failed: {e}',
|
||||
'files_changed': 0,
|
||||
'files_added': 0,
|
||||
'files_deleted': 0,
|
||||
'size_changed_mb': 0,
|
||||
'estimated_time_min': 0,
|
||||
'full_clone_time_min': 60
|
||||
}
|
||||
|
||||
def get_filesystem_usage(self, drive):
|
||||
"""Get filesystem usage information"""
|
||||
try:
|
||||
# Mount temporarily and get usage
|
||||
temp_mount = f"/tmp/fs_check_{os.getpid()}"
|
||||
os.makedirs(temp_mount, exist_ok=True)
|
||||
|
||||
# Find main partition
|
||||
partitions = subprocess.run(['lsblk', '-n', '-o', 'NAME', drive],
|
||||
capture_output=True, text=True).stdout.strip().split('\n')
|
||||
|
||||
main_partition = None
|
||||
for partition in partitions:
|
||||
if partition.strip() and partition.strip() != os.path.basename(drive):
|
||||
main_partition = f"/dev/{partition.strip()}"
|
||||
break
|
||||
|
||||
if not main_partition:
|
||||
return {'total_gb': 0, 'used_gb': 0, 'free_gb': 0}
|
||||
|
||||
try:
|
||||
subprocess.run(['sudo', 'mount', '-o', 'ro', main_partition, temp_mount],
|
||||
check=True, capture_output=True)
|
||||
|
||||
# Get filesystem usage
|
||||
statvfs = os.statvfs(temp_mount)
|
||||
total_bytes = statvfs.f_frsize * statvfs.f_blocks
|
||||
free_bytes = statvfs.f_frsize * statvfs.f_available
|
||||
used_bytes = total_bytes - free_bytes
|
||||
|
||||
return {
|
||||
'total_gb': total_bytes / (1024**3),
|
||||
'used_gb': used_bytes / (1024**3),
|
||||
'free_gb': free_bytes / (1024**3)
|
||||
}
|
||||
|
||||
finally:
|
||||
subprocess.run(['sudo', 'umount', temp_mount], capture_output=True)
|
||||
os.rmdir(temp_mount)
|
||||
|
||||
except Exception:
|
||||
# Fallback to drive size
|
||||
try:
|
||||
size_bytes = int(subprocess.run(['blockdev', '--getsize64', drive],
|
||||
capture_output=True, text=True).stdout.strip())
|
||||
total_gb = size_bytes / (1024**3)
|
||||
return {'total_gb': total_gb, 'used_gb': total_gb * 0.7, 'free_gb': total_gb * 0.3}
|
||||
except:
|
||||
return {'total_gb': 500, 'used_gb': 350, 'free_gb': 150} # Default estimates
|
||||
|
||||
def start_sync_operation(self, source, target, changes):
|
||||
"""Start smart sync operation"""
|
||||
self.operation_running = True
|
||||
self.sync_backup_btn.config(state="disabled")
|
||||
self.backup_btn.config(state="disabled")
|
||||
self.reboot_backup_btn.config(state="disabled")
|
||||
self.restore_btn.config(state="disabled")
|
||||
self.reboot_restore_btn.config(state="disabled")
|
||||
self.stop_btn.config(state="normal")
|
||||
self.progress.start()
|
||||
|
||||
sync_thread = threading.Thread(target=self.run_sync_operation, args=(source, target, changes))
|
||||
sync_thread.daemon = True
|
||||
sync_thread.start()
|
||||
|
||||
def run_sync_operation(self, source, target, changes):
|
||||
"""Swap source and target drives"""
|
||||
source = self.source_drive.get()
|
||||
target = self.target_drive.get()
|
||||
|
||||
self.source_drive.set(target)
|
||||
self.target_drive.set(source)
|
||||
|
||||
self.log("Swapped source and target drives")
|
||||
|
||||
def run_sync_operation(self, source, target, changes):
|
||||
"""Run smart filesystem sync operation"""
|
||||
try:
|
||||
self.log("Starting smart sync operation...")
|
||||
self.log(f"Syncing {changes['size_changed_mb']:.1f} MB of changes...")
|
||||
|
||||
# Mount both filesystems
|
||||
source_mount = f"/tmp/sync_source_{os.getpid()}"
|
||||
target_mount = f"/tmp/sync_target_{os.getpid()}"
|
||||
|
||||
os.makedirs(source_mount, exist_ok=True)
|
||||
os.makedirs(target_mount, exist_ok=True)
|
||||
|
||||
# Find main partitions
|
||||
source_partitions = subprocess.run(['lsblk', '-n', '-o', 'NAME', source],
|
||||
capture_output=True, text=True).stdout.strip().split('\n')
|
||||
target_partitions = subprocess.run(['lsblk', '-n', '-o', 'NAME', target],
|
||||
capture_output=True, text=True).stdout.strip().split('\n')
|
||||
|
||||
source_partition = f"/dev/{[p.strip() for p in source_partitions if p.strip() and p.strip() != os.path.basename(source)][0]}"
|
||||
target_partition = f"/dev/{[p.strip() for p in target_partitions if p.strip() and p.strip() != os.path.basename(target)][0]}"
|
||||
|
||||
try:
|
||||
# Mount filesystems
|
||||
subprocess.run(['sudo', 'mount', '-o', 'ro', source_partition, source_mount], check=True)
|
||||
subprocess.run(['sudo', 'mount', target_partition, target_mount], check=True)
|
||||
|
||||
self.log("Filesystems mounted, starting rsync...")
|
||||
|
||||
# Use rsync for efficient synchronization
|
||||
rsync_cmd = [
|
||||
'sudo', 'rsync', '-avHAXS',
|
||||
'--numeric-ids',
|
||||
'--delete',
|
||||
'--progress',
|
||||
'--exclude=/proc/*',
|
||||
'--exclude=/sys/*',
|
||||
'--exclude=/dev/*',
|
||||
'--exclude=/tmp/*',
|
||||
'--exclude=/run/*',
|
||||
'--exclude=/mnt/*',
|
||||
'--exclude=/media/*',
|
||||
'--exclude=/lost+found',
|
||||
f'{source_mount}/',
|
||||
f'{target_mount}/'
|
||||
]
|
||||
|
||||
self.log(f"Running rsync command")
|
||||
|
||||
process = subprocess.Popen(rsync_cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True, bufsize=1)
|
||||
|
||||
# Read output line by line
|
||||
for line in process.stdout:
|
||||
if self.operation_running:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('sent ') and not line.startswith('total size'):
|
||||
self.log(f"Sync: {line}")
|
||||
else:
|
||||
process.terminate()
|
||||
break
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0 and self.operation_running:
|
||||
self.log("Smart sync completed successfully!")
|
||||
|
||||
# Preserve backup tools
|
||||
try:
|
||||
self.log("Preserving backup tools on external drive...")
|
||||
restore_script = os.path.join(os.path.dirname(__file__), "restore_tools_after_backup.sh")
|
||||
if os.path.exists(restore_script):
|
||||
subprocess.run([restore_script, target], check=False, timeout=60)
|
||||
self.log("Backup tools preserved on external drive")
|
||||
except Exception as e:
|
||||
self.log(f"Warning: Could not preserve tools: {e}")
|
||||
|
||||
messagebox.showinfo("Success",
|
||||
f"Smart Sync completed successfully!\n\n"
|
||||
f"Synced: {changes['size_changed_mb']:.1f} MB\n"
|
||||
f"Much faster than full clone!")
|
||||
|
||||
elif not self.operation_running:
|
||||
self.log("Sync operation was cancelled")
|
||||
else:
|
||||
self.log(f"Sync failed with return code: {process.returncode}")
|
||||
messagebox.showerror("Error", "Smart sync failed! Consider using full clone backup.")
|
||||
|
||||
finally:
|
||||
# Unmount filesystems
|
||||
subprocess.run(['sudo', 'umount', source_mount], capture_output=True)
|
||||
subprocess.run(['sudo', 'umount', target_mount], capture_output=True)
|
||||
os.rmdir(source_mount)
|
||||
os.rmdir(target_mount)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error during smart sync: {e}")
|
||||
messagebox.showerror("Error", f"Smart sync failed: {e}")
|
||||
|
||||
finally:
|
||||
self.operation_running = False
|
||||
self.sync_backup_btn.config(state="normal")
|
||||
self.backup_btn.config(state="normal")
|
||||
self.reboot_backup_btn.config(state="normal")
|
||||
self.restore_btn.config(state="normal")
|
||||
self.reboot_restore_btn.config(state="normal")
|
||||
self.stop_btn.config(state="disabled")
|
||||
self.progress.stop()
|
||||
|
||||
def swap_drives(self):
|
||||
"""Swap source and target drives"""
|
||||
source = self.source_drive.get()
|
||||
target = self.target_drive.get()
|
||||
|
||||
self.source_drive.set(target)
|
||||
self.target_drive.set(source)
|
||||
|
||||
self.log("Swapped source and target drives")
|
||||
|
||||
def start_restore(self):
|
||||
"""Start the restore process"""
|
||||
if not self.validate_selection():
|
||||
return
|
||||
|
||||
if self.operation_running:
|
||||
self.log("Operation already running!")
|
||||
return
|
||||
|
||||
# Confirm restore
|
||||
source = self.source_drive.get().split()[0]
|
||||
target = self.target_drive.get().split()[0]
|
||||
|
||||
result = messagebox.askyesno("⚠️ CONFIRM RESTORE ⚠️",
|
||||
f"This will RESTORE from {source} to {target}.\n\n"
|
||||
f"🚨 CRITICAL WARNING 🚨\n"
|
||||
f"This will COMPLETELY OVERWRITE {target}!\n"
|
||||
f"ALL DATA on {target} will be DESTROYED!\n\n"
|
||||
f"This should typically restore FROM external TO internal.\n"
|
||||
f"Make sure you have the drives selected correctly!\n\n"
|
||||
f"Are you absolutely sure you want to continue?")
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
# Second confirmation for restore
|
||||
result2 = messagebox.askyesno("FINAL CONFIRMATION",
|
||||
f"LAST CHANCE TO CANCEL!\n\n"
|
||||
f"Restoring from: {source}\n"
|
||||
f"Overwriting: {target}\n\n"
|
||||
f"Type YES to continue or NO to cancel.")
|
||||
|
||||
if not result2:
|
||||
return
|
||||
|
||||
source = self.source_var.get().split()[0]
|
||||
target = self.target_var.get().split()[0]
|
||||
self.run_backup_script("restore", source, target)
|
||||
|
||||
def reboot_and_restore(self):
|
||||
"""Schedule reboot and restore"""
|
||||
if not self.validate_selection():
|
||||
return
|
||||
|
||||
result = messagebox.askyesno("⚠️ CONFIRM REBOOT & RESTORE ⚠️",
|
||||
"This will:\n"
|
||||
"1. Save current session\n"
|
||||
"2. Reboot the system\n"
|
||||
"3. Start RESTORE after reboot\n\n"
|
||||
"🚨 WARNING: This will OVERWRITE your internal drive! 🚨\n\n"
|
||||
"Continue?")
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
try:
|
||||
# Create restore script for after reboot
|
||||
script_content = self.create_reboot_operation_script("restore")
|
||||
|
||||
# Save script
|
||||
script_path = "/tmp/restore_after_reboot.sh"
|
||||
with open(script_path, 'w') as f:
|
||||
f.write(script_content)
|
||||
|
||||
os.chmod(script_path, 0o755)
|
||||
|
||||
self.log("Reboot restore script created")
|
||||
self.log("System will reboot in 5 seconds...")
|
||||
|
||||
# Schedule reboot
|
||||
subprocess.run(['sudo', 'shutdown', '-r', '+1'], check=True)
|
||||
|
||||
self.log("Reboot scheduled. Restore will start automatically after reboot.")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error scheduling reboot: {e}")
|
||||
messagebox.showerror("Error", f"Failed to schedule reboot: {e}")
|
||||
|
||||
def start_backup(self):
|
||||
"""Start the backup process"""
|
||||
if not self.validate_selection():
|
||||
return
|
||||
|
||||
# Confirm backup
|
||||
source = self.source_var.get().split()[0]
|
||||
target = self.target_var.get().split()[0]
|
||||
|
||||
result = messagebox.askyesno("Confirm Backup",
|
||||
f"This will clone {source} to {target}.\n\n"
|
||||
f"WARNING: All data on {target} will be destroyed!\n\n"
|
||||
f"Are you sure you want to continue?")
|
||||
|
||||
if result:
|
||||
self.run_backup_script("backup", source, target)
|
||||
|
||||
def start_operation(self, source, target):
|
||||
"""Start backup or restore operation"""
|
||||
# Start operation in thread
|
||||
self.operation_running = True
|
||||
self.sync_backup_btn.config(state="disabled")
|
||||
self.backup_btn.config(state="disabled")
|
||||
self.reboot_backup_btn.config(state="disabled")
|
||||
self.restore_btn.config(state="disabled")
|
||||
self.reboot_restore_btn.config(state="disabled")
|
||||
self.stop_btn.config(state="normal")
|
||||
self.progress.start()
|
||||
|
||||
operation_thread = threading.Thread(target=self.run_operation, args=(source, target, self.operation_type))
|
||||
operation_thread.daemon = True
|
||||
operation_thread.start()
|
||||
|
||||
def run_operation(self, source, target, operation_type):
|
||||
"""Run the actual backup or restore process"""
|
||||
try:
|
||||
if operation_type == "backup":
|
||||
self.log(f"Starting backup from {source} to {target}")
|
||||
else:
|
||||
self.log(f"Starting restore from {source} to {target}")
|
||||
|
||||
self.log("This may take a while depending on drive size...")
|
||||
|
||||
# Use dd for cloning
|
||||
cmd = [
|
||||
'sudo', 'dd',
|
||||
f'if={source}',
|
||||
f'of={target}',
|
||||
'bs=4M',
|
||||
'status=progress',
|
||||
'conv=fdatasync'
|
||||
]
|
||||
|
||||
self.log(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True)
|
||||
|
||||
# Read output
|
||||
for line in process.stdout:
|
||||
if self.operation_running: # Check if we should continue
|
||||
self.log(line.strip())
|
||||
else:
|
||||
process.terminate()
|
||||
break
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0 and self.operation_running:
|
||||
if operation_type == "backup":
|
||||
self.log("Backup completed successfully!")
|
||||
|
||||
# Restore backup tools to external drive
|
||||
try:
|
||||
self.log("Preserving backup tools on external drive...")
|
||||
restore_script = os.path.join(os.path.dirname(__file__), "restore_tools_after_backup.sh")
|
||||
if os.path.exists(restore_script):
|
||||
subprocess.run([restore_script, target], check=False, timeout=60)
|
||||
self.log("Backup tools preserved on external drive")
|
||||
else:
|
||||
self.log("Warning: Tool restoration script not found")
|
||||
except Exception as e:
|
||||
self.log(f"Warning: Could not preserve tools on external drive: {e}")
|
||||
|
||||
messagebox.showinfo("Success", "Backup completed successfully!\n\nBackup tools have been preserved on the external drive.")
|
||||
else:
|
||||
self.log("Restore completed successfully!")
|
||||
messagebox.showinfo("Success", "Restore completed successfully!")
|
||||
elif not self.operation_running:
|
||||
self.log(f"{operation_type.capitalize()} was cancelled by user")
|
||||
else:
|
||||
self.log(f"{operation_type.capitalize()} failed with return code: {process.returncode}")
|
||||
messagebox.showerror("Error", f"{operation_type.capitalize()} failed! Check the log for details.")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error during {operation_type}: {e}")
|
||||
messagebox.showerror("Error", f"{operation_type.capitalize()} failed: {e}")
|
||||
|
||||
finally:
|
||||
self.operation_running = False
|
||||
self.sync_backup_btn.config(state="normal")
|
||||
self.backup_btn.config(state="normal")
|
||||
self.reboot_backup_btn.config(state="normal")
|
||||
self.restore_btn.config(state="normal")
|
||||
self.reboot_restore_btn.config(state="normal")
|
||||
self.stop_btn.config(state="disabled")
|
||||
self.progress.stop()
|
||||
|
||||
def stop_operation(self):
|
||||
"""Stop the current operation"""
|
||||
self.operation_running = False
|
||||
self.log("Stopping operation...")
|
||||
|
||||
def reboot_and_backup(self):
|
||||
"""Schedule reboot and backup"""
|
||||
if not self.validate_selection():
|
||||
return
|
||||
|
||||
result = messagebox.askyesno("Confirm Reboot & Backup",
|
||||
"This will:\n"
|
||||
"1. Save current session\n"
|
||||
"2. Reboot the system\n"
|
||||
"3. Start backup after reboot\n\n"
|
||||
"Continue?")
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
try:
|
||||
# Create backup script for after reboot
|
||||
script_content = self.create_reboot_operation_script("backup")
|
||||
|
||||
# Save script
|
||||
script_path = "/tmp/backup_after_reboot.sh"
|
||||
with open(script_path, 'w') as f:
|
||||
f.write(script_content)
|
||||
|
||||
os.chmod(script_path, 0o755)
|
||||
|
||||
self.log("Reboot backup script created")
|
||||
self.log("System will reboot in 5 seconds...")
|
||||
|
||||
# Schedule reboot
|
||||
subprocess.run(['sudo', 'shutdown', '-r', '+1'], check=True)
|
||||
|
||||
self.log("Reboot scheduled. Backup will start automatically after reboot.")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error scheduling reboot: {e}")
|
||||
messagebox.showerror("Error", f"Failed to schedule reboot: {e}")
|
||||
|
||||
def create_reboot_operation_script(self, operation_type):
|
||||
"""Create script to run operation after reboot"""
|
||||
source = self.source_drive.get().split()[0]
|
||||
target = self.target_drive.get().split()[0]
|
||||
|
||||
if operation_type == "backup":
|
||||
action_desc = "backup"
|
||||
success_msg = "Backup Complete"
|
||||
fail_msg = "Backup Failed"
|
||||
else:
|
||||
action_desc = "restore"
|
||||
success_msg = "Restore Complete"
|
||||
fail_msg = "Restore Failed"
|
||||
|
||||
script = f"""#!/bin/bash
|
||||
# Auto-generated {action_desc} script
|
||||
|
||||
echo "Starting {action_desc} after reboot..."
|
||||
echo "Source: {source}"
|
||||
echo "Target: {target}"
|
||||
|
||||
# Wait for system to fully boot
|
||||
sleep 30
|
||||
|
||||
# Run {action_desc}
|
||||
sudo dd if={source} of={target} bs=4M status=progress conv=fdatasync
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "{action_desc.capitalize()} completed successfully!"
|
||||
notify-send "{success_msg}" "System {action_desc} finished successfully"
|
||||
else
|
||||
echo "{action_desc.capitalize()} failed!"
|
||||
notify-send "{fail_msg}" "System {action_desc} encountered an error"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f /tmp/{action_desc}_after_reboot.sh
|
||||
"""
|
||||
return script
|
||||
|
||||
def run(self):
|
||||
"""Start the GUI application"""
|
||||
self.root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if running as root for certain operations
|
||||
if os.geteuid() != 0:
|
||||
print("Note: Some operations may require sudo privileges")
|
||||
|
||||
app = BackupManager()
|
||||
app.run()
|
||||
576
backup_script.sh
Executable file
576
backup_script.sh
Executable file
@@ -0,0 +1,576 @@
|
||||
#!/bin/bash
|
||||
# System Backup Script - Command Line Version
|
||||
# For use with cron jobs or manual execution
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
SOURCE_DRIVE="" # Will be auto-detected
|
||||
TARGET_DRIVE="" # Will be detected or specified
|
||||
RESTORE_MODE=false # Restore mode flag
|
||||
SYNC_MODE=false # Smart sync mode flag
|
||||
ANALYZE_ONLY=false # Analysis only mode
|
||||
LOG_FILE="/var/log/system_backup.log"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local message="$1"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Log to console
|
||||
echo "${timestamp} - ${message}"
|
||||
|
||||
# Log to file if writable
|
||||
if [[ -w "$LOG_FILE" ]] || [[ -w "$(dirname "$LOG_FILE")" ]]; then
|
||||
echo "${timestamp} - ${message}" >> "$LOG_FILE" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Error handling
|
||||
error_exit() {
|
||||
log "${RED}ERROR: $1${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Success message
|
||||
success() {
|
||||
log "${GREEN}SUCCESS: $1${NC}"
|
||||
}
|
||||
|
||||
# Warning message
|
||||
warning() {
|
||||
log "${YELLOW}WARNING: $1${NC}"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
error_exit "This script must be run as root (use sudo)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect root filesystem drive
|
||||
detect_root_drive() {
|
||||
log "Detecting root filesystem drive..."
|
||||
|
||||
# Find the device containing the root filesystem
|
||||
local root_device=$(df / | tail -1 | awk '{print $1}')
|
||||
|
||||
# Remove partition number to get base device
|
||||
local base_device=$(echo "$root_device" | sed 's/[0-9]*$//')
|
||||
|
||||
# Handle nvme drives (e.g., /dev/nvme0n1p1 -> /dev/nvme0n1)
|
||||
base_device=$(echo "$base_device" | sed 's/p$//')
|
||||
|
||||
echo "$base_device"
|
||||
}
|
||||
|
||||
# Check if target has existing backup
|
||||
check_existing_backup() {
|
||||
local target_drive=$1
|
||||
local temp_mount="/tmp/backup_check_$$"
|
||||
|
||||
log "Checking for existing backup on $target_drive..."
|
||||
|
||||
# Get main partition
|
||||
local main_partition=$(lsblk -n -o NAME "$target_drive" | grep -v "^$(basename "$target_drive")$" | head -1)
|
||||
if [[ -z "$main_partition" ]]; then
|
||||
echo "false"
|
||||
return
|
||||
fi
|
||||
|
||||
main_partition="/dev/$main_partition"
|
||||
|
||||
# Try to mount and check
|
||||
mkdir -p "$temp_mount"
|
||||
if mount -o ro "$main_partition" "$temp_mount" 2>/dev/null; then
|
||||
if [[ -d "$temp_mount/etc" && -d "$temp_mount/home" && -d "$temp_mount/usr" ]]; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
umount "$temp_mount" 2>/dev/null
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
rmdir "$temp_mount" 2>/dev/null
|
||||
}
|
||||
|
||||
# Analyze changes between source and target
|
||||
analyze_changes() {
|
||||
local source_drive=$1
|
||||
local target_drive=$2
|
||||
|
||||
log "Analyzing changes between $source_drive and $target_drive..."
|
||||
|
||||
# Check if target has existing backup
|
||||
local has_backup=$(check_existing_backup "$target_drive")
|
||||
|
||||
if [[ "$has_backup" != "true" ]]; then
|
||||
log "No existing backup found. Full clone required."
|
||||
echo "FULL_CLONE_REQUIRED"
|
||||
return
|
||||
fi
|
||||
|
||||
# Get filesystem usage for both drives
|
||||
local source_size=$(get_filesystem_size "$source_drive")
|
||||
local target_size=$(get_filesystem_size "$target_drive")
|
||||
|
||||
# Calculate difference in GB
|
||||
local size_diff=$((${source_size} - ${target_size}))
|
||||
local size_diff_abs=${size_diff#-} # Absolute value
|
||||
|
||||
# Convert to GB (sizes are in KB)
|
||||
local size_diff_gb=$((size_diff_abs / 1024 / 1024))
|
||||
|
||||
log "Source filesystem size: $((source_size / 1024 / 1024)) GB"
|
||||
log "Target filesystem size: $((target_size / 1024 / 1024)) GB"
|
||||
log "Size difference: ${size_diff_gb} GB"
|
||||
|
||||
# Decision logic
|
||||
if [[ $size_diff_gb -lt 2 ]]; then
|
||||
log "Recommendation: Smart sync (minimal changes)"
|
||||
echo "SYNC_RECOMMENDED"
|
||||
elif [[ $size_diff_gb -lt 10 ]]; then
|
||||
log "Recommendation: Smart sync (moderate changes)"
|
||||
echo "SYNC_BENEFICIAL"
|
||||
else
|
||||
log "Recommendation: Full clone (major changes)"
|
||||
echo "FULL_CLONE_RECOMMENDED"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get filesystem size in KB
|
||||
get_filesystem_size() {
|
||||
local drive=$1
|
||||
local temp_mount="/tmp/size_check_$$"
|
||||
|
||||
# Get main partition
|
||||
local main_partition=$(lsblk -n -o NAME "$drive" | grep -v "^$(basename "$drive")$" | head -1)
|
||||
if [[ -z "$main_partition" ]]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
|
||||
main_partition="/dev/$main_partition"
|
||||
|
||||
# Mount and get usage
|
||||
mkdir -p "$temp_mount"
|
||||
if mount -o ro "$main_partition" "$temp_mount" 2>/dev/null; then
|
||||
local used_kb=$(df "$temp_mount" | tail -1 | awk '{print $3}')
|
||||
umount "$temp_mount" 2>/dev/null
|
||||
echo "$used_kb"
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
rmdir "$temp_mount" 2>/dev/null
|
||||
}
|
||||
|
||||
# Perform smart sync backup
|
||||
smart_sync_backup() {
|
||||
local source=$1
|
||||
local target=$2
|
||||
|
||||
log "Starting smart sync backup..."
|
||||
|
||||
# Mount both filesystems
|
||||
local source_mount="/tmp/sync_source_$$"
|
||||
local target_mount="/tmp/sync_target_$$"
|
||||
|
||||
mkdir -p "$source_mount" "$target_mount"
|
||||
|
||||
# Get main partitions
|
||||
local source_partition=$(lsblk -n -o NAME "$source" | grep -v "^$(basename "$source")$" | head -1)
|
||||
local target_partition=$(lsblk -n -o NAME "$target" | grep -v "^$(basename "$target")$" | head -1)
|
||||
|
||||
source_partition="/dev/$source_partition"
|
||||
target_partition="/dev/$target_partition"
|
||||
|
||||
log "Mounting filesystems for sync..."
|
||||
mount -o ro "$source_partition" "$source_mount" || error_exit "Failed to mount source"
|
||||
mount "$target_partition" "$target_mount" || error_exit "Failed to mount target"
|
||||
|
||||
# Perform rsync
|
||||
log "Starting rsync synchronization..."
|
||||
rsync -avHAXS \
|
||||
--numeric-ids \
|
||||
--delete \
|
||||
--progress \
|
||||
--exclude=/proc/* \
|
||||
--exclude=/sys/* \
|
||||
--exclude=/dev/* \
|
||||
--exclude=/tmp/* \
|
||||
--exclude=/run/* \
|
||||
--exclude=/mnt/* \
|
||||
--exclude=/media/* \
|
||||
--exclude=/lost+found \
|
||||
"$source_mount/" "$target_mount/" || {
|
||||
|
||||
# Cleanup on failure
|
||||
umount "$source_mount" 2>/dev/null
|
||||
umount "$target_mount" 2>/dev/null
|
||||
rmdir "$source_mount" "$target_mount" 2>/dev/null
|
||||
error_exit "Smart sync failed"
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
umount "$source_mount" 2>/dev/null
|
||||
umount "$target_mount" 2>/dev/null
|
||||
rmdir "$source_mount" "$target_mount" 2>/dev/null
|
||||
|
||||
success "Smart sync completed successfully!"
|
||||
}
|
||||
|
||||
# Detect external drives
|
||||
detect_external_drives() {
|
||||
log "Detecting external drives..."
|
||||
|
||||
# Get all block devices
|
||||
lsblk -d -n -o NAME,SIZE,TYPE,TRAN | while read -r line; do
|
||||
if [[ $line == *"disk"* ]] && [[ $line == *"usb"* ]]; then
|
||||
drive_name=$(echo "$line" | awk '{print $1}')
|
||||
drive_size=$(echo "$line" | awk '{print $2}')
|
||||
echo "/dev/$drive_name ($drive_size)"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Validate drives
|
||||
validate_drives() {
|
||||
if [[ ! -b "$SOURCE_DRIVE" ]]; then
|
||||
error_exit "Source drive $SOURCE_DRIVE does not exist or is not a block device"
|
||||
fi
|
||||
|
||||
if [[ ! -b "$TARGET_DRIVE" ]]; then
|
||||
error_exit "Target drive $TARGET_DRIVE does not exist or is not a block device"
|
||||
fi
|
||||
|
||||
if [[ "$SOURCE_DRIVE" == "$TARGET_DRIVE" ]]; then
|
||||
error_exit "Source and target drives cannot be the same"
|
||||
fi
|
||||
|
||||
# Check if target drive is mounted
|
||||
if mount | grep -q "$TARGET_DRIVE"; then
|
||||
warning "Target drive $TARGET_DRIVE is currently mounted. Unmounting..."
|
||||
umount "$TARGET_DRIVE"* 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Get drive size
|
||||
get_drive_size() {
|
||||
local drive=$1
|
||||
blockdev --getsize64 "$drive"
|
||||
}
|
||||
|
||||
# Clone drive
|
||||
clone_drive() {
|
||||
local source=$1
|
||||
local target=$2
|
||||
|
||||
log "Starting drive clone operation..."
|
||||
log "Source: $source"
|
||||
log "Target: $target"
|
||||
|
||||
# Get sizes
|
||||
source_size=$(get_drive_size "$source")
|
||||
target_size=$(get_drive_size "$target")
|
||||
|
||||
log "Source size: $(numfmt --to=iec-i --suffix=B $source_size)"
|
||||
log "Target size: $(numfmt --to=iec-i --suffix=B $target_size)"
|
||||
|
||||
if [[ $target_size -lt $source_size ]]; then
|
||||
error_exit "Target drive is smaller than source drive"
|
||||
fi
|
||||
|
||||
# Start cloning
|
||||
log "Starting clone operation with dd..."
|
||||
|
||||
if command -v pv >/dev/null 2>&1; then
|
||||
# Use pv for progress if available
|
||||
dd if="$source" bs=4M | pv -s "$source_size" | dd of="$target" bs=4M conv=fdatasync
|
||||
else
|
||||
# Use dd with status=progress
|
||||
dd if="$source" of="$target" bs=4M status=progress conv=fdatasync
|
||||
fi
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
success "Drive cloning completed successfully!"
|
||||
|
||||
# Restore backup tools to external drive if this was a backup operation
|
||||
if [[ "$RESTORE_MODE" != true ]]; then
|
||||
log "Preserving backup tools on external drive..."
|
||||
local restore_script="$(dirname "$0")/restore_tools_after_backup.sh"
|
||||
if [[ -f "$restore_script" ]]; then
|
||||
"$restore_script" "$target" || log "Warning: Could not preserve backup tools"
|
||||
else
|
||||
log "Warning: Tool preservation script not found"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sync to ensure all data is written
|
||||
log "Syncing data to disk..."
|
||||
sync
|
||||
|
||||
# Verify partition table
|
||||
log "Verifying partition table on target drive..."
|
||||
fdisk -l "$target" | head -20 | tee -a "$LOG_FILE"
|
||||
|
||||
success "Backup verification completed!"
|
||||
else
|
||||
error_exit "Drive cloning failed!"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create desktop entry
|
||||
create_desktop_entry() {
|
||||
local desktop_file="$HOME/Desktop/System-Backup.desktop"
|
||||
local script_path=$(realpath "$0")
|
||||
|
||||
cat > "$desktop_file" << EOF
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=System Backup
|
||||
Comment=Clone internal drive to external M.2 SSD
|
||||
Exec=gnome-terminal -- sudo "$script_path" --gui
|
||||
Icon=drive-harddisk
|
||||
Terminal=false
|
||||
Categories=System;Utility;
|
||||
EOF
|
||||
|
||||
chmod +x "$desktop_file"
|
||||
log "Desktop entry created: $desktop_file"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -s, --source DRIVE Source drive (auto-detected if not specified)"
|
||||
echo " -t, --target DRIVE Target drive (required)"
|
||||
echo " -r, --restore Restore mode (reverse source and target)"
|
||||
echo " --sync Smart sync mode (faster incremental backup)"
|
||||
echo " --analyze Analyze changes without performing backup"
|
||||
echo " -l, --list List available drives"
|
||||
echo " -d, --desktop Create desktop entry"
|
||||
echo " --gui Launch GUI version"
|
||||
echo " -h, --help Show this help"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 --list"
|
||||
echo " $0 --analyze --target /dev/sdb"
|
||||
echo " $0 --sync --target /dev/sdb"
|
||||
echo " $0 --source /dev/nvme0n1 --target /dev/sdb"
|
||||
echo " $0 --restore --source /dev/sdb --target /dev/nvme0n1"
|
||||
echo " $0 --desktop"
|
||||
echo " $0 --gui"
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
# Auto-detect source drive if not specified
|
||||
if [[ -z "$SOURCE_DRIVE" ]]; then
|
||||
SOURCE_DRIVE=$(detect_root_drive)
|
||||
log "Auto-detected root filesystem drive: $SOURCE_DRIVE"
|
||||
fi
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--source)
|
||||
SOURCE_DRIVE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--target)
|
||||
TARGET_DRIVE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--restore)
|
||||
RESTORE_MODE=true
|
||||
shift
|
||||
;;
|
||||
--sync)
|
||||
SYNC_MODE=true
|
||||
shift
|
||||
;;
|
||||
--analyze)
|
||||
ANALYZE_ONLY=true
|
||||
shift
|
||||
;;
|
||||
-l|--list)
|
||||
echo "Available drives:"
|
||||
lsblk -d -o NAME,SIZE,TYPE,TRAN
|
||||
echo ""
|
||||
echo "External drives:"
|
||||
detect_external_drives
|
||||
exit 0
|
||||
;;
|
||||
-d|--desktop)
|
||||
create_desktop_entry
|
||||
exit 0
|
||||
;;
|
||||
--gui)
|
||||
python3 "$(dirname "$0")/backup_manager.py"
|
||||
exit 0
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
error_exit "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# In restore mode, swap source and target for user convenience
|
||||
if [[ "$RESTORE_MODE" == true ]]; then
|
||||
if [[ -n "$SOURCE_DRIVE" && -n "$TARGET_DRIVE" ]]; then
|
||||
log "Restore mode: swapping source and target drives"
|
||||
TEMP_DRIVE="$SOURCE_DRIVE"
|
||||
SOURCE_DRIVE="$TARGET_DRIVE"
|
||||
TARGET_DRIVE="$TEMP_DRIVE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if target drive is specified
|
||||
if [[ -z "$TARGET_DRIVE" ]]; then
|
||||
echo "Available external drives:"
|
||||
detect_external_drives
|
||||
echo ""
|
||||
read -p "Enter target drive (e.g., /dev/sdb): " TARGET_DRIVE
|
||||
|
||||
if [[ -z "$TARGET_DRIVE" ]]; then
|
||||
error_exit "Target drive not specified"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check root privileges
|
||||
check_root
|
||||
|
||||
# Validate drives
|
||||
validate_drives
|
||||
|
||||
# Handle analyze mode
|
||||
if [[ "$ANALYZE_ONLY" == "true" ]]; then
|
||||
echo ""
|
||||
echo "🔍 ANALYZING CHANGES"
|
||||
echo "Source: $SOURCE_DRIVE"
|
||||
echo "Target: $TARGET_DRIVE"
|
||||
echo ""
|
||||
|
||||
analysis_result=$(analyze_changes "$SOURCE_DRIVE" "$TARGET_DRIVE")
|
||||
|
||||
case "$analysis_result" in
|
||||
"FULL_CLONE_REQUIRED")
|
||||
echo "📋 ANALYSIS RESULT: Full Clone Required"
|
||||
echo "• No existing backup found on target drive"
|
||||
echo "• Complete drive cloning is necessary"
|
||||
;;
|
||||
"SYNC_RECOMMENDED")
|
||||
echo "✅ ANALYSIS RESULT: Smart Sync Recommended"
|
||||
echo "• Minimal changes detected (< 2GB difference)"
|
||||
echo "• Smart sync will be much faster than full clone"
|
||||
;;
|
||||
"SYNC_BENEFICIAL")
|
||||
echo "⚡ ANALYSIS RESULT: Smart Sync Beneficial"
|
||||
echo "• Moderate changes detected (< 10GB difference)"
|
||||
echo "• Smart sync recommended for faster backup"
|
||||
;;
|
||||
"FULL_CLONE_RECOMMENDED")
|
||||
echo "🔄 ANALYSIS RESULT: Full Clone Recommended"
|
||||
echo "• Major changes detected (> 10GB difference)"
|
||||
echo "• Full clone may be more appropriate"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "Use --sync flag to perform smart sync backup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Handle sync mode
|
||||
if [[ "$SYNC_MODE" == "true" ]]; then
|
||||
echo ""
|
||||
echo "⚡ SMART SYNC BACKUP"
|
||||
echo "Source: $SOURCE_DRIVE"
|
||||
echo "Target: $TARGET_DRIVE"
|
||||
echo ""
|
||||
|
||||
# Check if sync is possible
|
||||
analysis_result=$(analyze_changes "$SOURCE_DRIVE" "$TARGET_DRIVE")
|
||||
|
||||
if [[ "$analysis_result" == "FULL_CLONE_REQUIRED" ]]; then
|
||||
error_exit "Smart sync not possible: No existing backup found. Use full backup first."
|
||||
fi
|
||||
|
||||
echo "Analysis: $analysis_result"
|
||||
echo ""
|
||||
read -p "Proceed with smart sync? (yes/no): " confirm
|
||||
if [[ "$confirm" != "yes" ]]; then
|
||||
log "Smart sync cancelled by user"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
smart_sync_backup "$SOURCE_DRIVE" "$TARGET_DRIVE"
|
||||
|
||||
success "Smart sync backup completed successfully!"
|
||||
echo "Smart sync completed! External drive is up to date."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Confirm operation
|
||||
echo ""
|
||||
if [[ "$RESTORE_MODE" == true ]]; then
|
||||
echo "🚨 RESTORE CONFIGURATION 🚨"
|
||||
echo "This will RESTORE (overwrite target with source data):"
|
||||
else
|
||||
echo "BACKUP CONFIGURATION:"
|
||||
echo "This will BACKUP (copy source to target):"
|
||||
fi
|
||||
echo "Source: $SOURCE_DRIVE"
|
||||
echo "Target: $TARGET_DRIVE"
|
||||
echo ""
|
||||
echo "WARNING: All data on $TARGET_DRIVE will be DESTROYED!"
|
||||
|
||||
if [[ "$RESTORE_MODE" == true ]]; then
|
||||
echo ""
|
||||
echo "⚠️ CRITICAL WARNING FOR RESTORE MODE ⚠️"
|
||||
echo "You are about to OVERWRITE $TARGET_DRIVE"
|
||||
echo "Make sure this is what you intend to do!"
|
||||
echo ""
|
||||
read -p "Type 'RESTORE' to confirm or anything else to cancel: " confirm
|
||||
if [[ "$confirm" != "RESTORE" ]]; then
|
||||
log "Restore operation cancelled by user"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
read -p "Are you sure you want to continue? (yes/no): " confirm
|
||||
if [[ "$confirm" != "yes" ]]; then
|
||||
log "Operation cancelled by user"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start operation
|
||||
if [[ "$RESTORE_MODE" == true ]]; then
|
||||
log "Starting system restore operation..."
|
||||
else
|
||||
log "Starting system backup operation..."
|
||||
fi
|
||||
clone_drive "$SOURCE_DRIVE" "$TARGET_DRIVE"
|
||||
|
||||
log "System backup completed successfully!"
|
||||
echo ""
|
||||
echo "Backup completed! You can now safely remove the external drive."
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
179
diagnose_boot_issue.sh
Executable file
179
diagnose_boot_issue.sh
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LVM Boot Diagnostics Script
|
||||
# Checks the current state of the LVM migration and identifies boot issues
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
|
||||
echo -e "${GREEN}=== LVM Boot Diagnostics ===${NC}"
|
||||
echo
|
||||
|
||||
# Check system state
|
||||
log "Checking current system state..."
|
||||
echo "Currently booted from: $(df / | tail -1 | awk '{print $1}')"
|
||||
echo "Running kernel: $(uname -r)"
|
||||
echo "System: $(hostname)"
|
||||
echo
|
||||
|
||||
# Check available drives
|
||||
log "Available block devices:"
|
||||
lsblk -f
|
||||
|
||||
echo
|
||||
|
||||
# Check LVM status
|
||||
log "LVM Status:"
|
||||
echo "Physical Volumes:"
|
||||
sudo pvs 2>/dev/null || echo "No PVs found"
|
||||
|
||||
echo "Volume Groups:"
|
||||
sudo vgs 2>/dev/null || echo "No VGs found"
|
||||
|
||||
echo "Logical Volumes:"
|
||||
sudo lvs 2>/dev/null || echo "No LVs found"
|
||||
|
||||
echo
|
||||
|
||||
# Check for system-vg specifically
|
||||
if sudo vgs system-vg >/dev/null 2>&1; then
|
||||
success "Found system-vg volume group"
|
||||
|
||||
log "system-vg details:"
|
||||
sudo vgs system-vg
|
||||
sudo lvs system-vg
|
||||
|
||||
# Try to mount and check contents
|
||||
log "Checking external system contents..."
|
||||
|
||||
if [ ! -d /tmp/check-external ]; then
|
||||
mkdir -p /tmp/check-external
|
||||
|
||||
if sudo mount /dev/system-vg/root /tmp/check-external >/dev/null 2>&1; then
|
||||
success "External root filesystem is mountable"
|
||||
|
||||
# Check key system directories
|
||||
for dir in "/etc" "/boot" "/usr" "/var"; do
|
||||
if [ -d "/tmp/check-external$dir" ]; then
|
||||
success "Found system directory: $dir"
|
||||
else
|
||||
warning "Missing system directory: $dir"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for GRUB files
|
||||
if [ -d "/tmp/check-external/boot/grub" ]; then
|
||||
success "GRUB directory found"
|
||||
if [ -f "/tmp/check-external/boot/grub/grub.cfg" ]; then
|
||||
success "GRUB configuration found"
|
||||
else
|
||||
warning "GRUB configuration missing"
|
||||
fi
|
||||
else
|
||||
warning "GRUB directory missing"
|
||||
fi
|
||||
|
||||
# Check fstab
|
||||
if [ -f "/tmp/check-external/etc/fstab" ]; then
|
||||
success "fstab found"
|
||||
log "fstab LVM entries:"
|
||||
grep -E "system-vg|UUID=" "/tmp/check-external/etc/fstab" || echo "No LVM entries found"
|
||||
else
|
||||
warning "fstab missing"
|
||||
fi
|
||||
|
||||
sudo umount /tmp/check-external
|
||||
else
|
||||
error "Cannot mount external root filesystem"
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
error "system-vg volume group not found"
|
||||
echo "This suggests the LVM migration did not complete successfully"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# Check EFI partition
|
||||
log "Checking for EFI boot partition..."
|
||||
if [ -b /dev/sda1 ]; then
|
||||
success "Found EFI partition /dev/sda1"
|
||||
|
||||
if [ ! -d /tmp/check-efi ]; then
|
||||
mkdir -p /tmp/check-efi
|
||||
|
||||
if sudo mount /dev/sda1 /tmp/check-efi >/dev/null 2>&1; then
|
||||
success "EFI partition is mountable"
|
||||
|
||||
if [ -d "/tmp/check-efi/EFI" ]; then
|
||||
success "EFI directory found"
|
||||
log "EFI boot entries:"
|
||||
ls -la "/tmp/check-efi/EFI/" 2>/dev/null || echo "No EFI entries"
|
||||
|
||||
if [ -f "/tmp/check-efi/EFI/debian/grubx64.efi" ]; then
|
||||
success "Debian GRUB EFI bootloader found"
|
||||
else
|
||||
warning "Debian GRUB EFI bootloader missing"
|
||||
fi
|
||||
else
|
||||
warning "EFI directory missing"
|
||||
fi
|
||||
|
||||
sudo umount /tmp/check-efi
|
||||
else
|
||||
error "Cannot mount EFI partition"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
error "EFI partition /dev/sda1 not found"
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# Provide diagnosis and recommendations
|
||||
log "=== DIAGNOSIS ==="
|
||||
|
||||
if sudo vgs system-vg >/dev/null 2>&1; then
|
||||
success "LVM migration appears to have completed"
|
||||
|
||||
if [ -b /dev/sda1 ] && sudo mount /dev/sda1 /tmp/check-efi >/dev/null 2>&1; then
|
||||
if [ -f "/tmp/check-efi/EFI/debian/grubx64.efi" ]; then
|
||||
success "GRUB bootloader appears to be installed"
|
||||
echo
|
||||
echo -e "${BLUE}Likely causes of boot reset issue:${NC}"
|
||||
echo "1. GRUB configuration points to wrong device"
|
||||
echo "2. initramfs missing LVM support"
|
||||
echo "3. BIOS/UEFI boot order incorrect"
|
||||
echo "4. Secure Boot enabled (conflicts with GRUB)"
|
||||
echo
|
||||
echo -e "${GREEN}Recommended action:${NC}"
|
||||
echo "Run: sudo ./fix_grub_boot.sh"
|
||||
else
|
||||
warning "GRUB bootloader missing"
|
||||
echo -e "${GREEN}Recommended action:${NC}"
|
||||
echo "Run: sudo ./fix_grub_boot.sh"
|
||||
fi
|
||||
sudo umount /tmp/check-efi 2>/dev/null || true
|
||||
else
|
||||
error "EFI partition issues detected"
|
||||
echo -e "${GREEN}Recommended action:${NC}"
|
||||
echo "Run: sudo ./fix_grub_boot.sh"
|
||||
fi
|
||||
else
|
||||
error "LVM migration incomplete or failed"
|
||||
echo -e "${GREEN}Recommended action:${NC}"
|
||||
echo "Re-run migration: sudo ./migrate_to_lvm.sh"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -rf /tmp/check-external /tmp/check-efi 2>/dev/null || true
|
||||
274
fix_grub_boot.sh
Executable file
274
fix_grub_boot.sh
Executable file
@@ -0,0 +1,274 @@
|
||||
#!/bin/bash
|
||||
|
||||
# GRUB Boot Repair Script for LVM Migration
|
||||
# Fixes GRUB bootloader issues after LVM migration to external M.2 drive
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
VG_NAME="system-vg"
|
||||
EXTERNAL_DRIVE="/dev/sda" # Adjust if needed
|
||||
|
||||
log() {
|
||||
local message="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
echo -e "${BLUE}$message${NC}"
|
||||
}
|
||||
|
||||
error() {
|
||||
local message="[ERROR] $1"
|
||||
echo -e "${RED}$message${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
warning() {
|
||||
local message="[WARNING] $1"
|
||||
echo -e "${YELLOW}$message${NC}"
|
||||
}
|
||||
|
||||
success() {
|
||||
local message="[SUCCESS] $1"
|
||||
echo -e "${GREEN}$message${NC}"
|
||||
}
|
||||
|
||||
confirm_action() {
|
||||
echo -e "${YELLOW}$1${NC}"
|
||||
read -p "Do you want to continue? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
error "Operation aborted by user"
|
||||
fi
|
||||
}
|
||||
|
||||
check_prerequisites() {
|
||||
log "Checking prerequisites for GRUB repair..."
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
error "This script must be run as root. Use: sudo $0"
|
||||
fi
|
||||
|
||||
# Check if running from live system
|
||||
local root_device=$(df / | tail -1 | awk '{print $1}')
|
||||
if [[ "$root_device" != *"loop"* ]] && [[ "$root_device" != *"overlay"* ]] && [[ "$root_device" != *"tmpfs"* ]]; then
|
||||
warning "This should be run from a live USB system for safety"
|
||||
confirm_action "Continue anyway? (Not recommended if booting from the problematic system)"
|
||||
fi
|
||||
|
||||
# Check required tools
|
||||
for tool in grub-install update-grub mount umount lvm; do
|
||||
if ! command -v $tool >/dev/null 2>&1; then
|
||||
error "$tool not found. Please install required packages."
|
||||
fi
|
||||
done
|
||||
|
||||
success "Prerequisites check passed"
|
||||
}
|
||||
|
||||
detect_external_drive() {
|
||||
log "Detecting external M.2 drive with LVM..."
|
||||
|
||||
# Check if LVM volume group exists
|
||||
if ! vgs "$VG_NAME" >/dev/null 2>&1; then
|
||||
# Try to activate volume groups
|
||||
vgchange -ay || warning "Could not activate volume groups"
|
||||
|
||||
if ! vgs "$VG_NAME" >/dev/null 2>&1; then
|
||||
error "Volume group '$VG_NAME' not found. Is the external drive connected?"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display volume group information
|
||||
log "Found volume group '$VG_NAME':"
|
||||
vgs "$VG_NAME"
|
||||
|
||||
# Check logical volumes
|
||||
local required_lvs=("root" "home" "boot")
|
||||
for lv in "${required_lvs[@]}"; do
|
||||
if ! lvs "$VG_NAME/$lv" >/dev/null 2>&1; then
|
||||
error "Logical volume '$VG_NAME/$lv' not found"
|
||||
fi
|
||||
done
|
||||
|
||||
success "External LVM drive detected successfully"
|
||||
}
|
||||
|
||||
mount_external_system() {
|
||||
log "Mounting external LVM system..."
|
||||
|
||||
local mount_base="/mnt/external-system"
|
||||
mkdir -p "$mount_base"
|
||||
|
||||
# Mount in correct order
|
||||
log "Mounting root filesystem..."
|
||||
mount "/dev/$VG_NAME/root" "$mount_base" || error "Failed to mount root"
|
||||
|
||||
log "Mounting boot filesystem..."
|
||||
mkdir -p "$mount_base/boot"
|
||||
mount "/dev/$VG_NAME/boot" "$mount_base/boot" || error "Failed to mount boot"
|
||||
|
||||
log "Mounting EFI partition..."
|
||||
mkdir -p "$mount_base/boot/efi"
|
||||
# Find EFI partition (usually first partition on external drive)
|
||||
local efi_partition="${EXTERNAL_DRIVE}1"
|
||||
if [ -b "$efi_partition" ]; then
|
||||
mount "$efi_partition" "$mount_base/boot/efi" || error "Failed to mount EFI partition"
|
||||
else
|
||||
error "EFI partition $efi_partition not found"
|
||||
fi
|
||||
|
||||
log "Mounting home filesystem..."
|
||||
mkdir -p "$mount_base/home"
|
||||
mount "/dev/$VG_NAME/home" "$mount_base/home" || error "Failed to mount home"
|
||||
|
||||
success "External system mounted at $mount_base"
|
||||
echo "$mount_base"
|
||||
}
|
||||
|
||||
repair_grub() {
|
||||
local mount_base="$1"
|
||||
log "Repairing GRUB bootloader..."
|
||||
|
||||
# Bind mount necessary filesystems for chroot
|
||||
log "Setting up chroot environment..."
|
||||
mount --bind /dev "$mount_base/dev" || error "Failed to bind /dev"
|
||||
mount --bind /proc "$mount_base/proc" || error "Failed to bind /proc"
|
||||
mount --bind /sys "$mount_base/sys" || error "Failed to bind /sys"
|
||||
mount --bind /run "$mount_base/run" || error "Failed to bind /run"
|
||||
|
||||
# Copy DNS resolution
|
||||
cp /etc/resolv.conf "$mount_base/etc/resolv.conf" 2>/dev/null || warning "Could not copy DNS settings"
|
||||
|
||||
log "Updating initramfs to include LVM support..."
|
||||
chroot "$mount_base" /bin/bash -c "
|
||||
# Ensure LVM is in initramfs
|
||||
echo 'GRUB_ENABLE_CRYPTODISK=y' >> /etc/default/grub 2>/dev/null || true
|
||||
|
||||
# Update initramfs with LVM support
|
||||
update-initramfs -u -k all
|
||||
|
||||
echo 'Initramfs updated successfully'
|
||||
" || warning "Initramfs update had issues"
|
||||
|
||||
log "Reinstalling GRUB bootloader..."
|
||||
chroot "$mount_base" /bin/bash -c "
|
||||
# Install GRUB to the external drive
|
||||
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian --recheck '$EXTERNAL_DRIVE'
|
||||
|
||||
echo 'GRUB installed successfully'
|
||||
" || error "GRUB installation failed"
|
||||
|
||||
log "Updating GRUB configuration..."
|
||||
chroot "$mount_base" /bin/bash -c "
|
||||
# Update GRUB configuration
|
||||
update-grub
|
||||
|
||||
echo 'GRUB configuration updated successfully'
|
||||
" || error "GRUB configuration update failed"
|
||||
|
||||
success "GRUB repair completed"
|
||||
}
|
||||
|
||||
check_grub_files() {
|
||||
local mount_base="$1"
|
||||
log "Checking GRUB installation..."
|
||||
|
||||
# Check EFI bootloader
|
||||
if [ -f "$mount_base/boot/efi/EFI/debian/grubx64.efi" ]; then
|
||||
success "GRUB EFI bootloader found"
|
||||
else
|
||||
warning "GRUB EFI bootloader missing"
|
||||
fi
|
||||
|
||||
# Check GRUB configuration
|
||||
if [ -f "$mount_base/boot/grub/grub.cfg" ]; then
|
||||
success "GRUB configuration found"
|
||||
|
||||
# Check if configuration mentions LVM
|
||||
if grep -q "$VG_NAME" "$mount_base/boot/grub/grub.cfg"; then
|
||||
success "GRUB configuration includes LVM volumes"
|
||||
else
|
||||
warning "GRUB configuration may not include LVM setup"
|
||||
fi
|
||||
else
|
||||
error "GRUB configuration missing"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_mounts() {
|
||||
local mount_base="$1"
|
||||
log "Cleaning up mount points..."
|
||||
|
||||
# Unmount in reverse order
|
||||
umount "$mount_base/run" 2>/dev/null || true
|
||||
umount "$mount_base/sys" 2>/dev/null || true
|
||||
umount "$mount_base/proc" 2>/dev/null || true
|
||||
umount "$mount_base/dev" 2>/dev/null || true
|
||||
umount "$mount_base/home" 2>/dev/null || true
|
||||
umount "$mount_base/boot/efi" 2>/dev/null || true
|
||||
umount "$mount_base/boot" 2>/dev/null || true
|
||||
umount "$mount_base" 2>/dev/null || true
|
||||
|
||||
# Remove mount directory
|
||||
rmdir "$mount_base" 2>/dev/null || true
|
||||
|
||||
success "Mount points cleaned up"
|
||||
}
|
||||
|
||||
show_next_steps() {
|
||||
echo
|
||||
echo -e "${GREEN}=== GRUB Repair Completed ===${NC}"
|
||||
echo
|
||||
echo -e "${BLUE}Next steps:${NC}"
|
||||
echo "1. Reboot your system"
|
||||
echo "2. Enter BIOS/UEFI settings"
|
||||
echo "3. Ensure boot order prioritizes the external M.2 SSD"
|
||||
echo "4. Look for 'debian' entry in the boot menu"
|
||||
echo "5. Boot from the external drive"
|
||||
echo
|
||||
echo -e "${YELLOW}If boot still fails:${NC}"
|
||||
echo "• Check BIOS/UEFI settings for Secure Boot (disable if necessary)"
|
||||
echo "• Verify UEFI mode is enabled (not Legacy/CSM)"
|
||||
echo "• Try different USB ports if using external enclosure"
|
||||
echo "• Run this script again from live USB if needed"
|
||||
echo
|
||||
echo -e "${GREEN}The system should now boot properly from the external M.2!${NC}"
|
||||
}
|
||||
|
||||
main() {
|
||||
echo -e "${GREEN}=== GRUB Boot Repair for LVM Migration ===${NC}"
|
||||
echo "This script repairs GRUB bootloader issues after LVM migration"
|
||||
echo
|
||||
|
||||
check_prerequisites
|
||||
detect_external_drive
|
||||
|
||||
echo
|
||||
echo -e "${YELLOW}This will repair GRUB on the external M.2 drive${NC}"
|
||||
echo "Drive: $EXTERNAL_DRIVE"
|
||||
echo "Volume Group: $VG_NAME"
|
||||
echo
|
||||
confirm_action "Proceed with GRUB repair?"
|
||||
|
||||
local mount_base=$(mount_external_system)
|
||||
|
||||
# Set trap to ensure cleanup
|
||||
trap "cleanup_mounts '$mount_base'" EXIT
|
||||
|
||||
repair_grub "$mount_base"
|
||||
check_grub_files "$mount_base"
|
||||
cleanup_mounts "$mount_base"
|
||||
|
||||
# Clear trap since we cleaned up successfully
|
||||
trap - EXIT
|
||||
|
||||
show_next_steps
|
||||
}
|
||||
|
||||
main "$@"
|
||||
204
install.sh
Executable file
204
install.sh
Executable file
@@ -0,0 +1,204 @@
|
||||
#!/bin/bash
|
||||
# Installation script for System Backup Tool
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Print colored output
|
||||
print_status() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
print_error "Do not run this installer as root. It will use sudo when needed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
print_status "Checking dependencies..."
|
||||
|
||||
# Check Python 3
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
print_error "Python 3 is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check tkinter
|
||||
if ! python3 -c "import tkinter" &> /dev/null; then
|
||||
print_warning "tkinter not available. Installing python3-tk..."
|
||||
sudo apt update
|
||||
sudo apt install -y python3-tk
|
||||
fi
|
||||
|
||||
# Check for pv (progress viewer)
|
||||
if ! command -v pv &> /dev/null; then
|
||||
print_warning "pv (progress viewer) not found. Installing..."
|
||||
sudo apt install -y pv
|
||||
fi
|
||||
|
||||
print_success "All dependencies satisfied"
|
||||
}
|
||||
|
||||
# Make scripts executable
|
||||
setup_permissions() {
|
||||
print_status "Setting up permissions..."
|
||||
|
||||
chmod +x backup_manager.py
|
||||
chmod +x backup_script.sh
|
||||
|
||||
print_success "Permissions set"
|
||||
}
|
||||
|
||||
# Create desktop entry
|
||||
create_desktop_entry() {
|
||||
print_status "Creating desktop entry..."
|
||||
|
||||
local desktop_dir="$HOME/Desktop"
|
||||
local applications_dir="$HOME/.local/share/applications"
|
||||
local script_path=$(realpath backup_manager.py)
|
||||
local icon_path=$(realpath ".")
|
||||
|
||||
# Create applications directory if it doesn't exist
|
||||
mkdir -p "$applications_dir"
|
||||
|
||||
# Create desktop entry content
|
||||
local desktop_content="[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=System Backup Manager
|
||||
Comment=Clone internal drive to external M.2 SSD
|
||||
Exec=python3 '$script_path'
|
||||
Icon=drive-harddisk
|
||||
Terminal=false
|
||||
Categories=System;Utility;
|
||||
StartupNotify=true"
|
||||
|
||||
# Create desktop file in applications
|
||||
echo "$desktop_content" > "$applications_dir/system-backup.desktop"
|
||||
chmod +x "$applications_dir/system-backup.desktop"
|
||||
|
||||
# Create desktop shortcut if Desktop directory exists
|
||||
if [[ -d "$desktop_dir" ]]; then
|
||||
echo "$desktop_content" > "$desktop_dir/System Backup.desktop"
|
||||
chmod +x "$desktop_dir/System Backup.desktop"
|
||||
print_success "Desktop shortcut created"
|
||||
fi
|
||||
|
||||
print_success "Application entry created"
|
||||
}
|
||||
|
||||
# Create systemd service (optional)
|
||||
create_systemd_service() {
|
||||
local service_dir="systemd"
|
||||
local script_path=$(realpath backup_script.sh)
|
||||
|
||||
print_status "Creating systemd service template..."
|
||||
|
||||
mkdir -p "$service_dir"
|
||||
|
||||
cat > "$service_dir/backup-service.service" << EOF
|
||||
[Unit]
|
||||
Description=System Backup Service
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=$script_path --target /dev/sdb
|
||||
User=root
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat > "$service_dir/README.md" << EOF
|
||||
# Systemd Service for Backup
|
||||
|
||||
To install the systemd service:
|
||||
|
||||
1. Edit the service file to specify your target drive
|
||||
2. Copy to systemd directory:
|
||||
\`\`\`bash
|
||||
sudo cp backup-service.service /etc/systemd/system/
|
||||
\`\`\`
|
||||
|
||||
3. Enable and start:
|
||||
\`\`\`bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable backup-service
|
||||
sudo systemctl start backup-service
|
||||
\`\`\`
|
||||
|
||||
4. Check status:
|
||||
\`\`\`bash
|
||||
sudo systemctl status backup-service
|
||||
\`\`\`
|
||||
EOF
|
||||
|
||||
print_success "Systemd service template created in systemd/"
|
||||
}
|
||||
|
||||
# Create log directory
|
||||
setup_logging() {
|
||||
print_status "Setting up logging..."
|
||||
|
||||
# Create log file with proper permissions
|
||||
sudo touch /var/log/system_backup.log
|
||||
sudo chmod 666 /var/log/system_backup.log
|
||||
|
||||
print_success "Log file created: /var/log/system_backup.log"
|
||||
}
|
||||
|
||||
# Main installation function
|
||||
main() {
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo " System Backup Tool Installer"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
check_root
|
||||
check_dependencies
|
||||
setup_permissions
|
||||
setup_logging
|
||||
create_desktop_entry
|
||||
create_systemd_service
|
||||
|
||||
echo ""
|
||||
print_success "Installation completed successfully!"
|
||||
echo ""
|
||||
echo "You can now:"
|
||||
echo " • Launch GUI: python3 backup_manager.py"
|
||||
echo " • Use CLI: ./backup_script.sh --help"
|
||||
echo " • Click desktop icon: System Backup Manager"
|
||||
echo ""
|
||||
print_warning "Remember to run the backup tool with appropriate privileges"
|
||||
print_warning "Always verify your drive selections before starting backup"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run installer
|
||||
main "$@"
|
||||
271
restore_tools_after_backup.sh
Executable file
271
restore_tools_after_backup.sh
Executable file
@@ -0,0 +1,271 @@
|
||||
#!/bin/bash
|
||||
# Restore backup tools after cloning operation
|
||||
# This script runs automatically after backup to preserve tools on external drive
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_status() {
|
||||
echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Get the external drive from parameter or auto-detect
|
||||
EXTERNAL_DRIVE="$1"
|
||||
if [[ -z "$EXTERNAL_DRIVE" ]]; then
|
||||
print_status "Auto-detecting external drive..."
|
||||
EXTERNAL_DRIVE=$(lsblk -d -o NAME,TRAN | grep usb | awk '{print "/dev/" $1}' | head -1)
|
||||
fi
|
||||
|
||||
if [[ -z "$EXTERNAL_DRIVE" ]]; then
|
||||
print_error "Could not detect external drive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_status "Restoring backup tools to $EXTERNAL_DRIVE"
|
||||
|
||||
# Find the tools partition (look for BACKUP_TOOLS label first)
|
||||
TOOLS_PARTITION=$(blkid -L "BACKUP_TOOLS" 2>/dev/null || echo "")
|
||||
|
||||
# If not found by label, try to find the last partition on the external drive
|
||||
if [[ -z "$TOOLS_PARTITION" ]]; then
|
||||
# Get all partitions on the external drive
|
||||
mapfile -t partitions < <(lsblk -n -o NAME "$EXTERNAL_DRIVE" | grep -v "^$(basename "$EXTERNAL_DRIVE")$")
|
||||
|
||||
if [[ ${#partitions[@]} -gt 0 ]]; then
|
||||
# Check the last partition
|
||||
last_partition="/dev/${partitions[-1]}"
|
||||
|
||||
# Check if it's small (likely our tools partition)
|
||||
partition_size=$(lsblk -n -o SIZE "$last_partition" | tr -d ' ')
|
||||
if [[ "$partition_size" == *M ]] && [[ ${partition_size%M} -le 1024 ]]; then
|
||||
TOOLS_PARTITION="$last_partition"
|
||||
print_status "Found potential tools partition: $TOOLS_PARTITION ($partition_size)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If still no tools partition found, create one
|
||||
if [[ -z "$TOOLS_PARTITION" ]]; then
|
||||
print_warning "No tools partition found. Creating one..."
|
||||
|
||||
# Create 512MB partition at the end
|
||||
if command -v parted >/dev/null 2>&1; then
|
||||
parted "$EXTERNAL_DRIVE" --script mkpart primary ext4 -512MiB 100% || {
|
||||
print_warning "Could not create partition. Backup tools will not be preserved."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Wait for partition to appear
|
||||
sleep 2
|
||||
|
||||
# Get the new partition
|
||||
mapfile -t partitions < <(lsblk -n -o NAME "$EXTERNAL_DRIVE" | grep -v "^$(basename "$EXTERNAL_DRIVE")$")
|
||||
TOOLS_PARTITION="/dev/${partitions[-1]}"
|
||||
|
||||
# Format it
|
||||
mkfs.ext4 -L "BACKUP_TOOLS" "$TOOLS_PARTITION" -F >/dev/null 2>&1 || {
|
||||
print_warning "Could not format tools partition"
|
||||
exit 0
|
||||
}
|
||||
|
||||
print_success "Created tools partition: $TOOLS_PARTITION"
|
||||
else
|
||||
print_warning "parted not available. Cannot create tools partition."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Mount tools partition
|
||||
MOUNT_POINT="/tmp/backup_tools_restore_$$"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
|
||||
mount "$TOOLS_PARTITION" "$MOUNT_POINT" 2>/dev/null || {
|
||||
print_error "Could not mount tools partition $TOOLS_PARTITION"
|
||||
rmdir "$MOUNT_POINT"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure we unmount on exit
|
||||
trap 'umount "$MOUNT_POINT" 2>/dev/null; rmdir "$MOUNT_POINT" 2>/dev/null' EXIT
|
||||
|
||||
# Get the directory where this script is located (source of backup tools)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Install/update backup tools
|
||||
if [[ -d "$MOUNT_POINT/backup_system" ]]; then
|
||||
print_status "Updating existing backup tools..."
|
||||
# Sync changes, excluding git and cache files
|
||||
rsync -av --delete --exclude='.git' --exclude='__pycache__' --exclude='*.pyc' \
|
||||
"$SCRIPT_DIR/" "$MOUNT_POINT/backup_system/"
|
||||
else
|
||||
print_status "Installing backup tools for first time..."
|
||||
mkdir -p "$MOUNT_POINT/backup_system"
|
||||
cp -r "$SCRIPT_DIR"/* "$MOUNT_POINT/backup_system/"
|
||||
fi
|
||||
|
||||
# Create portable launcher if it doesn't exist
|
||||
if [[ ! -f "$MOUNT_POINT/backup_system/launch_backup_tools.sh" ]]; then
|
||||
cat > "$MOUNT_POINT/backup_system/launch_backup_tools.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Portable launcher for backup tools
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Make sure scripts are executable
|
||||
chmod +x *.sh *.py
|
||||
|
||||
echo "=========================================="
|
||||
echo " Portable Backup Tools"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Available options:"
|
||||
echo "1. Launch GUI Backup Manager"
|
||||
echo "2. Command Line Backup"
|
||||
echo "3. Command Line Restore"
|
||||
echo "4. List Available Drives"
|
||||
echo "5. Create Desktop Entry"
|
||||
echo ""
|
||||
|
||||
# Check if GUI is available
|
||||
if [[ -n "$DISPLAY" ]] && command -v python3 >/dev/null 2>&1; then
|
||||
read -p "Select option (1-5): " choice
|
||||
case $choice in
|
||||
1)
|
||||
echo "Launching GUI Backup Manager..."
|
||||
python3 backup_manager.py
|
||||
;;
|
||||
2)
|
||||
echo "Starting command line backup..."
|
||||
./backup_script.sh
|
||||
;;
|
||||
3)
|
||||
echo "Starting command line restore..."
|
||||
./backup_script.sh --restore
|
||||
;;
|
||||
4)
|
||||
echo "Available drives:"
|
||||
./backup_script.sh --list
|
||||
;;
|
||||
5)
|
||||
./create_desktop_entry.sh
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "GUI not available. Use command line options:"
|
||||
./backup_script.sh --help
|
||||
fi
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Create desktop entry creator
|
||||
cat > "$MOUNT_POINT/backup_system/create_desktop_entry.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Create desktop entry for portable backup tools
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DESKTOP_DIR="$HOME/Desktop"
|
||||
APPLICATIONS_DIR="$HOME/.local/share/applications"
|
||||
|
||||
mkdir -p "$APPLICATIONS_DIR"
|
||||
|
||||
# Create application entry
|
||||
cat > "$APPLICATIONS_DIR/portable-backup.desktop" << EOL
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Portable Backup Manager
|
||||
Comment=Boot from external drive and restore to internal
|
||||
Exec=python3 "$SCRIPT_DIR/backup_manager.py"
|
||||
Icon=drive-harddisk
|
||||
Terminal=false
|
||||
Categories=System;Utility;
|
||||
StartupNotify=true
|
||||
EOL
|
||||
|
||||
# Create desktop shortcut
|
||||
if [[ -d "$DESKTOP_DIR" ]]; then
|
||||
cp "$APPLICATIONS_DIR/portable-backup.desktop" "$DESKTOP_DIR/"
|
||||
chmod +x "$DESKTOP_DIR/portable-backup.desktop"
|
||||
echo "Desktop shortcut created: $DESKTOP_DIR/portable-backup.desktop"
|
||||
fi
|
||||
|
||||
echo "Application entry created: $APPLICATIONS_DIR/portable-backup.desktop"
|
||||
EOF
|
||||
|
||||
# Make all scripts executable
|
||||
chmod +x "$MOUNT_POINT/backup_system"/*.sh
|
||||
chmod +x "$MOUNT_POINT/backup_system"/*.py
|
||||
|
||||
# Create a README for the external drive
|
||||
cat > "$MOUNT_POINT/backup_system/README_EXTERNAL.md" << 'EOF'
|
||||
# Portable Backup Tools
|
||||
|
||||
This external drive contains both:
|
||||
1. **Your system backup** (main partitions)
|
||||
2. **Backup tools** (this partition)
|
||||
|
||||
## When Booted From This External Drive:
|
||||
|
||||
### Quick Start:
|
||||
```bash
|
||||
# Mount tools and launch
|
||||
sudo mkdir -p /mnt/tools
|
||||
sudo mount LABEL=BACKUP_TOOLS /mnt/tools
|
||||
cd /mnt/tools/backup_system
|
||||
./launch_backup_tools.sh
|
||||
```
|
||||
|
||||
### Or Create Desktop Entry:
|
||||
```bash
|
||||
cd /mnt/tools/backup_system
|
||||
./create_desktop_entry.sh
|
||||
```
|
||||
|
||||
## Common Operations:
|
||||
|
||||
### Restore Internal Drive:
|
||||
1. Boot from this external drive
|
||||
2. Launch backup tools
|
||||
3. Select "Restore from External"
|
||||
4. Choose external → internal
|
||||
5. Click "Reboot & Restore"
|
||||
|
||||
### Update Backup:
|
||||
1. Boot normally from internal drive
|
||||
2. Connect this external drive
|
||||
3. Run backup as usual
|
||||
4. Tools will be automatically preserved
|
||||
|
||||
## Drive Layout:
|
||||
- Partition 1-2: System backup (bootable)
|
||||
- Last Partition: Backup tools (this)
|
||||
EOF
|
||||
|
||||
print_success "Backup tools preserved on external drive"
|
||||
print_status "Tools available at: $TOOLS_PARTITION"
|
||||
print_status "To access when booted from external: mount LABEL=BACKUP_TOOLS /mnt/tools"
|
||||
|
||||
exit 0
|
||||
368
setup_portable_tools.sh
Executable file
368
setup_portable_tools.sh
Executable file
@@ -0,0 +1,368 @@
|
||||
#!/bin/bash
|
||||
# Portable Backup Tool Installer for External M.2 SSD
|
||||
# This script sets up the backup tools on the external drive so they survive cloning operations
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_status() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Detect external drives
|
||||
detect_external_drive() {
|
||||
print_status "Detecting external M.2 SSD..."
|
||||
|
||||
# Look for USB-connected drives
|
||||
local external_drives=()
|
||||
while IFS= read -r line; do
|
||||
if [[ $line == *"disk"* ]] && [[ $line == *"usb"* ]]; then
|
||||
local drive_name=$(echo "$line" | awk '{print $1}')
|
||||
local drive_size=$(echo "$line" | awk '{print $4}')
|
||||
external_drives+=("/dev/$drive_name ($drive_size)")
|
||||
fi
|
||||
done < <(lsblk -d -o NAME,SIZE,TYPE,TRAN | grep -E "disk.*usb")
|
||||
|
||||
if [[ ${#external_drives[@]} -eq 0 ]]; then
|
||||
print_error "No external USB drives found. Please connect your M.2 SSD."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ${#external_drives[@]} -eq 1 ]]; then
|
||||
EXTERNAL_DRIVE=$(echo "${external_drives[0]}" | cut -d' ' -f1)
|
||||
print_success "Auto-detected external drive: ${external_drives[0]}"
|
||||
else
|
||||
print_status "Multiple external drives found:"
|
||||
for i in "${!external_drives[@]}"; do
|
||||
echo " $((i+1)). ${external_drives[i]}"
|
||||
done
|
||||
read -p "Select drive number: " selection
|
||||
EXTERNAL_DRIVE=$(echo "${external_drives[$((selection-1))]}" | cut -d' ' -f1)
|
||||
fi
|
||||
}
|
||||
|
||||
# Create backup tools partition
|
||||
create_tools_partition() {
|
||||
print_status "Setting up backup tools partition on $EXTERNAL_DRIVE..."
|
||||
|
||||
# Check if there's already a small partition at the end
|
||||
local last_partition=$(lsblk -n -o NAME "$EXTERNAL_DRIVE" | tail -1)
|
||||
local tools_partition="${EXTERNAL_DRIVE}p3" # Assuming nvme, adjust for sda
|
||||
|
||||
# If it's sda style, adjust
|
||||
if [[ $EXTERNAL_DRIVE == *"sda"* ]]; then
|
||||
tools_partition="${EXTERNAL_DRIVE}3"
|
||||
fi
|
||||
|
||||
# Check if tools partition already exists
|
||||
if lsblk | grep -q "$(basename "$tools_partition")"; then
|
||||
print_warning "Tools partition already exists: $tools_partition"
|
||||
TOOLS_PARTITION="$tools_partition"
|
||||
return
|
||||
fi
|
||||
|
||||
print_warning "This will create a 512MB partition at the end of $EXTERNAL_DRIVE"
|
||||
print_warning "This will slightly reduce the cloneable space but preserve backup tools"
|
||||
read -p "Continue? (yes/no): " confirm
|
||||
|
||||
if [[ "$confirm" != "yes" ]]; then
|
||||
print_error "Operation cancelled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create 512MB partition at the end using parted
|
||||
sudo parted "$EXTERNAL_DRIVE" --script mkpart primary ext4 -512MiB 100%
|
||||
|
||||
# Get the new partition name
|
||||
TOOLS_PARTITION=$(lsblk -n -o NAME "$EXTERNAL_DRIVE" | tail -1)
|
||||
TOOLS_PARTITION="/dev/$TOOLS_PARTITION"
|
||||
|
||||
# Format the partition
|
||||
sudo mkfs.ext4 -L "BACKUP_TOOLS" "$TOOLS_PARTITION"
|
||||
|
||||
print_success "Tools partition created: $TOOLS_PARTITION"
|
||||
}
|
||||
|
||||
# Install backup tools to external drive
|
||||
install_tools_to_external() {
|
||||
local mount_point="/mnt/backup_tools"
|
||||
|
||||
print_status "Installing backup tools to external drive..."
|
||||
|
||||
# Create mount point
|
||||
sudo mkdir -p "$mount_point"
|
||||
|
||||
# Mount tools partition
|
||||
sudo mount "$TOOLS_PARTITION" "$mount_point"
|
||||
|
||||
# Copy all backup tools
|
||||
sudo cp -r . "$mount_point/backup_system"
|
||||
|
||||
# Create launcher script that works from any location
|
||||
sudo tee "$mount_point/backup_system/launch_backup_tools.sh" > /dev/null << 'EOF'
|
||||
#!/bin/bash
|
||||
# Portable launcher for backup tools
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Make sure scripts are executable
|
||||
chmod +x *.sh *.py
|
||||
|
||||
# Launch GUI if X11 is available, otherwise show CLI options
|
||||
if [[ -n "$DISPLAY" ]]; then
|
||||
echo "Launching Backup Manager GUI..."
|
||||
python3 backup_manager.py
|
||||
else
|
||||
echo "No GUI available. Command line options:"
|
||||
./backup_script.sh --help
|
||||
fi
|
||||
EOF
|
||||
|
||||
sudo chmod +x "$mount_point/backup_system/launch_backup_tools.sh"
|
||||
|
||||
# Create desktop entry for when booted from external drive
|
||||
sudo tee "$mount_point/backup_system/create_desktop_entry.sh" > /dev/null << 'EOF'
|
||||
#!/bin/bash
|
||||
# Create desktop entry when booted from external drive
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DESKTOP_DIR="$HOME/Desktop"
|
||||
APPLICATIONS_DIR="$HOME/.local/share/applications"
|
||||
|
||||
mkdir -p "$APPLICATIONS_DIR"
|
||||
|
||||
# Create desktop entry
|
||||
cat > "$APPLICATIONS_DIR/portable-backup.desktop" << EOL
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Portable Backup Manager
|
||||
Comment=Restore internal drive from external backup
|
||||
Exec=python3 "$SCRIPT_DIR/backup_manager.py"
|
||||
Icon=drive-harddisk
|
||||
Terminal=false
|
||||
Categories=System;Utility;
|
||||
StartupNotify=true
|
||||
EOL
|
||||
|
||||
# Create desktop shortcut if Desktop exists
|
||||
if [[ -d "$DESKTOP_DIR" ]]; then
|
||||
cp "$APPLICATIONS_DIR/portable-backup.desktop" "$DESKTOP_DIR/"
|
||||
chmod +x "$DESKTOP_DIR/portable-backup.desktop"
|
||||
fi
|
||||
|
||||
echo "Desktop entry created for portable backup tools"
|
||||
EOF
|
||||
|
||||
sudo chmod +x "$mount_point/backup_system/create_desktop_entry.sh"
|
||||
|
||||
# Unmount
|
||||
sudo umount "$mount_point"
|
||||
|
||||
print_success "Backup tools installed to external drive"
|
||||
}
|
||||
|
||||
# Create post-backup restoration script
|
||||
create_restoration_script() {
|
||||
print_status "Creating post-backup tool restoration script..."
|
||||
|
||||
# This script will be called after each backup to restore the tools
|
||||
cat > "restore_tools_after_backup.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Restore backup tools after cloning operation
|
||||
# This script runs automatically after backup to preserve tools on external drive
|
||||
|
||||
set -e
|
||||
|
||||
print_status() {
|
||||
echo "[$(date '+%H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Detect the external drive (should be the target of backup)
|
||||
EXTERNAL_DRIVE="$1"
|
||||
if [[ -z "$EXTERNAL_DRIVE" ]]; then
|
||||
print_status "Auto-detecting external drive..."
|
||||
EXTERNAL_DRIVE=$(lsblk -d -o NAME,TRAN | grep usb | awk '{print "/dev/" $1}' | head -1)
|
||||
fi
|
||||
|
||||
if [[ -z "$EXTERNAL_DRIVE" ]]; then
|
||||
echo "Error: Could not detect external drive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_status "Restoring backup tools to $EXTERNAL_DRIVE"
|
||||
|
||||
# Find the tools partition (should be the last partition)
|
||||
TOOLS_PARTITION=$(lsblk -n -o NAME "$EXTERNAL_DRIVE" | tail -1)
|
||||
TOOLS_PARTITION="/dev/$TOOLS_PARTITION"
|
||||
|
||||
# Check if tools partition exists
|
||||
if ! lsblk | grep -q "$(basename "$TOOLS_PARTITION")"; then
|
||||
echo "Warning: Tools partition not found. Backup tools may have been overwritten."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Mount tools partition
|
||||
MOUNT_POINT="/mnt/backup_tools_restore"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
mount "$TOOLS_PARTITION" "$MOUNT_POINT"
|
||||
|
||||
# Check if tools exist
|
||||
if [[ -d "$MOUNT_POINT/backup_system" ]]; then
|
||||
print_status "Backup tools preserved on external drive"
|
||||
|
||||
# Update the tools with any changes from source
|
||||
rsync -av --exclude='.git' "$(dirname "$0")/" "$MOUNT_POINT/backup_system/"
|
||||
|
||||
print_status "Backup tools updated on external drive"
|
||||
else
|
||||
print_status "Installing backup tools to external drive for first time"
|
||||
cp -r "$(dirname "$0")" "$MOUNT_POINT/backup_system"
|
||||
fi
|
||||
|
||||
# Ensure scripts are executable
|
||||
chmod +x "$MOUNT_POINT/backup_system"/*.sh
|
||||
chmod +x "$MOUNT_POINT/backup_system"/*.py
|
||||
|
||||
umount "$MOUNT_POINT"
|
||||
print_status "Backup tools restoration complete"
|
||||
EOF
|
||||
|
||||
chmod +x "restore_tools_after_backup.sh"
|
||||
|
||||
print_success "Post-backup restoration script created"
|
||||
}
|
||||
|
||||
# Update backup scripts to call restoration
|
||||
update_backup_scripts() {
|
||||
print_status "Updating backup scripts to preserve tools..."
|
||||
|
||||
# Add tool restoration to GUI backup manager
|
||||
if ! grep -q "restore_tools_after_backup" backup_manager.py; then
|
||||
# Add restoration call after successful backup
|
||||
sed -i '/self\.log("Backup completed successfully!")/a\\n # Restore backup tools to external drive\n try:\n subprocess.run([os.path.join(os.path.dirname(__file__), "restore_tools_after_backup.sh"), target], check=False)\n except Exception as e:\n self.log(f"Warning: Could not restore tools to external drive: {e}")' backup_manager.py
|
||||
fi
|
||||
|
||||
# Add tool restoration to command line script
|
||||
if ! grep -q "restore_tools_after_backup" backup_script.sh; then
|
||||
sed -i '/success "Drive cloning completed successfully!"/a\\n # Restore backup tools to external drive\n log "Restoring backup tools to external drive..."\n if [[ -f "$(dirname "$0")/restore_tools_after_backup.sh" ]]; then\n "$(dirname "$0")/restore_tools_after_backup.sh" "$target" || log "Warning: Could not restore tools"\n fi' backup_script.sh
|
||||
fi
|
||||
|
||||
print_success "Backup scripts updated to preserve tools"
|
||||
}
|
||||
|
||||
# Create auto-mount script for tools partition
|
||||
create_automount_script() {
|
||||
print_status "Creating auto-mount script for backup tools..."
|
||||
|
||||
cat > "mount_backup_tools.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Auto-mount backup tools partition and create desktop shortcut
|
||||
|
||||
# Find tools partition by label
|
||||
TOOLS_PARTITION=$(blkid -L "BACKUP_TOOLS" 2>/dev/null)
|
||||
|
||||
if [[ -z "$TOOLS_PARTITION" ]]; then
|
||||
echo "Backup tools partition not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create mount point
|
||||
MOUNT_POINT="$HOME/backup_tools"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
|
||||
# Mount if not already mounted
|
||||
if ! mountpoint -q "$MOUNT_POINT"; then
|
||||
mount "$TOOLS_PARTITION" "$MOUNT_POINT" 2>/dev/null || {
|
||||
echo "Mounting with sudo..."
|
||||
sudo mount "$TOOLS_PARTITION" "$MOUNT_POINT"
|
||||
}
|
||||
fi
|
||||
|
||||
echo "Backup tools mounted at: $MOUNT_POINT"
|
||||
|
||||
# Create desktop entry if tools exist
|
||||
if [[ -d "$MOUNT_POINT/backup_system" ]]; then
|
||||
cd "$MOUNT_POINT/backup_system"
|
||||
./create_desktop_entry.sh
|
||||
echo "Desktop entry created for backup tools"
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x "mount_backup_tools.sh"
|
||||
|
||||
print_success "Auto-mount script created"
|
||||
}
|
||||
|
||||
# Main installation
|
||||
main() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Portable Backup Tools Installer"
|
||||
echo " For External M.2 SSD"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
print_warning "This installer will:"
|
||||
print_warning "1. Create a 512MB tools partition on your external M.2 SSD"
|
||||
print_warning "2. Install backup tools that survive cloning operations"
|
||||
print_warning "3. Set up automatic tool restoration after backups"
|
||||
print_warning "4. Enable booting from external drive with restore capability"
|
||||
echo ""
|
||||
|
||||
read -p "Continue? (yes/no): " confirm
|
||||
if [[ "$confirm" != "yes" ]]; then
|
||||
print_error "Installation cancelled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_external_drive
|
||||
create_tools_partition
|
||||
install_tools_to_external
|
||||
create_restoration_script
|
||||
update_backup_scripts
|
||||
create_automount_script
|
||||
|
||||
echo ""
|
||||
print_success "Portable backup tools installation complete!"
|
||||
echo ""
|
||||
echo "Your external M.2 SSD now has:"
|
||||
echo " • Preserved backup tools in separate partition"
|
||||
echo " • Automatic tool restoration after each backup"
|
||||
echo " • Bootable system restoration capability"
|
||||
echo ""
|
||||
echo "When booted from external drive:"
|
||||
echo " • Run: ~/backup_tools/backup_system/launch_backup_tools.sh"
|
||||
echo " • Or use desktop shortcut if available"
|
||||
echo ""
|
||||
print_warning "Note: Your external drive now has 512MB less space for cloning"
|
||||
print_warning "But the backup tools will always be available for system recovery!"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
print_error "Do not run as root. Script will use sudo when needed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user