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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -10
  3. data/app/controllers/rails_nexus/stats_controller.rb +0 -126
  4. data/app/views/layouts/rails_nexus/_sidebar_navigation.html.erb +0 -11
  5. data/app/views/layouts/rails_nexus/application.html.erb +1 -12
  6. data/app/views/rails_nexus/stats/index.html.erb +0 -101
  7. data/config/routes.rb +0 -11
  8. data/lib/generators/rails_nexus/templates/migration.rb +0 -82
  9. data/lib/rails_nexus/configuration.rb +0 -45
  10. data/lib/rails_nexus/engine.rb +6 -4
  11. data/lib/rails_nexus/version.rb +1 -1
  12. data/lib/tasks/rails_nexus.rake +0 -80
  13. data/rails_nexus.gemspec +1 -3
  14. metadata +5 -26
  15. data/app/controllers/rails_nexus/backup_controller.rb +0 -119
  16. data/app/jobs/rails_nexus/backup_job.rb +0 -18
  17. data/app/models/rails_nexus/backup.rb +0 -107
  18. data/app/models/rails_nexus/backup_config.rb +0 -189
  19. data/app/services/rails_nexus/backup_runner.rb +0 -62
  20. data/app/services/rails_nexus/backup_service.rb +0 -817
  21. data/app/views/rails_nexus/backup/_form.html.erb +0 -334
  22. data/app/views/rails_nexus/backup/config_history.html.erb +0 -67
  23. data/app/views/rails_nexus/backup/edit.html.erb +0 -15
  24. data/app/views/rails_nexus/backup/history.html.erb +0 -75
  25. data/app/views/rails_nexus/backup/index.html.erb +0 -112
  26. data/app/views/rails_nexus/backup/new.html.erb +0 -15
  27. data/db/seeds/backup_configs.rb +0 -140
  28. data/lib/generators/rails_nexus/backup_generator.rb +0 -70
  29. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +0 -152
  30. data/lib/generators/rails_nexus/templates/backup/config.rb +0 -27
  31. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +0 -24
  32. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +0 -35
  33. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +0 -43
  34. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +0 -15
  35. 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, 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.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, 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
@@ -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