rails_nexus 2.1.2 → 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/README.md +38 -10
- data/app/controllers/rails_nexus/stats_controller.rb +0 -126
- data/app/views/layouts/rails_nexus/_sidebar_navigation.html.erb +0 -11
- data/app/views/layouts/rails_nexus/application.html.erb +1 -12
- data/app/views/rails_nexus/stats/index.html.erb +0 -101
- data/config/routes.rb +0 -11
- data/lib/generators/rails_nexus/templates/migration.rb +0 -82
- data/lib/rails_nexus/configuration.rb +0 -45
- data/lib/rails_nexus/engine.rb +6 -4
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/tasks/rails_nexus.rake +0 -80
- data/rails_nexus.gemspec +1 -3
- metadata +5 -26
- data/app/controllers/rails_nexus/backup_controller.rb +0 -119
- 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 -189
- data/app/services/rails_nexus/backup_runner.rb +0 -62
- data/app/services/rails_nexus/backup_service.rb +0 -817
- 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/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
data/rails_nexus.gemspec
CHANGED
|
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.email = ["tamiruhailu@gmail.com"]
|
|
8
8
|
spec.homepage = "https://github.com/tamiru/rails_nexus"
|
|
9
9
|
spec.summary = "The extensible control plane for Rails applications"
|
|
10
|
-
spec.description = "RailsNexus is an extensible operations and administration console for Rails applications. It provides error monitoring,
|
|
10
|
+
spec.description = "RailsNexus is an extensible operations and administration console for Rails applications. It provides error monitoring, cron job tracking, server statistics, database health, real-time analytics, workflow management, source code viewing, N+1 query detection, and storm protection. Built with Hotwire, Stimulus, and Tailwind-compatible CSS."
|
|
11
11
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
12
12
|
spec.license = "MIT"
|
|
13
13
|
|
|
@@ -32,6 +32,4 @@ Gem::Specification.new do |spec|
|
|
|
32
32
|
|
|
33
33
|
spec.add_dependency "ransack", "~> 4.0"
|
|
34
34
|
|
|
35
|
-
# Note: Backup management requires 'backup' (~> 4.4) and 'whenever' (~> 1.1)
|
|
36
|
-
# gems to be added to your host application's Gemfile.
|
|
37
35
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_nexus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tamiru Hailu
|
|
@@ -86,10 +86,10 @@ dependencies:
|
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
87
|
version: '4.0'
|
|
88
88
|
description: RailsNexus is an extensible operations and administration console for
|
|
89
|
-
Rails applications. It provides error monitoring,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
Rails applications. It provides error monitoring, cron job tracking, server statistics,
|
|
90
|
+
database health, real-time analytics, workflow management, source code viewing,
|
|
91
|
+
N+1 query detection, and storm protection. Built with Hotwire, Stimulus, and Tailwind-compatible
|
|
92
|
+
CSS.
|
|
93
93
|
email:
|
|
94
94
|
- tamiruhailu@gmail.com
|
|
95
95
|
executables: []
|
|
@@ -108,7 +108,6 @@ files:
|
|
|
108
108
|
- app/assets/stylesheets/rails_nexus/application.css
|
|
109
109
|
- app/controllers/rails_nexus/analytics_controller.rb
|
|
110
110
|
- app/controllers/rails_nexus/application_controller.rb
|
|
111
|
-
- app/controllers/rails_nexus/backup_controller.rb
|
|
112
111
|
- app/controllers/rails_nexus/cron_jobs_controller.rb
|
|
113
112
|
- app/controllers/rails_nexus/database_health_controller.rb
|
|
114
113
|
- app/controllers/rails_nexus/logged_exceptions_controller.rb
|
|
@@ -126,28 +125,17 @@ files:
|
|
|
126
125
|
- app/javascript/controllers/rails_nexus_theme_controller.js
|
|
127
126
|
- app/javascript/rails_nexus/application.js
|
|
128
127
|
- app/jobs/rails_nexus/application_job.rb
|
|
129
|
-
- app/jobs/rails_nexus/backup_job.rb
|
|
130
128
|
- app/mailers/rails_nexus/application_mailer.rb
|
|
131
129
|
- app/models/rails_nexus/application_record.rb
|
|
132
|
-
- app/models/rails_nexus/backup.rb
|
|
133
|
-
- app/models/rails_nexus/backup_config.rb
|
|
134
130
|
- app/models/rails_nexus/base_record.rb
|
|
135
131
|
- app/models/rails_nexus/comment.rb
|
|
136
132
|
- app/models/rails_nexus/cron_job.rb
|
|
137
133
|
- app/models/rails_nexus/logged_exception.rb
|
|
138
134
|
- app/models/rails_nexus/logged_exception_n1.rb
|
|
139
135
|
- app/models/rails_nexus/webhook_delivery.rb
|
|
140
|
-
- app/services/rails_nexus/backup_runner.rb
|
|
141
|
-
- app/services/rails_nexus/backup_service.rb
|
|
142
136
|
- app/views/layouts/rails_nexus/_sidebar_navigation.html.erb
|
|
143
137
|
- app/views/layouts/rails_nexus/application.html.erb
|
|
144
138
|
- app/views/rails_nexus/analytics/index.html.erb
|
|
145
|
-
- app/views/rails_nexus/backup/_form.html.erb
|
|
146
|
-
- app/views/rails_nexus/backup/config_history.html.erb
|
|
147
|
-
- app/views/rails_nexus/backup/edit.html.erb
|
|
148
|
-
- app/views/rails_nexus/backup/history.html.erb
|
|
149
|
-
- app/views/rails_nexus/backup/index.html.erb
|
|
150
|
-
- app/views/rails_nexus/backup/new.html.erb
|
|
151
139
|
- app/views/rails_nexus/cron_jobs/index.html.erb
|
|
152
140
|
- app/views/rails_nexus/database_health/index.html.erb
|
|
153
141
|
- app/views/rails_nexus/logged_exceptions/_comments_list.html.erb
|
|
@@ -171,18 +159,9 @@ files:
|
|
|
171
159
|
- config/initializers/date_formats.rb
|
|
172
160
|
- config/locales/en.yml
|
|
173
161
|
- config/routes.rb
|
|
174
|
-
- db/seeds/backup_configs.rb
|
|
175
|
-
- lib/generators/rails_nexus/backup_generator.rb
|
|
176
162
|
- lib/generators/rails_nexus/customize_generator.rb
|
|
177
163
|
- lib/generators/rails_nexus/install_generator.rb
|
|
178
164
|
- lib/generators/rails_nexus/templates/INSTALL.md
|
|
179
|
-
- lib/generators/rails_nexus/templates/backup/backup_helper.sh
|
|
180
|
-
- lib/generators/rails_nexus/templates/backup/config.rb
|
|
181
|
-
- lib/generators/rails_nexus/templates/backup/models/daily_backup.rb
|
|
182
|
-
- lib/generators/rails_nexus/templates/backup/models/full_backup.rb
|
|
183
|
-
- lib/generators/rails_nexus/templates/backup/models/sync_backup.rb
|
|
184
|
-
- lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf
|
|
185
|
-
- lib/generators/rails_nexus/templates/backup/schedule.rb
|
|
186
165
|
- lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js
|
|
187
166
|
- lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js
|
|
188
167
|
- lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RailsNexus
|
|
4
|
-
class BackupController < ApplicationController
|
|
5
|
-
before_action :check_backup_enabled
|
|
6
|
-
before_action :set_config, only: %i[edit update destroy trigger]
|
|
7
|
-
|
|
8
|
-
# GET /rails_nexus/backup
|
|
9
|
-
def index
|
|
10
|
-
@configs = RailsNexus::BackupConfig.order(:name)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# GET /rails_nexus/backup/new
|
|
14
|
-
def new
|
|
15
|
-
@config = RailsNexus::BackupConfig.new(
|
|
16
|
-
adapter: "mysql",
|
|
17
|
-
host: "localhost",
|
|
18
|
-
port: 3306,
|
|
19
|
-
storage_path: "~/dumps",
|
|
20
|
-
keep_count: 30,
|
|
21
|
-
compress: true
|
|
22
|
-
)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# POST /rails_nexus/backup
|
|
26
|
-
def create
|
|
27
|
-
@config = RailsNexus::BackupConfig.new(config_params)
|
|
28
|
-
|
|
29
|
-
if @config.save
|
|
30
|
-
redirect_to backup_path, notice: "Backup config '#{@config.name}' created."
|
|
31
|
-
else
|
|
32
|
-
render :new, status: :unprocessable_entity
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# GET /rails_nexus/backup/:id/edit
|
|
37
|
-
def edit; end
|
|
38
|
-
|
|
39
|
-
# PATCH /rails_nexus/backup/:id
|
|
40
|
-
def update
|
|
41
|
-
# Strip blank password fields so they don't overwrite stored values
|
|
42
|
-
filtered = config_params
|
|
43
|
-
%w[password encryption_password gpg_password].each do |attr|
|
|
44
|
-
filtered = filtered.except(attr) if filtered[attr].blank?
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
if @config.update(filtered)
|
|
48
|
-
redirect_to backup_path, notice: "Backup config '#{@config.name}' updated."
|
|
49
|
-
else
|
|
50
|
-
render :edit, status: :unprocessable_entity
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# DELETE /rails_nexus/backup/:id
|
|
55
|
-
def destroy
|
|
56
|
-
name = @config.name
|
|
57
|
-
@config.destroy
|
|
58
|
-
redirect_to backup_path, notice: "Backup config '#{name}' deleted."
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# POST /rails_nexus/backup/:id/trigger
|
|
62
|
-
def trigger
|
|
63
|
-
begin
|
|
64
|
-
RailsNexus::BackupJob.perform_later(@config.id)
|
|
65
|
-
redirect_to backup_path, notice: "Backup '#{@config.name}' started in background."
|
|
66
|
-
rescue
|
|
67
|
-
# Fallback to synchronous if Active Job is not configured
|
|
68
|
-
result = RailsNexus::BackupService.run(@config)
|
|
69
|
-
if result[:success]
|
|
70
|
-
redirect_to backup_path, notice: "Backup '#{@config.name}' completed successfully."
|
|
71
|
-
else
|
|
72
|
-
redirect_to backup_path, alert: "Backup failed: #{result[:error]}"
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# GET /rails_nexus/backup/:id/history
|
|
78
|
-
def history
|
|
79
|
-
@config = RailsNexus::BackupConfig.find(params[:id])
|
|
80
|
-
@records = RailsNexus::Backup.where(config_name: @config.name)
|
|
81
|
-
.order(started_at: :desc)
|
|
82
|
-
.limit(50)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# GET /rails_nexus/backup/history
|
|
86
|
-
def all_history
|
|
87
|
-
@records = RailsNexus::Backup.order(started_at: :desc).limit(100)
|
|
88
|
-
render :history
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
def set_config
|
|
94
|
-
@config = RailsNexus::BackupConfig.find(params[:id])
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def config_params
|
|
98
|
-
params.require(:backup_config).permit(
|
|
99
|
-
:name, :description, :database_name, :adapter, :host, :port,
|
|
100
|
-
:username, :password, :storage_path, :keep_count,
|
|
101
|
-
:compress, :encrypted, :encryption_password,
|
|
102
|
-
:rsync_enabled, :rsync_host, :rsync_port, :rsync_user, :rsync_path, :rsync_mirror, :rsync_archive, :rsync_directories, :rsync_excludes,
|
|
103
|
-
:notify_command, :notify_on_success, :notify_on_failure,
|
|
104
|
-
:schedule_cron, :enabled,
|
|
105
|
-
:s3_enabled, :s3_access_key, :s3_secret_key, :s3_bucket, :s3_region, :s3_prefix,
|
|
106
|
-
:gpg_enabled, :gpg_password,
|
|
107
|
-
:email_notify, :email_to,
|
|
108
|
-
:archive_enabled, :bzip2_compress, :split_chunks, :encrypt_base64, :mysql_additional_options,
|
|
109
|
-
skip_tables: [], archive_paths: [], archive_excludes: []
|
|
110
|
-
)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def check_backup_enabled
|
|
114
|
-
unless RailsNexus.configuration.backup_enabled
|
|
115
|
-
render plain: "Backup management not enabled", status: :forbidden
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
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,189 +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 :name, format: {
|
|
30
|
-
with: /\A[a-zA-Z0-9_$][a-zA-Z0-9_$.-]*\z/,
|
|
31
|
-
message: "may contain only letters, numbers, dots, underscores, dollar signs, and hyphens"
|
|
32
|
-
}
|
|
33
|
-
validates :database_name, presence: true, if: -> { adapter != "redis" }
|
|
34
|
-
validates :adapter, presence: true, inclusion: { in: ADAPTERS }
|
|
35
|
-
validates :storage_path, presence: true
|
|
36
|
-
validates :keep_count, numericality: { greater_than: 0 }
|
|
37
|
-
validates :encryption_password, presence: true, if: :encrypted?
|
|
38
|
-
validates :rsync_host, presence: true, if: :rsync_enabled?
|
|
39
|
-
validates :rsync_user, presence: true, if: :rsync_enabled?
|
|
40
|
-
validates :s3_bucket, presence: true, if: :s3_enabled?
|
|
41
|
-
validates :s3_access_key, presence: true, if: :s3_enabled?
|
|
42
|
-
validates :s3_secret_key, presence: true, if: :s3_enabled?
|
|
43
|
-
validates :gpg_password, presence: true, if: :gpg_enabled?
|
|
44
|
-
validates :email_to, presence: true, if: :email_notify?
|
|
45
|
-
validates :port, :rsync_port,
|
|
46
|
-
numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 65_535 },
|
|
47
|
-
allow_nil: true
|
|
48
|
-
validate :database_identifier_is_safe
|
|
49
|
-
validate :remote_sync_values_are_safe
|
|
50
|
-
|
|
51
|
-
scope :enabled, -> { where(enabled: true) }
|
|
52
|
-
scope :disabled, -> { where(enabled: false) }
|
|
53
|
-
|
|
54
|
-
serialize :skip_tables, coder: JSON
|
|
55
|
-
serialize :archive_paths, coder: JSON
|
|
56
|
-
serialize :archive_excludes, coder: JSON
|
|
57
|
-
|
|
58
|
-
# ─── Skip Tables ───────────────────────────────────────────────
|
|
59
|
-
def skip_tables_list
|
|
60
|
-
return [] if skip_tables.blank?
|
|
61
|
-
skip_tables.is_a?(Array) ? skip_tables : JSON.parse(skip_tables.to_s) rescue []
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def skip_tables_list=(value)
|
|
65
|
-
return if value.blank?
|
|
66
|
-
self.skip_tables = if value.is_a?(String)
|
|
67
|
-
value.split(",").map(&:strip).reject(&:blank?)
|
|
68
|
-
else
|
|
69
|
-
value
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# ─── Archive Paths ─────────────────────────────────────────────
|
|
74
|
-
def archive_paths_list
|
|
75
|
-
return [] if archive_paths.blank?
|
|
76
|
-
archive_paths.is_a?(Array) ? archive_paths : JSON.parse(archive_paths.to_s) rescue []
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def archive_paths_list=(value)
|
|
80
|
-
return if value.blank?
|
|
81
|
-
self.archive_paths = if value.is_a?(String)
|
|
82
|
-
value.split(",").map(&:strip).reject(&:blank?)
|
|
83
|
-
else
|
|
84
|
-
value
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def archive_excludes_list
|
|
89
|
-
return [] if archive_excludes.blank?
|
|
90
|
-
archive_excludes.is_a?(Array) ? archive_excludes : JSON.parse(archive_excludes.to_s) rescue []
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def archive_excludes_list=(value)
|
|
94
|
-
return if value.blank?
|
|
95
|
-
self.archive_excludes = if value.is_a?(String)
|
|
96
|
-
value.split(",").map(&:strip).reject(&:blank?)
|
|
97
|
-
else
|
|
98
|
-
value
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# ─── Adapter checks ────────────────────────────────────────────
|
|
103
|
-
def mysql?; adapter == "mysql"; end
|
|
104
|
-
def postgresql?; adapter == "postgresql"; end
|
|
105
|
-
def sqlite?; adapter == "sqlite"; end
|
|
106
|
-
def mongodb?; adapter == "mongodb"; end
|
|
107
|
-
def redis?; adapter == "redis"; end
|
|
108
|
-
|
|
109
|
-
# ─── Path helpers ──────────────────────────────────────────────
|
|
110
|
-
def storage_path_expanded
|
|
111
|
-
storage_path.gsub("~", Dir.home)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def dump_filename
|
|
115
|
-
timestamp = Time.current.strftime("%Y%m%d_%H%M%S")
|
|
116
|
-
ext = dump_extension
|
|
117
|
-
"#{name}_#{timestamp}.#{ext}"
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def dump_filepath
|
|
121
|
-
File.join(storage_path_expanded, dump_filename)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# ─── Stats ─────────────────────────────────────────────────────
|
|
125
|
-
def recent_backups(limit: 10)
|
|
126
|
-
RailsNexus::Backup.where(config_name: name).order(started_at: :desc).limit(limit)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def last_failure
|
|
130
|
-
RailsNexus::Backup.where(config_name: name, status: "failed").order(started_at: :desc).first
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def stats
|
|
134
|
-
backups = RailsNexus::Backup.where(config_name: name)
|
|
135
|
-
recent = backups.where("started_at >= ?", 7.days.ago)
|
|
136
|
-
{
|
|
137
|
-
total: backups.count,
|
|
138
|
-
successful: backups.successful.count,
|
|
139
|
-
failed: backups.failed.count,
|
|
140
|
-
recent_count: recent.count,
|
|
141
|
-
recent_success: recent.successful.count,
|
|
142
|
-
recent_failed: recent.failed.count,
|
|
143
|
-
last_backup: backups.successful.latest_first.first,
|
|
144
|
-
total_size: backups.sum(:file_size).to_i,
|
|
145
|
-
total_size_human: human_size(backups.sum(:file_size).to_i)
|
|
146
|
-
}
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
private
|
|
150
|
-
|
|
151
|
-
def database_identifier_is_safe
|
|
152
|
-
return if database_name.blank? || sqlite?
|
|
153
|
-
|
|
154
|
-
unless database_name.match?(/\A[a-zA-Z0-9_$][a-zA-Z0-9_$.-]*\z/) && !database_name.include?("..")
|
|
155
|
-
errors.add(:database_name, "contains unsupported characters")
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def remote_sync_values_are_safe
|
|
160
|
-
return unless rsync_enabled?
|
|
161
|
-
|
|
162
|
-
unless rsync_host.to_s.match?(/\A[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?\z/)
|
|
163
|
-
errors.add(:rsync_host, "is not a valid hostname")
|
|
164
|
-
end
|
|
165
|
-
unless rsync_user.to_s.match?(/\A[a-zA-Z0-9_][a-zA-Z0-9_.-]*\z/)
|
|
166
|
-
errors.add(:rsync_user, "contains unsupported characters")
|
|
167
|
-
end
|
|
168
|
-
unless rsync_path.to_s.start_with?("/") && !rsync_path.to_s.match?(/[\0\r\n]/)
|
|
169
|
-
errors.add(:rsync_path, "must be an absolute remote path")
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def dump_extension
|
|
174
|
-
parts = ["sql"]
|
|
175
|
-
parts << "gz" if compress? && !bzip2_compress?
|
|
176
|
-
parts << "bz2" if bzip2_compress?
|
|
177
|
-
parts << "enc" if encrypted?
|
|
178
|
-
parts.join(".")
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def human_size(bytes)
|
|
182
|
-
return "0 B" if bytes.zero?
|
|
183
|
-
units = %w[B KB MB GB TB]
|
|
184
|
-
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
185
|
-
exp = units.size - 1 if exp >= units.size
|
|
186
|
-
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
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
|