kamal-backup 0.3.0.beta21 → 0.3.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 +4 -4
- data/exe/kamal-backup +7 -6
- data/lib/kamal_backup/app.rb +350 -356
- data/lib/kamal_backup/cli.rb +107 -111
- data/lib/kamal_backup/command.rb +165 -161
- data/lib/kamal_backup/config.rb +533 -511
- data/lib/kamal_backup/databases/base.rb +17 -14
- data/lib/kamal_backup/databases/mysql.rb +68 -67
- data/lib/kamal_backup/databases/postgres.rb +59 -58
- data/lib/kamal_backup/databases/sqlite.rb +21 -20
- data/lib/kamal_backup/errors.rb +3 -1
- data/lib/kamal_backup/evidence.rb +62 -61
- data/lib/kamal_backup/kamal_bridge.rb +254 -250
- data/lib/kamal_backup/rails_app.rb +102 -101
- data/lib/kamal_backup/redactor.rb +18 -13
- data/lib/kamal_backup/restic.rb +195 -167
- data/lib/kamal_backup/scheduler.rb +17 -14
- data/lib/kamal_backup/schema.rb +2 -0
- data/lib/kamal_backup/version.rb +3 -1
- data/lib/kamal_backup.rb +19 -17
- metadata +30 -2
data/lib/kamal_backup/restic.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'open3'
|
|
7
|
+
require 'time'
|
|
8
|
+
require_relative 'command'
|
|
6
9
|
|
|
7
10
|
module KamalBackup
|
|
8
11
|
class Restic
|
|
@@ -18,36 +21,42 @@ module KamalBackup
|
|
|
18
21
|
def ensure_repository
|
|
19
22
|
run(%w[snapshots --json], log_output: false)
|
|
20
23
|
rescue CommandError => e
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
raise e
|
|
26
|
-
end
|
|
24
|
+
raise e unless config.restic_init_if_missing?
|
|
25
|
+
|
|
26
|
+
log('restic repository not ready, running restic init')
|
|
27
|
+
run(%w[init])
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def backup_stream(command, filename:, tags:)
|
|
30
31
|
restic_command = CommandSpec.new(
|
|
31
|
-
argv: [
|
|
32
|
+
argv: %w[restic
|
|
33
|
+
backup] + host_args + ['--stdin', '--stdin-filename', filename] + tag_args(common_tags + tags),
|
|
32
34
|
env: restic_env
|
|
33
35
|
)
|
|
34
36
|
log("backing up stream as #{filename}")
|
|
35
|
-
pipe_commands(command, restic_command, producer_label:
|
|
37
|
+
pipe_commands(command, restic_command, producer_label: 'dump', consumer_label: 'restic backup')
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def backup_file(path, filename:, tags:)
|
|
39
41
|
command = CommandSpec.new(
|
|
40
|
-
argv: [
|
|
42
|
+
argv: %w[restic
|
|
43
|
+
backup] + host_args + ['--stdin', '--stdin-filename', filename] + tag_args(common_tags + tags),
|
|
41
44
|
env: restic_env
|
|
42
45
|
)
|
|
43
46
|
log("backing up file content as #{filename}")
|
|
44
47
|
|
|
45
|
-
File.open(path,
|
|
48
|
+
File.open(path, 'rb') do |file|
|
|
46
49
|
output = Command.output
|
|
47
50
|
context = output&.command_start(command, redactor: redactor)
|
|
48
51
|
Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
|
|
49
|
-
stdout_reader = Thread.new
|
|
50
|
-
|
|
52
|
+
stdout_reader = Thread.new do
|
|
53
|
+
Command.collect_stream(stdout, command_output: output, context: context, stream: :stdout,
|
|
54
|
+
redactor: redactor)
|
|
55
|
+
end
|
|
56
|
+
stderr_reader = Thread.new do
|
|
57
|
+
Command.collect_stream(stderr, command_output: output, context: context, stream: :stderr,
|
|
58
|
+
redactor: redactor)
|
|
59
|
+
end
|
|
51
60
|
copy_error = nil
|
|
52
61
|
begin
|
|
53
62
|
IO.copy_stream(file, stdin)
|
|
@@ -67,18 +76,19 @@ module KamalBackup
|
|
|
67
76
|
end
|
|
68
77
|
end
|
|
69
78
|
rescue Errno::ENOENT => e
|
|
70
|
-
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127,
|
|
79
|
+
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127,
|
|
80
|
+
stderr: e.message)
|
|
71
81
|
end
|
|
72
82
|
|
|
73
83
|
def backup_paths(paths, tags:)
|
|
74
84
|
paths = Array(paths).compact.map(&:to_s).reject(&:empty?)
|
|
75
85
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
return unless paths.any?
|
|
87
|
+
|
|
88
|
+
path_tags = paths.map { |path| "path:#{config.backup_path_label(path)}" }
|
|
89
|
+
excludes = config.backup_path_excludes(paths)
|
|
90
|
+
log("backing up #{paths.size} file path(s): #{paths.join(', ')}")
|
|
91
|
+
run(['backup'] + host_args + exclude_args(excludes) + paths + tag_args(common_tags + tags + path_tags))
|
|
82
92
|
end
|
|
83
93
|
|
|
84
94
|
def backup_path(path, tags:)
|
|
@@ -91,7 +101,7 @@ module KamalBackup
|
|
|
91
101
|
|
|
92
102
|
def prune
|
|
93
103
|
retention_tag_sets.map do |tags|
|
|
94
|
-
args = [
|
|
104
|
+
args = ['forget', '--prune', '--group-by', 'host'] + config.retention_args + filter_tag_args(tags)
|
|
95
105
|
log("running restic forget/prune with retention policy for #{retention_scope(tags)}")
|
|
96
106
|
run(args)
|
|
97
107
|
end
|
|
@@ -99,39 +109,40 @@ module KamalBackup
|
|
|
99
109
|
|
|
100
110
|
def check
|
|
101
111
|
args = %w[check]
|
|
102
|
-
args.concat([
|
|
112
|
+
args.concat(['--read-data-subset', config.check_read_data_subset]) if config.check_read_data_subset
|
|
103
113
|
started_at = Time.now.utc
|
|
104
114
|
result = run(args)
|
|
105
|
-
write_last_check(status:
|
|
115
|
+
write_last_check(status: 'ok', started_at: started_at, finished_at: Time.now.utc, output: result.stdout)
|
|
106
116
|
result
|
|
107
117
|
rescue CommandError => e
|
|
108
|
-
write_last_check(status:
|
|
118
|
+
write_last_check(status: 'failed', started_at: started_at || Time.now.utc, finished_at: Time.now.utc,
|
|
119
|
+
error: e.message)
|
|
109
120
|
raise
|
|
110
121
|
end
|
|
111
122
|
|
|
112
123
|
def snapshots(tags: common_tags)
|
|
113
|
-
run([
|
|
124
|
+
run(['snapshots'] + filter_tag_args(tags))
|
|
114
125
|
end
|
|
115
126
|
|
|
116
127
|
def snapshots_json(tags: common_tags)
|
|
117
|
-
output = run([
|
|
128
|
+
output = run(['snapshots', '--json'] + filter_tag_args(tags), log_output: false).stdout
|
|
118
129
|
snapshots = JSON.parse(output)
|
|
119
130
|
required_tags = tags.compact
|
|
120
131
|
snapshots.select do |snapshot|
|
|
121
|
-
snapshot_tags = Array(snapshot[
|
|
132
|
+
snapshot_tags = Array(snapshot['tags'])
|
|
122
133
|
required_tags.all? { |tag| snapshot_tags.include?(tag) }
|
|
123
134
|
end
|
|
124
135
|
end
|
|
125
136
|
|
|
126
137
|
def latest_snapshot(tags:)
|
|
127
138
|
snapshots = snapshots_json(tags: common_tags + tags)
|
|
128
|
-
snapshots.max_by { |snapshot| Time.parse(snapshot.fetch(
|
|
139
|
+
snapshots.max_by { |snapshot| Time.parse(snapshot.fetch('time')) }
|
|
129
140
|
rescue JSON::ParserError
|
|
130
141
|
nil
|
|
131
142
|
end
|
|
132
143
|
|
|
133
144
|
def ls_json(snapshot)
|
|
134
|
-
output = run([
|
|
145
|
+
output = run(['ls', '--json', snapshot], log_output: false).stdout
|
|
135
146
|
output.lines.filter_map do |line|
|
|
136
147
|
JSON.parse(line)
|
|
137
148
|
rescue JSON::ParserError
|
|
@@ -141,49 +152,52 @@ module KamalBackup
|
|
|
141
152
|
|
|
142
153
|
def database_file(snapshot, adapter, database_name: nil)
|
|
143
154
|
legacy_prefix = "databases/#{config.app_name}/#{adapter}/"
|
|
144
|
-
app = config.app_name.gsub(/[^A-Za-z0-9_.-]+/,
|
|
145
|
-
database = database_name.to_s.gsub(/[^A-Za-z0-9_.-]+/,
|
|
155
|
+
app = config.app_name.gsub(/[^A-Za-z0-9_.-]+/, '-')
|
|
156
|
+
database = database_name.to_s.gsub(/[^A-Za-z0-9_.-]+/, '-')
|
|
146
157
|
stable_prefix = database.empty? ? nil : "databases/#{app}/#{database}/#{adapter}."
|
|
147
158
|
flat_prefix = "databases-#{app}-#{adapter}-"
|
|
148
159
|
named_flat_prefix = database.empty? ? nil : "databases-#{app}-#{database}-#{adapter}-"
|
|
149
160
|
ls_json(snapshot).find do |entry|
|
|
150
|
-
next false unless entry[
|
|
161
|
+
next false unless entry['type'] == 'file'
|
|
151
162
|
|
|
152
|
-
normalized = entry[
|
|
163
|
+
normalized = entry['path'].to_s.sub(%r{\A/+}, '')
|
|
153
164
|
(stable_prefix && normalized.start_with?(stable_prefix)) ||
|
|
154
165
|
normalized.start_with?(legacy_prefix) ||
|
|
155
166
|
File.basename(normalized).start_with?(flat_prefix) ||
|
|
156
167
|
(named_flat_prefix && File.basename(normalized).start_with?(named_flat_prefix))
|
|
157
|
-
end&.fetch(
|
|
168
|
+
end&.fetch('path')
|
|
158
169
|
end
|
|
159
170
|
|
|
160
171
|
def pipe_dump_to_command(snapshot, filename, command)
|
|
161
|
-
restic_command = CommandSpec.new(argv: [
|
|
162
|
-
pipe_commands(restic_command, command, producer_label:
|
|
172
|
+
restic_command = CommandSpec.new(argv: ['restic', 'dump', snapshot, filename], env: restic_env)
|
|
173
|
+
pipe_commands(restic_command, command, producer_label: 'restic dump', consumer_label: command.argv.first)
|
|
163
174
|
end
|
|
164
175
|
|
|
165
176
|
def write_dump_to_path(snapshot, filename, target_path)
|
|
166
|
-
command = CommandSpec.new(argv: [
|
|
177
|
+
command = CommandSpec.new(argv: ['restic', 'dump', snapshot, filename], env: restic_env)
|
|
167
178
|
target_path = File.expand_path(target_path)
|
|
168
179
|
FileUtils.mkdir_p(File.dirname(target_path))
|
|
169
|
-
temp_path = "#{target_path}.kamal-backup-#{
|
|
180
|
+
temp_path = "#{target_path}.kamal-backup-#{$PROCESS_ID}.tmp"
|
|
170
181
|
|
|
171
182
|
output = Command.output
|
|
172
183
|
context = output&.command_start(command, redactor: redactor)
|
|
173
184
|
Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
|
|
174
185
|
stdin.close
|
|
175
|
-
stderr_reader = Thread.new
|
|
176
|
-
|
|
186
|
+
stderr_reader = Thread.new do
|
|
187
|
+
Command.collect_stream(stderr, command_output: output, context: context, stream: :stderr, redactor: redactor)
|
|
188
|
+
end
|
|
189
|
+
File.open(temp_path, 'wb') { |file| IO.copy_stream(stdout, file) }
|
|
177
190
|
err = stderr_reader.value
|
|
178
191
|
status = wait_thread.value
|
|
179
192
|
output&.command_exit(context, status.exitstatus)
|
|
180
|
-
raise_command_error(command, status,
|
|
193
|
+
raise_command_error(command, status, '', err) unless status.success?
|
|
181
194
|
end
|
|
182
195
|
File.rename(temp_path, target_path)
|
|
183
196
|
target_path
|
|
184
197
|
rescue Errno::ENOENT => e
|
|
185
198
|
FileUtils.rm_f(temp_path) if temp_path
|
|
186
|
-
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127,
|
|
199
|
+
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127,
|
|
200
|
+
stderr: e.message)
|
|
187
201
|
rescue StandardError
|
|
188
202
|
FileUtils.rm_f(temp_path) if temp_path
|
|
189
203
|
raise
|
|
@@ -191,162 +205,176 @@ module KamalBackup
|
|
|
191
205
|
|
|
192
206
|
def restore_snapshot(snapshot, target)
|
|
193
207
|
log("restoring file snapshot #{snapshot} to #{target}")
|
|
194
|
-
run([
|
|
208
|
+
run(['restore', snapshot, '--target', target])
|
|
195
209
|
end
|
|
196
210
|
|
|
197
211
|
def run(args, log_output: true)
|
|
198
212
|
Command.capture(
|
|
199
|
-
CommandSpec.new(argv: [
|
|
213
|
+
CommandSpec.new(argv: ['restic'] + args, env: restic_env),
|
|
200
214
|
redactor: redactor,
|
|
201
215
|
log_output: log_output
|
|
202
216
|
)
|
|
203
217
|
end
|
|
204
218
|
|
|
205
219
|
def common_tags
|
|
206
|
-
[
|
|
220
|
+
['kamal-backup', "app:#{config.app_name}"]
|
|
207
221
|
end
|
|
208
222
|
|
|
209
223
|
private
|
|
210
|
-
def retention_tag_sets
|
|
211
|
-
database_retention_tag_sets + file_retention_tag_sets
|
|
212
|
-
end
|
|
213
224
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
def retention_tag_sets
|
|
226
|
+
database_retention_tag_sets + file_retention_tag_sets
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def database_retention_tag_sets
|
|
230
|
+
config.databases.group_by(&:database_adapter).flat_map do |adapter, databases|
|
|
231
|
+
if databases.one?
|
|
232
|
+
# Pre-0.3 database snapshots did not include database:<name>, so keep
|
|
233
|
+
# the single-database filter broad enough for retention to prune them.
|
|
234
|
+
[common_tags + ['type:database', "adapter:#{adapter}"]]
|
|
235
|
+
else
|
|
236
|
+
databases.map do |database|
|
|
237
|
+
common_tags + ['type:database', "database:#{database.database_name}", "adapter:#{adapter}"]
|
|
224
238
|
end
|
|
225
239
|
end
|
|
226
240
|
end
|
|
241
|
+
end
|
|
227
242
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
243
|
+
def file_retention_tag_sets
|
|
244
|
+
config.backup_paths.any? ? [common_tags + ['type:files']] : []
|
|
245
|
+
end
|
|
231
246
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
247
|
+
def retention_scope(tags)
|
|
248
|
+
tags.reject { |tag| tag == 'kamal-backup' || tag.start_with?('app:') }.join(', ')
|
|
249
|
+
end
|
|
235
250
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
251
|
+
def tag_args(tags)
|
|
252
|
+
tags.compact.each_with_object([]) { |tag, args| args.concat(['--tag', tag]) }
|
|
253
|
+
end
|
|
239
254
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
255
|
+
def exclude_args(patterns)
|
|
256
|
+
patterns.compact.each_with_object([]) { |pattern, args| args.concat(['--exclude', pattern]) }
|
|
257
|
+
end
|
|
243
258
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
def host_args
|
|
260
|
+
['--host', restic_host]
|
|
261
|
+
end
|
|
247
262
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
def restic_host
|
|
264
|
+
normalize_restic_host([config.app_name, config.accessory_name || 'backup'].compact.join('-'))
|
|
265
|
+
end
|
|
251
266
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
267
|
+
def normalize_restic_host(value)
|
|
268
|
+
normalized = value.to_s.gsub(/[^A-Za-z0-9_.-]+/, '-').gsub(/\A-+|-+\z/, '')
|
|
269
|
+
normalized.empty? ? 'kamal-backup' : normalized
|
|
270
|
+
end
|
|
256
271
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
272
|
+
def filter_tag_args(tags)
|
|
273
|
+
tags = tags.compact
|
|
274
|
+
tags.empty? ? [] : ['--tag', tags.join(',')]
|
|
275
|
+
end
|
|
261
276
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
end
|
|
277
|
+
def restic_env
|
|
278
|
+
config.env.each_with_object({}) do |(key, value), env|
|
|
279
|
+
env[key] = value if key.to_s.match?(RESTIC_ENV_PATTERN)
|
|
266
280
|
end
|
|
281
|
+
end
|
|
267
282
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
copy_error = e
|
|
285
|
-
ensure
|
|
286
|
-
consumer_stdin.close unless consumer_stdin.closed?
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
copy_thread.join
|
|
290
|
-
producer_status = producer_wait.value
|
|
291
|
-
consumer_status = consumer_wait.value
|
|
292
|
-
output&.command_exit(producer_context, producer_status.exitstatus)
|
|
293
|
-
output&.command_exit(consumer_context, consumer_status.exitstatus)
|
|
294
|
-
|
|
295
|
-
producer_err = producer_err_reader.value
|
|
296
|
-
consumer_out = consumer_out_reader.value
|
|
297
|
-
consumer_err = consumer_err_reader.value
|
|
298
|
-
|
|
299
|
-
if copy_error
|
|
300
|
-
raise CommandError.new(
|
|
301
|
-
"failed to pipe #{producer_label} to #{consumer_label}: #{copy_error.message}",
|
|
302
|
-
command: consumer,
|
|
303
|
-
stderr: copy_error.message
|
|
304
|
-
)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
raise_command_error(producer, producer_status, "", producer_err) unless producer_status.success?
|
|
308
|
-
raise_command_error(consumer, consumer_status, consumer_out, consumer_err) unless consumer_status.success?
|
|
309
|
-
|
|
310
|
-
CommandResult.new(stdout: consumer_out, stderr: consumer_err, status: consumer_status.exitstatus)
|
|
283
|
+
def pipe_commands(producer, consumer, producer_label:, consumer_label:)
|
|
284
|
+
output = Command.output
|
|
285
|
+
producer_context = output&.command_start(producer, redactor: redactor)
|
|
286
|
+
Open3.popen3(producer.env, *producer.argv) do |producer_stdin, producer_stdout, producer_stderr, producer_wait|
|
|
287
|
+
producer_stdin.close
|
|
288
|
+
|
|
289
|
+
consumer_context = output&.command_start(consumer, redactor: redactor)
|
|
290
|
+
Open3.popen3(consumer.env,
|
|
291
|
+
*consumer.argv) do |consumer_stdin, consumer_stdout, consumer_stderr, consumer_wait|
|
|
292
|
+
producer_err_reader = Thread.new do
|
|
293
|
+
Command.collect_stream(producer_stderr, command_output: output, context: producer_context, stream: :stderr,
|
|
294
|
+
redactor: redactor)
|
|
295
|
+
end
|
|
296
|
+
consumer_out_reader = Thread.new do
|
|
297
|
+
Command.collect_stream(consumer_stdout, command_output: output, context: consumer_context, stream: :stdout,
|
|
298
|
+
redactor: redactor)
|
|
311
299
|
end
|
|
300
|
+
consumer_err_reader = Thread.new do
|
|
301
|
+
Command.collect_stream(consumer_stderr, command_output: output, context: consumer_context, stream: :stderr,
|
|
302
|
+
redactor: redactor)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
copy_error = nil
|
|
306
|
+
copy_thread = Thread.new do
|
|
307
|
+
IO.copy_stream(producer_stdout, consumer_stdin)
|
|
308
|
+
rescue StandardError => e
|
|
309
|
+
copy_error = e
|
|
310
|
+
ensure
|
|
311
|
+
consumer_stdin.close unless consumer_stdin.closed?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
copy_thread.join
|
|
315
|
+
producer_status = producer_wait.value
|
|
316
|
+
consumer_status = consumer_wait.value
|
|
317
|
+
output&.command_exit(producer_context, producer_status.exitstatus)
|
|
318
|
+
output&.command_exit(consumer_context, consumer_status.exitstatus)
|
|
319
|
+
|
|
320
|
+
producer_err = producer_err_reader.value
|
|
321
|
+
consumer_out = consumer_out_reader.value
|
|
322
|
+
consumer_err = consumer_err_reader.value
|
|
323
|
+
|
|
324
|
+
if copy_error
|
|
325
|
+
raise CommandError.new(
|
|
326
|
+
"failed to pipe #{producer_label} to #{consumer_label}: #{copy_error.message}",
|
|
327
|
+
command: consumer,
|
|
328
|
+
stderr: copy_error.message
|
|
329
|
+
)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
raise_command_error(producer, producer_status, '', producer_err) unless producer_status.success?
|
|
333
|
+
raise_command_error(consumer, consumer_status, consumer_out, consumer_err) unless consumer_status.success?
|
|
334
|
+
|
|
335
|
+
CommandResult.new(stdout: consumer_out, stderr: consumer_err, status: consumer_status.exitstatus)
|
|
312
336
|
end
|
|
313
|
-
rescue Errno::ENOENT => e
|
|
314
|
-
command = e.message.include?(producer.argv.first) ? producer : consumer
|
|
315
|
-
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127, stderr: e.message)
|
|
316
337
|
end
|
|
338
|
+
rescue Errno::ENOENT => e
|
|
339
|
+
command = e.message.include?(producer.argv.first) ? producer : consumer
|
|
340
|
+
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127,
|
|
341
|
+
stderr: e.message)
|
|
342
|
+
end
|
|
317
343
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
344
|
+
def raise_command_error(command, status, stdout, stderr)
|
|
345
|
+
raise CommandError.new(
|
|
346
|
+
"command failed (#{status.exitstatus}): #{command.display(redactor)}\n#{redactor.redact_string(stderr)}",
|
|
347
|
+
command: command,
|
|
348
|
+
status: status.exitstatus,
|
|
349
|
+
stdout: redactor.redact_string(stdout),
|
|
350
|
+
stderr: redactor.redact_string(stderr)
|
|
351
|
+
)
|
|
352
|
+
end
|
|
327
353
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
354
|
+
def raise_stream_error(command, error, stdout, stderr)
|
|
355
|
+
raise CommandError.new(
|
|
356
|
+
"failed to stream file to #{command.display(redactor)}: #{error.message}\n#{redactor.redact_string(stderr)}",
|
|
357
|
+
command: command,
|
|
358
|
+
stdout: redactor.redact_string(stdout),
|
|
359
|
+
stderr: redactor.redact_string(stderr)
|
|
360
|
+
)
|
|
361
|
+
end
|
|
336
362
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
363
|
+
def write_last_check(payload)
|
|
364
|
+
FileUtils.mkdir_p(config.state_dir)
|
|
365
|
+
File.write(config.last_check_path, JSON.pretty_generate(payload.transform_values do |value|
|
|
366
|
+
value.respond_to?(:iso8601) ? value.iso8601 : redactor.redact_string(value.to_s)
|
|
367
|
+
end))
|
|
368
|
+
rescue SystemCallError
|
|
369
|
+
nil
|
|
370
|
+
end
|
|
343
371
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
end
|
|
372
|
+
def log(message)
|
|
373
|
+
if Command.output
|
|
374
|
+
Command.output.info(message, redactor: redactor)
|
|
375
|
+
else
|
|
376
|
+
$stdout.puts("[kamal-backup] #{redactor.redact_string(message)}")
|
|
350
377
|
end
|
|
378
|
+
end
|
|
351
379
|
end
|
|
352
380
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
2
4
|
|
|
3
5
|
module KamalBackup
|
|
4
6
|
class Scheduler
|
|
@@ -32,21 +34,22 @@ module KamalBackup
|
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
private
|
|
35
|
-
def install_signal_handlers
|
|
36
|
-
%w[TERM INT].each do |signal|
|
|
37
|
-
Signal.trap(signal) { @stop = true }
|
|
38
|
-
rescue ArgumentError
|
|
39
|
-
nil
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
def install_signal_handlers
|
|
39
|
+
%w[TERM INT].each do |signal|
|
|
40
|
+
Signal.trap(signal) { @stop = true }
|
|
41
|
+
rescue ArgumentError
|
|
42
|
+
nil
|
|
46
43
|
end
|
|
44
|
+
end
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
def sleep_interruptibly(seconds)
|
|
47
|
+
deadline = Time.now + seconds
|
|
48
|
+
sleep([deadline - Time.now, 1].min) while !@stop && Time.now < deadline
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def log(message)
|
|
52
|
+
$stdout.puts("[kamal-backup] #{message}")
|
|
53
|
+
end
|
|
51
54
|
end
|
|
52
55
|
end
|
data/lib/kamal_backup/schema.rb
CHANGED
data/lib/kamal_backup/version.rb
CHANGED
data/lib/kamal_backup.rb
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
14
|
-
require_relative
|
|
15
|
-
require_relative
|
|
16
|
-
require_relative
|
|
17
|
-
require_relative
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'kamal_backup/version'
|
|
4
|
+
require_relative 'kamal_backup/schema'
|
|
5
|
+
require_relative 'kamal_backup/errors'
|
|
6
|
+
require_relative 'kamal_backup/command'
|
|
7
|
+
require_relative 'kamal_backup/redactor'
|
|
8
|
+
require_relative 'kamal_backup/config'
|
|
9
|
+
require_relative 'kamal_backup/rails_app'
|
|
10
|
+
require_relative 'kamal_backup/kamal_bridge'
|
|
11
|
+
require_relative 'kamal_backup/restic'
|
|
12
|
+
require_relative 'kamal_backup/evidence'
|
|
13
|
+
require_relative 'kamal_backup/scheduler'
|
|
14
|
+
require_relative 'kamal_backup/databases/base'
|
|
15
|
+
require_relative 'kamal_backup/databases/postgres'
|
|
16
|
+
require_relative 'kamal_backup/databases/mysql'
|
|
17
|
+
require_relative 'kamal_backup/databases/sqlite'
|
|
18
|
+
require_relative 'kamal_backup/app'
|
|
19
|
+
require_relative 'kamal_backup/cli'
|