rails_nexus 2.1.2 → 2.1.4

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +41 -10
  4. data/app/controllers/rails_nexus/stats_controller.rb +0 -126
  5. data/app/views/layouts/rails_nexus/_sidebar_navigation.html.erb +0 -11
  6. data/app/views/layouts/rails_nexus/application.html.erb +1 -12
  7. data/app/views/rails_nexus/stats/index.html.erb +0 -101
  8. data/config/routes.rb +0 -11
  9. data/lib/generators/rails_nexus/templates/migration.rb +0 -82
  10. data/lib/rails_nexus/configuration.rb +0 -45
  11. data/lib/rails_nexus/engine.rb +6 -4
  12. data/lib/rails_nexus/version.rb +1 -1
  13. data/lib/tasks/rails_nexus.rake +0 -80
  14. data/rails_nexus.gemspec +1 -3
  15. metadata +6 -27
  16. data/app/controllers/rails_nexus/backup_controller.rb +0 -119
  17. data/app/jobs/rails_nexus/backup_job.rb +0 -18
  18. data/app/models/rails_nexus/backup.rb +0 -107
  19. data/app/models/rails_nexus/backup_config.rb +0 -189
  20. data/app/services/rails_nexus/backup_runner.rb +0 -62
  21. data/app/services/rails_nexus/backup_service.rb +0 -817
  22. data/app/views/rails_nexus/backup/_form.html.erb +0 -334
  23. data/app/views/rails_nexus/backup/config_history.html.erb +0 -67
  24. data/app/views/rails_nexus/backup/edit.html.erb +0 -15
  25. data/app/views/rails_nexus/backup/history.html.erb +0 -75
  26. data/app/views/rails_nexus/backup/index.html.erb +0 -112
  27. data/app/views/rails_nexus/backup/new.html.erb +0 -15
  28. data/db/seeds/backup_configs.rb +0 -140
  29. data/lib/generators/rails_nexus/backup_generator.rb +0 -70
  30. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +0 -152
  31. data/lib/generators/rails_nexus/templates/backup/config.rb +0 -27
  32. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +0 -24
  33. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +0 -35
  34. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +0 -43
  35. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +0 -15
  36. data/lib/generators/rails_nexus/templates/backup/schedule.rb +0 -28
@@ -228,84 +228,4 @@ namespace :rails_nexus do
228
228
  puts "✓ Deleted #{count} webhook delivery records older than #{days} days"
229
229
  end
230
230
 
231
- # ─── Backup Tasks ──────────────────────────────────────────────
232
-
233
- desc "Run a specific backup by name: rails_nexus:backup[daily_backup]"
234
- task :backup, [:name] => :environment do |_t, args|
235
- name = args[:name]
236
- if name.blank?
237
- puts "Usage: rake rails_nexus:backup[config_name]"
238
- puts "Available configs:"
239
- RailsNexus::BackupConfig.enabled.pluck(:name).each { |n| puts " - #{n}" }
240
- next
241
- end
242
-
243
- result = RailsNexus::BackupRunner.run(name)
244
- if result[:success]
245
- puts "✓ Backup "#{name}" completed successfully (#{result[:record].duration_human})"
246
- else
247
- puts "✗ Backup "#{name}" failed: #{result[:error]}"
248
- exit 1
249
- end
250
- end
251
-
252
- desc "Run all enabled backup configs"
253
- task backup_all: :environment do
254
- results = RailsNexus::BackupRunner.run_all
255
- results.each do |r|
256
- if r[:success]
257
- puts "✓ #{r[:record].config_name} completed"
258
- else
259
- puts "✗ #{r[:record]&.config_name || '?'} failed: #{r[:error]}"
260
- end
261
- end
262
- end
263
-
264
- desc "Run backups matching current cron schedule"
265
- task backup_scheduled: :environment do
266
- results = RailsNexus::BackupRunner.run_scheduled
267
- if results.empty?
268
- puts "No backups scheduled for this time"
269
- else
270
- results.each do |r|
271
- if r[:success]
272
- puts "✓ #{r[:record].config_name} completed"
273
- else
274
- puts "✗ #{r[:record]&.config_name || '?'} failed: #{r[:error]}"
275
- end
276
- end
277
- end
278
- end
279
-
280
- desc "Clean up old backup records and files (default: 30 days)"
281
- task :backup_cleanup, [:days] => :environment do |_t, args|
282
- days = (args[:days] || 30).to_i
283
- count = RailsNexus::Backup.where("started_at < ?", days.days.ago).delete_all
284
- puts "✓ Deleted #{count} backup records older than #{days} days"
285
- end
286
-
287
- desc "List all backup configs and their status"
288
- task backup_list: :environment do
289
- configs = RailsNexus::BackupConfig.order(:name)
290
- if configs.empty?
291
- puts "No backup configs found"
292
- else
293
- configs.each do |c|
294
- status = c.enabled? ? "✓" : "✗"
295
- last = c.recent_backups(limit: 1).first
296
- last_str = last ? "#{last.status} #{last.started_at&.strftime('%b %d %H:%M')}" : "never"
297
- puts "#{status} #{c.name.ljust(20)} #{c.adapter.ljust(12)} #{c.database_name.ljust(20)} last: #{last_str}"
298
- end
299
- end
300
- end
301
-
302
- desc "Seed backup configs from ~/Backup gem models"
303
- task seed_backups: :environment do
304
- seed_file = File.join(RailsNexus::Engine.root, "db", "seeds", "backup_configs.rb")
305
- if File.exist?(seed_file)
306
- load seed_file
307
- else
308
- puts "Seed file not found: #{seed_file}"
309
- end
310
- end
311
231
  end
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, backup management, 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."
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.2
4
+ version: 2.1.4
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, backup management, cron job tracking,
90
- server statistics, database health, real-time analytics, workflow management, source
91
- code viewing, N+1 query detection, and storm protection. Built with Hotwire, Stimulus,
92
- and Tailwind-compatible CSS.
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
@@ -233,7 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
233
212
  - !ruby/object:Gem::Version
234
213
  version: '0'
235
214
  requirements: []
236
- rubygems_version: 4.0.10
215
+ rubygems_version: 3.6.9
237
216
  specification_version: 4
238
217
  summary: The extensible control plane for Rails applications
239
218
  test_files: []
@@ -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