Compare commits

..

9 Commits

Author SHA1 Message Date
1190fd34af Update README.md 2024-03-11 06:54:28 -06:00
VetheonGames
8a37aac565 Update readme 2023-10-26 19:02:05 -06:00
VetheonGames
1333780943 Update readme 2023-10-26 18:58:33 -06:00
VetheonGames
354278b765 Update Readme 2023-10-07 10:50:23 -06:00
VetheonGames
3e6e3c0f4a update license 2023-09-19 23:09:21 -06:00
VetheonGames
26ad9a667e Fix line too long warning 2023-07-30 16:08:44 -06:00
VetheonGames
e55cbadec4 Rm .bundle fix literal stngs, incl rubocop.yml 2023-07-30 16:08:05 -06:00
VetheonGames
688bddf428 Fix the broken branch and reconcile Git 2023-07-30 09:48:59 -06:00
VetheonGames
c68b51785d Fix some nonsense 2023-07-30 09:38:50 -06:00
13 changed files with 263 additions and 84 deletions

View File

@ -1,6 +0,0 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gem 'rubocop', '1.46.0'
gem 'json', '2.6.3'

View File

@ -1,35 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
json (2.6.3)
parallel (1.22.1)
parser (3.2.1.0)
ast (~> 2.4.1)
rainbow (3.1.1)
regexp_parser (2.7.0)
rexml (3.2.5)
rubocop (1.46.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.26.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.26.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.11.0)
unicode-display_width (2.4.2)
PLATFORMS
x64-mingw-ucrt
DEPENDENCIES
json (= 2.6.3)
rubocop (= 1.46.0)
BUNDLED WITH
2.4.6

View File

@ -1,4 +0,0 @@
---
BUNDLE_PATH: "/home/runner/work/Ru-b2-SQL-Backups/Ru-b2-SQL-Backups/vendor/bundle"
BUNDLE_DEPLOYMENT: "true"
BUNDLE_JOBS: "4"

18
.rubocop.yml Normal file
View File

@ -0,0 +1,18 @@
# .rubocop.yml
# Enable all cops
# Target Ruby version
AllCops:
DisabledByDefault: false
TargetRubyVersion: 3.2.2
# Add other configuration options as needed
# For example, you can exclude certain files or directories
# from being analyzed by Rubocop using the `Exclude` option.
# AllCops:
# Exclude:
# - path/to/excluded/file.rb
# - path/to/excluded_directory/**

10
Gemfile
View File

@ -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'

View File

@ -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

View File

@ -11,10 +11,10 @@ If you need more direct help, join our [Discord](https://discord.pixelridgesoftw
## Installation ## Installation
To use this program, you'll need to run the following: (this is fixed now!) To use this program, you'll need to run the following: (this is fixed now!)
[Source](https://git.pixelridgesoftworks.com/PixelRidge-Softworks/Installers/src/branch/main/rub2/install.sh) [Source](https://git.pixelridgesoftworks.com/PixelRidge-Softworks/Installers/raw/branch/main/install.sh)
```bash ```bash
wget https://git.pixelridgesoftworks.com/PixelRidge-Softworks/Installers/src/branch/main/rub2/install.sh && bash ./install.sh wget https://git.pixelridgesoftworks.com/PixelRidge-Softworks/Installers/raw/branch/main/install.sh && bash ./install.sh
``` ```
## Configuration ## Configuration
@ -27,20 +27,19 @@ 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
``` ```
For easier access, you can also add this to your system path: For easier access, you can also add this to your system path:
```bash ```bash
/usr/bin/PixelRidge-Softworks/Ruby/Ru-B2-SQL-Backups/rub2 /usr/bin/PixelRidge-Softworks/Ruby/Ru-B2-SQL-Backups/rub2
``` ```

BIN
b2 Executable file

Binary file not shown.

View File

@ -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

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)', 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
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.")

52
rub2 Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative 'mysql_database_config'
require_relative 'mysql_database_backup'
require_relative 'mysql_database_restore'
require_relative 'loggman'
config_file = 'config.json'
logger = Loggman.new
begin
if ARGV[0] == '--restore'
unless File.exist?(config_file)
logger.error('Configuration file does not exist. Please run the first time setup.')
exit 1
end
config = JSON.parse(File.read(config_file))
backup_dir = config['backup_dir'] || '.'
b2_enabled = config['b2_enabled'] || false
local_backups = Dir[File.join(backup_dir, '*_*.sql')]
remote_backups = b2_enabled ? `./b2 ls #{config['b2']['bucket_name']}`.split("\n") : []
if local_backups.empty? && remote_backups.empty?
logger.error('No backup found. Please ensure a backup exists in the local directory
or in the B2 bucket (if B2 is enabled).')
exit 1
end
logger.info('Restoring backup...')
restore = MysqlDatabaseRestore.new(config_file, logger)
restore.restore
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, logger)
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

View File

@ -1,25 +0,0 @@
# 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
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.')
rescue StandardError => e
logger.error("An error occurred: #{e.message}")
logger.debug("Backtrace: #{e.backtrace}")
end