Fix some nonsense

This commit is contained in:
VetheonGames 2023-07-30 09:38:50 -06:00
parent 6e6d185cb9
commit c68b51785d
8 changed files with 226 additions and 13 deletions

10
Gemfile
View File

@ -4,3 +4,13 @@ source 'https://rubygems.org'
gem 'json', '2.6.3'
gem 'rubocop', '1.46.0'
gem 'sqlite3', '~> 1.6'
gem 'sequel', '~> 5.70'
gem 'tty-prompt', '~> 0.23.1'
gem 'tty-progressbar', '~> 0.18.2'
gem 'open3', '~> 0.1.2'

View File

@ -3,10 +3,13 @@ GEM
specs:
ast (2.4.2)
json (2.6.3)
open3 (0.1.2)
parallel (1.23.0)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
pastel (0.8.0)
tty-color (~> 0.5)
racc (1.7.1)
rainbow (3.1.1)
regexp_parser (2.8.1)
@ -24,7 +27,27 @@ GEM
rubocop-ast (1.29.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.13.0)
sequel (5.70.0)
sqlite3 (1.6.3-x64-mingw-ucrt)
sqlite3 (1.6.3-x86_64-linux)
strings-ansi (0.2.0)
tty-color (0.6.0)
tty-cursor (0.7.1)
tty-progressbar (0.18.2)
strings-ansi (~> 0.2)
tty-cursor (~> 0.7)
tty-screen (~> 0.8)
unicode-display_width (>= 1.6, < 3.0)
tty-prompt (0.23.1)
pastel (~> 0.8)
tty-reader (~> 0.8)
tty-reader (0.9.0)
tty-cursor (~> 0.7)
tty-screen (~> 0.8)
wisper (~> 2.0)
tty-screen (0.8.1)
unicode-display_width (2.4.2)
wisper (2.0.1)
PLATFORMS
x64-mingw-ucrt
@ -32,7 +55,12 @@ PLATFORMS
DEPENDENCIES
json (= 2.6.3)
open3 (~> 0.1.2)
rubocop (= 1.46.0)
sequel (~> 5.70)
sqlite3 (~> 1.6)
tty-progressbar (~> 0.18.2)
tty-prompt (~> 0.23.1)
BUNDLED WITH
2.4.16
2.4.17

View File

@ -24,16 +24,16 @@ When you run the program for the first time, it will prompt you for configuratio
## Usage
To run the program, simply execute the `starter.rb` file using the following command from inside the cloned directory:
To run the program, simply execute the `./rub2` command from inside the cloned directory:
```bash
ruby starter.rb
./rub2
```
You can also run this program via Cron. For example, this Crontab would run the program every 6 hours:
```bash
0 */6 * * * /usr/bin/PixelatedStudios/Ruby/Ru-B2-SQL-Backups/starter.rb
0 */6 * * * /usr/bin/PixelRidge-Softworks/Ruby/Ru-B2-SQL-Backups/rub2
```
@ -46,12 +46,12 @@ To update the program, simply pull the latest changes from the Git repository an
If you need to change the configuration settings, simply delete the `config.json` file and run the program again to be prompted for new configuration details.
~~To delete old backups, the program will check for backups that are older than `local_retention_days` days (default 30) and delete them. To modify this time window, edit the `max_age_days` variable in the `delete_old_backups` method of the `MysqlDatabaseBackup` class.~~
^^ This is no longer applicable, you can now set the retention time in the config.json
## Compatibility
This program is compatible with Debian and RHEL based systems, but could be made to work with any systems compatible with Ruby, Python3, and Bash.
This program is compatible with Debian and RHEL based systems, but could be made to work with any systems compatible with Ruby, Python3, and Bash.
This program was also built on Arch Linux, so it should also run fine on Manjaro, Arch, and any other Arch based distro.

BIN
b2 Executable file

