commit 0367c3f7e65b4b976e4def39162881c876722fc1 Author: root Date: Sat Sep 13 22:14:36 2025 +0200 Initial commit: Complete backup system with portable tools - GUI and CLI backup/restore functionality - Auto-detection of internal system drive - Smart drive classification (internal vs external) - Reboot integration for clean backups/restores - Portable tools that survive cloning operations - Tool preservation system for external M.2 SSD - Complete disaster recovery workflow - Safety features and multiple confirmations - Desktop integration and launcher scripts - Comprehensive documentation diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..53bd2f8 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,46 @@ + +- [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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..837571a --- /dev/null +++ b/README.md @@ -0,0 +1,327 @@ +# 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 + +- **GUI Application**: Easy-to-use graphical interface with drive detection +- **Auto-Detection**: Automatically identifies your internal system drive as source +- **Smart Drive Classification**: Distinguishes between internal and external drives +- **Command Line Script**: For automated backups and scripting +- **Reboot Integration**: Option to reboot and perform backup automatically +- **Drive Validation**: Ensures safe operation with proper drive detection +- **Progress Monitoring**: Real-time backup progress and logging +- **Desktop Integration**: Creates desktop shortcuts for easy access +- **Portable Tools**: Backup tools survive cloning and work when booted from external drive +- **Tool Preservation**: Automatic restoration of backup tools after each clone operation + +## 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 + 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**: + - **Start Backup**: Immediate backup (while system running) + - **Reboot & Backup**: Reboot system then backup (recommended) +- **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 + +# Perform 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 +``` + +## How It Works + +### GUI Mode +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. diff --git a/__pycache__/backup_manager.cpython-312.pyc b/__pycache__/backup_manager.cpython-312.pyc new file mode 100644 index 0000000..f40f97d Binary files /dev/null and b/__pycache__/backup_manager.cpython-312.pyc differ diff --git a/access_tools.sh b/access_tools.sh new file mode 100755 index 0000000..28818f2 --- /dev/null +++ b/access_tools.sh @@ -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 diff --git a/backup_manager.py b/backup_manager.py new file mode 100755 index 0000000..8bc2440 --- /dev/null +++ b/backup_manager.py @@ -0,0 +1,535 @@ +#!/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.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.backup_btn = ttk.Button(backup_frame, text="Start Backup", + command=self.start_backup, style="Accent.TButton") + self.backup_btn.pack(side=tk.TOP, pady=2) + + self.reboot_backup_btn = ttk.Button(backup_frame, text="Reboot & Backup", + 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) + + # 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 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 + + self.operation_type = "restore" + self.start_operation(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 + + if self.operation_running: + self.log("Operation already running!") + return + + # Confirm backup + source = self.source_drive.get().split()[0] + target = self.target_drive.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 not result: + return + + self.operation_type = "backup" + self.start_operation(source, target) + + def start_operation(self, source, target): + """Start backup or restore operation""" + # Start operation in thread + self.operation_running = True + 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.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() diff --git a/backup_script.sh b/backup_script.sh new file mode 100755 index 0000000..8486a99 --- /dev/null +++ b/backup_script.sh @@ -0,0 +1,338 @@ +#!/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 +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" +} + +# 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 " -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 --source /dev/sda --target /dev/sdb" + echo " $0 --restore --source /dev/sdb --target /dev/sda" + 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 + ;; + -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 + + # 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 "$@" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1350141 --- /dev/null +++ b/install.sh @@ -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 "$@" diff --git a/restore_tools_after_backup.sh b/restore_tools_after_backup.sh new file mode 100755 index 0000000..604456e --- /dev/null +++ b/restore_tools_after_backup.sh @@ -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 diff --git a/setup_portable_tools.sh b/setup_portable_tools.sh new file mode 100755 index 0000000..00b7efb --- /dev/null +++ b/setup_portable_tools.sh @@ -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 "$@"