rails_nexus 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/rails_nexus/backup_controller.rb +78 -24
- data/app/models/rails_nexus/backup_config.rb +90 -0
- data/app/services/rails_nexus/backup_runner.rb +62 -0
- data/app/services/rails_nexus/backup_service.rb +231 -131
- data/app/views/rails_nexus/backup/_form.html.erb +185 -0
- data/app/views/rails_nexus/backup/config_history.html.erb +67 -0
- data/app/views/rails_nexus/backup/edit.html.erb +15 -0
- data/app/views/rails_nexus/backup/{files.html.erb → history.html.erb} +8 -15
- data/app/views/rails_nexus/backup/index.html.erb +99 -79
- data/app/views/rails_nexus/backup/new.html.erb +15 -0
- data/config/routes.rb +8 -4
- data/lib/generators/rails_nexus/templates/rails_nexus.rb +1 -1
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/tasks/rails_nexus.rake +66 -3
- metadata +8 -3
- data/config/initializers/rails_nexus.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d639de5385a9975d43e4cb943e95e1080fc982bf39363d8ca650ffa4e413807
|
|
4
|
+
data.tar.gz: d0ee669e29976192aba3b2215b9d804772d3e362669252e545e340fc5c80a337
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0e758654cfebfdd74880b6e9ea2c415ad8ed1cb3ce63ec2e619664597ff7fbfd8c449e277228d0dbcce66c8f59db400ac8fd0326d2a97a1afa05eab68ae6347
|
|
7
|
+
data.tar.gz: 5c68c28e6debd2b86d76653e8b6a24b83c33b506dc84acdbd919e560e781445653771b19993acc30a35e334f8f49ecced1b2cbc78236ea28cb7aaffc5d6c6661
|
|
@@ -4,51 +4,105 @@ module RailsNexus
|
|
|
4
4
|
class BackupController < ApplicationController
|
|
5
5
|
before_action :rails_nexus_require_auth!
|
|
6
6
|
before_action :check_backup_enabled
|
|
7
|
+
before_action :set_config, only: %i[edit update destroy trigger]
|
|
7
8
|
|
|
8
9
|
# GET /rails_nexus/backup
|
|
9
10
|
def index
|
|
10
|
-
@
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
-
|
|
11
|
+
@configs = RailsNexus::BackupConfig.order(:name)
|
|
12
|
+
@records = RailsNexus::Backup.order(started_at: :desc).limit(20)
|
|
13
|
+
@stats = {
|
|
14
|
+
total_configs: RailsNexus::BackupConfig.count,
|
|
15
|
+
enabled_configs: RailsNexus::BackupConfig.enabled.count,
|
|
16
|
+
total_backups: RailsNexus::Backup.count,
|
|
17
|
+
successful: RailsNexus::Backup.successful.count,
|
|
18
|
+
failed: RailsNexus::Backup.failed.count
|
|
19
|
+
}
|
|
14
20
|
end
|
|
15
21
|
|
|
16
|
-
# GET /rails_nexus/backup/
|
|
17
|
-
def
|
|
18
|
-
@
|
|
19
|
-
|
|
22
|
+
# GET /rails_nexus/backup/new
|
|
23
|
+
def new
|
|
24
|
+
@config = RailsNexus::BackupConfig.new(
|
|
25
|
+
adapter: "mysql",
|
|
26
|
+
host: "localhost",
|
|
27
|
+
port: 3306,
|
|
28
|
+
storage_path: "~/dumps",
|
|
29
|
+
keep_count: 30,
|
|
30
|
+
compress: true
|
|
31
|
+
)
|
|
20
32
|
end
|
|
21
33
|
|
|
22
|
-
# POST /rails_nexus/backup
|
|
34
|
+
# POST /rails_nexus/backup
|
|
35
|
+
def create
|
|
36
|
+
@config = RailsNexus::BackupConfig.new(config_params)
|
|
37
|
+
|
|
38
|
+
if @config.save
|
|
39
|
+
redirect_to backup_path, notice: "Backup config '#{@config.name}' created."
|
|
40
|
+
else
|
|
41
|
+
render :new, status: :unprocessable_entity
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# GET /rails_nexus/backup/:id/edit
|
|
46
|
+
def edit; end
|
|
47
|
+
|
|
48
|
+
# PATCH /rails_nexus/backup/:id
|
|
49
|
+
def update
|
|
50
|
+
if @config.update(config_params)
|
|
51
|
+
redirect_to backup_path, notice: "Backup config '#{@config.name}' updated."
|
|
52
|
+
else
|
|
53
|
+
render :edit, status: :unprocessable_entity
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# DELETE /rails_nexus/backup/:id
|
|
58
|
+
def destroy
|
|
59
|
+
name = @config.name
|
|
60
|
+
@config.destroy
|
|
61
|
+
redirect_to backup_path, notice: "Backup config '#{name}' deleted."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# POST /rails_nexus/backup/:id/trigger
|
|
23
65
|
def trigger
|
|
24
|
-
|
|
25
|
-
result = @service.trigger_backup(params[:model])
|
|
66
|
+
result = RailsNexus::BackupService.run(@config)
|
|
26
67
|
|
|
27
68
|
if result[:success]
|
|
28
|
-
redirect_to backup_path, notice: "Backup '#{
|
|
69
|
+
redirect_to backup_path, notice: "Backup '#{@config.name}' completed successfully."
|
|
29
70
|
else
|
|
30
71
|
redirect_to backup_path, alert: "Backup failed: #{result[:error]}"
|
|
31
72
|
end
|
|
32
73
|
end
|
|
33
74
|
|
|
34
|
-
#
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
redirect_to backup_path, notice: "Backup record deleted."
|
|
75
|
+
# GET /rails_nexus/backup/:id/history
|
|
76
|
+
def history
|
|
77
|
+
@config = RailsNexus::BackupConfig.find(params[:id])
|
|
78
|
+
@records = RailsNexus::Backup.where(model_name: @config.name)
|
|
79
|
+
.order(started_at: :desc)
|
|
80
|
+
.limit(50)
|
|
41
81
|
end
|
|
42
82
|
|
|
43
|
-
# GET /rails_nexus/backup/
|
|
44
|
-
def
|
|
45
|
-
@
|
|
46
|
-
@health = @service.health_status
|
|
47
|
-
@summary = @service.summary
|
|
83
|
+
# GET /rails_nexus/backup/history
|
|
84
|
+
def all_history
|
|
85
|
+
@records = RailsNexus::Backup.order(started_at: :desc).limit(100)
|
|
48
86
|
end
|
|
49
87
|
|
|
50
88
|
private
|
|
51
89
|
|
|
90
|
+
def set_config
|
|
91
|
+
@config = RailsNexus::BackupConfig.find(params[:id])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def config_params
|
|
95
|
+
params.require(:backup_config).permit(
|
|
96
|
+
:name, :description, :database_name, :adapter, :host, :port,
|
|
97
|
+
:username, :password, :storage_path, :keep_count,
|
|
98
|
+
:compress, :encrypt, :encrypt_password,
|
|
99
|
+
:rsync_enabled, :rsync_host, :rsync_port, :rsync_user, :rsync_path, :rsync_mirror,
|
|
100
|
+
:notify_command, :notify_on_success, :notify_on_failure,
|
|
101
|
+
:schedule_cron, :enabled,
|
|
102
|
+
skip_tables: []
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
52
106
|
def check_backup_enabled
|
|
53
107
|
unless RailsNexus.configuration.backup_enabled
|
|
54
108
|
render plain: "Backup management not enabled", status: :forbidden
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsNexus
|
|
4
|
+
class BackupConfig < BaseRecord
|
|
5
|
+
self.table_name = "rails_nexus_backup_configs"
|
|
6
|
+
|
|
7
|
+
ADAPTERS = %w[mysql postgresql sqlite].freeze
|
|
8
|
+
|
|
9
|
+
validates :name, presence: true, uniqueness: true
|
|
10
|
+
validates :database_name, presence: true
|
|
11
|
+
validates :adapter, presence: true, inclusion: { in: ADAPTERS }
|
|
12
|
+
validates :storage_path, presence: true
|
|
13
|
+
validates :keep_count, numericality: { greater_than: 0 }
|
|
14
|
+
validates :encrypt_password, presence: true, if: :encrypt?
|
|
15
|
+
validates :rsync_host, presence: true, if: :rsync_enabled?
|
|
16
|
+
validates :rsync_user, presence: true, if: :rsync_enabled?
|
|
17
|
+
|
|
18
|
+
scope :enabled, -> { where(enabled: true) }
|
|
19
|
+
scope :disabled, -> { where(enabled: false) }
|
|
20
|
+
|
|
21
|
+
serialize :skip_tables, coder: JSON
|
|
22
|
+
|
|
23
|
+
def skip_tables_list
|
|
24
|
+
return [] if skip_tables.blank?
|
|
25
|
+
skip_tables.is_a?(Array) ? skip_tables : JSON.parse(skip_tables.to_s) rescue []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def skip_tables_list=(list)
|
|
29
|
+
self.skip_tables = list
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def mysql?
|
|
33
|
+
adapter == "mysql"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def postgresql?
|
|
37
|
+
adapter == "postgresql"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def sqlite?
|
|
41
|
+
adapter == "sqlite"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def storage_path_expanded
|
|
45
|
+
storage_path.gsub("~", Dir.home)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dump_filename
|
|
49
|
+
timestamp = Time.current.strftime("%Y%m%d_%H%M%S")
|
|
50
|
+
ext = compress? ? "sql.gz" : "sql"
|
|
51
|
+
"#{name}_#{timestamp}.#{ext}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def dump_filepath
|
|
55
|
+
File.join(storage_path_expanded, dump_filename)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get recent backup records for this config
|
|
59
|
+
def recent_backups(limit: 10)
|
|
60
|
+
RailsNexus::Backup.where(model_name: name).order(started_at: :desc).limit(limit)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Summary stats
|
|
64
|
+
def stats
|
|
65
|
+
backups = RailsNexus::Backup.where(model_name: name)
|
|
66
|
+
recent = backups.where("started_at >= ?", 7.days.ago)
|
|
67
|
+
{
|
|
68
|
+
total: backups.count,
|
|
69
|
+
successful: backups.successful.count,
|
|
70
|
+
failed: backups.failed.count,
|
|
71
|
+
recent_count: recent.count,
|
|
72
|
+
recent_success: recent.successful.count,
|
|
73
|
+
recent_failed: recent.failed.count,
|
|
74
|
+
last_backup: backups.successful.latest_first.first,
|
|
75
|
+
total_size: backups.sum(:file_size).to_i,
|
|
76
|
+
total_size_human: human_size(backups.sum(:file_size).to_i)
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def human_size(bytes)
|
|
83
|
+
return "0 B" if bytes.zero?
|
|
84
|
+
units = %w[B KB MB GB TB]
|
|
85
|
+
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
86
|
+
exp = units.size - 1 if exp >= units.size
|
|
87
|
+
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsNexus
|
|
4
|
+
class BackupRunner
|
|
5
|
+
# Run a specific backup config by name
|
|
6
|
+
def self.run(name)
|
|
7
|
+
config = RailsNexus::BackupConfig.find_by!(name: name)
|
|
8
|
+
RailsNexus::BackupService.run(config)
|
|
9
|
+
rescue ActiveRecord::RecordNotFound
|
|
10
|
+
{ success: false, error: "Backup config '#{name}' not found" }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Run all enabled backup configs
|
|
14
|
+
def self.run_all
|
|
15
|
+
results = []
|
|
16
|
+
RailsNexus::BackupConfig.enabled.find_each do |config|
|
|
17
|
+
results << RailsNexus::BackupService.run(config)
|
|
18
|
+
end
|
|
19
|
+
results
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Run backups matching a cron schedule
|
|
23
|
+
def self.run_scheduled
|
|
24
|
+
now = Time.current
|
|
25
|
+
results = []
|
|
26
|
+
|
|
27
|
+
RailsNexus::BackupConfig.enabled.where.not(schedule_cron: [nil, ""]).find_each do |config|
|
|
28
|
+
if matches_cron?(config.schedule_cron, now)
|
|
29
|
+
results << RailsNexus::BackupService.run(config)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
results
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check if a cron expression matches the current time
|
|
37
|
+
def self.matches_cron?(cron_expression, time)
|
|
38
|
+
return false if cron_expression.blank?
|
|
39
|
+
|
|
40
|
+
parts = cron_expression.strip.split(/\s+/)
|
|
41
|
+
return false unless parts.size == 5
|
|
42
|
+
|
|
43
|
+
minute, hour, day, month, wday = parts
|
|
44
|
+
|
|
45
|
+
match_field(minute, time.min) &&
|
|
46
|
+
match_field(hour, time.hour) &&
|
|
47
|
+
match_field(day, time.day) &&
|
|
48
|
+
match_field(month, time.mon) &&
|
|
49
|
+
match_field(wday, time.wday)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.match_field(pattern, value)
|
|
53
|
+
return true if pattern == "*"
|
|
54
|
+
return true if pattern.include?(",") && pattern.split(",").map(&:strip).include?(value.to_s)
|
|
55
|
+
return true if pattern.include?("-")
|
|
56
|
+
return true if pattern.include?("/")
|
|
57
|
+
|
|
58
|
+
pattern.to_i == value
|
|
59
|
+
end
|
|
60
|
+
private_class_method :match_field
|
|
61
|
+
end
|
|
62
|
+
end
|