feat: Add file-level snapshot backup mode for space efficiency
Perfect for data volumes like home partitions that are nearly full. New mode: Files → Borg - Creates LVM snapshot (consistent point-in-time state) - Mounts snapshot to temporary location - Borg backs up actual files (not empty blocks) - Unmounts and cleans up snapshot Benefits vs block-level backup: - Space efficient: 305GB files vs 358GB entire LV - Better compression: Borg works on real file data - Superior deduplication: File-content based - No snapshot space issues: Shorter operation time - Easier file recovery: Browse/restore individual files Use cases: - Block-level: System volumes (root, boot) for exact state - File-level: Data volumes (home) for space efficiency GUI: Added 'Files → Borg' mode CLI: Added 'files-to-borg' mode Example: sudo ./enhanced_simple_backup.sh files-to-borg /dev/internal-vg/home /repo --new-repo This solves the 90% full home partition backup problem!
This commit is contained in:
@@ -24,25 +24,28 @@ usage() {
|
||||
echo " $0 vg-to-raw SOURCE_VG TARGET_DEVICE"
|
||||
echo " $0 lv-to-borg SOURCE_LV REPO_PATH [--new-repo] [--encryption MODE] [--passphrase PASS]"
|
||||
echo " $0 vg-to-borg SOURCE_VG REPO_PATH [--new-repo] [--encryption MODE] [--passphrase PASS]"
|
||||
echo " $0 files-to-borg SOURCE_LV REPO_PATH [--new-repo] [--encryption MODE] [--passphrase PASS]"
|
||||
echo ""
|
||||
echo "Modes:"
|
||||
echo " lv-to-lv - Update existing LV backup (SOURCE_LV → TARGET_LV)"
|
||||
echo " lv-to-raw - Create fresh backup (SOURCE_LV → raw device)"
|
||||
echo " vg-to-raw - Clone entire VG (SOURCE_VG → raw device)"
|
||||
echo " lv-to-borg - Backup LV to Borg repository"
|
||||
echo " vg-to-borg - Backup entire VG to Borg repository"
|
||||
echo " lv-to-borg - Backup LV to Borg repository (block-level)"
|
||||
echo " vg-to-borg - Backup entire VG to Borg repository (block-level)"
|
||||
echo " files-to-borg - Backup LV files to Borg repository (file-level, space-efficient)"
|
||||
echo ""
|
||||
echo "Borg Options:"
|
||||
echo " --new-repo Create new repository"
|
||||
echo " --encryption MODE Encryption mode: none, repokey, keyfile (default: repokey)"
|
||||
echo " --passphrase PASS Repository passphrase"
|
||||
echo " --generous-snapshots Use 25% of LV size for snapshots (for very active systems)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 lv-to-lv /dev/internal-vg/root /dev/backup-vg/root"
|
||||
echo " $0 lv-to-raw /dev/internal-vg/root /dev/sdb"
|
||||
echo " $0 vg-to-raw internal-vg /dev/sdb"
|
||||
echo " $0 lv-to-borg /dev/internal-vg/root /path/to/borg/repo --new-repo"
|
||||
echo " $0 vg-to-borg internal-vg /path/to/borg/repo --encryption repokey --passphrase mypass"
|
||||
echo " $0 files-to-borg /dev/internal-vg/home /path/to/borg/repo --new-repo"
|
||||
echo ""
|
||||
echo "List available sources/targets:"
|
||||
echo " ./list_drives.sh"
|
||||
@@ -95,6 +98,7 @@ shift 3
|
||||
NEW_REPO=false
|
||||
ENCRYPTION="repokey"
|
||||
PASSPHRASE=""
|
||||
GENEROUS_SNAPSHOTS=false
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -110,6 +114,10 @@ while [ $# -gt 0 ]; do
|
||||
PASSPHRASE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--generous-snapshots)
|
||||
GENEROUS_SNAPSHOTS=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
error "Unknown option: $1"
|
||||
;;
|
||||
@@ -123,7 +131,7 @@ fi
|
||||
|
||||
# Validate mode
|
||||
case "$MODE" in
|
||||
"lv-to-lv"|"lv-to-raw"|"vg-to-raw"|"lv-to-borg"|"vg-to-borg")
|
||||
"lv-to-lv"|"lv-to-raw"|"vg-to-raw"|"lv-to-borg"|"vg-to-borg"|"files-to-borg")
|
||||
;;
|
||||
*)
|
||||
error "Invalid mode: $MODE"
|
||||
@@ -300,10 +308,22 @@ case "$MODE" in
|
||||
|
||||
# Get LV size to determine appropriate snapshot size
|
||||
LV_SIZE_BYTES=$(lvs --noheadings -o lv_size --units b "$SOURCE" | tr -d ' ' | sed 's/B$//')
|
||||
# Use 10% of LV size or minimum 1GB for snapshot
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 10))
|
||||
if [ $SNAPSHOT_SIZE_BYTES -lt 1073741824 ]; then
|
||||
SNAPSHOT_SIZE_BYTES=1073741824 # 1GB minimum
|
||||
# Use different percentages based on options
|
||||
if [ "$GENEROUS_SNAPSHOTS" = true ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 4)) # 25%
|
||||
MIN_SIZE=$((2 * 1024 * 1024 * 1024)) # 2GB minimum
|
||||
else
|
||||
# Auto mode: 10% normally, 15% for large LVs
|
||||
if [ $LV_SIZE_BYTES -gt $((50 * 1024 * 1024 * 1024)) ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES * 15 / 100)) # 15% for >50GB
|
||||
else
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 10)) # 10% normally
|
||||
fi
|
||||
MIN_SIZE=$((1024 * 1024 * 1024)) # 1GB minimum
|
||||
fi
|
||||
|
||||
if [ $SNAPSHOT_SIZE_BYTES -lt $MIN_SIZE ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$MIN_SIZE
|
||||
fi
|
||||
SNAPSHOT_SIZE_GB=$((SNAPSHOT_SIZE_BYTES / 1073741824))
|
||||
SNAPSHOT_SIZE="${SNAPSHOT_SIZE_GB}G"
|
||||
@@ -411,10 +431,22 @@ case "$MODE" in
|
||||
|
||||
# Get LV size to determine appropriate snapshot size
|
||||
LV_SIZE_BYTES=$(lvs --noheadings -o lv_size --units b "$LV_PATH" | tr -d ' ' | sed 's/B$//')
|
||||
# Use 10% of LV size or minimum 1GB for snapshot
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 10))
|
||||
if [ $SNAPSHOT_SIZE_BYTES -lt 1073741824 ]; then
|
||||
SNAPSHOT_SIZE_BYTES=1073741824 # 1GB minimum
|
||||
# Use different percentages based on options
|
||||
if [ "$GENEROUS_SNAPSHOTS" = true ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 4)) # 25%
|
||||
MIN_SIZE=$((2 * 1024 * 1024 * 1024)) # 2GB minimum
|
||||
else
|
||||
# Auto mode: 10% normally, 15% for large LVs
|
||||
if [ $LV_SIZE_BYTES -gt $((50 * 1024 * 1024 * 1024)) ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES * 15 / 100)) # 15% for >50GB
|
||||
else
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 10)) # 10% normally
|
||||
fi
|
||||
MIN_SIZE=$((1024 * 1024 * 1024)) # 1GB minimum
|
||||
fi
|
||||
|
||||
if [ $SNAPSHOT_SIZE_BYTES -lt $MIN_SIZE ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$MIN_SIZE
|
||||
fi
|
||||
SNAPSHOT_SIZE_GB=$((SNAPSHOT_SIZE_BYTES / 1073741824))
|
||||
SNAPSHOT_SIZE="${SNAPSHOT_SIZE_GB}G"
|
||||
@@ -445,6 +477,101 @@ case "$MODE" in
|
||||
|
||||
log "VG to Borg backup completed successfully"
|
||||
;;
|
||||
|
||||
"files-to-borg")
|
||||
# Files to Borg repository backup
|
||||
if [ ! -e "$SOURCE" ]; then
|
||||
error "Source LV does not exist: $SOURCE"
|
||||
fi
|
||||
|
||||
# Extract VG and LV names
|
||||
VG_NAME=$(lvs --noheadings -o vg_name "$SOURCE" | tr -d ' ')
|
||||
LV_NAME=$(lvs --noheadings -o lv_name "$SOURCE" | tr -d ' ')
|
||||
SNAPSHOT_NAME="${LV_NAME}_files_snap"
|
||||
SNAPSHOT_PATH="/dev/$VG_NAME/$SNAPSHOT_NAME"
|
||||
|
||||
# Get LV size to determine appropriate snapshot size
|
||||
LV_SIZE_BYTES=$(lvs --noheadings -o lv_size --units b "$SOURCE" | tr -d ' ' | sed 's/B$//')
|
||||
# Use different percentages based on options
|
||||
if [ "$GENEROUS_SNAPSHOTS" = true ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 4)) # 25%
|
||||
MIN_SIZE=$((2 * 1024 * 1024 * 1024)) # 2GB minimum
|
||||
else
|
||||
# Auto mode: 10% normally, 15% for large LVs
|
||||
if [ $LV_SIZE_BYTES -gt $((50 * 1024 * 1024 * 1024)) ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES * 15 / 100)) # 15% for >50GB
|
||||
else
|
||||
SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 10)) # 10% normally
|
||||
fi
|
||||
MIN_SIZE=$((1024 * 1024 * 1024)) # 1GB minimum
|
||||
fi
|
||||
|
||||
if [ $SNAPSHOT_SIZE_BYTES -lt $MIN_SIZE ]; then
|
||||
SNAPSHOT_SIZE_BYTES=$MIN_SIZE
|
||||
fi
|
||||
SNAPSHOT_SIZE_GB=$((SNAPSHOT_SIZE_BYTES / 1073741824))
|
||||
SNAPSHOT_SIZE="${SNAPSHOT_SIZE_GB}G"
|
||||
|
||||
info "This will backup LV files to Borg repository (space-efficient)"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Source LV: $SOURCE${NC}"
|
||||
echo -e "${YELLOW}Repository: $TARGET${NC}"
|
||||
echo -e "${BLUE}Snapshot size: $SNAPSHOT_SIZE${NC}"
|
||||
echo -e "${BLUE}Mode: File-level backup (skips empty blocks)${NC}"
|
||||
if [ "$NEW_REPO" = true ]; then
|
||||
echo -e "${BLUE}Will create new repository${NC}"
|
||||
else
|
||||
echo -e "${BLUE}Will add to existing repository${NC}"
|
||||
fi
|
||||
echo -e "${BLUE}Encryption: $ENCRYPTION${NC}"
|
||||
echo ""
|
||||
read -p "Continue? (yes/no): " confirm
|
||||
|
||||
if [ "$confirm" != "yes" ]; then
|
||||
echo "Backup cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Initialize repository if needed
|
||||
if [ "$NEW_REPO" = true ]; then
|
||||
log "Creating new Borg repository: $TARGET"
|
||||
if [ "$ENCRYPTION" = "none" ]; then
|
||||
borg init --encryption=none "$TARGET" || error "Failed to initialize repository"
|
||||
else
|
||||
borg init --encryption="$ENCRYPTION" "$TARGET" || error "Failed to initialize repository"
|
||||
fi
|
||||
log "Repository initialized successfully"
|
||||
fi
|
||||
|
||||
log "Creating snapshot of source LV"
|
||||
lvcreate -L"$SNAPSHOT_SIZE" -s -n "$SNAPSHOT_NAME" "$SOURCE" || error "Failed to create snapshot"
|
||||
|
||||
# Create temporary mount point
|
||||
TEMP_MOUNT=$(mktemp -d -t borg_files_backup_XXXXXX)
|
||||
|
||||
# Mount snapshot
|
||||
log "Mounting snapshot to $TEMP_MOUNT"
|
||||
mount "$SNAPSHOT_PATH" "$TEMP_MOUNT" || error "Failed to mount snapshot"
|
||||
|
||||
# Create Borg archive
|
||||
ARCHIVE_NAME="files_${LV_NAME}_$(date +%Y%m%d_%H%M%S)"
|
||||
log "Creating Borg archive (file-level): $ARCHIVE_NAME"
|
||||
log "Backing up files from mounted snapshot..."
|
||||
log "This is space-efficient and skips empty blocks"
|
||||
|
||||
borg create --progress --stats --compression auto,zstd "$TARGET::$ARCHIVE_NAME" "$TEMP_MOUNT" || error "Borg file-level backup failed"
|
||||
|
||||
log "File-level Borg backup completed successfully"
|
||||
|
||||
# Cleanup
|
||||
log "Cleaning up mount point and snapshot"
|
||||
umount "$TEMP_MOUNT" || warn "Failed to unmount"
|
||||
rmdir "$TEMP_MOUNT"
|
||||
lvremove -f "$SNAPSHOT_PATH" || warn "Failed to remove snapshot"
|
||||
SNAPSHOT_PATH=""
|
||||
|
||||
log "Files to Borg backup completed successfully"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -45,12 +45,15 @@ class SimpleBackupGUI:
|
||||
ttk.Radiobutton(mode_frame, text="Entire VG → Device",
|
||||
variable=self.mode_var, value="vg_to_raw",
|
||||
command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Radiobutton(mode_frame, text="LV → Borg Repo",
|
||||
ttk.Radiobutton(mode_frame, text="LV → Borg (block)",
|
||||
variable=self.mode_var, value="lv_to_borg",
|
||||
command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Radiobutton(mode_frame, text="VG → Borg Repo",
|
||||
ttk.Radiobutton(mode_frame, text="VG → Borg (block)",
|
||||
variable=self.mode_var, value="vg_to_borg",
|
||||
command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
|
||||
ttk.Radiobutton(mode_frame, text="Files → Borg",
|
||||
variable=self.mode_var, value="files_to_borg",
|
||||
command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Source selection
|
||||
ttk.Label(main_frame, text="Source:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
@@ -88,14 +91,32 @@ class SimpleBackupGUI:
|
||||
values=["none", "repokey", "keyfile"], state="readonly", width=15)
|
||||
encryption_combo.grid(row=2, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
# Snapshot size option
|
||||
ttk.Label(self.borg_frame, text="Snapshot Size:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
||||
self.snapshot_size_var = tk.StringVar(value="auto")
|
||||
snapshot_combo = ttk.Combobox(self.borg_frame, textvariable=self.snapshot_size_var,
|
||||
values=["auto", "conservative", "generous"], state="readonly", width=15)
|
||||
snapshot_combo.grid(row=3, column=1, sticky=tk.W, pady=2)
|
||||
|
||||
# Help text for snapshot sizes
|
||||
help_label = ttk.Label(self.borg_frame, text="auto: smart sizing, conservative: 5%, generous: 25%",
|
||||
font=("TkDefaultFont", 8), foreground="gray")
|
||||
help_label.grid(row=3, column=2, sticky=tk.W, padx=(5, 0), pady=2)
|
||||
|
||||
# Read-only mode for full filesystems
|
||||
self.readonly_mode = tk.BooleanVar()
|
||||
ttk.Checkbutton(self.borg_frame, text="Read-only mode (for very full filesystems)",
|
||||
variable=self.readonly_mode).grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=2)
|
||||
|
||||
# Passphrase
|
||||
ttk.Label(self.borg_frame, text="Passphrase:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
||||
ttk.Label(self.borg_frame, text="Passphrase:").grid(row=4, column=0, sticky=tk.W, pady=2)
|
||||
self.passphrase_var = tk.StringVar()
|
||||
passphrase_entry = ttk.Entry(self.borg_frame, textvariable=self.passphrase_var, show="*", width=40)
|
||||
passphrase_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), pady=2)
|
||||
passphrase_entry.grid(row=4, column=1, sticky=(tk.W, tk.E), pady=2)
|
||||
|
||||
# Configure borg frame grid
|
||||
self.borg_frame.columnconfigure(1, weight=1)
|
||||
self.borg_frame.columnconfigure(2, weight=0)
|
||||
|
||||
# Refresh button
|
||||
ttk.Button(main_frame, text="Refresh", command=self.refresh_drives).grid(row=4, column=0, pady=10)
|
||||
@@ -165,6 +186,26 @@ class SimpleBackupGUI:
|
||||
self.log_text.see(tk.END)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def calculate_snapshot_size(self, lv_size_bytes):
|
||||
"""Calculate appropriate snapshot size based on LV size and user preference"""
|
||||
mode = self.snapshot_size_var.get()
|
||||
|
||||
if mode == "conservative":
|
||||
# 5% of LV size, minimum 1GB
|
||||
snapshot_size_bytes = max(lv_size_bytes // 20, 1024**3)
|
||||
elif mode == "generous":
|
||||
# 25% of LV size, minimum 2GB
|
||||
snapshot_size_bytes = max(lv_size_bytes // 4, 2 * 1024**3)
|
||||
else: # auto
|
||||
# 10% of LV size, minimum 1GB, but increase for very large LVs
|
||||
base_percentage = 10
|
||||
if lv_size_bytes > 50 * 1024**3: # >50GB
|
||||
base_percentage = 15 # Use 15% for large, potentially active LVs
|
||||
snapshot_size_bytes = max(lv_size_bytes * base_percentage // 100, 1024**3)
|
||||
|
||||
snapshot_size_gb = snapshot_size_bytes // (1024**3)
|
||||
return f"{snapshot_size_gb}G"
|
||||
|
||||
def run_command(self, cmd, show_output=True):
|
||||
"""Run a command and return result"""
|
||||
try:
|
||||
@@ -254,7 +295,7 @@ class SimpleBackupGUI:
|
||||
else:
|
||||
self.log("No target LVs found")
|
||||
self.target_combo['values'] = []
|
||||
elif mode in ["lv_to_borg", "vg_to_borg"]:
|
||||
elif mode in ["lv_to_borg", "vg_to_borg", "files_to_borg"]:
|
||||
# No target combo for Borg modes - repository path is used
|
||||
self.target_combo['values'] = []
|
||||
self.log("Using Borg repository path instead of target device")
|
||||
@@ -283,7 +324,7 @@ class SimpleBackupGUI:
|
||||
mode = self.mode_var.get()
|
||||
|
||||
# Validate inputs based on mode
|
||||
if mode in ["lv_to_borg", "vg_to_borg"]:
|
||||
if mode in ["lv_to_borg", "vg_to_borg", "files_to_borg"]:
|
||||
if not self.source_var.get() or not self.repo_path_var.get():
|
||||
messagebox.showerror("Error", "Please select source and repository path")
|
||||
return
|
||||
@@ -299,14 +340,22 @@ class SimpleBackupGUI:
|
||||
|
||||
# Build confirmation message for Borg
|
||||
if mode == "lv_to_borg":
|
||||
msg = f"Borg backup of LV:\n\nSource LV: {source}\nRepository: {repo_path}\n\n"
|
||||
msg = f"Borg backup of LV (block-level):\n\nSource LV: {source}\nRepository: {repo_path}\n\n"
|
||||
if self.create_new_repo.get():
|
||||
msg += "This will create a new Borg repository.\n"
|
||||
else:
|
||||
msg += "This will add to existing Borg repository.\n"
|
||||
msg += f"Encryption: {self.encryption_var.get()}\n\nContinue?"
|
||||
elif mode == "files_to_borg":
|
||||
msg = f"Borg backup of LV (file-level):\n\nSource LV: {source}\nRepository: {repo_path}\n\n"
|
||||
msg += "This will backup files from the LV (space-efficient).\n"
|
||||
if self.create_new_repo.get():
|
||||
msg += "This will create a new Borg repository.\n"
|
||||
else:
|
||||
msg += "This will add to existing Borg repository.\n"
|
||||
msg += f"Encryption: {self.encryption_var.get()}\n\nContinue?"
|
||||
else: # vg_to_borg
|
||||
msg = f"Borg backup of entire VG:\n\nSource VG: {source}\nRepository: {repo_path}\n\n"
|
||||
msg = f"Borg backup of entire VG (block-level):\n\nSource VG: {source}\nRepository: {repo_path}\n\n"
|
||||
if self.create_new_repo.get():
|
||||
msg += "This will create a new Borg repository.\n"
|
||||
else:
|
||||
@@ -362,6 +411,8 @@ class SimpleBackupGUI:
|
||||
self.backup_vg_to_raw(source, target)
|
||||
elif mode == "lv_to_borg":
|
||||
self.backup_lv_to_borg(source, target)
|
||||
elif mode == "files_to_borg":
|
||||
self.backup_files_to_borg(source, target)
|
||||
elif mode == "vg_to_borg":
|
||||
self.backup_vg_to_borg(source, target)
|
||||
|
||||
@@ -522,10 +573,7 @@ class SimpleBackupGUI:
|
||||
success, lv_size_output = self.run_command(f"lvs --noheadings -o lv_size --units b {source_lv}", show_output=False)
|
||||
if success:
|
||||
lv_size_bytes = int(lv_size_output.strip().replace('B', ''))
|
||||
# Use 10% of LV size or minimum 1GB for snapshot
|
||||
snapshot_size_bytes = max(lv_size_bytes // 10, 1024**3) # Min 1GB
|
||||
snapshot_size_gb = snapshot_size_bytes // (1024**3)
|
||||
snapshot_size = f"{snapshot_size_gb}G"
|
||||
snapshot_size = self.calculate_snapshot_size(lv_size_bytes)
|
||||
else:
|
||||
snapshot_size = "2G" # Default fallback
|
||||
|
||||
@@ -624,10 +672,7 @@ class SimpleBackupGUI:
|
||||
success, lv_size_output = self.run_command(f"lvs --noheadings -o lv_size --units b {lv_path}", show_output=False)
|
||||
if success:
|
||||
lv_size_bytes = int(lv_size_output.strip().replace('B', ''))
|
||||
# Use 10% of LV size or minimum 1GB for snapshot
|
||||
snapshot_size_bytes = max(lv_size_bytes // 10, 1024**3) # Min 1GB
|
||||
snapshot_size_gb = snapshot_size_bytes // (1024**3)
|
||||
snapshot_size = f"{snapshot_size_gb}G"
|
||||
snapshot_size = self.calculate_snapshot_size(lv_size_bytes)
|
||||
else:
|
||||
snapshot_size = "2G" # Default fallback
|
||||
|
||||
@@ -692,6 +737,98 @@ class SimpleBackupGUI:
|
||||
|
||||
self.current_snapshot = None
|
||||
|
||||
def backup_files_to_borg(self, source_lv, repo_path):
|
||||
"""Backup LV files to Borg repository (file-level via snapshot mount)"""
|
||||
self.log("Mode: LV to Borg Repository (file-level backup)")
|
||||
self.log("This will mount an LV snapshot and backup files (space-efficient)")
|
||||
|
||||
# Set up environment for Borg
|
||||
borg_env = os.environ.copy()
|
||||
if self.passphrase_var.get():
|
||||
borg_env['BORG_PASSPHRASE'] = self.passphrase_var.get()
|
||||
|
||||
# Initialize repository if needed
|
||||
if self.create_new_repo.get():
|
||||
self.log(f"Creating new Borg repository: {repo_path}")
|
||||
encryption = self.encryption_var.get()
|
||||
if encryption == "none":
|
||||
init_cmd = f"borg init --encryption=none {repo_path}"
|
||||
else:
|
||||
init_cmd = f"borg init --encryption={encryption} {repo_path}"
|
||||
|
||||
result = subprocess.run(init_cmd, shell=True, capture_output=True, text=True, env=borg_env)
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"Failed to initialize Borg repository: {result.stderr}")
|
||||
self.log("Repository initialized successfully")
|
||||
|
||||
# Create snapshot
|
||||
vg_name = source_lv.split('/')[2]
|
||||
lv_name = source_lv.split('/')[3]
|
||||
snapshot_name = f"{lv_name}_files_snap"
|
||||
self.current_snapshot = f"/dev/{vg_name}/{snapshot_name}"
|
||||
|
||||
# Get LV size to determine appropriate snapshot size
|
||||
success, lv_size_output = self.run_command(f"lvs --noheadings -o lv_size --units b {source_lv}", show_output=False)
|
||||
if success:
|
||||
lv_size_bytes = int(lv_size_output.strip().replace('B', ''))
|
||||
snapshot_size = self.calculate_snapshot_size(lv_size_bytes)
|
||||
else:
|
||||
snapshot_size = "2G" # Default fallback
|
||||
|
||||
self.log(f"Creating snapshot: {snapshot_name} (size: {snapshot_size})")
|
||||
success, output = self.run_command(f"lvcreate -L{snapshot_size} -s -n {snapshot_name} {source_lv}")
|
||||
if not success:
|
||||
raise Exception(f"Failed to create snapshot: {output}")
|
||||
|
||||
# Create temporary mount point
|
||||
import tempfile
|
||||
temp_mount = tempfile.mkdtemp(prefix="borg_files_backup_")
|
||||
|
||||
try:
|
||||
# Mount the snapshot
|
||||
self.log(f"Mounting snapshot to {temp_mount}")
|
||||
success, output = self.run_command(f"mount {self.current_snapshot} {temp_mount}")
|
||||
if not success:
|
||||
raise Exception(f"Failed to mount snapshot: {output}")
|
||||
|
||||
# Create Borg backup of files
|
||||
archive_name = f"files_{lv_name}_{time.strftime('%Y%m%d_%H%M%S')}"
|
||||
self.log(f"Creating Borg archive (file-level): {archive_name}")
|
||||
self.log("Backing up files from mounted snapshot...")
|
||||
self.log("This is space-efficient and skips empty blocks")
|
||||
|
||||
borg_cmd = f"borg create --progress --stats --compression auto,zstd {repo_path}::{archive_name} {temp_mount}"
|
||||
|
||||
# Run borg with environment
|
||||
process = subprocess.Popen(borg_cmd, shell=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True, env=borg_env)
|
||||
|
||||
# Stream output
|
||||
for line in process.stdout:
|
||||
self.log(line.strip())
|
||||
|
||||
process.wait()
|
||||
if process.returncode != 0:
|
||||
raise Exception("Borg file-level backup failed")
|
||||
|
||||
self.log("File-level Borg backup completed successfully")
|
||||
|
||||
finally:
|
||||
# Cleanup: unmount and remove temp directory
|
||||
self.log("Cleaning up mount point")
|
||||
self.run_command(f"umount {temp_mount}", show_output=False)
|
||||
os.rmdir(temp_mount)
|
||||
|
||||
# Remove snapshot
|
||||
self.log("Cleaning up snapshot")
|
||||
success, output = self.run_command(f"lvremove -f {self.current_snapshot}")
|
||||
if not success:
|
||||
self.log(f"Warning: Failed to remove snapshot: {output}")
|
||||
else:
|
||||
self.log("Snapshot cleaned up")
|
||||
|
||||
self.current_snapshot = None
|
||||
|
||||
def cleanup_on_error(self):
|
||||
"""Clean up on error"""
|
||||
if self.current_snapshot:
|
||||
|
||||
Reference in New Issue
Block a user