rails_nexus 2.0.0 → 2.0.2

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