kamal-backup 0.3.0 → 0.3.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/README.md +1 -0
- data/lib/kamal_backup/app.rb +145 -202
- data/lib/kamal_backup/cli/helpers.rb +298 -0
- data/lib/kamal_backup/cli.rb +4 -294
- data/lib/kamal_backup/command.rb +0 -185
- data/lib/kamal_backup/command_output.rb +189 -0
- data/lib/kamal_backup/config.rb +77 -481
- data/lib/kamal_backup/config_file.rb +376 -0
- data/lib/kamal_backup/databases/base.rb +11 -0
- data/lib/kamal_backup/databases/postgres.rb +3 -3
- data/lib/kamal_backup/evidence.rb +0 -3
- data/lib/kamal_backup/kamal_bridge.rb +39 -27
- data/lib/kamal_backup/rails_app.rb +6 -17
- data/lib/kamal_backup/restic.rb +41 -45
- data/lib/kamal_backup/version.rb +1 -1
- data/lib/kamal_backup/yaml_access.rb +13 -0
- data/lib/kamal_backup.rb +3 -0
- metadata +48 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 801ab5f224e1e018805b65650da3c0946d6c785eb8b2a3e3e3256bcf97649abb
|
|
4
|
+
data.tar.gz: 60e98bfe053103b3405d483902a167ee2d86f7a723448e06eacb2937b56fd4d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1dbd9a03222917490629d56a4bfca69e1abbac79ed241bf480ba0da50453e67e1224dcf5f6d315ce23039b31febe2d449300dae167ddf19e627decd07c6116e
|
|
7
|
+
data.tar.gz: c4392a6048ceea5ec81b20cedad84f15d864ecb37a83337571cc857de8c730c0a84629df335e9b6c4f273868ad86a6ddcc631c00315ff8ff965ac0230bcf1f5a
|
data/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://rubygems.org/gems/kamal-backup)
|
|
10
10
|
[](https://github.com/crmne/kamal-backup/actions/workflows/ci.yml)
|
|
11
|
+
[](https://codecov.io/gh/crmne/kamal-backup)
|
|
11
12
|
[](https://github.com/crmne/kamal-backup/pkgs/container/kamal-backup)
|
|
12
13
|
[](https://kamal-backup.dev)
|
|
13
14
|
[](LICENSE)
|
data/lib/kamal_backup/app.rb
CHANGED
|
@@ -22,14 +22,11 @@ module KamalBackup
|
|
|
22
22
|
|
|
23
23
|
attr_reader :config, :redactor
|
|
24
24
|
|
|
25
|
-
def initialize(env: ENV, config: nil, redactor: nil, restic: nil, database: nil
|
|
26
|
-
scheduler_class: Scheduler)
|
|
25
|
+
def initialize(env: ENV, config: nil, redactor: nil, restic: nil, database: nil)
|
|
27
26
|
@config = config || Config.new(env: env)
|
|
28
27
|
@redactor = redactor || Redactor.new(env: @config.env)
|
|
29
28
|
@restic = restic
|
|
30
29
|
@database = database
|
|
31
|
-
@evidence_class = evidence_class
|
|
32
|
-
@scheduler_class = scheduler_class
|
|
33
30
|
end
|
|
34
31
|
|
|
35
32
|
def backup(force: false)
|
|
@@ -37,13 +34,11 @@ module KamalBackup
|
|
|
37
34
|
config.validate_backup
|
|
38
35
|
return skipped_backup_result(started_at) unless force || backup_due?(started_at)
|
|
39
36
|
|
|
40
|
-
require_restic!
|
|
41
|
-
|
|
42
37
|
restic.ensure_repository
|
|
43
38
|
databases.each { |database| database.backup(restic) }
|
|
44
39
|
restic.backup_paths(config.backup_paths, tags: ['type:files'])
|
|
45
40
|
|
|
46
|
-
restic.
|
|
41
|
+
restic.prune if config.forget_after_backup?
|
|
47
42
|
|
|
48
43
|
restic.check if config.check_after_backup?
|
|
49
44
|
|
|
@@ -60,46 +55,39 @@ module KamalBackup
|
|
|
60
55
|
|
|
61
56
|
def restore_to_local_machine(snapshot = 'latest')
|
|
62
57
|
validate_local_machine_restore
|
|
63
|
-
require_restic!
|
|
64
58
|
|
|
65
59
|
build_restore_result('local', snapshot) do |result|
|
|
66
60
|
databases.each { |adapter| validate_local_machine_database_target(adapter) }
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
assign_restore_results(result, database_results, file_results)
|
|
61
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
62
|
+
result[:files] = perform_replacement_file_restore(snapshot, production_source: false)
|
|
70
63
|
end
|
|
71
64
|
end
|
|
72
65
|
|
|
73
66
|
def restore_to_production(snapshot = 'latest')
|
|
74
67
|
validate_production_restore
|
|
75
|
-
require_restic!
|
|
76
68
|
|
|
77
69
|
build_restore_result('production', snapshot) do |result|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
assign_restore_results(result, database_results, file_results)
|
|
70
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
71
|
+
result[:files] = perform_replacement_file_restore(snapshot, production_source: true)
|
|
81
72
|
end
|
|
82
73
|
end
|
|
83
74
|
|
|
84
75
|
def drill_on_local_machine(snapshot = 'latest', check_command: nil)
|
|
85
76
|
validate_local_machine_restore
|
|
86
|
-
require_restic!
|
|
87
77
|
|
|
88
78
|
run_drill('local', snapshot, check_command: check_command) do |result|
|
|
89
79
|
databases.each { |adapter| validate_local_machine_database_target(adapter) }
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
assign_restore_results(result, database_results, file_results)
|
|
80
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
81
|
+
result[:files] = perform_replacement_file_restore(snapshot, production_source: false)
|
|
93
82
|
end
|
|
94
83
|
end
|
|
95
84
|
|
|
96
85
|
def drill_on_production(snapshot = 'latest', database_name: nil, sqlite_path: nil, file_target: '/restore/files',
|
|
97
86
|
check_command: nil)
|
|
98
87
|
validate_production_drill(file_target, database_name, sqlite_path)
|
|
99
|
-
require_restic!
|
|
100
88
|
|
|
101
89
|
run_drill('production', snapshot, check_command: check_command) do |result|
|
|
102
|
-
|
|
90
|
+
result[:databases] = databases.map do |adapter|
|
|
103
91
|
perform_database_restore_to_scratch(
|
|
104
92
|
snapshot,
|
|
105
93
|
adapter: adapter,
|
|
@@ -107,44 +95,33 @@ module KamalBackup
|
|
|
107
95
|
sqlite_path: sqlite_path
|
|
108
96
|
)
|
|
109
97
|
end
|
|
110
|
-
|
|
111
|
-
assign_restore_results(result, database_results, file_results)
|
|
98
|
+
result[:files] = perform_file_restore(snapshot, target: file_target)
|
|
112
99
|
end
|
|
113
100
|
end
|
|
114
101
|
|
|
115
|
-
def drill_failed?(result)
|
|
116
|
-
result.fetch(:status) != 'ok'
|
|
117
|
-
rescue KeyError
|
|
118
|
-
true
|
|
119
|
-
end
|
|
120
|
-
|
|
121
102
|
def snapshots
|
|
122
103
|
config.validate_restic
|
|
123
|
-
require_restic!
|
|
124
104
|
restic.snapshots.stdout
|
|
125
105
|
end
|
|
126
106
|
|
|
127
107
|
def check
|
|
128
108
|
config.validate_restic
|
|
129
|
-
require_restic!
|
|
130
109
|
restic.check.stdout
|
|
131
110
|
end
|
|
132
111
|
|
|
133
112
|
def prune
|
|
134
113
|
config.validate_backup(check_files: false)
|
|
135
|
-
require_restic!
|
|
136
114
|
restic.prune
|
|
137
115
|
end
|
|
138
116
|
|
|
139
117
|
def evidence
|
|
140
118
|
config.validate_restic
|
|
141
|
-
|
|
142
|
-
@evidence_class.new(config, restic: restic, redactor: redactor).to_json
|
|
119
|
+
Evidence.new(config, restic: restic, redactor: redactor).to_json
|
|
143
120
|
end
|
|
144
121
|
|
|
145
122
|
def schedule
|
|
146
123
|
config.validate_backup
|
|
147
|
-
|
|
124
|
+
Scheduler.new(config) { backup(force: true) }.run
|
|
148
125
|
end
|
|
149
126
|
|
|
150
127
|
private
|
|
@@ -155,7 +132,7 @@ module KamalBackup
|
|
|
155
132
|
end
|
|
156
133
|
|
|
157
134
|
def skipped_backup_result(now)
|
|
158
|
-
|
|
135
|
+
Schema.record(
|
|
159
136
|
kind: 'backup_result',
|
|
160
137
|
status: 'skipped',
|
|
161
138
|
reason: 'not_due',
|
|
@@ -163,7 +140,7 @@ module KamalBackup
|
|
|
163
140
|
next_backup_at: next_backup_at&.iso8601,
|
|
164
141
|
force_command: 'kamal-backup backup --force',
|
|
165
142
|
finished_at: now.iso8601
|
|
166
|
-
|
|
143
|
+
)
|
|
167
144
|
end
|
|
168
145
|
|
|
169
146
|
def next_backup_at
|
|
@@ -172,7 +149,7 @@ module KamalBackup
|
|
|
172
149
|
|
|
173
150
|
def last_backup_finished_at
|
|
174
151
|
@last_backup_finished_at ||= begin
|
|
175
|
-
value = last_backup_record['finished_at']
|
|
152
|
+
value = last_backup_record['finished_at']
|
|
176
153
|
value ? Time.parse(value.to_s).utc : nil
|
|
177
154
|
rescue ArgumentError
|
|
178
155
|
nil
|
|
@@ -187,6 +164,68 @@ module KamalBackup
|
|
|
187
164
|
end
|
|
188
165
|
end
|
|
189
166
|
|
|
167
|
+
def backup_summary(started_at:, finished_at:)
|
|
168
|
+
Schema.record(
|
|
169
|
+
kind: 'backup_result',
|
|
170
|
+
status: 'ok',
|
|
171
|
+
started_at: started_at.iso8601,
|
|
172
|
+
finished_at: finished_at.iso8601,
|
|
173
|
+
databases: databases.map { |adapter| backup_snapshot_summary(adapter) },
|
|
174
|
+
files: backup_file_snapshot_summary
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def backup_snapshot_summary(adapter)
|
|
179
|
+
snapshot = restic.latest_snapshot(tags: database_snapshot_tags(adapter))
|
|
180
|
+
snapshot_summary(snapshot).merge(
|
|
181
|
+
database: database_config_name(adapter),
|
|
182
|
+
adapter: adapter.adapter_name
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def backup_file_snapshot_summary
|
|
187
|
+
return nil if config.backup_paths.empty?
|
|
188
|
+
|
|
189
|
+
snapshot_summary(restic.latest_snapshot(tags: ['type:files']))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def snapshot_summary(snapshot)
|
|
193
|
+
{
|
|
194
|
+
snapshot: snapshot && (snapshot['short_id'] || snapshot['id']),
|
|
195
|
+
time: snapshot && snapshot['time']
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def validate_fresh_backup_summary!(summary, started_at:)
|
|
200
|
+
stale_databases = summary.fetch(:databases).reject { |entry| fresh_snapshot?(entry, started_at) }
|
|
201
|
+
|
|
202
|
+
unless stale_databases.empty?
|
|
203
|
+
names = stale_databases.map { |entry| entry.fetch(:database) }.join(', ')
|
|
204
|
+
raise ConfigurationError, "backup did not create a fresh database snapshot for #{names}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
files = summary[:files]
|
|
208
|
+
return unless files && !fresh_snapshot?(files, started_at)
|
|
209
|
+
|
|
210
|
+
raise ConfigurationError, 'backup did not create a fresh file snapshot'
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def fresh_snapshot?(entry, started_at)
|
|
214
|
+
snapshot_time = Time.parse(entry[:time].to_s)
|
|
215
|
+
snapshot_time >= started_at - FRESH_BACKUP_GRACE_SECONDS
|
|
216
|
+
rescue ArgumentError
|
|
217
|
+
false
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def write_last_backup(result)
|
|
221
|
+
FileUtils.mkdir_p(config.state_dir)
|
|
222
|
+
File.write(config.last_backup_path, JSON.pretty_generate(result))
|
|
223
|
+
@last_backup_record = result.transform_keys(&:to_s)
|
|
224
|
+
@last_backup_finished_at = Time.parse(result.fetch(:finished_at)).utc
|
|
225
|
+
rescue SystemCallError, ArgumentError
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
228
|
+
|
|
190
229
|
def build_restore_result(scope, snapshot)
|
|
191
230
|
started_at = Time.now.utc
|
|
192
231
|
result = Schema.record(
|
|
@@ -197,10 +236,8 @@ module KamalBackup
|
|
|
197
236
|
started_at: started_at.iso8601,
|
|
198
237
|
finished_at: nil,
|
|
199
238
|
error: nil,
|
|
200
|
-
database: nil,
|
|
201
239
|
databases: [],
|
|
202
|
-
files: nil
|
|
203
|
-
paths: nil
|
|
240
|
+
files: nil
|
|
204
241
|
)
|
|
205
242
|
yield(result)
|
|
206
243
|
result[:finished_at] = Time.now.utc.iso8601
|
|
@@ -218,10 +255,8 @@ module KamalBackup
|
|
|
218
255
|
started_at: started_at.iso8601,
|
|
219
256
|
finished_at: nil,
|
|
220
257
|
error: nil,
|
|
221
|
-
database: nil,
|
|
222
258
|
databases: [],
|
|
223
259
|
files: nil,
|
|
224
|
-
paths: nil,
|
|
225
260
|
check: nil
|
|
226
261
|
)
|
|
227
262
|
|
|
@@ -247,6 +282,41 @@ module KamalBackup
|
|
|
247
282
|
result
|
|
248
283
|
end
|
|
249
284
|
|
|
285
|
+
def drill_operator
|
|
286
|
+
config.value('USER') || config.value('USERNAME')
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def run_drill_check(command)
|
|
290
|
+
result = Command.capture(
|
|
291
|
+
CommandSpec.new(argv: ['sh', '-lc', command]),
|
|
292
|
+
redactor: redactor
|
|
293
|
+
)
|
|
294
|
+
output = result.stdout.empty? ? result.stderr : result.stdout
|
|
295
|
+
|
|
296
|
+
{
|
|
297
|
+
status: 'ok',
|
|
298
|
+
command: redactor.redact_string(command),
|
|
299
|
+
output: redactor.redact_string(output.strip)
|
|
300
|
+
}
|
|
301
|
+
rescue CommandError => e
|
|
302
|
+
{
|
|
303
|
+
status: 'failed',
|
|
304
|
+
command: redactor.redact_string(command),
|
|
305
|
+
error: redactor.redact_string(e.message)
|
|
306
|
+
}
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def write_last_restore_drill(payload)
|
|
310
|
+
FileUtils.mkdir_p(config.state_dir)
|
|
311
|
+
File.write(config.last_restore_drill_path, JSON.pretty_generate(payload))
|
|
312
|
+
rescue SystemCallError
|
|
313
|
+
nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def perform_database_restores_to_current(snapshot)
|
|
317
|
+
databases.map { |adapter| perform_database_restore_to_current(snapshot, adapter: adapter) }
|
|
318
|
+
end
|
|
319
|
+
|
|
250
320
|
def perform_database_restore_to_current(snapshot, adapter:)
|
|
251
321
|
resolved_snapshot = resolve_snapshot(snapshot, tags: database_snapshot_tags(adapter))
|
|
252
322
|
filename = restic.database_file(resolved_snapshot, adapter.adapter_name,
|
|
@@ -270,114 +340,37 @@ module KamalBackup
|
|
|
270
340
|
summarize_database_restore(adapter, resolved_snapshot, filename, adapter.scratch_target_identifier(target))
|
|
271
341
|
end
|
|
272
342
|
|
|
273
|
-
def
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
source_paths: source_paths,
|
|
283
|
-
target_paths: config.backup_paths
|
|
284
|
-
)
|
|
285
|
-
]
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def assign_restore_results(result, database_results, file_results)
|
|
289
|
-
result[:databases] = database_results
|
|
290
|
-
result[:database] = database_results.first
|
|
291
|
-
result[:paths] = file_results.first
|
|
292
|
-
result[:files] = file_results.size == 1 ? file_results.first : file_results
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def backup_summary(started_at:, finished_at:)
|
|
296
|
-
{
|
|
297
|
-
kind: 'backup_result',
|
|
298
|
-
status: 'ok',
|
|
299
|
-
started_at: started_at.iso8601,
|
|
300
|
-
finished_at: finished_at.iso8601,
|
|
301
|
-
databases: databases.map { |adapter| backup_snapshot_summary(adapter) },
|
|
302
|
-
files: backup_file_snapshot_summary
|
|
303
|
-
}
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def backup_snapshot_summary(adapter)
|
|
307
|
-
snapshot = restic.latest_snapshot(tags: database_snapshot_tags(adapter))
|
|
308
|
-
snapshot_summary(snapshot).merge(
|
|
309
|
-
database: database_config_name(adapter),
|
|
310
|
-
adapter: adapter.adapter_name
|
|
311
|
-
)
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def backup_file_snapshot_summary
|
|
315
|
-
return nil if config.backup_paths.empty?
|
|
316
|
-
|
|
317
|
-
snapshot_summary(restic.latest_snapshot(tags: ['type:files']))
|
|
343
|
+
def scratch_database_target(adapter, database_name, sqlite_path)
|
|
344
|
+
case adapter.adapter_name
|
|
345
|
+
when 'sqlite'
|
|
346
|
+
target = sqlite_path || raise(ConfigurationError, 'scratch SQLite path is required')
|
|
347
|
+
databases.size > 1 ? File.join(target, "#{database_config_name(adapter)}.sqlite3") : target
|
|
348
|
+
else
|
|
349
|
+
target = database_name || raise(ConfigurationError, 'scratch database name is required')
|
|
350
|
+
databases.size > 1 ? "#{target}_#{database_config_name(adapter)}" : target
|
|
351
|
+
end
|
|
318
352
|
end
|
|
319
353
|
|
|
320
|
-
def
|
|
354
|
+
def summarize_database_restore(adapter, snapshot, filename, target)
|
|
321
355
|
{
|
|
322
|
-
snapshot: snapshot
|
|
323
|
-
|
|
356
|
+
snapshot: snapshot,
|
|
357
|
+
adapter: adapter.adapter_name,
|
|
358
|
+
filename: filename,
|
|
359
|
+
target: redactor.redact_string(target.to_s)
|
|
324
360
|
}
|
|
325
361
|
end
|
|
326
362
|
|
|
327
|
-
def validate_fresh_backup_summary!(summary, started_at:)
|
|
328
|
-
stale_databases = summary.fetch(:databases).reject { |entry| fresh_snapshot?(entry, started_at) }
|
|
329
|
-
|
|
330
|
-
unless stale_databases.empty?
|
|
331
|
-
names = stale_databases.map { |entry| entry.fetch(:database) }.join(', ')
|
|
332
|
-
raise ConfigurationError, "backup did not create a fresh database snapshot for #{names}"
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
files = summary[:files]
|
|
336
|
-
return unless files && !fresh_snapshot?(files, started_at)
|
|
337
|
-
|
|
338
|
-
raise ConfigurationError, 'backup did not create a fresh file snapshot'
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def fresh_snapshot?(entry, started_at)
|
|
342
|
-
snapshot_time = Time.parse(entry[:time].to_s)
|
|
343
|
-
snapshot_time >= started_at - FRESH_BACKUP_GRACE_SECONDS
|
|
344
|
-
rescue ArgumentError
|
|
345
|
-
false
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
def write_last_backup(result)
|
|
349
|
-
FileUtils.mkdir_p(config.state_dir)
|
|
350
|
-
File.write(config.last_backup_path, JSON.pretty_generate(result))
|
|
351
|
-
@last_backup_record = result.transform_keys(&:to_s)
|
|
352
|
-
@last_backup_finished_at = Time.parse(result.fetch(:finished_at)).utc
|
|
353
|
-
rescue SystemCallError, ArgumentError
|
|
354
|
-
nil
|
|
355
|
-
end
|
|
356
|
-
|
|
357
363
|
def database_snapshot_tags(adapter)
|
|
358
364
|
['type:database', "database:#{database_config_name(adapter)}", "adapter:#{adapter.adapter_name}"]
|
|
359
365
|
end
|
|
360
366
|
|
|
361
367
|
def database_config_name(adapter)
|
|
362
|
-
|
|
363
|
-
adapter.config.database_name
|
|
364
|
-
else
|
|
365
|
-
config.database_name
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
def perform_file_restore(snapshot, target:)
|
|
370
|
-
resolved_snapshot = resolve_snapshot(snapshot, tags: ['type:files'])
|
|
371
|
-
validated_target = config.validate_file_restore_target(target)
|
|
372
|
-
restic.restore_snapshot(resolved_snapshot, validated_target)
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
snapshot: resolved_snapshot,
|
|
376
|
-
target: validated_target
|
|
377
|
-
}
|
|
368
|
+
adapter.config.database_name
|
|
378
369
|
end
|
|
379
370
|
|
|
380
|
-
def perform_replacement_file_restore(snapshot,
|
|
371
|
+
def perform_replacement_file_restore(snapshot, production_source:)
|
|
372
|
+
source_paths = production_source ? config.backup_paths : config.local_restore_source_paths
|
|
373
|
+
target_paths = config.backup_paths
|
|
381
374
|
resolved_snapshot = resolve_snapshot(snapshot, tags: ['type:files'])
|
|
382
375
|
Dir.mktmpdir('kamal-backup-restore-') do |stage_dir|
|
|
383
376
|
restic.restore_snapshot(resolved_snapshot, stage_dir)
|
|
@@ -412,26 +405,17 @@ module KamalBackup
|
|
|
412
405
|
File.join(stage_dir, path.to_s.sub(%r{\A/+}, ''))
|
|
413
406
|
end
|
|
414
407
|
|
|
415
|
-
def
|
|
408
|
+
def perform_file_restore(snapshot, target:)
|
|
409
|
+
resolved_snapshot = resolve_snapshot(snapshot, tags: ['type:files'])
|
|
410
|
+
validated_target = config.validate_file_restore_target(target)
|
|
411
|
+
restic.restore_snapshot(resolved_snapshot, validated_target)
|
|
412
|
+
|
|
416
413
|
{
|
|
417
|
-
snapshot:
|
|
418
|
-
|
|
419
|
-
filename: filename,
|
|
420
|
-
target: redactor.redact_string(target.to_s)
|
|
414
|
+
snapshot: resolved_snapshot,
|
|
415
|
+
target: validated_target
|
|
421
416
|
}
|
|
422
417
|
end
|
|
423
418
|
|
|
424
|
-
def scratch_database_target(adapter, database_name, sqlite_path)
|
|
425
|
-
case adapter.adapter_name
|
|
426
|
-
when 'sqlite'
|
|
427
|
-
target = sqlite_path || raise(ConfigurationError, 'scratch SQLite path is required')
|
|
428
|
-
databases.size > 1 ? File.join(target, "#{database_config_name(adapter)}.sqlite3") : target
|
|
429
|
-
else
|
|
430
|
-
target = database_name || raise(ConfigurationError, 'scratch database name is required')
|
|
431
|
-
databases.size > 1 ? "#{target}_#{database_config_name(adapter)}" : target
|
|
432
|
-
end
|
|
433
|
-
end
|
|
434
|
-
|
|
435
419
|
def validate_local_machine_restore
|
|
436
420
|
config.validate_restic
|
|
437
421
|
config.validate_database_backup
|
|
@@ -463,55 +447,15 @@ module KamalBackup
|
|
|
463
447
|
config.validate_local_database_restore_target(adapter.current_target_identifier)
|
|
464
448
|
end
|
|
465
449
|
|
|
466
|
-
def require_restic!
|
|
467
|
-
return unless using_builtin_restic?
|
|
468
|
-
return if Command.available?('restic')
|
|
469
|
-
|
|
470
|
-
raise ConfigurationError,
|
|
471
|
-
'restic is required on PATH for commands that run on this machine. Install restic locally and try again.'
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
def run_drill_check(command)
|
|
475
|
-
result = Command.capture(
|
|
476
|
-
CommandSpec.new(argv: ['sh', '-lc', command]),
|
|
477
|
-
redactor: redactor
|
|
478
|
-
)
|
|
479
|
-
output = result.stdout.empty? ? result.stderr : result.stdout
|
|
480
|
-
|
|
481
|
-
{
|
|
482
|
-
status: 'ok',
|
|
483
|
-
command: redactor.redact_string(command),
|
|
484
|
-
output: redactor.redact_string(output.strip)
|
|
485
|
-
}
|
|
486
|
-
rescue CommandError => e
|
|
487
|
-
{
|
|
488
|
-
status: 'failed',
|
|
489
|
-
command: redactor.redact_string(command),
|
|
490
|
-
error: redactor.redact_string(e.message)
|
|
491
|
-
}
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def write_last_restore_drill(payload)
|
|
495
|
-
FileUtils.mkdir_p(config.state_dir)
|
|
496
|
-
File.write(config.last_restore_drill_path, JSON.pretty_generate(payload))
|
|
497
|
-
rescue SystemCallError
|
|
498
|
-
nil
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
def drill_operator
|
|
502
|
-
config.value('USER') || config.value('USERNAME')
|
|
503
|
-
end
|
|
504
|
-
|
|
505
450
|
def restic
|
|
506
|
-
@restic ||=
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
end
|
|
451
|
+
@restic ||= begin
|
|
452
|
+
unless Command.available?('restic')
|
|
453
|
+
raise ConfigurationError,
|
|
454
|
+
'restic is required on PATH for commands that run on this machine. Install restic locally and try again.'
|
|
455
|
+
end
|
|
512
456
|
|
|
513
|
-
|
|
514
|
-
|
|
457
|
+
Restic.new(config, redactor: redactor)
|
|
458
|
+
end
|
|
515
459
|
end
|
|
516
460
|
|
|
517
461
|
def databases
|
|
@@ -529,7 +473,6 @@ module KamalBackup
|
|
|
529
473
|
raise ConfigurationError, "no restic snapshot found for #{tags.join(', ')}" unless snapshot
|
|
530
474
|
|
|
531
475
|
snapshot['short_id'] || snapshot['id']
|
|
532
|
-
|
|
533
476
|
else
|
|
534
477
|
argument
|
|
535
478
|
end
|