Loci is a python script that can backup a directory to a server using rsync - It keeps track of the backups that have been done. Multiple backups may be kept. Rsync is used to handle the backups so only the needfull is copied and single files can be recovered from the backup if needed. loci -b tag : Backup under the tag given (I used days of the week)

loci -l : List backups showing those tags unused, backups that are needed, and backups that been run more than 5 times. I refresh these.

loci -r tag : Refresh a tag’s backup - delete the files under that tag and backuplog entries to prepare for a fresh backup using loci -b

~/.backuplog a file in .csv format that keeps track of backups done.

~/.config/loci/settings Settings file. Fully commented.

  • demeaning_casually@infosec.pub
    link
    fedilink
    English
    arrow-up
    3
    arrow-down
    2
    ·
    3 days ago
    #!/bin/bash
    read_settings() {
      settings_file="$HOME/.config/loci/settings"
      if [[ -f "$settings_file" ]]; then
        while IFS='=' read -r key value || [[ -n "$key" ]]; do
          if [[ ! -z "$key" && ! "$key" =~ ^# && ! "$key" =~ ^\[ ]]; then
            key=$(echo "$key" | xargs)
            value=$(echo "$value" | xargs)
            declare -g "$key"="$value"
          fi
        done < "$settings_file"
      else
        echo "Settings file not found: $settings_file"
        exit 1
      fi
    }
    
    # Function to perform the backup
    backup() {
      local tag="$1"
      read_settings
      
      log_path="$HOME/.backuplog"
      
      # Check if header exists in log file, if not, create it
      if [[ ! -f "$log_path" ]]; then
        echo "\"tag\",\"timestamp\",\"command\",\"completion_time\"" > "$log_path"
      elif [[ $(head -1 "$log_path") != "\"tag\",\"timestamp\",\"command\",\"completion_time\"" ]]; then
        # Add header if it doesn't exist
        temp_file=$(mktemp)
        echo "\"tag\",\"timestamp\",\"command\",\"completion_time\"" > "$temp_file"
        cat "$log_path" >> "$temp_file"
        mv "$temp_file" "$log_path"
      fi
      
      # Create backup directory if it doesn't exist
      backup_dest="$backup_root/$tag"
      mkdir -p "$backup_dest" 2>/dev/null
      
      # Rsync command for backup
      target="$user@$server:/home/$user/$backup_root/$tag"
      rsync_cmd="rsync -avh $source_dir $target"
      # If exclude_files is defined and not empty, add it to rsync command
      if [[ -n "$exclude_files" ]]; then
        rsync_cmd="rsync -avh --exclude='$exclude_files' $source_dir $target"
      fi
      
      echo "Starting backup for tag '$tag' at $(date '+%Y-%m-%d %H:%M:%S')"
      echo "Command: $rsync_cmd"
      
      # Record start time
      start_timestamp=$(date +"%Y-%m-%d %H:%M:%S")
      
      # Execute the backup
      eval "$rsync_cmd"
      backup_status=$?
      
      # Record completion time
      completion_timestamp=$(date +"%Y-%m-%d %H:%M:%S")
      
      # Calculate duration
      start_seconds=$(date -d "$start_timestamp" +%s)
      end_seconds=$(date -d "$completion_timestamp" +%s)
      duration=$((end_seconds - start_seconds))
      
      # Format duration
      if [[ $duration -ge 3600 ]]; then
        formatted_duration="$((duration / 3600))h $((duration % 3600 / 60))m $((duration % 60))s"
      elif [[ $duration -ge 60 ]]; then
        formatted_duration="$((duration / 60))m $((duration % 60))s"
      else
        formatted_duration="${duration}s"
      fi
      
      # Log the backup information as proper CSV
      echo "\"$tag\",\"$start_timestamp\",\"$rsync_cmd\",\"$completion_timestamp\"" >> "$log_path"
      
      if [[ $backup_status -eq 0 ]]; then
        echo -e "\e[32mBackup for '$tag' completed successfully\e[0m"
        echo "Duration: $formatted_duration"
        echo "Logged to: $log_path"
      else
        echo -e "\e[31mBackup for '$tag' failed with status $backup_status\e[0m"
      fi
    }
    
    # Function to remove the backup
    remove_backup() {
      local tag="$1"
      read_settings
      
      echo "Removing backup for tag '$tag'..."
      
      # Rsync remove command
      rmfile="/home/$user/$backup_root/$tag"
      rm_cmd="ssh $user@$server rm -rf $rmfile"
      
      # Execute the removal command
      eval "$rm_cmd"
      rm_status=$?
      
      if [[ $rm_status -ne 0 ]]; then
        echo -e "\e[31mError: Failed to remove remote backup for tag '$tag'\e[0m"
        echo "Command failed: $rm_cmd"
        return 1
      fi
      
      # Remove log entries while preserving header
      log_path="$HOME/.backuplog"
      if [[ -f "$log_path" ]]; then
        # Create a temporary file
        temp_file=$(mktemp)
        
        # Copy header (first line) if it exists
        if [[ -s "$log_path" ]]; then
          head -1 "$log_path" > "$temp_file"
          # Only copy non-matching lines after header
          tail -n +2 "$log_path" | grep -v "^\"$tag\"," >> "$temp_file"
        else
          # If log is empty, add header
          echo "\"tag\",\"timestamp\",\"command\",\"completion_time\"" > "$temp_file"
        fi
        
        # Replace the original with filtered content
        mv "$temp_file" "$log_path"
        
        echo -e "\e[32mBackup '$tag' removed successfully\e[0m"
        echo "Log entries for '$tag' have been removed from $log_path"
      else
        echo -e "\e[32mBackup '$tag' removed successfully\e[0m"
        echo "No log file found at $log_path"
      fi
    }
    
    # Function to list the backups with detailed timing information
    list_backups() {
      read_settings
      log_path="$HOME/.backuplog"
      
      echo "Backup Status Report ($(date '+%Y-%m-%d %H:%M:%S'))"
      echo "========================================================="
      printf "%-8s %-15s %-10s %-20s %-15s\n" "TAG" "STATUS" "COUNT" "LAST BACKUP" "DAYS AGO"
      echo "--------------------------------------------------------"
      
      # Check if header exists in log file, if not, create it
      if [[ ! -f "$log_path" ]]; then
        echo "\"tag\",\"timestamp\",\"command\",\"completion_time\"" > "$log_path"
        echo "Created new log file with CSV headers."
      elif [[ $(head -1 "$log_path") != "\"tag\",\"timestamp\",\"command\",\"completion_time\"" ]]; then
        # Add header if it doesn't exist
        temp_file=$(mktemp)
        echo "\"tag\",\"timestamp\",\"command\",\"completion_time\"" > "$temp_file"
        cat "$log_path" >> "$temp_file"
        mv "$temp_file" "$log_path"
        echo "Added CSV headers to existing log file."
      fi
      
      # Loop through each tag in the taglist
      for tag in $taglist; do
        # Count occurrences of the tag in the log
        count=0
        youngest=""
        days_ago="N/A"
        
        if [[ -f "$log_path" ]]; then
          # Skip header when counting
          count=$(grep -c "^\"$tag\"," "$log_path")
          
          # Get the newest backup date for this tag
          if [[ $count -gt 0 ]]; then
            # Extract dates and find the newest one
            dates=$(grep "^\"$tag\"," "$log_path" | cut -d',' -f2)
            youngest=$(echo "$dates" | sort -r | head -1)
            
            # Calculate days since last backup
            if [[ ! -z "$youngest" ]]; then
              youngest_seconds=$(date -d "$youngest" +%s)
              now_seconds=$(date +%s)
              days_diff=$(( (now_seconds - youngest_seconds) / 86400 ))
              days_ago="$days_diff days"
            fi
          fi
        fi
        
        # Determine status with colored output
        if [[ $count -eq 0 ]]; then
          status="Missing"
          status_color="\e[31m$status\e[0m" # Red
        elif [[ $count -gt 5 ]]; then
          status="Needs renewal"
          status_color="\e[33m$status\e[0m" # Yellow
        elif [[ ! -z "$youngest" ]]; then
          # Calculate days since last backup
          youngest_seconds=$(date -d "$youngest" +%s)
          now_seconds=$(date +%s)
          days_diff=$(( (now_seconds - youngest_seconds) / 86400 ))
          
          if [[ $days_diff -gt 7 ]]; then
            status="Needs to be run"
            status_color="\e[33m$status\e[0m" # Yellow
          else
            status="Up to date"
            status_color="\e[32m$status\e[0m" # Green
          fi
        else
          status="Missing"
          status_color="\e[31m$status\e[0m" # Red
        fi
        
        printf "%-8s %-15b %-10s %-20s %-15s\n" "$tag" "$status_color" "$count" "${youngest:-N/A}" "$days_ago"
      done
      
      echo "--------------------------------------------------------"
      echo "CSV log file: $log_path"
      echo "Run 'loci -l' to refresh this status report"
    }
    
    # Function to show backup stats
    show_stats() {
      read_settings
      log_path="$HOME/.backuplog"
      
      if [[ ! -f "$log_path" ]]; then
        echo "No backup log found at $log_path"
        return 1
      fi
      
      echo "Backup Statistics"
      echo "================="
      
      # Total number of backups
      total_backups=$(grep -v "^\"tag\"" "$log_path" | wc -l)
      echo "Total backups: $total_backups"
      
      # Backups per tag
      echo -e "\nBackups per tag:"
      for tag in $taglist; do
        count=$(grep "^\"$tag\"," "$log_path" | wc -l)
        echo "  $tag: $count"
      done
      
      # Last backup time for each tag
      echo -e "\nLast backup time:"
      for tag in $taglist; do
        latest=$(grep "^\"$tag\"," "$log_path" | cut -d',' -f2 | sort -r | head -1)
        if [[ -z "$latest" ]]; then
          echo "  $tag: Never"
        else
          # Calculate days ago
          latest_seconds=$(date -d "$latest" +%s)
          now_seconds=$(date +%s)
          days_diff=$(( (now_seconds - latest_seconds) / 86400 ))
          echo "  $tag: $latest ($days_diff days ago)"
        fi
      done
      
      echo -e "\nBackup log file: $log_path"
      echo "To view in a spreadsheet: cp $log_path ~/backups.csv"
    }
    
    # Function to export log to CSV
    export_csv() {
      read_settings
      log_path="$HOME/.backuplog"
      export_path="${1:-$HOME/backup_export.csv}"
      
      if [[ ! -f "$log_path" ]]; then
        echo "No backup log found at $log_path"
        return 1
      fi
      
      # Copy the log file to export location
      cp "$log_path" "$export_path"
      echo "Backup log exported to: $export_path"
      echo "You can now open this file in your spreadsheet application."
    }
    
    # Main function
    main() {
      if [[ "$1" == "-b" || "$1" == "--backup" ]] && [[ ! -z "$2" ]]; then
        backup "$2"
      elif [[ "$1" == "-r" || "$1" == "--remove" ]] && [[ ! -z "$2" ]]; then
        remove_backup "$2"
      elif [[ "$1" == "-l" || "$1" == "--list" ]]; then
        list_backups
      elif [[ "$1" == "-s" || "$1" == "--stats" ]]; then
        show_stats
      elif [[ "$1" == "-e" || "$1" == "--export" ]]; then
        export_csv "$2"
      elif [[ "$1" == "-h" || "$1" == "--help" ]]; then
        echo "Loci Backup Management Tool"
        echo "Usage:"
        echo "  loci -b, --backup <tag>   Create a backup with the specified tag"
        echo "  loci -r, --remove <tag>   Remove a backup with the specified tag"
        echo "  loci -l, --list           List all backup statuses"
        echo "  loci -s, --stats          Show backup statistics"
        echo "  loci -e, --export [path]  Export backup log to CSV (default: ~/backup_export.csv)"
        echo "  loci -h, --help           Show this help message"
      else
        echo "Usage: loci -b <tag> | loci -r <tag> | loci -l | loci -s | loci -e [path] | loci -h"
      fi
    }