rails_nexus 2.0.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b3560dacbd2f1c044a47737102cf9cf3a12d158663d36842b901f8bcb83f375
4
- data.tar.gz: f5301d5c1f68c608d8f856203c55d12416d528e489b1fd0acd596b4e2b2752bb
3
+ metadata.gz: 1d639de5385a9975d43e4cb943e95e1080fc982bf39363d8ca650ffa4e413807
4
+ data.tar.gz: d0ee669e29976192aba3b2215b9d804772d3e362669252e545e340fc5c80a337
5
5
  SHA512:
6
- metadata.gz: 1082abffeb01634510cce7be4760b28f595e45f55b4fc86b8eb981b804e16962ee67061d893f12041bfbde8e287d977e3f07814932365aec26b4039b0979ad75
7
- data.tar.gz: e2e8691ea3e6463161f1ac3e7fd2d846a472d34278b6c3e133cabcb21cbeb0bc0ca8d03c8b082b7cceff3c4692b4adad0064d1353c2651066bff08616798e30d
6
+ metadata.gz: a0e758654cfebfdd74880b6e9ea2c415ad8ed1cb3ce63ec2e619664597ff7fbfd8c449e277228d0dbcce66c8f59db400ac8fd0326d2a97a1afa05eab68ae6347
7
+ data.tar.gz: 5c68c28e6debd2b86d76653e8b6a24b83c33b506dc84acdbd919e560e781445653771b19993acc30a35e334f8f49ecced1b2cbc78236ea28cb7aaffc5d6c6661
@@ -4,51 +4,105 @@ module RailsNexus
4
4
  class BackupController < ApplicationController
5
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
- @service = BackupService.new
11
- @health = @service.health_status
12
- @summary = @service.summary
13
- @records = @service.backup_records
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
+ }
14
20
  end
15
21
 
16
- # GET /rails_nexus/backup/files
17
- def files
18
- @service = BackupService.new
19
- @records = @service.backup_records(limit: 100)
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
+ )
20
32
  end
21
33
 
22
- # POST /rails_nexus/backup/trigger/:model
34
+ # POST /rails_nexus/backup
35
+ def create
36
+ @config = RailsNexus::BackupConfig.new(config_params)
37
+
38
+ if @config.save
39
+ redirect_to backup_path, notice: "Backup config '#{@config.name}' created."
40
+ else
41
+ render :new, status: :unprocessable_entity
42
+ end
43
+ end
44
+
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
55
+ end
56
+
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."
62
+ end
63
+
64
+ # POST /rails_nexus/backup/:id/trigger
23
65
  def trigger
24
- @service = BackupService.new
25
- result = @service.trigger_backup(params[:model])
66
+ result = RailsNexus::BackupService.run(@config)
26
67
 
27
68
  if result[:success]
28
- redirect_to backup_path, notice: "Backup '#{params[:model]}' completed successfully."
69
+ redirect_to backup_path, notice: "Backup '#{@config.name}' completed successfully."
29
70
  else
30
71
  redirect_to backup_path, alert: "Backup failed: #{result[:error]}"
31
72
  end
32
73
  end
33
74
 
34
- # DELETE /rails_nexus/backup/:id
35
- def destroy
36
- record = RailsNexus::Backup.find(params[:id])
37
- # Delete the file if it exists
38
- File.delete(record.file_path) if record.file_path && File.exist?(record.file_path)
39
- record.destroy
40
- redirect_to backup_path, notice: "Backup record deleted."
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)
41
81
  end
42
82
 
43
- # GET /rails_nexus/backup/health
44
- def health
45
- @service = BackupService.new
46
- @health = @service.health_status
47
- @summary = @service.summary
83
+ # GET /rails_nexus/backup/history
84
+ def all_history
85
+ @records = RailsNexus::Backup.order(started_at: :desc).limit(100)
48
86
  end
49
87
 
50
88
  private
51
89
 
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
+ )
104
+ end
105
+
52
106
  def check_backup_enabled
53
107
  unless RailsNexus.configuration.backup_enabled
54
108
  render plain: "Backup management not enabled", status: :forbidden
@@ -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