rails_nexus 2.0.0 → 2.0.1
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/app/controllers/rails_nexus/backup_controller.rb +23 -56
- data/app/services/rails_nexus/backup_service.rb +119 -218
- data/app/views/layouts/rails_nexus/application.html.erb +8 -0
- data/app/views/rails_nexus/backup/files.html.erb +30 -16
- data/app/views/rails_nexus/backup/index.html.erb +82 -158
- data/config/routes.rb +1 -2
- data/lib/generators/rails_nexus/install_generator.rb +1 -13
- data/lib/generators/rails_nexus/templates/migration.rb +119 -11
- data/lib/rails_nexus/version.rb +1 -1
- data/lib/tasks/rails_nexus.rake +8 -0
- metadata +1 -14
- data/app/models/rails_nexus/database_stat.rb +0 -128
- data/app/models/rails_nexus/event.rb +0 -101
- data/app/models/rails_nexus/metric.rb +0 -107
- data/app/models/rails_nexus/nginx_metric.rb +0 -112
- data/app/models/rails_nexus/server_metric.rb +0 -168
- data/app/views/rails_nexus/backup/settings.html.erb +0 -158
- data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +0 -22
- data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +0 -30
- data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +0 -22
- data/lib/generators/rails_nexus/templates/migration_new_schema.rb +0 -120
- data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +0 -11
- data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +0 -21
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b3560dacbd2f1c044a47737102cf9cf3a12d158663d36842b901f8bcb83f375
|
|
4
|
+
data.tar.gz: f5301d5c1f68c608d8f856203c55d12416d528e489b1fd0acd596b4e2b2752bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1082abffeb01634510cce7be4760b28f595e45f55b4fc86b8eb981b804e16962ee67061d893f12041bfbde8e287d977e3f07814932365aec26b4039b0979ad75
|
|
7
|
+
data.tar.gz: e2e8691ea3e6463161f1ac3e7fd2d846a472d34278b6c3e133cabcb21cbeb0bc0ca8d03c8b082b7cceff3c4692b4adad0064d1353c2651066bff08616798e30d
|
|
@@ -2,90 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsNexus
|
|
4
4
|
class BackupController < ApplicationController
|
|
5
|
-
before_action :
|
|
5
|
+
before_action :rails_nexus_require_auth!
|
|
6
6
|
before_action :check_backup_enabled
|
|
7
7
|
|
|
8
8
|
# GET /rails_nexus/backup
|
|
9
9
|
def index
|
|
10
|
-
@
|
|
11
|
-
@health = @
|
|
12
|
-
@summary = @
|
|
13
|
-
@
|
|
14
|
-
@schedule = @backup_service.cron_schedule
|
|
10
|
+
@service = BackupService.new
|
|
11
|
+
@health = @service.health_status
|
|
12
|
+
@summary = @service.summary
|
|
13
|
+
@records = @service.backup_records
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
# GET /rails_nexus/backup/files
|
|
18
17
|
def files
|
|
19
|
-
@
|
|
20
|
-
@
|
|
18
|
+
@service = BackupService.new
|
|
19
|
+
@records = @service.backup_records(limit: 100)
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
# POST /rails_nexus/backup/trigger/:model
|
|
24
23
|
def trigger
|
|
25
|
-
@
|
|
26
|
-
result = @
|
|
24
|
+
@service = BackupService.new
|
|
25
|
+
result = @service.trigger_backup(params[:model])
|
|
27
26
|
|
|
28
27
|
if result[:success]
|
|
29
|
-
redirect_to backup_path, notice: "Backup '#{params[:model]}'
|
|
28
|
+
redirect_to backup_path, notice: "Backup '#{params[:model]}' completed successfully."
|
|
30
29
|
else
|
|
31
|
-
redirect_to backup_path, alert: "
|
|
30
|
+
redirect_to backup_path, alert: "Backup failed: #{result[:error]}"
|
|
32
31
|
end
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
#
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# GET /rails_nexus/backup/settings
|
|
43
|
-
def settings
|
|
44
|
-
@backup_config = BackupService.load_config
|
|
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."
|
|
45
41
|
end
|
|
46
42
|
|
|
47
|
-
#
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
redirect_to backup_settings_path, notice: "Backup settings saved successfully."
|
|
53
|
-
else
|
|
54
|
-
redirect_to backup_settings_path, alert: "Failed to save: #{result[:error]}"
|
|
55
|
-
end
|
|
43
|
+
# GET /rails_nexus/backup/health
|
|
44
|
+
def health
|
|
45
|
+
@service = BackupService.new
|
|
46
|
+
@health = @service.health_status
|
|
47
|
+
@summary = @service.summary
|
|
56
48
|
end
|
|
57
49
|
|
|
58
50
|
private
|
|
59
51
|
|
|
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
|
|
66
|
-
end
|
|
67
|
-
|
|
68
52
|
def check_backup_enabled
|
|
69
53
|
unless RailsNexus.configuration.backup_enabled
|
|
70
54
|
render plain: "Backup management not enabled", status: :forbidden
|
|
71
55
|
end
|
|
72
56
|
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
57
|
end
|
|
91
58
|
end
|
|
@@ -1,269 +1,170 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "yaml"
|
|
4
|
-
|
|
5
3
|
module RailsNexus
|
|
6
4
|
class BackupService
|
|
7
|
-
attr_reader :
|
|
8
|
-
|
|
9
|
-
CONFIG_FILE = "rails_nexus_backup.yml"
|
|
5
|
+
attr_reader :dump_path
|
|
10
6
|
|
|
11
7
|
def initialize
|
|
12
|
-
|
|
13
|
-
@config_path = saved[:config_path] || RailsNexus.configuration.backup_config_path || default_config_path
|
|
14
|
-
@models_path = saved[:models_path] || RailsNexus.configuration.backup_models_path || default_models_path
|
|
15
|
-
@dump_path = saved[:dump_path] || RailsNexus.configuration.backup_dump_path || default_dump_path
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Load saved config from YAML file
|
|
19
|
-
def self.load_config
|
|
20
|
-
path = config_file_path
|
|
21
|
-
return default_config unless File.exist?(path)
|
|
22
|
-
|
|
23
|
-
data = YAML.safe_load_file(path, permitted_classes: [Symbol, Date, Time]) || {}
|
|
24
|
-
default_config.merge(data.transform_keys(&:to_sym))
|
|
25
|
-
rescue StandardError
|
|
26
|
-
default_config
|
|
8
|
+
@dump_path = RailsNexus.configuration.backup_dump_path || default_dump_path
|
|
27
9
|
end
|
|
28
10
|
|
|
29
|
-
#
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
11
|
+
# Run a backup for a model and record it in the table
|
|
12
|
+
def trigger_backup(model_name)
|
|
13
|
+
record = RailsNexus::Backup.start!(model_name: model_name, triggered_by: "ui")
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
FileUtils.mkdir_p(@dump_path)
|
|
17
|
+
|
|
18
|
+
timestamp = Time.current.strftime("%Y%m%d_%H%M%S")
|
|
19
|
+
filename = "#{model_name}_#{timestamp}.sql"
|
|
20
|
+
file_path = File.join(@dump_path, filename)
|
|
21
|
+
|
|
22
|
+
# Detect database adapter and run appropriate dump
|
|
23
|
+
result = run_dump(model_name, file_path)
|
|
24
|
+
|
|
25
|
+
if result[:success]
|
|
26
|
+
file_size = File.exist?(file_path) ? File.size(file_path) : 0
|
|
27
|
+
record.succeed!(file_path: file_path, file_size: file_size)
|
|
28
|
+
{ success: true, record_id: record.id, file_path: file_path }
|
|
29
|
+
else
|
|
30
|
+
record.fail!(error_message: result[:error])
|
|
31
|
+
# Clean up failed file
|
|
32
|
+
File.delete(file_path) if File.exist?(file_path)
|
|
33
|
+
{ success: false, error: result[:error], record_id: record.id }
|
|
34
|
+
end
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
record.fail!(error_message: e.message)
|
|
37
|
+
{ success: false, error: e.message, record_id: record.id }
|
|
36
38
|
end
|
|
37
|
-
|
|
38
|
-
# Ensure directory exists
|
|
39
|
-
dir = File.dirname(config_file_path)
|
|
40
|
-
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
41
|
-
|
|
42
|
-
# Write YAML
|
|
43
|
-
File.write(config_file_path, config.to_yaml)
|
|
44
|
-
|
|
45
|
-
{ success: true }
|
|
46
|
-
rescue StandardError => e
|
|
47
|
-
{ success: false, error: e.message }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Delete saved config
|
|
51
|
-
def self.reset_config
|
|
52
|
-
File.delete(config_file_path) if File.exist?(config_file_path)
|
|
53
|
-
{ success: true }
|
|
54
|
-
rescue StandardError => e
|
|
55
|
-
{ success: false, error: e.message }
|
|
56
39
|
end
|
|
57
40
|
|
|
58
|
-
#
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Get all backup models
|
|
64
|
-
def models
|
|
65
|
-
return [] unless configured?
|
|
66
|
-
|
|
67
|
-
Dir.glob(File.join(@models_path, "*.rb")).map do |path|
|
|
68
|
-
parse_model_file(path)
|
|
69
|
-
end.compact
|
|
41
|
+
# Get backup records from the table
|
|
42
|
+
def backup_records(limit: 50)
|
|
43
|
+
RailsNexus::Backup.order(started_at: :desc).limit(limit)
|
|
70
44
|
end
|
|
71
45
|
|
|
72
|
-
# Get
|
|
73
|
-
def backup_files
|
|
74
|
-
return [] unless @dump_path && Dir.exist?(@dump_path)
|
|
75
|
-
|
|
76
|
-
Dir.glob(File.join(@dump_path, "*")).select { |f| File.file?(f) }.map do |path|
|
|
77
|
-
{
|
|
78
|
-
name: File.basename(path),
|
|
79
|
-
path: path,
|
|
80
|
-
size: File.size(path),
|
|
81
|
-
size_human: human_size(File.size(path)),
|
|
82
|
-
created_at: File.mtime(path),
|
|
83
|
-
age_hours: ((Time.current - File.mtime(path)) / 3600).round(1),
|
|
84
|
-
encrypted: path.end_with?(".enc"),
|
|
85
|
-
compressed: path.end_with?(".gz") || path.end_with?(".tar.gz")
|
|
86
|
-
}
|
|
87
|
-
end.sort_by { |f| -f[:created_at].to_i }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Get backup summary stats
|
|
46
|
+
# Get summary stats from the table
|
|
91
47
|
def summary
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
total_files: files.size,
|
|
95
|
-
total_size: files.sum { |f| f[:size] },
|
|
96
|
-
total_size_human: human_size(files.sum { |f| f[:size] }),
|
|
97
|
-
last_backup: files.first&.dig(:created_at),
|
|
98
|
-
last_backup_age_hours: files.first&.dig(:age_hours),
|
|
99
|
-
oldest_backup: files.last&.dig(:created_at),
|
|
100
|
-
models_count: models.size,
|
|
101
|
-
healthy: last_backup_healthy?,
|
|
102
|
-
alerts: generate_alerts
|
|
103
|
-
}
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Get cron schedule from whenever config
|
|
107
|
-
def cron_schedule
|
|
108
|
-
schedule_file = File.join(File.dirname(@config_path), "config", "schedule.rb")
|
|
109
|
-
return nil unless File.exist?(schedule_file)
|
|
110
|
-
|
|
111
|
-
content = File.read(schedule_file)
|
|
48
|
+
recent = RailsNexus::Backup.where("started_at >= ?", 7.days.ago)
|
|
112
49
|
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
50
|
+
total: RailsNexus::Backup.count,
|
|
51
|
+
successful: RailsNexus::Backup.successful.count,
|
|
52
|
+
failed: RailsNexus::Backup.failed.count,
|
|
53
|
+
recent_count: recent.count,
|
|
54
|
+
recent_success: recent.successful.count,
|
|
55
|
+
recent_failed: recent.failed.count,
|
|
56
|
+
total_size: RailsNexus::Backup.sum(:file_size).to_i,
|
|
57
|
+
total_size_human: human_size(RailsNexus::Backup.sum(:file_size).to_i),
|
|
58
|
+
last_backup: RailsNexus::Backup.successful.latest_first.first
|
|
116
59
|
}
|
|
117
60
|
end
|
|
118
61
|
|
|
119
|
-
#
|
|
120
|
-
def trigger_backup(model_name)
|
|
121
|
-
return { success: false, error: "Backup not configured" } unless configured?
|
|
122
|
-
return { success: false, error: "Invalid model: #{model_name}" } unless valid_model?(model_name)
|
|
123
|
-
|
|
124
|
-
runner = find_runner_script
|
|
125
|
-
return { success: false, error: "Runner script not found" } unless runner
|
|
126
|
-
|
|
127
|
-
log_file = File.join(File.dirname(@config_path), "log", "cron.log")
|
|
128
|
-
error_log = File.join(File.dirname(@config_path), "log", "cron-error.log")
|
|
129
|
-
|
|
130
|
-
command = "bash #{runner} #{model_name} >> #{log_file} 2>> #{error_log}"
|
|
131
|
-
pid = Process.spawn(command)
|
|
132
|
-
Process.detach(pid)
|
|
133
|
-
|
|
134
|
-
{ success: true, pid: pid, model: model_name }
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Get backup health status
|
|
62
|
+
# Get health status based on table records
|
|
138
63
|
def health_status
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
summary_data = summary
|
|
142
|
-
threshold = saved_config[:alert_threshold_hours]&.to_i || RailsNexus.configuration.backup_alert_threshold_hours || 24
|
|
64
|
+
last = RailsNexus::Backup.successful.latest_first.first
|
|
65
|
+
threshold = RailsNexus.configuration.backup_alert_threshold_hours || 24
|
|
143
66
|
|
|
144
|
-
if
|
|
145
|
-
{ status: "warning", message: "No backups
|
|
146
|
-
elsif
|
|
147
|
-
{ status: "critical", message: "Last backup #{
|
|
148
|
-
elsif summary_data[:last_backup_age_hours] > threshold
|
|
149
|
-
{ status: "warning", message: "Last backup #{summary_data[:last_backup_age_hours]}h ago (>#{threshold}h)", details: summary_data }
|
|
67
|
+
if last.nil?
|
|
68
|
+
{ status: "warning", message: "No backups recorded yet" }
|
|
69
|
+
elsif last.duration && last.duration > threshold * 3600
|
|
70
|
+
{ status: "critical", message: "Last backup #{((Time.current - last.started_at) / 3600).round(1)}h ago" }
|
|
150
71
|
else
|
|
151
|
-
{ status: "healthy", message: "Last backup #{
|
|
72
|
+
{ status: "healthy", message: "Last backup #{last.model_name} — #{last.duration_human}" }
|
|
152
73
|
end
|
|
153
74
|
end
|
|
154
75
|
|
|
155
|
-
#
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
private
|
|
161
|
-
|
|
162
|
-
def self.config_file_path
|
|
163
|
-
Rails.root.join("config", CONFIG_FILE)
|
|
164
|
-
end
|
|
76
|
+
# Cleanup old backup records and files
|
|
77
|
+
def cleanup!(retention_days: nil)
|
|
78
|
+
days = retention_days || RailsNexus.configuration.backup_alert_threshold_hours&.div(24) || 30
|
|
79
|
+
old_records = RailsNexus::Backup.where("started_at < ?", days.days.ago)
|
|
165
80
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
models_path: nil,
|
|
171
|
-
dump_path: nil,
|
|
172
|
-
notify_command: nil,
|
|
173
|
-
alert_threshold_hours: 24,
|
|
174
|
-
rsync_host: nil,
|
|
175
|
-
rsync_port: 22,
|
|
176
|
-
rsync_user: nil,
|
|
177
|
-
rsync_path: nil,
|
|
178
|
-
encrypt_password: nil,
|
|
179
|
-
auto_cleanup_days: 30
|
|
180
|
-
}
|
|
181
|
-
end
|
|
81
|
+
# Delete files first
|
|
82
|
+
old_records.find_each do |record|
|
|
83
|
+
File.delete(record.file_path) if record.file_path && File.exist?(record.file_path)
|
|
84
|
+
end
|
|
182
85
|
|
|
183
|
-
|
|
184
|
-
|
|
86
|
+
count = old_records.delete_all
|
|
87
|
+
{ deleted: count }
|
|
185
88
|
end
|
|
186
89
|
|
|
187
|
-
|
|
188
|
-
Rails.root.join("config", "backup", "models").to_s
|
|
189
|
-
end
|
|
90
|
+
private
|
|
190
91
|
|
|
191
92
|
def default_dump_path
|
|
192
93
|
Rails.root.join("storage", "rails_nexus", "backups").to_s
|
|
193
94
|
end
|
|
194
95
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return nil unless name_match
|
|
96
|
+
# Detect adapter and run appropriate dump command
|
|
97
|
+
def run_dump(model_name, file_path)
|
|
98
|
+
config = ActiveRecord::Base.connection_db_config.configuration_hash
|
|
99
|
+
adapter = config[:adapter]
|
|
201
100
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
skip_tables: content.scan(/skip_tables\s*=\s*\[([^\]]+)\]/).flatten.first,
|
|
212
|
-
keep_count: content.scan(/keep\s*=\s*(\d+)/).flatten.first&.to_i
|
|
213
|
-
}
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def parse_schedule(content)
|
|
217
|
-
entries = []
|
|
218
|
-
content.scan(/every\s+([^,]+)(?:,\s*at:\s*"([^"]+)")?\s+do\s+command\s+"([^"]+)"/) do |match|
|
|
219
|
-
entries << {
|
|
220
|
-
frequency: match[0].strip,
|
|
221
|
-
time: match[1],
|
|
222
|
-
command: match[2]
|
|
223
|
-
}
|
|
101
|
+
case adapter
|
|
102
|
+
when /sqlite/
|
|
103
|
+
run_sqlite_dump(config, file_path)
|
|
104
|
+
when /mysql/
|
|
105
|
+
run_mysql_dump(config, file_path)
|
|
106
|
+
when /postgresql/, /postgres/
|
|
107
|
+
run_postgresql_dump(config, file_path)
|
|
108
|
+
else
|
|
109
|
+
{ success: false, error: "Unsupported adapter: #{adapter}" }
|
|
224
110
|
end
|
|
225
|
-
entries
|
|
226
111
|
end
|
|
227
112
|
|
|
228
|
-
def
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
end
|
|
113
|
+
def run_sqlite_dump(config, file_path)
|
|
114
|
+
db_path = config[:database]
|
|
115
|
+
return { success: false, error: "No database path configured" } unless db_path
|
|
232
116
|
|
|
233
|
-
|
|
234
|
-
|
|
117
|
+
# SQLite: .dump produces SQL output
|
|
118
|
+
output = `sqlite3 "#{db_path}" .dump 2>&1`
|
|
119
|
+
if $?.success?
|
|
120
|
+
File.write(file_path, output)
|
|
121
|
+
{ success: true }
|
|
122
|
+
else
|
|
123
|
+
{ success: false, error: "sqlite3 dump failed: #{output}" }
|
|
124
|
+
end
|
|
235
125
|
end
|
|
236
126
|
|
|
237
|
-
def
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
127
|
+
def run_mysql_dump(config, file_path)
|
|
128
|
+
cmd = [
|
|
129
|
+
"mysqldump",
|
|
130
|
+
"--user=#{config[:username] || 'root'}",
|
|
131
|
+
config[:password] ? "--password=#{config[:password]}" : nil,
|
|
132
|
+
"--host=#{config[:host] || 'localhost'}",
|
|
133
|
+
"--port=#{config[:port] || 3306}",
|
|
134
|
+
"--result-file=#{file_path}",
|
|
135
|
+
config[:database]
|
|
136
|
+
].compact.join(" ")
|
|
137
|
+
|
|
138
|
+
output = `#{cmd} 2>&1`
|
|
139
|
+
if $?.success?
|
|
140
|
+
{ success: true }
|
|
141
|
+
else
|
|
142
|
+
{ success: false, error: "mysqldump failed: #{output}" }
|
|
143
|
+
end
|
|
243
144
|
end
|
|
244
145
|
|
|
245
|
-
def
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
146
|
+
def run_postgresql_dump(config, file_path)
|
|
147
|
+
cmd = [
|
|
148
|
+
"pg_dump",
|
|
149
|
+
"--username=#{config[:username] || 'postgres'}",
|
|
150
|
+
config[:host] ? "--host=#{config[:host]}" : nil,
|
|
151
|
+
config[:port] ? "--port=#{config[:port]}" : nil,
|
|
152
|
+
"--file=#{file_path}",
|
|
153
|
+
config[:database]
|
|
154
|
+
].compact.join(" ")
|
|
249
155
|
|
|
250
|
-
|
|
251
|
-
alerts << { level: "warning", message: "No backup files found" }
|
|
252
|
-
elsif files.first[:age_hours] > threshold
|
|
253
|
-
alerts << { level: "critical", message: "No backup in #{files.first[:age_hours]}h (threshold: #{threshold}h)" }
|
|
254
|
-
end
|
|
156
|
+
env = config[:password] ? { "PGPASSWORD" => config[:password].to_s } : {}
|
|
255
157
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
158
|
+
output = IO.capture2e(env, cmd)
|
|
159
|
+
if output.last.success?
|
|
160
|
+
{ success: true }
|
|
161
|
+
else
|
|
162
|
+
{ success: false, error: "pg_dump failed: #{output.first}" }
|
|
260
163
|
end
|
|
261
|
-
|
|
262
|
-
alerts
|
|
263
164
|
end
|
|
264
165
|
|
|
265
166
|
def human_size(bytes)
|
|
266
|
-
return "0 B" if bytes
|
|
167
|
+
return "0 B" if bytes.zero?
|
|
267
168
|
|
|
268
169
|
units = %w[B KB MB GB TB]
|
|
269
170
|
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
@@ -82,6 +82,14 @@
|
|
|
82
82
|
<%# ─── Tools section ──────────────────────────────────────── %>
|
|
83
83
|
<div class="rn-sidebar-section-divider"></div>
|
|
84
84
|
|
|
85
|
+
<% if main_app.respond_to?(:root_path) %>
|
|
86
|
+
<%= link_to main_app.root_path,
|
|
87
|
+
class: "rn-nav-item rn-nav-item--muted" do %>
|
|
88
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" /></svg>
|
|
89
|
+
Admin Panel
|
|
90
|
+
<% end %>
|
|
91
|
+
<% end %>
|
|
92
|
+
|
|
85
93
|
<%= link_to feed_logged_exceptions_path(format: :rss),
|
|
86
94
|
class: "rn-nav-item rn-nav-item--muted" do %>
|
|
87
95
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12.75 19.5v-.75a7.5 7.5 0 00-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M6 18.75a.375.375 0 11-.75 0 .375.375 0 01.75 0z" /></svg>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<% page_title t(".title", default: "Backup Files") %>
|
|
2
2
|
|
|
3
3
|
<div class="space-y-6">
|
|
4
|
-
<%# ─── Header ──────────────────────────────────────────────── %>
|
|
5
4
|
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
6
5
|
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
7
6
|
<span class="rn-breadcrumb-sep">/</span>
|
|
@@ -10,34 +9,49 @@
|
|
|
10
9
|
<span class="rn-breadcrumb-current">Files</span>
|
|
11
10
|
</nav>
|
|
12
11
|
|
|
13
|
-
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup Files</h1>
|
|
14
|
-
|
|
15
|
-
<%# ─── Files Table ─────────────────────────────────────────── %>
|
|
16
12
|
<div class="rn-card">
|
|
17
|
-
|
|
13
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
14
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">All Backup Records</h2>
|
|
15
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Complete history of backup runs</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% if @records.any? %>
|
|
18
19
|
<div class="rn-table-wrap">
|
|
19
20
|
<table class="rn-table">
|
|
20
21
|
<thead>
|
|
21
22
|
<tr>
|
|
23
|
+
<th>ID</th>
|
|
24
|
+
<th>Model</th>
|
|
25
|
+
<th>Status</th>
|
|
22
26
|
<th>File</th>
|
|
23
27
|
<th>Size</th>
|
|
24
|
-
<th>
|
|
25
|
-
<th>
|
|
26
|
-
<th>
|
|
28
|
+
<th>Duration</th>
|
|
29
|
+
<th>Triggered By</th>
|
|
30
|
+
<th>Started</th>
|
|
31
|
+
<th></th>
|
|
27
32
|
</tr>
|
|
28
33
|
</thead>
|
|
29
34
|
<tbody>
|
|
30
|
-
<% @
|
|
35
|
+
<% @records.each do |record| %>
|
|
31
36
|
<tr>
|
|
37
|
+
<td><span class="text-xs rn-mono" style="color: var(--rn-text-muted)">#<%= record.id %></span></td>
|
|
38
|
+
<td><span class="font-medium" style="color: var(--rn-text)"><%= record.model_name %></span></td>
|
|
39
|
+
<td>
|
|
40
|
+
<span class="rn-badge rn-badge-<%= record.status == 'success' ? 'success' : record.status == 'failed' ? 'danger' : 'warning' %>">
|
|
41
|
+
<%= record.status.capitalize %>
|
|
42
|
+
</span>
|
|
43
|
+
</td>
|
|
32
44
|
<td>
|
|
33
|
-
<span class="rn-mono
|
|
45
|
+
<span class="text-xs rn-mono" style="color: var(--rn-text-secondary)"><%= record.file_path ? File.basename(record.file_path) : "—" %></span>
|
|
34
46
|
</td>
|
|
35
|
-
<td><span class="text-
|
|
36
|
-
<td><span class="text-xs" style="color: var(--rn-text-
|
|
37
|
-
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%=
|
|
47
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.file_size ? number_to_human_size(record.file_size) : "—" %></span></td>
|
|
48
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.duration ? "#{record.duration.round(1)}s" : "—" %></span></td>
|
|
49
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.triggered_by || "—" %></span></td>
|
|
50
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.started_at&.strftime("%b %d %H:%M") %></span></td>
|
|
38
51
|
<td>
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
<% if record.error_message.present? %>
|
|
53
|
+
<span class="text-xs" style="color: var(--rn-danger)" title="<%= record.error_message %>">⚠</span>
|
|
54
|
+
<% end %>
|
|
41
55
|
</td>
|
|
42
56
|
</tr>
|
|
43
57
|
<% end %>
|
|
@@ -46,7 +60,7 @@
|
|
|
46
60
|
</div>
|
|
47
61
|
<% else %>
|
|
48
62
|
<div class="p-8 text-center">
|
|
49
|
-
<p class="text-sm" style="color: var(--rn-text-muted)">No backup
|
|
63
|
+
<p class="text-sm" style="color: var(--rn-text-muted)">No backup records found.</p>
|
|
50
64
|
</div>
|
|
51
65
|
<% end %>
|
|
52
66
|
</div>
|