Fix some nonsense
This commit is contained in:
parent
6e6d185cb9
commit
c68b51785d
10
Gemfile
10
Gemfile
|
@ -4,3 +4,13 @@ source 'https://rubygems.org'
|
||||||
|
|
||||||
gem 'json', '2.6.3'
|
gem 'json', '2.6.3'
|
||||||
gem 'rubocop', '1.46.0'
|
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'
|
||||||
|
|
30
Gemfile.lock
30
Gemfile.lock
|
@ -3,10 +3,13 @@ GEM
|
||||||
specs:
|
specs:
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
json (2.6.3)
|
json (2.6.3)
|
||||||
|
open3 (0.1.2)
|
||||||
parallel (1.23.0)
|
parallel (1.23.0)
|
||||||
parser (3.2.2.3)
|
parser (3.2.2.3)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
|
pastel (0.8.0)
|
||||||
|
tty-color (~> 0.5)
|
||||||
racc (1.7.1)
|
racc (1.7.1)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.8.1)
|
||||||
|
@ -24,7 +27,27 @@ GEM
|
||||||
rubocop-ast (1.29.0)
|
rubocop-ast (1.29.0)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.2.1.0)
|
||||||
ruby-progressbar (1.13.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)
|
unicode-display_width (2.4.2)
|
||||||
|
wisper (2.0.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x64-mingw-ucrt
|
x64-mingw-ucrt
|
||||||
|
@ -32,7 +55,12 @@ PLATFORMS
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
json (= 2.6.3)
|
json (= 2.6.3)
|
||||||
|
open3 (~> 0.1.2)
|
||||||
rubocop (= 1.46.0)
|
rubocop (= 1.46.0)
|
||||||
|
sequel (~> 5.70)
|
||||||
|
sqlite3 (~> 1.6)
|
||||||
|
tty-progressbar (~> 0.18.2)
|
||||||
|
tty-prompt (~> 0.23.1)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.16
|
2.4.17
|
||||||
|
|
|
@ -24,16 +24,16 @@ When you run the program for the first time, it will prompt you for configuratio
|
||||||
|
|
||||||
## Usage
|
## 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
|
```bash
|
||||||
ruby starter.rb
|
./rub2
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also run this program via Cron. For example, this Crontab would run the program every 6 hours:
|
You can also run this program via Cron. For example, this Crontab would run the program every 6 hours:
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'json'
|
require 'json'
|
||||||
|
require 'sequel'
|
||||||
|
require 'securerandom'
|
||||||
require_relative 'loggman'
|
require_relative 'loggman'
|
||||||
|
|
||||||
# class for creating, managing and deleting backups both locally and in B2
|
# 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
|
@local_retention_days = config['local_retention_days'] || 30
|
||||||
@b2_retention_days = config['b2']&.dig('retention_days') || 30
|
@b2_retention_days = config['b2']&.dig('retention_days') || 30
|
||||||
@logger = logger
|
@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
|
end
|
||||||
|
|
||||||
def backup # rubocop:disable Metrics/MethodLength
|
def backup # rubocop:disable Metrics/MethodLength
|
||||||
|
@ -30,6 +54,11 @@ class MysqlDatabaseBackup
|
||||||
|
|
||||||
databases.each do |database_name|
|
databases.each do |database_name|
|
||||||
backup_file = File.join(@backup_dir, "#{database_name}_#{timestamp}.sql")
|
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("Backup file path: #{backup_file}")
|
||||||
@logger.info("MySQL Info: #{@host} #{@username} #{@password} #{backup_file}")
|
@logger.info("MySQL Info: #{@host} #{@username} #{@password} #{backup_file}")
|
||||||
|
|
||||||
|
@ -38,7 +67,7 @@ class MysqlDatabaseBackup
|
||||||
|
|
||||||
delete_old_backups
|
delete_old_backups
|
||||||
|
|
||||||
upload_to_b2(backup_file) if @b2_enabled
|
upload_to_b2(backup_file, backup_id) if @b2_enabled
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,19 +96,26 @@ class MysqlDatabaseBackup
|
||||||
end
|
end
|
||||||
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_name = File.basename(backup_file)
|
||||||
b2_file_url = "b2://#{@b2_bucket_name}/#{b2_file_name}"
|
b2_file_url = "b2://#{@b2_bucket_name}/#{b2_file_name}"
|
||||||
|
|
||||||
# Upload the backup file to the B2 bucket
|
# 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}")
|
@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
|
# Calculate the cutoff date based on b2_retention_days
|
||||||
max_age_days = @b2_retention_days
|
max_age_days = @b2_retention_days
|
||||||
cutoff_date = Time.now - (max_age_days * 24 * 60 * 60)
|
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?
|
return if existing_files.empty?
|
||||||
|
|
||||||
|
@ -95,8 +131,14 @@ class MysqlDatabaseBackup
|
||||||
next unless file_timestamp < cutoff_date
|
next unless file_timestamp < cutoff_date
|
||||||
|
|
||||||
file_id = line.match(/"fileId": "([^"]+)"/)[1]
|
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}")
|
@logger.info("Deleted old backup file from B2 bucket: #{file_name}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_backup_id
|
||||||
|
SecureRandom.alphanumeric(4)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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)',
|
cron_interval = prompt('How often do you want the program to run? (in minutes, e.g. "60" for every hour)',
|
||||||
default: '60').to_i
|
default: '60').to_i
|
||||||
# write the cron job to crontab
|
# 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.")
|
@logger.info("Cron job added to /etc/crontab to run every #{cron_interval} minutes.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -80,7 +81,7 @@ class MysqlDatabaseConfig
|
||||||
end
|
end
|
||||||
|
|
||||||
def prompt_bool(message, default: false)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
99
mysql_database_restore.rb
Normal file
99
mysql_database_restore.rb
Normal 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
33
rub2
Normal 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
|
Loading…
Reference in New Issue
Block a user