update +++

This commit is contained in:
Kaby_Kun 2025-05-24 00:30:33 +02:00
parent 0692bf0a34
commit 2ff30823d2

View file

@ -11,7 +11,14 @@ TMP_BACKUP="/tmp/$BACKUP_NAME"
CHUNK_SIZE="10G" CHUNK_SIZE="10G"
MIN_SPACE_REQUIRED="5G" # Minimum space required in /tmp MIN_SPACE_REQUIRED="5G" # Minimum space required in /tmp
LOG="/var/log/pbs-smb-backup.log" LOG="/var/log/pbs-smb-backup.log"
SRC="/etc /root /mypool" SRC="/mypool /etc" # Only scan these directories
DB_DIR="/var/lib/pbs-backup"
DB_FILE="$DB_DIR/index.db"
# Global variables for cleanup
CLEANUP_IN_PROGRESS=0
MONITOR_PID=0 # No longer used for background monitor, kept for potential future use
CURRENT_SCAN_DIR=""
# Exclusion patterns # Exclusion patterns
EXCLUDE_PATTERNS=( EXCLUDE_PATTERNS=(
@ -58,7 +65,577 @@ EXCLUDE_PATTERNS=(
"*.vswp" "*.vswp"
) )
MAX_BACKUPS=3 # Keep exactly 3 backups # Build find command exclusions
build_find_exclusions() {
# Create a temporary file with exclusion patterns
local exclude_file="$TMP_BACKUP/exclude_patterns.txt"
printf "%s\n" "${EXCLUDE_PATTERNS[@]}" > "$exclude_file"
# Build the find command with optimized exclusions
echo "-type f -not -path '*/\.*' -not -path '*/node_modules/*' $(printf -- "-not -name '%s' " "${EXCLUDE_PATTERNS[@]}")"
}
# Function to handle script termination
handle_termination() {
local signal=$1
local exit_code=$2
# Prevent multiple cleanup attempts
if [ "$CLEANUP_IN_PROGRESS" -eq 1 ]; then
exit $exit_code
fi
CLEANUP_IN_PROGRESS=1
# Clear the current line before printing cleanup message
echo -ne "\r\033[K"
echo "[$(date)] ⚠️ Received signal $signal. Cleaning up..." | tee -a "$LOG"
# Kill background processes (No background monitor anymore, but clean up potential child find/stat processes)
# We can add more specific process killing here if needed later
pgrep -P $$ | xargs -r kill
# Cleanup temporary files
if [ -d "$TMP_BACKUP" ]; then
echo "[$(date)] Cleaning up temporary files..." | tee -a "$LOG"
rm -rf "$TMP_BACKUP"
fi
# Unmount share if mounted
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Unmounting share..." | tee -a "$LOG"
cleanup_processes "$MOUNT_POINT"
unmount_share
fi
# If it was a Ctrl+Z, kill the script
if [ "$signal" = "SIGTSTP" ]; then
echo "[$(date)] ❌ Script terminated by Ctrl+Z" | tee -a "$LOG"
kill -9 $$
fi
# For Ctrl+C, exit gracefully
if [ "$signal" = "SIGINT" ]; then
echo "[$(date)] 👋 Script terminated by Ctrl+C" | tee -a "$LOG"
exit $exit_code
fi
}
# Set up signal handlers
trap 'handle_termination SIGINT 130' SIGINT
trap 'handle_termination SIGTSTP 146' SIGTSTP
trap 'handle_termination EXIT $?' EXIT
# Function to process a batch of files
process_file_batch() {
local batch_file="$1"
local db_file="$2"
local temp_sql="$TMP_BACKUP/temp_$(basename "$batch_file").sql"
local batch_num=$(basename "$batch_file" | sed 's/batch_//')
echo "[$(date)] 📦 Processing batch $batch_num..." | tee -a "$LOG"
# Create SQL file with all inserts
echo "BEGIN TRANSACTION;" > "$temp_sql"
local count=0
while read -r file; do
((count++))
# Get file metadata
local size=$(stat -c %s "$file" 2>/dev/null)
local mtime=$(stat -c %Y "$file" 2>/dev/null)
local hash=$(md5sum "$file" 2>/dev/null | cut -d' ' -f1)
# Show file being processed every 100 files
if ((count % 100 == 0)); then
echo "[$(date)] 📄 Processing: $file (${size} bytes)" | tee -a "$LOG"
fi
# Escape single quotes in file path
file=$(echo "$file" | sed "s/'/''/g")
# Add to SQL file
echo "INSERT OR REPLACE INTO file_index (path, size, mtime, hash, file_exists) VALUES ('$file', $size, $mtime, '$hash', 1);" >> "$temp_sql"
done < "$batch_file"
echo "COMMIT;" >> "$temp_sql"
echo "[$(date)] 💾 Writing batch $batch_num to database ($count files)..." | tee -a "$LOG"
# Execute SQL file
sqlite3 "$db_file" < "$temp_sql"
rm -f "$temp_sql"
echo "[$(date)] ✅ Completed batch $batch_num" | tee -a "$LOG"
}
# Function to check and update database schema
check_and_update_schema() {
echo "[$(date)] 🔍 Checking database schema..." | tee -a "$LOG"
# Check if scan_time column exists
if ! sqlite3 "$DB_FILE" "SELECT scan_time FROM file_index LIMIT 1;" > /dev/null 2>&1; then
echo "[$(date)] ⚠️ Database schema needs update. Adding scan_time column..." | tee -a "$LOG"
# Create backup of current database
local backup_file="${DB_FILE}.bak.$(date +%s)"
cp "$DB_FILE" "$backup_file"
echo "[$(date)] 📦 Created database backup: $backup_file" | tee -a "$LOG"
# Add scan_time column
sqlite3 "$DB_FILE" << EOF
BEGIN TRANSACTION;
ALTER TABLE file_index ADD COLUMN scan_time INTEGER;
CREATE INDEX IF NOT EXISTS idx_scan_time ON file_index(scan_time);
COMMIT;
EOF
if [ $? -eq 0 ]; then
echo "[$(date)] ✅ Database schema updated successfully" | tee -a "$LOG"
else
echo "[$(date)] ❌ Failed to update schema. Restoring backup..." | tee -a "$LOG"
mv "$backup_file" "$DB_FILE"
exit 1
fi
fi
}
# Initialize SQLite database if it doesn't exist
init_database() {
echo "[$(date)] 📊 Initializing SQLite database..." | tee -a "$LOG"
# Create database directory if it doesn't exist
if [ ! -d "$DB_DIR" ]; then
echo "[$(date)] Creating database directory $DB_DIR..." | tee -a "$LOG"
mkdir -p "$DB_DIR" || {
echo "[$(date)] ❌ Failed to create database directory. Trying alternative location..." | tee -a "$LOG"
DB_DIR="/root/.pbs-backup"
DB_FILE="$DB_DIR/index.db"
mkdir -p "$DB_DIR" || {
echo "[$(date)] ❌ Failed to create alternative database directory. Exiting." | tee -a "$LOG"
exit 1
}
}
fi
# Ensure proper permissions
chmod 700 "$DB_DIR"
# Create database if it doesn't exist
if [ ! -f "$DB_FILE" ]; then
echo "[$(date)] Creating new database file..." | tee -a "$LOG"
sqlite3 "$DB_FILE" << EOF
CREATE TABLE IF NOT EXISTS file_index (
path TEXT PRIMARY KEY,
size INTEGER,
mtime INTEGER,
hash TEXT,
last_backup TEXT,
file_exists INTEGER DEFAULT 1,
scan_time INTEGER
);
CREATE INDEX IF NOT EXISTS idx_mtime ON file_index(mtime);
CREATE INDEX IF NOT EXISTS idx_exists ON file_index(file_exists);
CREATE INDEX IF NOT EXISTS idx_scan_time ON file_index(scan_time);
EOF
chmod 600 "$DB_FILE"
else
# Check and update schema if needed
check_and_update_schema
fi
# Verify database is accessible
if ! sqlite3 "$DB_FILE" "SELECT 1;" > /dev/null 2>&1; then
echo "[$(date)] ❌ Failed to access database. Please check permissions." | tee -a "$LOG"
exit 1
fi
echo "[$(date)] ✅ Database initialized successfully at $DB_FILE" | tee -a "$LOG"
}
# Function to scan a single directory from a file list
scan_directory() {
local file_list="$1"
local dir=$(dirname "$file_list") # Infer directory from the file list path
local current_scan_time=$(date +%s) # Use a single scan time for the directory
local temp_file_prefix="$TMP_BACKUP/scan_$(basename "$dir")"
local start_time=$(date +%s)
local count=0
local files_per_second=0
local batch_size=1000
local current_batch=0
local changed_files=0
local new_files=0
local unchanged_files=0
local total_files_in_dir=$(wc -l < "$file_list") # Accurate count from the find output
local last_status_update_time=$start_time
local batch_start_time=$start_time
CURRENT_SCAN_DIR="$dir"
echo "[$(date)] 🔍 Scanning directory: $dir ($total_files_in_dir files)" | tee -a "$LOG"
# Create temporary directory for batch files
mkdir -p "$TMP_BACKUP/batches"
local current_batch_sql_file="$TMP_BACKUP/batches/batch_$current_batch.sql"
> "$current_batch_sql_file" # Create/clear the first batch file
# --- Step 1: Pre-load existing file metadata from DB ---
echo "[$(date)] 📚 Pre-loading existing file metadata from database for $dir..." | tee -a "$LOG"
local db_metadata_file="$TMP_BACKUP/db_metadata_$(basename "$dir").csv"
# Export relevant data from DB to a temporary file
sqlite3 -separator '|' "$DB_FILE" "SELECT path, size, mtime, hash FROM file_index WHERE path LIKE '$dir/%';" > "$db_metadata_file"
# Read DB metadata into an associative array (if bash version supports it)
declare -A db_files_metadata
if [ -f "$db_metadata_file" ]; then
while IFS='|' read -r path size mtime hash; do
db_files_metadata["$path"]="$size|$mtime|$hash"
done < "$db_metadata_file"
rm -f "$db_metadata_file"
fi
echo "[$(date)] ✅ Pre-loading complete. Loaded ${#db_files_metadata[@]} entries."
# ---
# Read files from the pre-generated list and process them
while read -r file; do
# Skip if file doesn't exist (shouldn't happen with find output, but safety)
[ -f "$file" ] || continue
((count++))
# Get file metadata from filesystem
local size=$(stat -c %s "$file" 2>/dev/null)
local mtime=$(stat -c %Y "$file" 2>/dev/null)
local hash=$(md5sum "$file" 2>/dev/null | cut -d' ' -f1)
# Escape single quotes in file path for SQL
local escaped_file=$(echo "$file" | sed "s/'/''/g")
local needs_batch_update=0
# --- Step 2: Check against pre-loaded metadata instead of querying DB ---
local db_data=${db_files_metadata["$file"]}
if [ -n "$db_data" ]; then
IFS='|' read -r db_size db_mtime db_hash <<< "$db_data"
if [ "$size" = "$db_size" ] && [ "$mtime" = "$db_mtime" ] && [ "$hash" = "$db_hash" ]; then
# File unchanged, count and skip adding to batch
((unchanged_files++))
# No DB write needed here, file_exists will be set to 1 by INSERT OR REPLACE if it's in the batch
# But since unchanged files are NOT added to the batch, their file_exists=0 status
# from the initial UPDATE needs to be flipped. This is a key difference.
# Let's add them to a separate temporary file for a batch UPDATE at the end of the directory scan.
echo "$escaped_file" >> "$TMP_BACKUP/batches/unchanged_batch_$current_batch.sql"
needs_batch_update=0
else
# File changed
((changed_files++))
needs_batch_update=1
fi
else
# New file
((new_files++))
needs_batch_update=1
fi
# ---
# Add to current batch only if new or changed
if [ $needs_batch_update -eq 1 ]; then
echo "INSERT OR REPLACE INTO file_index (path, size, mtime, hash, file_exists, scan_time) VALUES ('$escaped_file', $size, $mtime, '$hash', 1, $current_scan_time);" >> "$current_batch_sql_file"
fi
# Update status line periodically (e.g., every 100 files or every 2 seconds)
local current_time=$(date +%s)
local elapsed_status_time=$((current_time - last_status_update_time))
# Update terminal every 100 files or 2 seconds
if ((count % 100 == 0 || elapsed_status_time >= 2)); then
local processed_rate=$((count / $((current_time - start_time + 1)) )) # Files per second overall
local progress=$((count * 100 / total_files_in_dir))
if [ $progress -gt 100 ]; then progress=100; fi
local bar_length=50
local filled=$((progress * bar_length / 100))
local empty=$((bar_length - filled))
local bar="["
for ((i=0; i<filled; i++)); do bar+="="; done
for ((i=0; i<empty; i++)); do bar+=" "; done
bar+="]"
# Status line using in-memory counts, not querying DB frequently
echo -ne "\r[$(date)] 📊 $bar $progress% | Processed: $count (N:$new_files C:$changed_files U:$unchanged_files) | Rate: ${processed_rate}/s | Batch: $current_batch"
last_status_update_time=$current_time
fi
# Process batch if it reaches batch_size and contains new/changed files
local current_batch_lines=$(wc -l < "$current_batch_sql_file" || echo 0)
if (( current_batch_lines >= batch_size )); then
local batch_write_start_time=$(date +%s)
# Execute batch
# Clear current line before printing batch write message
echo -ne "\r\033[K"
echo "[$(date)] 💾 Writing batch $current_batch to database..." | tee -a "$LOG"
# Add retry logic for database locked error
local db_write_success=0
local retry_count=0
while [ $retry_count -lt 15 ]; do # Increased retry attempts
if sqlite3 "$DB_FILE" "BEGIN TRANSACTION;\n$(cat "$current_batch_sql_file")\nCOMMIT;" 2>/dev/null; then
db_write_success=1
break
else
retry_count=$((retry_count + 1))
echo "[$(date)] ⚠️ Database locked, retry $retry_count..." | tee -a "$LOG"
sleep 0.3 # Shorter sleep for quicker retries
fi
done
if [ $db_write_success -eq 0 ]; then
echo "[$(date)] ❌ Failed to write batch $current_batch after multiple retries. Exiting." | tee -a "$LOG"
exit 1
fi
echo "[$(date)] ✅ Batch $current_batch written." | tee -a "$LOG"
# Calculate batch write rate
local batch_write_end_time=$(date +%s)
local batch_write_elapsed=$((batch_write_end_time - batch_write_start_time + 1))
local batch_file_count=$((current_batch_lines))
local batch_write_rate=$((batch_file_count / batch_write_elapsed))
echo "[$(date)] ⚡ Batch Write Rate: ${batch_write_rate}/s" | tee -a "$LOG"
# Update overall status line after batch commit to reflect new DB state
local current_time=$(date +%s)
local processed_rate=$((count / $((current_time - start_time + 1)) ))
local progress=$((count * 100 / total_files_in_dir))
if [ $progress -gt 100 ]; then progress=100; fi
local bar_length=50
local filled=$((progress * bar_length / 100))
local empty=$((bar_length - filled))
local bar="["
for ((i=0; i<filled; i++)); do bar+="="; done
for ((i=0; i<empty; i++)); do bar+=" "; done
bar+="]"
# Get current DB status for combined output (only read after a batch commit)
local db_size=$(stat -c %s "$DB_FILE" 2>/dev/null)
local db_size_mb=$((db_size / 1024 / 1024))
local db_files=$(sqlite3 "$DB_FILE" "SELECT COUNT(*) FROM file_index WHERE file_exists = 1;" 2>/dev/null)
echo -ne "\r[$(date)] 📊 $bar $progress% | Processed: $count (N:$new_files C:$changed_files U:$unchanged_files) | Rate: ${processed_rate}/s | DB: ${db_size_mb}MB, Indexed: $db_files"
last_status_update_time=$current_time
# Move to next batch file
rm -f "$current_batch_sql_file"
((current_batch++))
current_batch_sql_file="$TMP_BACKUP/batches/batch_$current_batch.sql"
> "$current_batch_sql_file" # Create/clear the next batch file
fi
done < "$file_list"
# Process any remaining files in the last batch
local current_batch_lines=$(wc -l < "$current_batch_sql_file" || echo 0)
if (( current_batch_lines > 0 )); then # Check if the last batch file has content
local batch_write_start_time=$(date +%s)
# Clear current line before printing final batch write message
echo -ne "\r\033[K"
echo "[$(date)] 💾 Writing final batch to database..." | tee -a "$LOG"
# Add retry logic for database locked error
local db_write_success=0
local retry_count=0
while [ $retry_count -lt 15 ]; do # Increased retry attempts
if sqlite3 "$DB_FILE" "BEGIN TRANSACTION;\n$(cat "$current_batch_sql_file")\nCOMMIT;" 2>/dev/null; then
db_write_success=1
break
else
retry_count=$((retry_count + 1))
echo "[$(date)] ⚠️ Database locked, retry $retry_count..." | tee -a "$LOG"
sleep 0.5 # Shorter sleep for quicker retries
fi
done
if [ $db_write_success -eq 0 ]; then
echo "[$(date)] ❌ Failed to write final batch after multiple retries. Exiting." | tee -a "$LOG"
exit 1
fi
echo "[$(date)] ✅ Final batch written." | tee -a "$LOG"
# Calculate batch write rate
local batch_write_end_time=$(date +%s)
local batch_write_elapsed=$((batch_write_end_time - batch_write_start_time + 1))
local batch_file_count=$((current_batch_lines))
local batch_write_rate=$((batch_file_count / batch_write_elapsed))
echo "[$(date)] ⚡ Final Batch Write Rate: ${batch_write_rate}/s" | tee -a "$LOG"
# Cleanup batch file
rm -f "$current_batch_sql_file"
else
# If the last batch file is empty, just remove it
rm -f "$current_batch_sql_file"
fi
# --- Step 3: Batch update scan_time for unchanged files ---
local unchanged_batch_file="$TMP_BACKUP/batches/unchanged_batch_$current_batch.sql"
if [ -f "$unchanged_batch_file" ] && [ $(wc -l < "$unchanged_batch_file" || echo 0) -gt 0 ]; then
echo "[$(date)] 🔄 Updating scan time for unchanged files in database..." | tee -a "$LOG"
local update_success=0
local retry_count=0
while [ $retry_count -lt 15 ]; do # Increased retry attempts
if sqlite3 "$DB_FILE" "BEGIN TRANSACTION;\nUPDATE file_index SET scan_time = $current_scan_time WHERE path IN ('$(cat "$unchanged_batch_file" | paste -sd ',' -)');\nCOMMIT;" 2>/dev/null; then
update_success=1
break
else
retry_count=$((retry_count + 1))
echo "[$(date)] ⚠️ Database locked during unchanged update, retry $retry_count..." | tee -a "$LOG"
sleep 0.5
fi
done
if [ $update_success -eq 0 ]; then
echo "[$(date)] ❌ Failed to update scan time for unchanged files. Exiting." | tee -a "$LOG"
exit 1
fi
echo "[$(date)] ✅ Scan time updated for $unchanged_files unchanged files."
rm -f "$unchanged_batch_file"
elif [ -f "$unchanged_batch_file" ]; then
rm -f "$unchanged_batch_file"
fi
# ---
CURRENT_SCAN_DIR=""
# Final verification and statistics
# Clear the last progress line before printing final stats
echo -ne "\r\033[K"
local total_in_db=$(sqlite3 "$DB_FILE" "SELECT COUNT(*) FROM file_index;")
local db_size=$(stat -c %s "$DB_FILE" 2>/dev/null)
local db_size_mb=$((db_size / 1024 / 1024))
echo "[$(date)] 📊 Directory scan completed: $dir" | tee -a "$LOG"
echo "[$(date)] - Files processed: $count" | tee -a "$LOG"
echo "[$(date)] - New files: $new_files" | tee -a "$LOG"
echo "[$(date)] - Changed files: $changed_files" | tee -a "$LOG"
echo "[$(date)] - Unchanged files: $unchanged_files" | tee -a "$LOG"
echo "[$(date)] - Total files in database: $total_in_db" | tee -a "$LOG"
echo "[$(date)] - Database size: ${db_size_mb}MB" | tee -a "$LOG"
# Cleanup batch directory (done in update_file_list after all directories)
# rm -rf "$TMP_BACKUP/batches"
}
# Function to update file list in database (Incremental Scan)
update_file_list() {
echo "[$(date)] 🔄 Starting incremental file system scan..." | tee -a "$LOG"
# Mark all existing files as not found - will be updated to 1 during scan if found
echo "[$(date)] 🏷️ Marking existing files as potentially removed..." | tee -a "$LOG"
sqlite3 "$DB_FILE" "BEGIN TRANSACTION;"
sqlite3 "$DB_FILE" "UPDATE file_index SET file_exists = 0;"
sqlite3 "$DB_FILE" "COMMIT;"
# Create temporary directory for the find output and batches
mkdir -p "$TMP_BACKUP"
# Scan each directory
for dir in $SRC; do
if [ -d "$dir" ]; then
# Use find to create a temporary list of files first
local find_output_file="$TMP_BACKUP/scan_$(basename "$dir").find"
echo "[$(date)] 📋 Finding files in $dir..." | tee -a "$LOG"
# Add progress for the find command if 'pv' is available
if command -v pv >/dev/null 2>&1; then
find "$dir" -type f 2>/dev/null | pv -l -c -N "Finding files in $dir" > "$find_output_file"
else
find "$dir" -type f 2>/dev/null > "$find_output_file"
fi
if [ -s "$find_output_file" ]; then # Check if the find output file is not empty
scan_directory "$find_output_file"
else
echo "[$(date)] ⚠️ No files found in $dir" | tee -a "$LOG"
fi
rm -f "$find_output_file"
else
echo "[$(date)] ⚠️ Directory not found: $dir" | tee -a "$LOG"
fi
done
# Remove files that no longer exist
echo "[$(date)] 🧹 Cleaning up removed files..." | tee -a "$LOG"
sqlite3 "$DB_FILE" "BEGIN TRANSACTION;"
local removed_count=$(sqlite3 "$DB_FILE" "SELECT COUNT(*) FROM file_index WHERE file_exists = 0;")
sqlite3 "$DB_FILE" "DELETE FROM file_index WHERE file_exists = 0;"
sqlite3 "$DB_FILE" "COMMIT;"
echo "[$(date)] 🗑️ Removed $removed_count non-existent files" | tee -a "$LOG"
# Get final statistics
local total_files=$(sqlite3 "$DB_FILE" "SELECT COUNT(*) FROM file_index;")
local db_size=$(stat -c %s "$DB_FILE" 2>/dev/null)
local db_size_mb=$((db_size / 1024 / 1024))
echo "[$(date)] 📊 Scan completed:" | tee -a "$LOG"
echo "[$(date)] - Total files: $total_files" | tee -a "$LOG"
echo "[$(date)] - Database size: ${db_size_mb}MB" | tee -a "$LOG"
echo "[$(date)] - Directories scanned: $SRC" | tee -a "$LOG"
# Verify database integrity
echo "[$(date)] 🔍 Verifying database integrity..." | tee -a "$LOG"
if sqlite3 "$DB_FILE" "PRAGMA integrity_check;" | grep -q "ok"; then
echo "[$(date)] ✅ Database integrity check passed" | tee -a "$LOG"
else
echo "[$(date)] ❌ Database integrity check failed!" | tee -a "$LOG"
fi
# Cleanup temporary batch directory after all directories are scanned
rm -rf "$TMP_BACKUP/batches"
# Cleanup main temporary directory (if empty)
rmdir "$TMP_BACKUP" 2>/dev/null
}
# Function to get files that need backup
get_files_to_backup() {
sqlite3 "$DB_FILE" "SELECT path FROM file_index WHERE needs_backup = 1;"
}
# Function to check if file needs backup
needs_backup() {
local file="$1"
local current_mtime=$(stat -c %Y "$file")
local current_size=$(stat -c %s "$file")
local current_hash=$(md5sum "$file" | cut -d' ' -f1)
local db_data=$(sqlite3 "$DB_FILE" "SELECT size, mtime, hash FROM file_index WHERE path='$file';")
if [ -z "$db_data" ]; then
return 0 # File not in database, needs backup
fi
IFS='|' read -r db_size db_mtime db_hash <<< "$db_data"
if [ "$current_mtime" -ne "$db_mtime" ] || [ "$current_size" -ne "$db_size" ] || [ "$current_hash" != "$db_hash" ]; then
return 0 # File changed, needs backup
fi
return 1 # File unchanged, no backup needed
}
# Initialize database at script start
init_database
# Update file list in database
update_file_list
# Create a temporary file list with files that need backup
echo "[$(date)] 📋 Creating backup list..." | tee -a "$LOG"
get_files_to_backup > "$TMP_BACKUP/filelist.txt"
# Initialize variables for chunk creation
chunk_num=1
current_chunk_size=0
current_chunk_files=()
total_files=$(wc -l < "$TMP_BACKUP/filelist.txt")
processed_files=0
skipped_files=0
echo "[$(date)] 🔍 Starting backup process (files to backup: $total_files)..." | tee -a "$LOG"
# === Cleanup function === # === Cleanup function ===
cleanup_old_backups() { cleanup_old_backups() {
@ -141,25 +718,31 @@ cleanup_processes() {
echo "[$(date)] Attempt $attempt: Found processes: $pids" | tee -a "$LOG" echo "[$(date)] Attempt $attempt: Found processes: $pids" | tee -a "$LOG"
# First try SIGTERM # Kill each process individually and verify it's gone
kill -15 $pids 2>/dev/null for pid in $pids; do
sleep 5 if [ -e "/proc/$pid" ]; then
echo "[$(date)] Terminating process $pid..." | tee -a "$LOG"
kill -15 "$pid" 2>/dev/null
sleep 1
# Check if processes are still running # Check if process still exists
pids=$(lsof -t "$mount_point" 2>/dev/null | grep -v "$$") if [ -e "/proc/$pid" ]; then
if [ -n "$pids" ]; then echo "[$(date)] Process $pid still running, sending SIGKILL..." | tee -a "$LOG"
echo "[$(date)] Processes still running, sending SIGKILL..." | tee -a "$LOG" kill -9 "$pid" 2>/dev/null
kill -9 $pids 2>/dev/null sleep 1
sleep 5
fi fi
fi
done
# Final check # Final check for any remaining processes
if [ -z "$(lsof -t "$mount_point" 2>/dev/null | grep -v "$$")" ]; then pids=$(lsof -t "$mount_point" 2>/dev/null | grep -v "$$")
if [ -z "$pids" ]; then
echo "[$(date)] Successfully cleaned up all processes" | tee -a "$LOG" echo "[$(date)] Successfully cleaned up all processes" | tee -a "$LOG"
return 0 return 0
fi fi
((attempt++)) ((attempt++))
sleep 2
done done
echo "[$(date)] ⚠️ Warning: Could not clean up all processes after $max_attempts attempts" | tee -a "$LOG" echo "[$(date)] ⚠️ Warning: Could not clean up all processes after $max_attempts attempts" | tee -a "$LOG"
@ -173,32 +756,25 @@ mount_share() {
if mountpoint -q "$MOUNT_POINT"; then if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Share already mounted, attempting to unmount..." | tee -a "$LOG" echo "[$(date)] Share already mounted, attempting to unmount..." | tee -a "$LOG"
# Clean up any processes first # First try lazy unmount
echo "[$(date)] Attempting lazy unmount..." | tee -a "$LOG"
umount -l "$MOUNT_POINT" 2>/dev/null
sleep 2
# If still mounted, try to clean up processes and unmount
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Share still mounted, cleaning up processes..." | tee -a "$LOG"
cleanup_processes "$MOUNT_POINT" cleanup_processes "$MOUNT_POINT"
# Try to unmount gracefully first # Try normal unmount
umount "$MOUNT_POINT" 2>/dev/null umount "$MOUNT_POINT" 2>/dev/null
sleep 2 sleep 2
# If still mounted, try force unmount # If still mounted, try force unmount
if mountpoint -q "$MOUNT_POINT"; then if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Force unmounting..." | tee -a "$LOG" echo "[$(date)] Attempting force unmount..." | tee -a "$LOG"
umount -f "$MOUNT_POINT" 2>/dev/null umount -f "$MOUNT_POINT" 2>/dev/null
sleep 2 sleep 2
# If still mounted, try lazy unmount
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Lazy unmounting..." | tee -a "$LOG"
umount -l "$MOUNT_POINT" 2>/dev/null
sleep 2
# If still mounted, try one more time with process cleanup
if mountpoint -q "$MOUNT_POINT"; then
cleanup_processes "$MOUNT_POINT"
sleep 2
umount -f "$MOUNT_POINT" 2>/dev/null
sleep 2
fi
fi fi
fi fi
@ -365,40 +941,82 @@ mkdir -p "$BACKUP_DIR"
# Step 5: Create backup in chunks with space management # Step 5: Create backup in chunks with space management
echo "[$(date)] 🗜️ Starting backup with space management..." | tee -a "$LOG" echo "[$(date)] 🗜️ Starting backup with space management..." | tee -a "$LOG"
# Build find command exclusions
build_find_exclusions() {
local exclusions=""
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
exclusions+=" -not -path '*/$pattern/*' -not -name '$pattern'"
done
echo "$exclusions"
}
# Create a temporary file list with exclusions # Create a temporary file list with exclusions
echo "[$(date)] 📋 Creating file list (excluding unnecessary files)..." | tee -a "$LOG" echo "[$(date)] 📋 Creating file list (excluding unnecessary files)..." | tee -a "$LOG"
eval "find $SRC -type f $(build_find_exclusions) > \"$TMP_BACKUP/filelist.txt\""
# Function to check if pv is installed
check_pv() {
command -v pv >/dev/null 2>&1
}
# Function to show progress bar
show_progress() {
if check_pv; then
pv -l -s "$1" -N "Scanning files"
else
local count=0
local total="$1"
while read -r line; do
((count++))
echo -ne "\r[$(date)] 📋 Scanning files: $count/$total ($((count * 100 / total))%)" | tee -a "$LOG"
echo "$line"
done
echo
fi
}
# First, count total files to scan
echo "[$(date)] 🔍 Counting files to scan..." | tee -a "$LOG"
total_files=$(eval "find $SRC -type f $(build_find_exclusions) | wc -l")
echo "[$(date)] 📊 Found $total_files files to process" | tee -a "$LOG"
# Create file list with progress
echo "[$(date)] 📋 Creating file list with exclusions..." | tee -a "$LOG"
eval "find $SRC -type f $(build_find_exclusions)" | show_progress "$total_files" > "$TMP_BACKUP/filelist.txt"
# Initialize variables for chunk creation # Initialize variables for chunk creation
chunk_num=1 chunk_num=1
current_chunk_size=0 current_chunk_size=0
current_chunk_files=() current_chunk_files=()
processed_files=0
skipped_files=0
echo "[$(date)] 🔍 Starting file analysis (total files: $total_files)..." | tee -a "$LOG"
# Process files and create chunks # Process files and create chunks
while IFS= read -r file; do while IFS= read -r file; do
# Skip if file doesn't exist # Skip if file doesn't exist
[ -f "$file" ] || continue [ -f "$file" ] || continue
# Get file size ((processed_files++))
echo -ne "[$(date)] 📊 Progress: $processed_files/$total_files files processed (${skipped_files} skipped)\r" | tee -a "$LOG"
# Check if file needs backup using database
if ! needs_backup "$file"; then
((skipped_files++))
continue
fi
# Get file size and metadata
file_size=$(stat -c %s "$file") file_size=$(stat -c %s "$file")
file_mtime=$(stat -c %Y "$file")
file_hash=$(md5sum "$file" | cut -d' ' -f1)
# Update database with new file metadata
sqlite3 "$DB_FILE" << EOF
INSERT OR REPLACE INTO file_index (path, size, mtime, hash, file_exists)
VALUES ('$file', $file_size, $file_mtime, '$file_hash', 1);
EOF
# If adding this file would exceed chunk size, create the chunk # If adding this file would exceed chunk size, create the chunk
if [ $((current_chunk_size + file_size)) -gt $(numfmt --from=iec $CHUNK_SIZE) ]; then if [ $((current_chunk_size + file_size)) -gt $(numfmt --from=iec $CHUNK_SIZE) ]; then
if [ ${#current_chunk_files[@]} -gt 0 ]; then if [ ${#current_chunk_files[@]} -gt 0 ]; then
echo "[$(date)] 📦 Creating chunk $chunk_num with ${#current_chunk_files[@]} files..." | tee -a "$LOG" echo -e "\n[$(date)] 📦 Creating chunk $chunk_num with ${#current_chunk_files[@]} files..." | tee -a "$LOG"
# Create the chunk and wait for it to complete # Create the chunk with verbose output
chunk_file="$TMP_BACKUP/chunk_$(printf "%03d" $chunk_num).7z" chunk_file="$TMP_BACKUP/chunk_$(printf "%03d" $chunk_num).7z"
7z a -y -spf -t7z -m0=lzma2 -mx=5 "$chunk_file" "${current_chunk_files[@]}" | tee -a "$LOG" echo "[$(date)] 🔄 Compressing files into chunk $chunk_num..." | tee -a "$LOG"
7z a -y -spf -t7z -m0=lzma2 -mx=5 -v "$chunk_file" "${current_chunk_files[@]}" | tee -a "$LOG"
# Wait for 7z to complete and verify the chunk exists # Wait for 7z to complete and verify the chunk exists
if [ -f "$chunk_file" ]; then if [ -f "$chunk_file" ]; then
@ -435,11 +1053,17 @@ while IFS= read -r file; do
current_chunk_size=$((current_chunk_size + file_size)) current_chunk_size=$((current_chunk_size + file_size))
done < "$TMP_BACKUP/filelist.txt" done < "$TMP_BACKUP/filelist.txt"
echo -e "\n[$(date)] 📊 Backup summary:" | tee -a "$LOG"
echo "[$(date)] - Total files processed: $total_files" | tee -a "$LOG"
echo "[$(date)] - Files skipped (unchanged): $skipped_files" | tee -a "$LOG"
echo "[$(date)] - Files backed up: $((total_files - skipped_files))" | tee -a "$LOG"
# Create final chunk if there are remaining files # Create final chunk if there are remaining files
if [ ${#current_chunk_files[@]} -gt 0 ]; then if [ ${#current_chunk_files[@]} -gt 0 ]; then
echo "[$(date)] 📦 Creating final chunk $chunk_num with ${#current_chunk_files[@]} files..." | tee -a "$LOG" echo "[$(date)] 📦 Creating final chunk $chunk_num with ${#current_chunk_files[@]} files..." | tee -a "$LOG"
chunk_file="$TMP_BACKUP/chunk_$(printf "%03d" $chunk_num).7z" chunk_file="$TMP_BACKUP/chunk_$(printf "%03d" $chunk_num).7z"
7z a -y -spf -t7z -m0=lzma2 -mx=5 "$chunk_file" "${current_chunk_files[@]}" | tee -a "$LOG" echo "[$(date)] 🔄 Compressing files into final chunk..." | tee -a "$LOG"
7z a -y -spf -t7z -m0=lzma2 -mx=5 -v "$chunk_file" "${current_chunk_files[@]}" | tee -a "$LOG"
# Wait for 7z to complete and verify the chunk exists # Wait for 7z to complete and verify the chunk exists
if [ -f "$chunk_file" ]; then if [ -f "$chunk_file" ]; then