Compare commits
9 Commits
main
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
1190fd34af | |||
|
8a37aac565 | ||
|
1333780943 | ||
|
354278b765 | ||
|
3e6e3c0f4a | ||
|
26ad9a667e | ||
|
e55cbadec4 | ||
|
688bddf428 | ||
|
c68b51785d |
|
@ -1,6 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'rubocop', '1.46.0'
|
||||
gem 'json', '2.6.3'
|
|
@ -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
|
|
@ -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
18
.rubocop.yml
Normal 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
10
Gemfile
|
@ -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'
|
||||
|
|
30
Gemfile.lock
30
Gemfile.lock
|
@ -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
|
||||
|
|
11
README.md
11
README.md
|
@ -11,10 +11,10 @@ If you need more direct help, join our [Discord](https://discord.pixelridgesoftw
|
|||
## Installation
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -27,20 +27,19 @@ 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
|
||||
```
|
||||
|
||||
For easier access, you can also add this to your system path:
|
||||
|
||||
```bash
|
||||
/usr/bin/PixelRidge-Softworks/Ruby/Ru-B2-SQL-Backups/rub2
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
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.")
|
52
rub2
Executable file
52
rub2
Executable 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
|
25
starter.rb
25
starter.rb
|
@ -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
|
Loading…
Reference in New Issue
Block a user