kamal-backup 0.3.0.beta20 → 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/README.md +1 -0
- 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 +555 -437
- 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 +196 -163
- 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,17 +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
|
-
|
|
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))
|
|
81
92
|
end
|
|
82
93
|
|
|
83
94
|
def backup_path(path, tags:)
|
|
@@ -90,7 +101,7 @@ module KamalBackup
|
|
|
90
101
|
|
|
91
102
|
def prune
|
|
92
103
|
retention_tag_sets.map do |tags|
|
|
93
|
-
args = [
|
|
104
|
+
args = ['forget', '--prune', '--group-by', 'host'] + config.retention_args + filter_tag_args(tags)
|
|
94
105
|
log("running restic forget/prune with retention policy for #{retention_scope(tags)}")
|
|
95
106
|
run(args)
|
|
96
107
|
end
|
|
@@ -98,39 +109,40 @@ module KamalBackup
|
|
|
98
109
|
|
|
99
110
|
def check
|
|
100
111
|
args = %w[check]
|
|
101
|
-
args.concat([
|
|
112
|
+
args.concat(['--read-data-subset', config.check_read_data_subset]) if config.check_read_data_subset
|
|
102
113
|
started_at = Time.now.utc
|
|
103
114
|
result = run(args)
|
|
104
|
-
write_last_check(status:
|
|
115
|
+
write_last_check(status: 'ok', started_at: started_at, finished_at: Time.now.utc, output: result.stdout)
|
|
105
116
|
result
|
|
106
117
|
rescue CommandError => e
|
|
107
|
-
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)
|
|
108
120
|
raise
|
|
109
121
|
end
|
|
110
122
|
|
|
111
123
|
def snapshots(tags: common_tags)
|
|
112
|
-
run([
|
|
124
|
+
run(['snapshots'] + filter_tag_args(tags))
|
|
113
125
|
end
|
|
114
126
|
|
|
115
127
|
def snapshots_json(tags: common_tags)
|
|
116
|
-
output = run([
|
|
128
|
+
output = run(['snapshots', '--json'] + filter_tag_args(tags), log_output: false).stdout
|
|
117
129
|
snapshots = JSON.parse(output)
|
|
118
130
|
required_tags = tags.compact
|
|
119
131
|
snapshots.select do |snapshot|
|
|
120
|
-
snapshot_tags = Array(snapshot[
|
|
132
|
+
snapshot_tags = Array(snapshot['tags'])
|
|
121
133
|
required_tags.all? { |tag| snapshot_tags.include?(tag) }
|
|
122
134
|
end
|
|
123
135
|
end
|
|
124
136
|
|
|
125
137
|
def latest_snapshot(tags:)
|
|
126
138
|
snapshots = snapshots_json(tags: common_tags + tags)
|
|
127
|
-
snapshots.max_by { |snapshot| Time.parse(snapshot.fetch(
|
|
139
|
+
snapshots.max_by { |snapshot| Time.parse(snapshot.fetch('time')) }
|
|
128
140
|
rescue JSON::ParserError
|
|
129
141
|
nil
|
|
130
142
|
end
|
|
131
143
|
|
|
132
144
|
def ls_json(snapshot)
|
|
133
|
-
output = run([
|
|
145
|
+
output = run(['ls', '--json', snapshot], log_output: false).stdout
|
|
134
146
|
output.lines.filter_map do |line|
|
|
135
147
|
JSON.parse(line)
|
|
136
148
|
rescue JSON::ParserError
|
|
@@ -140,49 +152,52 @@ module KamalBackup
|
|
|
140
152
|
|
|
141
153
|
def database_file(snapshot, adapter, database_name: nil)
|
|
142
154
|
legacy_prefix = "databases/#{config.app_name}/#{adapter}/"
|
|
143
|
-
app = config.app_name.gsub(/[^A-Za-z0-9_.-]+/,
|
|
144
|
-
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_.-]+/, '-')
|
|
145
157
|
stable_prefix = database.empty? ? nil : "databases/#{app}/#{database}/#{adapter}."
|
|
146
158
|
flat_prefix = "databases-#{app}-#{adapter}-"
|
|
147
159
|
named_flat_prefix = database.empty? ? nil : "databases-#{app}-#{database}-#{adapter}-"
|
|
148
160
|
ls_json(snapshot).find do |entry|
|
|
149
|
-
next false unless entry[
|
|
161
|
+
next false unless entry['type'] == 'file'
|
|
150
162
|
|
|
151
|
-
normalized = entry[
|
|
163
|
+
normalized = entry['path'].to_s.sub(%r{\A/+}, '')
|
|
152
164
|
(stable_prefix && normalized.start_with?(stable_prefix)) ||
|
|
153
165
|
normalized.start_with?(legacy_prefix) ||
|
|
154
166
|
File.basename(normalized).start_with?(flat_prefix) ||
|
|
155
167
|
(named_flat_prefix && File.basename(normalized).start_with?(named_flat_prefix))
|
|
156
|
-
end&.fetch(
|
|
168
|
+
end&.fetch('path')
|
|
157
169
|
end
|
|
158
170
|
|
|
159
171
|
def pipe_dump_to_command(snapshot, filename, command)
|
|
160
|
-
restic_command = CommandSpec.new(argv: [
|
|
161
|
-
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)
|
|
162
174
|
end
|
|
163
175
|
|
|
164
176
|
def write_dump_to_path(snapshot, filename, target_path)
|
|
165
|
-
command = CommandSpec.new(argv: [
|
|
177
|
+
command = CommandSpec.new(argv: ['restic', 'dump', snapshot, filename], env: restic_env)
|
|
166
178
|
target_path = File.expand_path(target_path)
|
|
167
179
|
FileUtils.mkdir_p(File.dirname(target_path))
|
|
168
|
-
temp_path = "#{target_path}.kamal-backup-#{
|
|
180
|
+
temp_path = "#{target_path}.kamal-backup-#{$PROCESS_ID}.tmp"
|
|
169
181
|
|
|
170
182
|
output = Command.output
|
|
171
183
|
context = output&.command_start(command, redactor: redactor)
|
|
172
184
|
Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
|
|
173
185
|
stdin.close
|
|
174
|
-
stderr_reader = Thread.new
|
|
175
|
-
|
|
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) }
|
|
176
190
|
err = stderr_reader.value
|
|
177
191
|
status = wait_thread.value
|
|
178
192
|
output&.command_exit(context, status.exitstatus)
|
|
179
|
-
raise_command_error(command, status,
|
|
193
|
+
raise_command_error(command, status, '', err) unless status.success?
|
|
180
194
|
end
|
|
181
195
|
File.rename(temp_path, target_path)
|
|
182
196
|
target_path
|
|
183
197
|
rescue Errno::ENOENT => e
|
|
184
198
|
FileUtils.rm_f(temp_path) if temp_path
|
|
185
|
-
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)
|
|
186
201
|
rescue StandardError
|
|
187
202
|
FileUtils.rm_f(temp_path) if temp_path
|
|
188
203
|
raise
|
|
@@ -190,158 +205,176 @@ module KamalBackup
|
|
|
190
205
|
|
|
191
206
|
def restore_snapshot(snapshot, target)
|
|
192
207
|
log("restoring file snapshot #{snapshot} to #{target}")
|
|
193
|
-
run([
|
|
208
|
+
run(['restore', snapshot, '--target', target])
|
|
194
209
|
end
|
|
195
210
|
|
|
196
211
|
def run(args, log_output: true)
|
|
197
212
|
Command.capture(
|
|
198
|
-
CommandSpec.new(argv: [
|
|
213
|
+
CommandSpec.new(argv: ['restic'] + args, env: restic_env),
|
|
199
214
|
redactor: redactor,
|
|
200
215
|
log_output: log_output
|
|
201
216
|
)
|
|
202
217
|
end
|
|
203
218
|
|
|
204
219
|
def common_tags
|
|
205
|
-
[
|
|
220
|
+
['kamal-backup', "app:#{config.app_name}"]
|
|
206
221
|
end
|
|
207
222
|
|
|
208
223
|
private
|
|
209
|
-
def retention_tag_sets
|
|
210
|
-
database_retention_tag_sets + file_retention_tag_sets
|
|
211
|
-
end
|
|
212
224
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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}"]
|
|
223
238
|
end
|
|
224
239
|
end
|
|
225
240
|
end
|
|
241
|
+
end
|
|
226
242
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
def file_retention_tag_sets
|
|
244
|
+
config.backup_paths.any? ? [common_tags + ['type:files']] : []
|
|
245
|
+
end
|
|
230
246
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
247
|
+
def retention_scope(tags)
|
|
248
|
+
tags.reject { |tag| tag == 'kamal-backup' || tag.start_with?('app:') }.join(', ')
|
|
249
|
+
end
|
|
234
250
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
def tag_args(tags)
|
|
252
|
+
tags.compact.each_with_object([]) { |tag, args| args.concat(['--tag', tag]) }
|
|
253
|
+
end
|
|
238
254
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
255
|
+
def exclude_args(patterns)
|
|
256
|
+
patterns.compact.each_with_object([]) { |pattern, args| args.concat(['--exclude', pattern]) }
|
|
257
|
+
end
|
|
242
258
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
259
|
+
def host_args
|
|
260
|
+
['--host', restic_host]
|
|
261
|
+
end
|
|
246
262
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
end
|
|
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
|
|
276
|
+
|
|
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)
|
|
261
280
|
end
|
|
281
|
+
end
|
|
262
282
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
copy_error = e
|
|
280
|
-
ensure
|
|
281
|
-
consumer_stdin.close unless consumer_stdin.closed?
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
copy_thread.join
|
|
285
|
-
producer_status = producer_wait.value
|
|
286
|
-
consumer_status = consumer_wait.value
|
|
287
|
-
output&.command_exit(producer_context, producer_status.exitstatus)
|
|
288
|
-
output&.command_exit(consumer_context, consumer_status.exitstatus)
|
|
289
|
-
|
|
290
|
-
producer_err = producer_err_reader.value
|
|
291
|
-
consumer_out = consumer_out_reader.value
|
|
292
|
-
consumer_err = consumer_err_reader.value
|
|
293
|
-
|
|
294
|
-
if copy_error
|
|
295
|
-
raise CommandError.new(
|
|
296
|
-
"failed to pipe #{producer_label} to #{consumer_label}: #{copy_error.message}",
|
|
297
|
-
command: consumer,
|
|
298
|
-
stderr: copy_error.message
|
|
299
|
-
)
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
raise_command_error(producer, producer_status, "", producer_err) unless producer_status.success?
|
|
303
|
-
raise_command_error(consumer, consumer_status, consumer_out, consumer_err) unless consumer_status.success?
|
|
304
|
-
|
|
305
|
-
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)
|
|
306
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)
|
|
307
336
|
end
|
|
308
|
-
rescue Errno::ENOENT => e
|
|
309
|
-
command = e.message.include?(producer.argv.first) ? producer : consumer
|
|
310
|
-
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127, stderr: e.message)
|
|
311
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
|
|
312
343
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
322
353
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
331
362
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
338
371
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
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)}")
|
|
345
377
|
end
|
|
378
|
+
end
|
|
346
379
|
end
|
|
347
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'
|