rails_nexus 2.1.0 → 2.1.3
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/CHANGELOG.md +110 -0
- data/README.md +68 -13
- data/SECURITY.md +11 -0
- data/THIRD_PARTY_NOTICES.md +14 -0
- data/app/assets/config/rails_nexus_manifest.js +1 -1
- data/app/assets/javascripts/rails_nexus/application.js +55 -0
- data/app/controllers/rails_nexus/analytics_controller.rb +13 -26
- data/app/controllers/rails_nexus/application_controller.rb +6 -2
- data/app/controllers/rails_nexus/cron_jobs_controller.rb +1 -9
- data/app/controllers/rails_nexus/database_health_controller.rb +0 -10
- data/app/controllers/rails_nexus/logged_exceptions_controller.rb +0 -2
- data/app/controllers/rails_nexus/n1_patterns_controller.rb +0 -2
- data/app/controllers/rails_nexus/settings_controller.rb +1 -39
- data/app/controllers/rails_nexus/source_code_controller.rb +9 -13
- data/app/controllers/rails_nexus/stats_controller.rb +33 -159
- data/app/helpers/rails_nexus/logged_exceptions_helper.rb +4 -49
- data/app/helpers/rails_nexus/source_code_helper.rb +49 -10
- data/app/javascript/rails_nexus/application.js +15 -3
- data/app/models/rails_nexus/logged_exception.rb +27 -13
- data/app/views/layouts/rails_nexus/_sidebar_navigation.html.erb +0 -11
- data/app/views/layouts/rails_nexus/application.html.erb +2 -13
- data/app/views/rails_nexus/stats/index.html.erb +1 -102
- data/config/routes.rb +0 -11
- data/lib/generators/rails_nexus/install_generator.rb +1 -1
- data/lib/generators/rails_nexus/templates/INSTALL.md +1 -0
- data/lib/generators/rails_nexus/templates/migration.rb +0 -82
- data/lib/generators/rails_nexus/templates/rails_nexus.rb +3 -0
- data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +1 -1
- data/lib/rails_nexus/configuration.rb +10 -45
- data/lib/rails_nexus/database_adapter.rb +53 -0
- data/lib/rails_nexus/engine.rb +6 -17
- data/lib/rails_nexus/logger.rb +1 -12
- data/lib/rails_nexus/notifications.rb +27 -20
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/rails_nexus/webhook_client.rb +151 -0
- data/lib/rails_nexus.rb +37 -23
- data/lib/tasks/rails_nexus.rake +0 -80
- data/rails_nexus.gemspec +4 -5
- metadata +12 -41
- data/app/controllers/rails_nexus/backup_controller.rb +0 -120
- data/app/jobs/rails_nexus/backup_job.rb +0 -18
- data/app/models/rails_nexus/backup.rb +0 -107
- data/app/models/rails_nexus/backup_config.rb +0 -158
- data/app/services/rails_nexus/backup_runner.rb +0 -62
- data/app/services/rails_nexus/backup_service.rb +0 -682
- data/app/views/rails_nexus/backup/_form.html.erb +0 -334
- data/app/views/rails_nexus/backup/config_history.html.erb +0 -67
- data/app/views/rails_nexus/backup/edit.html.erb +0 -15
- data/app/views/rails_nexus/backup/history.html.erb +0 -75
- data/app/views/rails_nexus/backup/index.html.erb +0 -112
- data/app/views/rails_nexus/backup/new.html.erb +0 -15
- data/config/importmap.rb +0 -15
- data/db/seeds/backup_configs.rb +0 -140
- data/lib/generators/rails_nexus/backup_generator.rb +0 -70
- data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +0 -152
- data/lib/generators/rails_nexus/templates/backup/config.rb +0 -27
- data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +0 -24
- data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +0 -35
- data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +0 -43
- data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +0 -15
- data/lib/generators/rails_nexus/templates/backup/schedule.rb +0 -28
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RailsNexus
|
|
4
|
-
class BackupController < ApplicationController
|
|
5
|
-
before_action :rails_nexus_require_auth!
|
|
6
|
-
before_action :check_backup_enabled
|
|
7
|
-
before_action :set_config, only: %i[edit update destroy trigger]
|
|
8
|
-
|
|
9
|
-
# GET /rails_nexus/backup
|
|
10
|
-
def index
|
|
11
|
-
@configs = RailsNexus::BackupConfig.order(:name)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# GET /rails_nexus/backup/new
|
|
15
|
-
def new
|
|
16
|
-
@config = RailsNexus::BackupConfig.new(
|
|
17
|
-
adapter: "mysql",
|
|
18
|
-
host: "localhost",
|
|
19
|
-
port: 3306,
|
|
20
|
-
storage_path: "~/dumps",
|
|
21
|
-
keep_count: 30,
|
|
22
|
-
compress: true
|
|
23
|
-
)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# POST /rails_nexus/backup
|
|
27
|
-
def create
|
|
28
|
-
@config = RailsNexus::BackupConfig.new(config_params)
|
|
29
|
-
|
|
30
|
-
if @config.save
|
|
31
|
-
redirect_to backup_path, notice: "Backup config '#{@config.name}' created."
|
|
32
|
-
else
|
|
33
|
-
render :new, status: :unprocessable_entity
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# GET /rails_nexus/backup/:id/edit
|
|
38
|
-
def edit; end
|
|
39
|
-
|
|
40
|
-
# PATCH /rails_nexus/backup/:id
|
|
41
|
-
def update
|
|
42
|
-
# Strip blank password fields so they don't overwrite stored values
|
|
43
|
-
filtered = config_params
|
|
44
|
-
%w[password encryption_password gpg_password].each do |attr|
|
|
45
|
-
filtered = filtered.except(attr) if filtered[attr].blank?
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
if @config.update(filtered)
|
|
49
|
-
redirect_to backup_path, notice: "Backup config '#{@config.name}' updated."
|
|
50
|
-
else
|
|
51
|
-
render :edit, status: :unprocessable_entity
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# DELETE /rails_nexus/backup/:id
|
|
56
|
-
def destroy
|
|
57
|
-
name = @config.name
|
|
58
|
-
@config.destroy
|
|
59
|
-
redirect_to backup_path, notice: "Backup config '#{name}' deleted."
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# POST /rails_nexus/backup/:id/trigger
|
|
63
|
-
def trigger
|
|
64
|
-
begin
|
|
65
|
-
RailsNexus::BackupJob.perform_later(@config.id)
|
|
66
|
-
redirect_to backup_path, notice: "Backup '#{@config.name}' started in background."
|
|
67
|
-
rescue => e
|
|
68
|
-
# Fallback to synchronous if Active Job is not configured
|
|
69
|
-
result = RailsNexus::BackupService.run(@config)
|
|
70
|
-
if result[:success]
|
|
71
|
-
redirect_to backup_path, notice: "Backup '#{@config.name}' completed successfully."
|
|
72
|
-
else
|
|
73
|
-
redirect_to backup_path, alert: "Backup failed: #{result[:error]}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# GET /rails_nexus/backup/:id/history
|
|
79
|
-
def history
|
|
80
|
-
@config = RailsNexus::BackupConfig.find(params[:id])
|
|
81
|
-
@records = RailsNexus::Backup.where(config_name: @config.name)
|
|
82
|
-
.order(started_at: :desc)
|
|
83
|
-
.limit(50)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# GET /rails_nexus/backup/history
|
|
87
|
-
def all_history
|
|
88
|
-
@records = RailsNexus::Backup.order(started_at: :desc).limit(100)
|
|
89
|
-
render :history
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
private
|
|
93
|
-
|
|
94
|
-
def set_config
|
|
95
|
-
@config = RailsNexus::BackupConfig.find(params[:id])
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def config_params
|
|
99
|
-
params.require(:backup_config).permit(
|
|
100
|
-
:name, :description, :database_name, :adapter, :host, :port,
|
|
101
|
-
:username, :password, :storage_path, :keep_count,
|
|
102
|
-
:compress, :encrypted, :encryption_password,
|
|
103
|
-
:rsync_enabled, :rsync_host, :rsync_port, :rsync_user, :rsync_path, :rsync_mirror, :rsync_archive, :rsync_directories, :rsync_excludes,
|
|
104
|
-
:notify_command, :notify_on_success, :notify_on_failure,
|
|
105
|
-
:schedule_cron, :enabled,
|
|
106
|
-
:s3_enabled, :s3_access_key, :s3_secret_key, :s3_bucket, :s3_region, :s3_prefix,
|
|
107
|
-
:gpg_enabled, :gpg_password,
|
|
108
|
-
:email_notify, :email_to,
|
|
109
|
-
:archive_enabled, :bzip2_compress, :split_chunks, :encrypt_base64, :mysql_additional_options,
|
|
110
|
-
skip_tables: [], archive_paths: [], archive_excludes: []
|
|
111
|
-
)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def check_backup_enabled
|
|
115
|
-
unless RailsNexus.configuration.backup_enabled
|
|
116
|
-
render plain: "Backup management not enabled", status: :forbidden
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RailsNexus
|
|
4
|
-
class BackupJob < ApplicationJob
|
|
5
|
-
queue_as :rails_nexus_backups
|
|
6
|
-
|
|
7
|
-
# Accept either a BackupConfig record ID or a BackupConfig record
|
|
8
|
-
def perform(backup_config_id_or_record)
|
|
9
|
-
config = if backup_config_id_or_record.is_a?(RailsNexus::BackupConfig)
|
|
10
|
-
backup_config_id_or_record
|
|
11
|
-
else
|
|
12
|
-
RailsNexus::BackupConfig.find(backup_config_id_or_record)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
RailsNexus::BackupService.run(config)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RailsNexus
|
|
4
|
-
class Backup < BaseRecord
|
|
5
|
-
self.table_name = "rails_nexus_backups"
|
|
6
|
-
|
|
7
|
-
validates :config_name, presence: true
|
|
8
|
-
validates :status, presence: true, inclusion: { in: %w[running success failed] }
|
|
9
|
-
validates :started_at, presence: true
|
|
10
|
-
|
|
11
|
-
scope :recent, ->(hours = 24) { where("started_at >= ?", hours.hours.ago) }
|
|
12
|
-
scope :successful, -> { where(status: "success") }
|
|
13
|
-
scope :failed, -> { where(status: "failed") }
|
|
14
|
-
scope :running, -> { where(status: "running") }
|
|
15
|
-
scope :by_config, ->(name) { where(config_name: name) }
|
|
16
|
-
scope :latest_first, -> { order(started_at: :desc) }
|
|
17
|
-
|
|
18
|
-
# Start tracking a backup run
|
|
19
|
-
def self.start!(config_name:, triggered_by: "system")
|
|
20
|
-
create!(
|
|
21
|
-
config_name: config_name,
|
|
22
|
-
status: "running",
|
|
23
|
-
triggered_by: triggered_by,
|
|
24
|
-
started_at: Time.current
|
|
25
|
-
)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Mark backup as completed
|
|
29
|
-
def succeed!(file_path: nil, file_size: nil)
|
|
30
|
-
update!(
|
|
31
|
-
status: "success",
|
|
32
|
-
file_path: file_path,
|
|
33
|
-
file_size: file_size,
|
|
34
|
-
duration: (Time.current - started_at).round(2),
|
|
35
|
-
completed_at: Time.current
|
|
36
|
-
)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Mark backup as failed
|
|
40
|
-
def fail!(error_message:)
|
|
41
|
-
update!(
|
|
42
|
-
status: "failed",
|
|
43
|
-
error_message: error_message,
|
|
44
|
-
duration: (Time.current - started_at).round(2),
|
|
45
|
-
completed_at: Time.current
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Get success rate for a model over the last N days
|
|
50
|
-
def self.success_rate(config_name:, days: 7)
|
|
51
|
-
records = by_config(config_name).where("started_at >= ?", days.days.ago).where.not(status: "running")
|
|
52
|
-
return 0 if records.empty?
|
|
53
|
-
(records.successful.count.to_f / records.count * 100).round(1)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Get backup health summary
|
|
57
|
-
def self.health_summary(alert_threshold_hours: 24)
|
|
58
|
-
configs = RailsNexus::BackupConfig.pluck(:name)
|
|
59
|
-
results = configs.map do |config_name|
|
|
60
|
-
latest = by_config(config_name).successful.latest_first.first
|
|
61
|
-
age_hours = latest ? ((Time.current - latest.started_at) / 3600).round(1) : nil
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
config_name: config_name,
|
|
65
|
-
last_backup: latest&.started_at,
|
|
66
|
-
age_hours: age_hours,
|
|
67
|
-
status: age_hours.nil? ? "unknown" : (age_hours < alert_threshold_hours ? "healthy" : "stale"),
|
|
68
|
-
success_rate: success_rate(config_name: config_name),
|
|
69
|
-
last_file_size: latest&.file_size
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
healthy: results.all? { |r| r[:status] == "healthy" },
|
|
75
|
-
models: results,
|
|
76
|
-
alerts: results.select { |r| r[:status] == "stale" || r[:status] == "unknown" }
|
|
77
|
-
}
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Cleanup old backup records
|
|
81
|
-
def self.cleanup!(retention_days: 30)
|
|
82
|
-
where("started_at < ?", retention_days.days.ago).delete_all
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Format file size
|
|
86
|
-
def file_size_human
|
|
87
|
-
return "—" unless file_size
|
|
88
|
-
units = ["B", "KB", "MB", "GB"]
|
|
89
|
-
size = file_size.to_f
|
|
90
|
-
units.each do |unit|
|
|
91
|
-
return "#{size.round(1)} #{unit}" if size < 1024
|
|
92
|
-
size /= 1024
|
|
93
|
-
end
|
|
94
|
-
"#{size.round(1)} TB"
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Duration formatted
|
|
98
|
-
def duration_human
|
|
99
|
-
return "—" unless duration
|
|
100
|
-
if duration < 60
|
|
101
|
-
"#{duration.round(1)}s"
|
|
102
|
-
else
|
|
103
|
-
"#{(duration / 60).floor}m #{(duration % 60).round}s"
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
@@ -1,158 +0,0 @@
|
|
|
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 mongodb redis].freeze
|
|
8
|
-
|
|
9
|
-
# ─── Boolean defaults ──────────────────────────────────────────
|
|
10
|
-
# Rails doesn't apply DB defaults to new in-memory records, so
|
|
11
|
-
# check_box helpers in form_for render unchecked. These declarations
|
|
12
|
-
# ensure the form reflects the intended defaults.
|
|
13
|
-
attribute :enabled, :boolean, default: true
|
|
14
|
-
attribute :compress, :boolean, default: true
|
|
15
|
-
attribute :encrypted, :boolean, default: false
|
|
16
|
-
attribute :encrypt_base64, :boolean, default: false
|
|
17
|
-
attribute :rsync_enabled, :boolean, default: false
|
|
18
|
-
attribute :rsync_mirror, :boolean, default: false
|
|
19
|
-
attribute :notify_on_success, :boolean, default: true
|
|
20
|
-
attribute :notify_on_failure, :boolean, default: true
|
|
21
|
-
attribute :s3_enabled, :boolean, default: false
|
|
22
|
-
attribute :gpg_enabled, :boolean, default: false
|
|
23
|
-
attribute :email_notify, :boolean, default: false
|
|
24
|
-
attribute :archive_enabled, :boolean, default: false
|
|
25
|
-
attribute :bzip2_compress, :boolean, default: false
|
|
26
|
-
attribute :split_chunks, :boolean, default: false
|
|
27
|
-
|
|
28
|
-
validates :name, presence: true, uniqueness: true
|
|
29
|
-
validates :database_name, presence: true, if: -> { adapter != "redis" }
|
|
30
|
-
validates :adapter, presence: true, inclusion: { in: ADAPTERS }
|
|
31
|
-
validates :storage_path, presence: true
|
|
32
|
-
validates :keep_count, numericality: { greater_than: 0 }
|
|
33
|
-
validates :encryption_password, presence: true, if: :encrypted?
|
|
34
|
-
validates :rsync_host, presence: true, if: :rsync_enabled?
|
|
35
|
-
validates :rsync_user, presence: true, if: :rsync_enabled?
|
|
36
|
-
validates :s3_bucket, presence: true, if: :s3_enabled?
|
|
37
|
-
validates :s3_access_key, presence: true, if: :s3_enabled?
|
|
38
|
-
validates :s3_secret_key, presence: true, if: :s3_enabled?
|
|
39
|
-
validates :gpg_password, presence: true, if: :gpg_enabled?
|
|
40
|
-
validates :email_to, presence: true, if: :email_notify?
|
|
41
|
-
|
|
42
|
-
scope :enabled, -> { where(enabled: true) }
|
|
43
|
-
scope :disabled, -> { where(enabled: false) }
|
|
44
|
-
|
|
45
|
-
serialize :skip_tables, coder: JSON
|
|
46
|
-
serialize :archive_paths, coder: JSON
|
|
47
|
-
serialize :archive_excludes, coder: JSON
|
|
48
|
-
|
|
49
|
-
# ─── Skip Tables ───────────────────────────────────────────────
|
|
50
|
-
def skip_tables_list
|
|
51
|
-
return [] if skip_tables.blank?
|
|
52
|
-
skip_tables.is_a?(Array) ? skip_tables : JSON.parse(skip_tables.to_s) rescue []
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def skip_tables_list=(value)
|
|
56
|
-
return if value.blank?
|
|
57
|
-
self.skip_tables = if value.is_a?(String)
|
|
58
|
-
value.split(",").map(&:strip).reject(&:blank?)
|
|
59
|
-
else
|
|
60
|
-
value
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# ─── Archive Paths ─────────────────────────────────────────────
|
|
65
|
-
def archive_paths_list
|
|
66
|
-
return [] if archive_paths.blank?
|
|
67
|
-
archive_paths.is_a?(Array) ? archive_paths : JSON.parse(archive_paths.to_s) rescue []
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def archive_paths_list=(value)
|
|
71
|
-
return if value.blank?
|
|
72
|
-
self.archive_paths = if value.is_a?(String)
|
|
73
|
-
value.split(",").map(&:strip).reject(&:blank?)
|
|
74
|
-
else
|
|
75
|
-
value
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def archive_excludes_list
|
|
80
|
-
return [] if archive_excludes.blank?
|
|
81
|
-
archive_excludes.is_a?(Array) ? archive_excludes : JSON.parse(archive_excludes.to_s) rescue []
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def archive_excludes_list=(value)
|
|
85
|
-
return if value.blank?
|
|
86
|
-
self.archive_excludes = if value.is_a?(String)
|
|
87
|
-
value.split(",").map(&:strip).reject(&:blank?)
|
|
88
|
-
else
|
|
89
|
-
value
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# ─── Adapter checks ────────────────────────────────────────────
|
|
94
|
-
def mysql?; adapter == "mysql"; end
|
|
95
|
-
def postgresql?; adapter == "postgresql"; end
|
|
96
|
-
def sqlite?; adapter == "sqlite"; end
|
|
97
|
-
def mongodb?; adapter == "mongodb"; end
|
|
98
|
-
def redis?; adapter == "redis"; end
|
|
99
|
-
|
|
100
|
-
# ─── Path helpers ──────────────────────────────────────────────
|
|
101
|
-
def storage_path_expanded
|
|
102
|
-
storage_path.gsub("~", Dir.home)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def dump_filename
|
|
106
|
-
timestamp = Time.current.strftime("%Y%m%d_%H%M%S")
|
|
107
|
-
ext = dump_extension
|
|
108
|
-
"#{name}_#{timestamp}.#{ext}"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def dump_filepath
|
|
112
|
-
File.join(storage_path_expanded, dump_filename)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# ─── Stats ─────────────────────────────────────────────────────
|
|
116
|
-
def recent_backups(limit: 10)
|
|
117
|
-
RailsNexus::Backup.where(config_name: name).order(started_at: :desc).limit(limit)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def last_failure
|
|
121
|
-
RailsNexus::Backup.where(config_name: name, status: "failed").order(started_at: :desc).first
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def stats
|
|
125
|
-
backups = RailsNexus::Backup.where(config_name: name)
|
|
126
|
-
recent = backups.where("started_at >= ?", 7.days.ago)
|
|
127
|
-
{
|
|
128
|
-
total: backups.count,
|
|
129
|
-
successful: backups.successful.count,
|
|
130
|
-
failed: backups.failed.count,
|
|
131
|
-
recent_count: recent.count,
|
|
132
|
-
recent_success: recent.successful.count,
|
|
133
|
-
recent_failed: recent.failed.count,
|
|
134
|
-
last_backup: backups.successful.latest_first.first,
|
|
135
|
-
total_size: backups.sum(:file_size).to_i,
|
|
136
|
-
total_size_human: human_size(backups.sum(:file_size).to_i)
|
|
137
|
-
}
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
private
|
|
141
|
-
|
|
142
|
-
def dump_extension
|
|
143
|
-
parts = ["sql"]
|
|
144
|
-
parts << "gz" if compress? && !bzip2_compress?
|
|
145
|
-
parts << "bz2" if bzip2_compress?
|
|
146
|
-
parts << "enc" if encrypted?
|
|
147
|
-
parts.join(".")
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def human_size(bytes)
|
|
151
|
-
return "0 B" if bytes.zero?
|
|
152
|
-
units = %w[B KB MB GB TB]
|
|
153
|
-
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
154
|
-
exp = units.size - 1 if exp >= units.size
|
|
155
|
-
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
@@ -1,62 +0,0 @@
|
|
|
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
|