backup_nexus 0.1.0
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 +7 -0
- data/CHANGELOG.md +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +82 -0
- data/Rakefile +10 -0
- data/app/controllers/backup_nexus/application_controller.rb +31 -0
- data/app/controllers/backup_nexus/backup_controller.rb +119 -0
- data/app/helpers/backup_nexus/application_helper.rb +51 -0
- data/app/jobs/backup_nexus/backup_job.rb +18 -0
- data/app/models/backup_nexus/backup.rb +107 -0
- data/app/models/backup_nexus/backup_config.rb +189 -0
- data/app/services/backup_nexus/backup_runner.rb +62 -0
- data/app/services/backup_nexus/backup_service.rb +817 -0
- data/app/views/backup_nexus/backup/_form.html.erb +334 -0
- data/app/views/backup_nexus/backup/config_history.html.erb +67 -0
- data/app/views/backup_nexus/backup/edit.html.erb +15 -0
- data/app/views/backup_nexus/backup/history.html.erb +75 -0
- data/app/views/backup_nexus/backup/index.html.erb +112 -0
- data/app/views/backup_nexus/backup/new.html.erb +15 -0
- data/backup_nexus.gemspec +30 -0
- data/config/routes.rb +15 -0
- data/db/seeds/backup_configs_safe.rb +61 -0
- data/lib/backup_nexus/base_record.rb +7 -0
- data/lib/backup_nexus/configuration.rb +19 -0
- data/lib/backup_nexus/engine.rb +51 -0
- data/lib/backup_nexus/version.rb +3 -0
- data/lib/backup_nexus.rb +26 -0
- data/lib/generators/backup_nexus/backup_generator.rb +70 -0
- data/lib/generators/backup_nexus/install_generator.rb +27 -0
- data/lib/generators/backup_nexus/templates/backup/backup/backup_helper.sh +152 -0
- data/lib/generators/backup_nexus/templates/backup/backup/config.rb +27 -0
- data/lib/generators/backup_nexus/templates/backup/backup/models/daily_backup.rb +24 -0
- data/lib/generators/backup_nexus/templates/backup/backup/models/full_backup.rb +35 -0
- data/lib/generators/backup_nexus/templates/backup/backup/models/sync_backup.rb +43 -0
- data/lib/generators/backup_nexus/templates/backup/backup/mysql-config/db_config.cnf +15 -0
- data/lib/generators/backup_nexus/templates/backup/backup/schedule.rb +28 -0
- data/lib/generators/backup_nexus/templates/migration.rb +105 -0
- data/lib/tasks/backup_nexus.rake +81 -0
- metadata +138 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<%= form_for @config, url: (@config.persisted? ? backup_update_path(@config) : backup_path), class: "space-y-6" do |f| %>
|
|
2
|
+
<% if @config.errors.any? %>
|
|
3
|
+
<div class="rn-card" style="border-left: 3px solid var(--rn-danger)">
|
|
4
|
+
<div class="p-4">
|
|
5
|
+
<p class="text-sm font-semibold" style="color: var(--rn-danger)"><%= @config.errors.count %> error(s) prevented this config from being saved:</p>
|
|
6
|
+
<ul class="mt-2 text-xs" style="color: var(--rn-text-secondary)">
|
|
7
|
+
<% @config.errors.full_messages.each do |msg| %>
|
|
8
|
+
<li>• <%= msg %></li>
|
|
9
|
+
<% end %>
|
|
10
|
+
</ul>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<%# ─── General ─────────────────────────────────────────────── %>
|
|
16
|
+
<div class="rn-card">
|
|
17
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
18
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">General</h2>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
21
|
+
<div class="rn-filter-group">
|
|
22
|
+
<label>Name *</label>
|
|
23
|
+
<%= f.text_field :name, class: "rn-input", placeholder: "e.g. daily_backup", required: true %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="rn-filter-group">
|
|
26
|
+
<label>Description</label>
|
|
27
|
+
<%= f.text_field :description, class: "rn-input", placeholder: "e.g. TTI Daily Backup" %>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="rn-filter-group">
|
|
30
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
31
|
+
<%= f.check_box :enabled %>
|
|
32
|
+
Enabled
|
|
33
|
+
</label>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<%# ─── Database ────────────────────────────────────────────── %>
|
|
39
|
+
<div class="rn-card">
|
|
40
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
41
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Database</h2>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
44
|
+
<div class="rn-filter-group">
|
|
45
|
+
<label>Adapter *</label>
|
|
46
|
+
<%= f.select :adapter, options_for_select([
|
|
47
|
+
["MySQL", "mysql"],
|
|
48
|
+
["PostgreSQL", "postgresql"],
|
|
49
|
+
["SQLite", "sqlite"],
|
|
50
|
+
["MongoDB", "mongodb"],
|
|
51
|
+
["Redis", "redis"]
|
|
52
|
+
], @config.adapter), {}, class: "rn-select" %>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="rn-filter-group">
|
|
55
|
+
<label>Database Name *</label>
|
|
56
|
+
<%= f.text_field :database_name, class: "rn-input", placeholder: "e.g. myapp_production" %>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="rn-filter-group">
|
|
59
|
+
<label>Host</label>
|
|
60
|
+
<%= f.text_field :host, class: "rn-input", placeholder: "localhost" %>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="rn-filter-group">
|
|
63
|
+
<label>Port</label>
|
|
64
|
+
<%= f.number_field :port, class: "rn-input", placeholder: "3306" %>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="rn-filter-group">
|
|
67
|
+
<label>Username</label>
|
|
68
|
+
<%= f.text_field :username, class: "rn-input", placeholder: "root" %>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="rn-filter-group">
|
|
71
|
+
<label>Password</label>
|
|
72
|
+
<%= f.password_field :password, class: "rn-input", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" %>
|
|
73
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">ENV[VAR], ${VAR}, credentials[:key], or plain text</p>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="rn-filter-group md:col-span-3">
|
|
76
|
+
<label>Skip Tables (comma-separated)</label>
|
|
77
|
+
<%= f.text_field :skip_tables, value: @config.skip_tables_list.join(", "), class: "rn-input", placeholder: "e.g. sessions, versions, internal_logs" %>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<%# ─── Storage ─────────────────────────────────────────────── %>
|
|
83
|
+
<div class="rn-card">
|
|
84
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
85
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Storage</h2>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
88
|
+
<div class="rn-filter-group">
|
|
89
|
+
<label>Storage Path *</label>
|
|
90
|
+
<%= f.text_field :storage_path, class: "rn-input", placeholder: "~/dumps" %>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="rn-filter-group">
|
|
93
|
+
<label>Keep Count</label>
|
|
94
|
+
<%= f.number_field :keep_count, class: "rn-input", min: 1 %>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<%# ─── Archives ────────────────────────────────────────────── %>
|
|
100
|
+
<div class="rn-card">
|
|
101
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
102
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Archives (File/Directory Backup)</h2>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
105
|
+
<div class="rn-filter-group">
|
|
106
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
107
|
+
<%= f.check_box :archive_enabled %>
|
|
108
|
+
Enable Archives
|
|
109
|
+
</label>
|
|
110
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Include additional files/directories in the backup tar</p>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="rn-filter-group md:col-span-2" id="archive-paths-group">
|
|
113
|
+
<label>Archive Paths (comma-separated)</label>
|
|
114
|
+
<%= f.text_field :archive_paths, value: @config.archive_paths_list.join(", "), class: "rn-input", placeholder: "e.g. /var/log, /etc/nginx, /home/app/uploads" %>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="rn-filter-group md:col-span-2" id="archive-excludes-group">
|
|
117
|
+
<label>Exclude Patterns</label>
|
|
118
|
+
<%= f.text_field :archive_excludes, value: @config.archive_excludes_list.join(", "), class: "rn-input", placeholder: "e.g. *.log, tmp/*, node_modules" %>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<%# ─── Compression & Encryption ────────────────────────────── %>
|
|
124
|
+
<div class="rn-card">
|
|
125
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
126
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Compression & Encryption</h2>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
129
|
+
<div class="rn-filter-group">
|
|
130
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
131
|
+
<%= f.check_box :compress %>
|
|
132
|
+
Gzip Compression
|
|
133
|
+
</label>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="rn-filter-group">
|
|
136
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
137
|
+
<%= f.check_box :bzip2_compress %>
|
|
138
|
+
Bzip2 Compression
|
|
139
|
+
</label>
|
|
140
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Better ratio but slower than Gzip</p>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="rn-filter-group">
|
|
143
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
144
|
+
<%= f.check_box :encrypted %>
|
|
145
|
+
OpenSSL AES-256 Encryption
|
|
146
|
+
</label>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="rn-filter-group" id="encrypt-password-group" style="<%= 'display:none' unless @config.encrypted? %>">
|
|
149
|
+
<label>Encryption Password</label>
|
|
150
|
+
<%= f.password_field :encryption_password, class: "rn-input", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" %>
|
|
151
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">ENV[VAR], ${VAR}, credentials[:key], or plain text</p>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="rn-filter-group">
|
|
154
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
155
|
+
<%= f.check_box :gpg_enabled %>
|
|
156
|
+
GPG Encryption
|
|
157
|
+
</label>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="rn-filter-group" id="gpg-password-group" style="<%= 'display:none' unless @config.gpg_enabled? %>">
|
|
160
|
+
<label>GPG Password</label>
|
|
161
|
+
<%= f.password_field :gpg_password, class: "rn-input", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" %>
|
|
162
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">ENV[VAR], ${VAR}, credentials[:key], or plain text</p>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="rn-filter-group">
|
|
165
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
166
|
+
<%= f.check_box :split_chunks %>
|
|
167
|
+
Split into Chunks (50MB)
|
|
168
|
+
</label>
|
|
169
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Split large backups into 50MB files</p>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<%# ─── Remote Sync (RSync) ────────────────────────────────── %>
|
|
175
|
+
<div class="rn-card">
|
|
176
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
177
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Remote Sync (RSync)</h2>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
180
|
+
<div class="rn-filter-group">
|
|
181
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
182
|
+
<%= f.check_box :rsync_enabled %>
|
|
183
|
+
Enable RSync
|
|
184
|
+
</label>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="rn-filter-group" id="rsync-fields" style="<%= 'display:none' unless @config.rsync_enabled? %>">
|
|
187
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
188
|
+
<div>
|
|
189
|
+
<label>Host</label>
|
|
190
|
+
<%= f.text_field :rsync_host, class: "rn-input", placeholder: "192.168.1.100" %>
|
|
191
|
+
</div>
|
|
192
|
+
<div>
|
|
193
|
+
<label>Port</label>
|
|
194
|
+
<%= f.number_field :rsync_port, class: "rn-input", value: @config.rsync_port || 22 %>
|
|
195
|
+
</div>
|
|
196
|
+
<div>
|
|
197
|
+
<label>SSH User</label>
|
|
198
|
+
<%= f.text_field :rsync_user, class: "rn-input", placeholder: "backup" %>
|
|
199
|
+
</div>
|
|
200
|
+
<div>
|
|
201
|
+
<label>Remote Path</label>
|
|
202
|
+
<%= f.text_field :rsync_path, class: "rn-input", placeholder: "/backups" %>
|
|
203
|
+
</div>
|
|
204
|
+
<div>
|
|
205
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
206
|
+
<%= f.check_box :rsync_mirror %>
|
|
207
|
+
Mirror Mode
|
|
208
|
+
</label>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<%# ─── S3 Storage ──────────────────────────────────────────── %>
|
|
216
|
+
<div class="rn-card">
|
|
217
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
218
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">S3 Storage</h2>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
221
|
+
<div class="rn-filter-group">
|
|
222
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
223
|
+
<%= f.check_box :s3_enabled %>
|
|
224
|
+
Enable S3 Upload
|
|
225
|
+
</label>
|
|
226
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Requires aws-cli or s3cmd installed</p>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="rn-filter-group md:col-span-2" id="s3-fields" style="<%= 'display:none' unless @config.s3_enabled? %>">
|
|
229
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
230
|
+
<div>
|
|
231
|
+
<label>Access Key *</label>
|
|
232
|
+
<%= f.text_field :s3_access_key, class: "rn-input", placeholder: "AKIA..." %>
|
|
233
|
+
</div>
|
|
234
|
+
<div>
|
|
235
|
+
<label>Secret Key *</label>
|
|
236
|
+
<%= f.password_field :s3_secret_key, class: "rn-input", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" %>
|
|
237
|
+
</div>
|
|
238
|
+
<div>
|
|
239
|
+
<label>Bucket *</label>
|
|
240
|
+
<%= f.text_field :s3_bucket, class: "rn-input", placeholder: "my-backups-bucket" %>
|
|
241
|
+
</div>
|
|
242
|
+
<div>
|
|
243
|
+
<label>Region</label>
|
|
244
|
+
<%= f.text_field :s3_region, class: "rn-input", placeholder: "us-east-1", value: @config.s3_region || "us-east-1" %>
|
|
245
|
+
</div>
|
|
246
|
+
<div>
|
|
247
|
+
<label>Key Prefix</label>
|
|
248
|
+
<%= f.text_field :s3_prefix, class: "rn-input", placeholder: "backups/production" %>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<%# ─── Notifications ──────────────────────────────────────── %>
|
|
256
|
+
<div class="rn-card">
|
|
257
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
258
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Notifications</h2>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
261
|
+
<div class="rn-filter-group md:col-span-2">
|
|
262
|
+
<label>Notify Command (Webhook / Script)</label>
|
|
263
|
+
<%= f.text_field :notify_command, class: "rn-input", placeholder: "e.g. curl -X POST https://hooks.slack.com/..." %>
|
|
264
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Variables: {STATUS}, {MODEL}, {ERROR}, {TIME}</p>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="rn-filter-group">
|
|
267
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
268
|
+
<%= f.check_box :notify_on_success %>
|
|
269
|
+
Notify on Success
|
|
270
|
+
</label>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="rn-filter-group">
|
|
273
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
274
|
+
<%= f.check_box :notify_on_failure %>
|
|
275
|
+
Notify on Failure
|
|
276
|
+
</label>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="rn-filter-group">
|
|
279
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
|
280
|
+
<%= f.check_box :email_notify %>
|
|
281
|
+
Email Notifications
|
|
282
|
+
</label>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="rn-filter-group" id="email-to-group" style="<%= 'display:none' unless @config.email_notify? %>">
|
|
285
|
+
<label>Email To</label>
|
|
286
|
+
<%= f.text_field :email_to, class: "rn-input", placeholder: "admin@example.com" %>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<%# ─── Schedule ────────────────────────────────────────────── %>
|
|
292
|
+
<div class="rn-card">
|
|
293
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
294
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Schedule</h2>
|
|
295
|
+
</div>
|
|
296
|
+
<div class="p-4">
|
|
297
|
+
<div class="rn-filter-group" style="max-width: 300px">
|
|
298
|
+
<label>Cron Expression</label>
|
|
299
|
+
<%= f.text_field :schedule_cron, class: "rn-input", placeholder: "e.g. 0 2 * * * (daily at 2am)" %>
|
|
300
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">min hour day month wday — leave blank for manual only</p>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<%# ─── Submit ──────────────────────────────────────────────── %>
|
|
306
|
+
<div class="flex items-center gap-3">
|
|
307
|
+
<%= f.submit @config.persisted? ? "Update Config" : "Create Config", class: "rn-btn rn-btn-primary" %>
|
|
308
|
+
<%= link_to "Cancel", backup_path, class: "rn-btn rn-btn-ghost" %>
|
|
309
|
+
</div>
|
|
310
|
+
<% end %>
|
|
311
|
+
|
|
312
|
+
<script>
|
|
313
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
314
|
+
// Toggle visibility for dependent sections based on checkboxes
|
|
315
|
+
function setupToggle(checkboxId, targetId) {
|
|
316
|
+
var cb = document.getElementById(checkboxId);
|
|
317
|
+
var target = document.getElementById(targetId);
|
|
318
|
+
if (!cb || !target) return;
|
|
319
|
+
cb.addEventListener("change", function() {
|
|
320
|
+
target.style.display = cb.checked ? "" : "none";
|
|
321
|
+
});
|
|
322
|
+
// Set initial state
|
|
323
|
+
target.style.display = cb.checked ? "" : "none";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
setupToggle("backup_config_encrypted", "encrypt-password-group");
|
|
327
|
+
setupToggle("backup_config_gpg_enabled", "gpg-password-group");
|
|
328
|
+
setupToggle("backup_config_rsync_enabled", "rsync-fields");
|
|
329
|
+
setupToggle("backup_config_s3_enabled", "s3-fields");
|
|
330
|
+
setupToggle("backup_config_email_notify", "email-to-group");
|
|
331
|
+
setupToggle("backup_config_archive_enabled", "archive-paths-group");
|
|
332
|
+
setupToggle("backup_config_archive_enabled", "archive-excludes-group");
|
|
333
|
+
});
|
|
334
|
+
</script>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<% page_title "History: #{@config.name}" %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
5
|
+
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
6
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
7
|
+
<a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
|
|
8
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
9
|
+
<span class="rn-breadcrumb-current"><%= @config.name %> History</span>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<div class="flex items-center justify-between">
|
|
13
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">History: <%= @config.name %></h1>
|
|
14
|
+
<div class="flex gap-2">
|
|
15
|
+
<%= button_to "Run Backup", backup_trigger_path(@config),
|
|
16
|
+
method: :post,
|
|
17
|
+
class: "rn-btn rn-btn-primary",
|
|
18
|
+
data: { turbo_confirm: "Run #{@config.name} backup now?" } %>
|
|
19
|
+
<%= link_to "Edit Config", edit_backup_path(@config), class: "rn-btn rn-btn-ghost" %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="rn-card">
|
|
24
|
+
<% if @records.any? %>
|
|
25
|
+
<div class="rn-table-wrap">
|
|
26
|
+
<table class="rn-table">
|
|
27
|
+
<thead>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>Status</th>
|
|
30
|
+
<th>File</th>
|
|
31
|
+
<th>Size</th>
|
|
32
|
+
<th>Duration</th>
|
|
33
|
+
<th>Triggered By</th>
|
|
34
|
+
<th>Time</th>
|
|
35
|
+
<th>Error</th>
|
|
36
|
+
</tr>
|
|
37
|
+
</thead>
|
|
38
|
+
<tbody>
|
|
39
|
+
<% @records.each do |record| %>
|
|
40
|
+
<tr>
|
|
41
|
+
<td>
|
|
42
|
+
<span class="rn-badge rn-badge-<%= record.status == 'success' ? 'success' : record.status == 'failed' ? 'danger' : 'warning' %>">
|
|
43
|
+
<%= record.status.capitalize %>
|
|
44
|
+
</span>
|
|
45
|
+
</td>
|
|
46
|
+
<td><span class="text-xs rn-mono" style="color: var(--rn-text-secondary)"><%= record.file_path ? File.basename(record.file_path) : "—" %></span></td>
|
|
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>
|
|
51
|
+
<td>
|
|
52
|
+
<% if record.error_message.present? %>
|
|
53
|
+
<span class="text-xs" style="color: var(--rn-danger)" title="<%= record.error_message %>">⚠</span>
|
|
54
|
+
<% end %>
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
<% end %>
|
|
58
|
+
</tbody>
|
|
59
|
+
</table>
|
|
60
|
+
</div>
|
|
61
|
+
<% else %>
|
|
62
|
+
<div class="p-8 text-center">
|
|
63
|
+
<p class="text-sm" style="color: var(--rn-text-muted)">No backups recorded for this config yet.</p>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% page_title "Edit #{@config.name}" %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
5
|
+
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
6
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
7
|
+
<a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
|
|
8
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
9
|
+
<span class="rn-breadcrumb-current">Edit <%= @config.name %></span>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">Edit: <%= @config.name %></h1>
|
|
13
|
+
|
|
14
|
+
<%= render "form" %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<% page_title "Backup History" %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
5
|
+
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
6
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
7
|
+
<a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
|
|
8
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
9
|
+
<span class="rn-breadcrumb-current">History</span>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<div class="flex items-center justify-between">
|
|
13
|
+
<div>
|
|
14
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup History</h1>
|
|
15
|
+
<p class="text-sm mt-1" style="color: var(--rn-text-muted)">Review completed runs, failures, and backup logs.</p>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="flex items-center gap-2">
|
|
18
|
+
<%= link_to "Configurations", backup_path, class: "rn-btn rn-btn-ghost" %>
|
|
19
|
+
<%= link_to "New Config", new_backup_path, class: "rn-btn rn-btn-primary" %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="rn-card">
|
|
24
|
+
<% if @records.any? %>
|
|
25
|
+
<div class="rn-table-wrap">
|
|
26
|
+
<table class="rn-table">
|
|
27
|
+
<thead>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>Config</th>
|
|
30
|
+
<th>Status</th>
|
|
31
|
+
<th>File</th>
|
|
32
|
+
<th>Size</th>
|
|
33
|
+
<th>Duration</th>
|
|
34
|
+
<th>Triggered By</th>
|
|
35
|
+
<th>Time</th>
|
|
36
|
+
<th>Error</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
<% @records.each do |record| %>
|
|
41
|
+
<tr>
|
|
42
|
+
<td><span class="font-medium" style="color: var(--rn-text)"><%= record.config_name %></span></td>
|
|
43
|
+
<td>
|
|
44
|
+
<span class="rn-badge rn-badge-<%= record.status == 'success' ? 'success' : record.status == 'failed' ? 'danger' : 'warning' %>">
|
|
45
|
+
<%= record.status.capitalize %>
|
|
46
|
+
</span>
|
|
47
|
+
<% if record.status == 'failed' && record.error_message.present? %>
|
|
48
|
+
<div class="text-xs mt-1" style="color: var(--rn-danger); cursor: pointer; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap" title="Click to expand" onclick="var el=this.nextElementSibling; el.style.display=el.style.display==='none'?'block':'none'">
|
|
49
|
+
✖ <%= truncate(record.error_message, length: 60) %>
|
|
50
|
+
</div>
|
|
51
|
+
<pre class="text-xs" style="display:none; color: var(--rn-danger); background: var(--rn-bg-secondary); padding: 8px; border-radius: 4px; margin-top: 4px; max-height: 120px; overflow: auto; white-space: pre-wrap; word-break: break-word"><%= record.error_message %></pre>
|
|
52
|
+
<% end %>
|
|
53
|
+
</td>
|
|
54
|
+
<td><span class="text-xs rn-mono" style="color: var(--rn-text-secondary)"><%= record.file_path ? File.basename(record.file_path) : "—" %></span></td>
|
|
55
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.file_size ? number_to_human_size(record.file_size) : "—" %></span></td>
|
|
56
|
+
<td><span class="text-xs" style="color: var(--rn-text-secondary)"><%= record.duration ? "#{record.duration.round(1)}s" : "—" %></span></td>
|
|
57
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.triggered_by || "—" %></span></td>
|
|
58
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= record.started_at&.strftime("%b %d %H:%M") %></span></td>
|
|
59
|
+
<td>
|
|
60
|
+
<% if record.error_message.present? %>
|
|
61
|
+
<span class="text-xs" style="color: var(--rn-danger)" title="<%= record.error_message %>">⚠</span>
|
|
62
|
+
<% end %>
|
|
63
|
+
</td>
|
|
64
|
+
</tr>
|
|
65
|
+
<% end %>
|
|
66
|
+
</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
</div>
|
|
69
|
+
<% else %>
|
|
70
|
+
<div class="p-8 text-center">
|
|
71
|
+
<p class="text-sm" style="color: var(--rn-text-muted)">No backup records found.</p>
|
|
72
|
+
</div>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<% page_title "Backup Management" %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
5
|
+
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
6
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
7
|
+
<span class="rn-breadcrumb-current">Backups</span>
|
|
8
|
+
</nav>
|
|
9
|
+
|
|
10
|
+
<div class="flex items-center justify-between">
|
|
11
|
+
<div>
|
|
12
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">Backup Configurations</h1>
|
|
13
|
+
<p class="text-sm mt-1" style="color: var(--rn-text-muted)">Configure storage, schedules, and run backups.</p>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="flex items-center gap-2">
|
|
16
|
+
<%= link_to "Backup History", backup_history_path, class: "rn-btn rn-btn-ghost" %>
|
|
17
|
+
<%= link_to "New Config", new_backup_path, class: "rn-btn rn-btn-primary" %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<%# ─── Backup Configs ─────────────────────────────────────── %>
|
|
22
|
+
<div class="rn-card">
|
|
23
|
+
<div class="p-4" style="border-bottom: 1px solid var(--rn-border-light)">
|
|
24
|
+
<h2 class="text-sm font-semibold" style="color: var(--rn-text)">Backup Configurations</h2>
|
|
25
|
+
<p class="text-xs mt-1" style="color: var(--rn-text-muted)">Configure and run database backups</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<% if @configs.any? %>
|
|
29
|
+
<div class="rn-table-wrap">
|
|
30
|
+
<table class="rn-table">
|
|
31
|
+
<thead>
|
|
32
|
+
<tr>
|
|
33
|
+
<th>Name</th>
|
|
34
|
+
<th>Adapter</th>
|
|
35
|
+
<th>Database</th>
|
|
36
|
+
<th>Storage</th>
|
|
37
|
+
<th>Schedule</th>
|
|
38
|
+
<th>Status</th>
|
|
39
|
+
<th style="text-align: right">Actions</th>
|
|
40
|
+
</tr>
|
|
41
|
+
</thead>
|
|
42
|
+
<tbody>
|
|
43
|
+
<% @configs.each do |config| %>
|
|
44
|
+
<tr>
|
|
45
|
+
<td>
|
|
46
|
+
<span class="font-medium" style="color: var(--rn-text)"><%= config.name %></span>
|
|
47
|
+
<% if config.description.present? %>
|
|
48
|
+
<br><span class="text-xs" style="color: var(--rn-text-muted)"><%= config.description %></span>
|
|
49
|
+
<% end %>
|
|
50
|
+
</td>
|
|
51
|
+
<td><span class="rn-badge rn-badge-info"><%= config.adapter.upcase %></span></td>
|
|
52
|
+
<td><span class="text-xs rn-mono" style="color: var(--rn-text-secondary)"><%= config.database_name %></span></td>
|
|
53
|
+
<td><span class="text-xs" style="color: var(--rn-text-muted)"><%= config.storage_path %> (keep <%= config.keep_count %>)</span></td>
|
|
54
|
+
<td>
|
|
55
|
+
<% if config.schedule_cron.present? %>
|
|
56
|
+
<span class="rn-badge rn-badge-warning"><%= config.schedule_cron %></span>
|
|
57
|
+
<% else %>
|
|
58
|
+
<span class="text-xs" style="color: var(--rn-text-muted)">Manual</span>
|
|
59
|
+
<% end %>
|
|
60
|
+
</td>
|
|
61
|
+
<td>
|
|
62
|
+
<span class="rn-badge <%= config.enabled? ? 'rn-badge-success' : 'rn-badge-muted' %>">
|
|
63
|
+
<%= config.enabled? ? 'Enabled' : 'Disabled' %>
|
|
64
|
+
</span>
|
|
65
|
+
<% if config.encrypted? %><span class="rn-badge rn-badge-warning">Encrypted</span><% end %>
|
|
66
|
+
<% if config.rsync_enabled? %><span class="rn-badge rn-badge-info">RSync</span><% end %>
|
|
67
|
+
</td>
|
|
68
|
+
<td style="text-align: right">
|
|
69
|
+
<div class="flex items-center justify-end gap-1">
|
|
70
|
+
<% running = BackupNexus::Backup.where(config_name: config.name, status: "running").exists? %>
|
|
71
|
+
<% if running %>
|
|
72
|
+
<span class="rn-badge rn-badge-info">
|
|
73
|
+
<svg class="animate-spin" style="width:12px;height:12px;display:inline" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"/><path d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" fill="currentColor"/></svg>
|
|
74
|
+
Running
|
|
75
|
+
</span>
|
|
76
|
+
<% else %>
|
|
77
|
+
<%= button_to "Run", backup_trigger_path(config),
|
|
78
|
+
method: :post,
|
|
79
|
+
class: "rn-btn rn-btn-primary rn-btn-xs",
|
|
80
|
+
data: { turbo_confirm: "Run #{config.name} backup now?" } %>
|
|
81
|
+
<% end %>
|
|
82
|
+
<%= link_to "Edit", edit_backup_path(config), class: "rn-btn rn-btn-ghost rn-btn-xs" %>
|
|
83
|
+
<%= button_to "Delete", backup_destroy_path(config),
|
|
84
|
+
method: :delete,
|
|
85
|
+
class: "rn-btn rn-btn-ghost rn-btn-xs",
|
|
86
|
+
style: "color: var(--rn-danger)",
|
|
87
|
+
data: { turbo_confirm: "Delete config '#{config.name}'?" } %>
|
|
88
|
+
</div>
|
|
89
|
+
</td>
|
|
90
|
+
</tr>
|
|
91
|
+
<% end %>
|
|
92
|
+
</tbody>
|
|
93
|
+
</table>
|
|
94
|
+
</div>
|
|
95
|
+
<% else %>
|
|
96
|
+
<div class="p-8 text-center">
|
|
97
|
+
<p class="text-sm" style="color: var(--rn-text-muted)">No backup configurations yet.</p>
|
|
98
|
+
<%= link_to "Create your first config", new_backup_path, class: "mt-3 inline-block text-xs font-medium", style: "color: var(--rn-primary)" %>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<script>
|
|
105
|
+
// Auto-refresh when a backup is running
|
|
106
|
+
(function() {
|
|
107
|
+
var running = document.querySelector('.animate-spin');
|
|
108
|
+
if (running) {
|
|
109
|
+
setTimeout(function() { location.reload(); }, 5000);
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<% page_title "New Backup Config" %>
|
|
2
|
+
|
|
3
|
+
<div class="space-y-6">
|
|
4
|
+
<nav class="rn-breadcrumbs" aria-label="Breadcrumb">
|
|
5
|
+
<a href="<%= root_path %>" class="rn-breadcrumb">Dashboard</a>
|
|
6
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
7
|
+
<a href="<%= backup_path %>" class="rn-breadcrumb">Backups</a>
|
|
8
|
+
<span class="rn-breadcrumb-sep">/</span>
|
|
9
|
+
<span class="rn-breadcrumb-current">New Config</span>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<h1 class="text-2xl font-bold" style="color: var(--rn-text)">New Backup Configuration</h1>
|
|
13
|
+
|
|
14
|
+
<%= render "form" %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative "lib/backup_nexus/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "backup_nexus"
|
|
5
|
+
spec.version = BackupNexus::VERSION
|
|
6
|
+
spec.authors = ["Tamiru Hailu"]
|
|
7
|
+
spec.email = ["tamiruhailu@gmail.com"]
|
|
8
|
+
spec.homepage = "https://github.com/tamiru/backup_nexus"
|
|
9
|
+
spec.summary = "Backup management for Rails applications"
|
|
10
|
+
spec.description = "A Rails engine for configuring, running, tracking, and retaining database and file backups."
|
|
11
|
+
spec.license = "MIT"
|
|
12
|
+
|
|
13
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
14
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/tamiru/backup_nexus/tree/main"
|
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/tamiru/backup_nexus/blob/main/CHANGELOG.md"
|
|
18
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/tamiru/backup_nexus/issues"
|
|
19
|
+
|
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
21
|
+
Dir["{app,config,db,lib}/**/*", "README.md", "CHANGELOG.md", "MIT-LICENSE", "Rakefile", "backup_nexus.gemspec"]
|
|
22
|
+
.reject { |file| file.match?(%r{(?:^|/)(?:log|tmp|storage|pkg)(?:/|$)|\.(?:gem|sqlite3|log)\z}) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
26
|
+
|
|
27
|
+
spec.add_dependency "rails", "~> 8.0", ">= 8.0.0"
|
|
28
|
+
spec.add_dependency "rails_nexus", ">= 2.1.3", "< 3.0"
|
|
29
|
+
spec.add_dependency "pagy", "~> 6.5"
|
|
30
|
+
end
|