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.

    • WasPentalive@lemmy.oneOP
      link
      fedilink
      English
      arrow-up
      2
      arrow-down
      2
      ·
      3 days ago

      Especially one that lets you know how long it’s been since you took time to run a backup, keeps track of which set of backups could be updated, and which should be refreshed, and keeps a log file up to date and in .csv format so you can mess with it in a spreadsheet?

      • 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
        }