Binary file not shown.

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
require 'json'
require 'sequel'
require 'securerandom'
require_relative 'loggman'
# class for creating, managing and deleting backups both locally and in B2
@ -18,6 +20,28 @@ class MysqlDatabaseBackup
@local_retention_days = config['local_retention_days'] || 30
@b2_retention_days = config['b2']&.dig('retention_days') || 30
@logger = logger
# Authenticate with B2
output = `b2 authorize-account #{@b2_key_id} #{@b2_application_key}`
unless $CHILD_STATUS.success?
@logger.error("Failed to authenticate with B2: #{output}")
exit 1
end
# Create a SQLite database
@db = Sequel.sqlite('backups.db')
# Create a table for storing backup information
@db.create_table?(:backups) do
primary_key :id
String :backup_id
String :backup_name
Time :timestamp
String :backup_type # New column for the backup type
end
# Get a reference to the table
@backups_table = @db[:backups]
end
def backup # rubocop:disable Metrics/MethodLength
@ -30,6 +54,11 @@ class MysqlDatabaseBackup
databases.each do |database_name|
backup_file = File.join(@backup_dir, "#{database_name}_#{timestamp}.sql")
backup_id = generate_backup_id
# Store the backup information in the SQLite database
@backups_table.insert(backup_id:, backup_name: backup_file, timestamp: Time.now, backup_type: 'local')
@logger.info("Backup file path: #{backup_file}")
@logger.info("MySQL Info: #{@host} #{@username} #{@password} #{backup_file}")
@ -38,7 +67,7 @@ class MysqlDatabaseBackup
delete_old_backups
upload_to_b2(backup_file) if @b2_enabled
upload_to_b2(backup_file, backup_id) if @b2_enabled
end
end
@ -67,19 +96,26 @@ class MysqlDatabaseBackup
end
end
def upload_to_b2(backup_file) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def upload_to_b2(backup_file, backup_id) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
b2_file_name = File.basename(backup_file)
b2_file_url = "b2://#{@b2_bucket_name}/#{b2_file_name}"
# Upload the backup file to the B2 bucket
`b2 upload-file #{@b2_bucket_name} #{backup_file} #{b2_file_name}`
`./b2 upload-file #{@b2_bucket_name} #{backup_file} #{b2_file_name}`
@logger.info("Uploaded backup file to B2 bucket: #{b2_file_url}")
# Update the backup type in the SQLite database
@backups_table.where(backup_id:).update(backup_type: 'remote')
# Delete the local backup file
File.delete(backup_file)
@logger.info("Deleted local backup file: #{backup_file}")
# Calculate the cutoff date based on b2_retention_days
max_age_days = @b2_retention_days
cutoff_date = Time.now - (max_age_days * 24 * 60 * 60)
existing_files = `b2 ls #{@b2_bucket_name}`
existing_files = `./b2 ls #{@b2_bucket_name}`
return if existing_files.empty?
@ -95,8 +131,14 @@ class MysqlDatabaseBackup
next unless file_timestamp < cutoff_date
file_id = line.match(/"fileId": "([^"]+)"/)[1]
`b2 delete-file-version #{@b2_bucket_name} #{file_name} #{file_id}`
`./b2 delete-file-version #{@b2_bucket_name} #{file_name} #{file_id}`
@logger.info("Deleted old backup file from B2 bucket: #{file_name}")
end
end
private
def generate_backup_id
SecureRandom.alphanumeric(4)
end
end

View File

@ -63,7 +63,8 @@ class MysqlDatabaseConfig
cron_interval = prompt('How often do you want the program to run? (in minutes, e.g. "60" for every hour)',
default: '60').to_i
# write the cron job to crontab
`echo "*/#{cron_interval} * * * * /usr/bin/PixelatedStudios/Ruby/Ru-b2-SQL-Backups/starter.rb" >> /etc/crontab`
`echo "*/#{cron_interval} * * * * /usr/bin/PixelRidge-Softworks/Ruby/Ru-b2-SQL-Backups/rub2" >>
/etc/crontab`
@logger.info("Cron job added to /etc/crontab to run every #{cron_interval} minutes.")
end
end
@ -80,7 +81,7 @@ class MysqlDatabaseConfig
end
def prompt_bool(message, default: false)
prompt("#{message} (y/n)", default: default) =~ /y|yes/i
prompt("#{message} (y/n)", default:) =~ /y|yes/i
end
end

99
mysql_database_restore.rb Normal file
View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
require 'English'
require 'tty-prompt'
require 'tty-progressbar'
require 'sequel'
require 'open3'
config = JSON.parse(File.read('config.json'))
host = config['mysql']['host']
username = config['mysql']['username']
password = config['mysql']['password']
b2_bucket_name = config['b2']&.dig('bucket_name')
prompt = TTY::Prompt.new
# Connect to the SQLite database
db = Sequel.sqlite('backups.db')
# Get a reference to the backups table
backups_table = db[:backups]
# Query the database for remote and local backups
remote_backups = backups_table.where(backup_type: 'remote').map(:backup_name)
local_backups = backups_table.where(backup_type: 'local').map(:backup_name)
# Define the categories with the queried lists
choices = [
{
name: 'Remote Backups',
choices: remote_backups
},
{
name: 'Local Backups',
choices: local_backups
}
]
user_choice = prompt.select('Choose a backup to restore:', choices)
# Determine if the chosen backup is local or remote
backup_type = backups_table.where(backup_name: user_choice).get(:backup_type)
if backup_type == 'remote'
# Download the remote backup from B2
`./b2 download-file-by-name #{b2_bucket_name} #{user_choice} ./#{user_choice}`
end
# Perform a "dry run" of the SQL script
output = `mysql --host=#{host} --user=#{username} --password=#{password} #{user_choice}
--execute="START TRANSACTION; SOURCE #{user_choice}; ROLLBACK;"`
if $CHILD_STATUS.success?
prompt.say('SQL file passed the dry run.')
else
prompt.say("Error in SQL file: #{output}")
exit 1
end
# Ask the user for confirmation before dropping the database
if prompt.yes?("Are you sure you want to drop the database and restore it from the backup #{user_choice}?")
`mysql --host=#{host} --user=#{username} --password=#{password} --execute="DROP DATABASE IF EXISTS #{user_choice};
CREATE DATABASE #{user_choice};"`
else
prompt.say('Database restoration cancelled.')
exit 0
end
# Create a progress bar
total_size = File.size(user_choice)
bar = TTY::ProgressBar.new('Restoring [:bar] :percent', total: total_size)
# Open the SQL file and the MySQL process
File.open(user_choice) do |file|
sql_chunk = ''
file.each_line do |line|
sql_chunk += line
# If the line ends with a semicolon and the chunk size is over 1024 bytes, execute the chunk
next unless line.strip.end_with?(';') && sql_chunk.bytesize >= 1024
# Write the chunk to the MySQL process
`mysql --host=#{host} --user=#{username} --password=#{password} #{user_choice} --execute="#{sql_chunk}"`
# Update the progress bar
bar.advance(sql_chunk.bytesize)
sql_chunk = ''
end
# Write the remaining SQL commands to the MySQL process
unless sql_chunk.empty?
`mysql --host=#{host} --user=#{username} --password=#{password} #{user_choice} --execute="#{sql_chunk}"`
bar.advance(sql_chunk.bytesize)
end
end
if $CHILD_STATUS.success?
prompt.say('Import completed successfully.')
else
prompt.say("Error during import: #{output}")
end
prompt.say("Backup #{user_choice} restored successfully.")

33
rub2 Normal file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative 'mysql_database_config'
require_relative 'mysql_database_backup'
require_relative 'loggman'
config_file = 'config.json'
logger = Loggman.new
begin
if ARGV[0] == '--restore'
# Call your restore function here with backup_id
logger.info("Restoring backup with ID: #{backup_id}")
else
logger.info('Starting script.')
config_generator = MysqlDatabaseConfig.new(config_file)
config_generator.generate
logger.info("Generated MySQL database configuration file: #{config_file}.")
backup = MysqlDatabaseBackup.new(config_file)
backup.backup
logger.info('Performed MySQL database backup.')
logger.info('Script completed successfully.')
end
rescue StandardError => e
logger.error("An error occurred: #{e.message}")
logger.debug("Backtrace: #{e.backtrace}")
end