kamal-backup 0.3.0.beta5 → 0.3.0.beta7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b92197e0f2d480ca2cebd5e2505304686b739ca0ed4c61c24b4b4caca39054d
4
- data.tar.gz: ff2b00e495d6690956d1ed8070bc43989ca1a9e4b9846b6503ababa26adbd01f
3
+ metadata.gz: 841b29d2ee8cc4598cb1880da4a7bf884cef141816432a9d960ff400b7535d25
4
+ data.tar.gz: 3784f1bfe50f1480f457bfb40a18e30ab5905db4a300a193fe5d68ac50d67f2f
5
5
  SHA512:
6
- metadata.gz: c8f93d59add03f4bf66b48ae0ea61cf46c7682c527ac604fcbd98a8c0790e6cc77d9d45759fabbfb5d6fcbce25da0e82b421f56cc8771820c57b1933539634c2
7
- data.tar.gz: ebd54357f4bdc1829a71cb76fcef4e60000661ce7eb8f4d1a974fb22438b1054fe96bfa26095509d74fd50e5a13d5e234fa0c845440c8f33ddfd49e798b307f4
6
+ metadata.gz: 9929349441963b8297254c10faa1446cf4f7602638a7637ab655ffcadb32d31d012ea8827890fc29175dfc4e344a5afc1edacbf960cfe86d6aec2a83c88385c6
7
+ data.tar.gz: fbfee00eae64424a92e5d039512ff51d01abecb6dd6a61d8d44ec97dc2e86208f06fd5c191947107a9c9adaa4da80a77a9eebabc9e9e12b96d7e60dcb0ce0e06
@@ -20,11 +20,17 @@ module KamalBackup
20
20
  end
21
21
 
22
22
  def direct_app
23
- @direct_app ||= App.new(config: Config.new(env: command_env), redactor: redactor)
23
+ @direct_app ||= App.new(
24
+ config: Config.new(env: command_env),
25
+ redactor: redactor
26
+ )
24
27
  end
25
28
 
26
- def local_app
27
- @local_app ||= App.new(config: local_command_config, redactor: redactor)
29
+ def local_restore_app
30
+ @local_restore_app ||= App.new(
31
+ config: local_command_config,
32
+ redactor: redactor
33
+ )
28
34
  end
29
35
 
30
36
  def local_preferences
@@ -65,7 +71,9 @@ module KamalBackup
65
71
  redactor: redactor,
66
72
  config_file: options[:config_file],
67
73
  destination: options[:destination],
68
- env: command_env
74
+ env: command_env,
75
+ stdout: $stdout,
76
+ stderr: $stderr
69
77
  )
70
78
  end
71
79
 
@@ -94,9 +102,11 @@ module KamalBackup
94
102
 
95
103
  result = bridge.execute_on_accessory(
96
104
  accessory_name: accessory_name,
97
- command: Shellwords.join(argv)
105
+ command: Shellwords.join(argv),
106
+ stream: true
98
107
  )
99
- print(result.stdout)
108
+ print(result.stdout) unless result.streamed
109
+ $stderr.print(result.stderr) if !result.streamed && !result.stderr.empty?
100
110
  result
101
111
  end
102
112
 
@@ -276,7 +286,7 @@ module KamalBackup
276
286
  desc "local [SNAPSHOT]", "Restore the backup into the local database and Active Storage path"
277
287
  def local(snapshot = "latest")
278
288
  confirm!("Restore #{snapshot} into the local database and Active Storage path? This will overwrite local data.")
279
- puts(JSON.pretty_generate(local_app.restore_to_local_machine(snapshot)))
289
+ puts(JSON.pretty_generate(local_restore_app.restore_to_local_machine(snapshot)))
280
290
  end
281
291
 
282
292
  method_option :"confirm-production-restore", type: :boolean, default: false, desc: "Confirm production restore without interactive prompts"
@@ -301,9 +311,9 @@ module KamalBackup
301
311
  desc "local [SNAPSHOT]", "Run a restore drill on the local machine"
302
312
  def local(snapshot = "latest")
303
313
  confirm!("Run a local restore drill for #{snapshot}? This will overwrite local data.")
304
- result = local_app.drill_on_local_machine(snapshot, check_command: options[:check])
314
+ result = local_restore_app.drill_on_local_machine(snapshot, check_command: options[:check])
305
315
  puts(JSON.pretty_generate(result))
306
- exit(1) if local_app.drill_failed?(result)
316
+ exit(1) if local_restore_app.drill_failed?(result)
307
317
  end
308
318
 
309
319
  method_option :database, type: :string, desc: "Scratch database name for PostgreSQL or MySQL"
@@ -389,7 +399,9 @@ module KamalBackup
389
399
 
390
400
  def self.start(argv = ARGV, env: ENV)
391
401
  self.command_env = env
392
- super(normalize_global_options(argv))
402
+ Command.with_output(CommandOutput.new(io: $stderr)) do
403
+ super(normalize_global_options(argv))
404
+ end
393
405
  rescue Error => e
394
406
  warn("kamal-backup: #{Redactor.new(env: env).redact_string(e.message)}")
395
407
  exit(1)
@@ -1,4 +1,6 @@
1
1
  require "open3"
2
+ require "securerandom"
3
+ require "shellwords"
2
4
  require_relative "errors"
3
5
 
4
6
  module KamalBackup
@@ -18,14 +20,185 @@ module KamalBackup
18
20
 
19
21
  def display(redactor)
20
22
  env_prefix = env.keys.sort.map { |key| "#{key}=#{redactor.redact_value(key, env[key])}" }
21
- redactor.redact_string((env_prefix + argv).join(" "))
23
+ redactor.redact_string((env_prefix + [argv.shelljoin]).join(" "))
22
24
  end
23
25
  end
24
26
 
25
- CommandResult = Struct.new(:stdout, :stderr, :status, keyword_init: true)
27
+ CommandResult = Struct.new(:stdout, :stderr, :status, :streamed, keyword_init: true)
28
+
29
+ class CommandOutput
30
+ LEVELS = {
31
+ "DEBUG" => 0,
32
+ "INFO" => 1,
33
+ "WARN" => 2,
34
+ "ERROR" => 3,
35
+ "FATAL" => 4
36
+ }.freeze
37
+ LEVEL_COLORS = {
38
+ "DEBUG" => :black,
39
+ "INFO" => :blue,
40
+ "WARN" => :yellow,
41
+ "ERROR" => :red,
42
+ "FATAL" => :red
43
+ }.freeze
44
+ COLOR_CODES = {
45
+ black: 30,
46
+ red: 31,
47
+ green: 32,
48
+ yellow: 33,
49
+ blue: 34,
50
+ magenta: 35,
51
+ cyan: 36,
52
+ white: 37,
53
+ light_black: 90,
54
+ light_red: 91,
55
+ light_green: 92,
56
+ light_yellow: 93,
57
+ light_blue: 94,
58
+ light_magenta: 95,
59
+ light_cyan: 96,
60
+ light_white: 97
61
+ }.freeze
62
+
63
+ def initialize(io: $stdout, env: ENV, verbosity: :info)
64
+ @io = io
65
+ @env = env
66
+ @verbosity = LEVELS.fetch(verbosity.to_s.upcase)
67
+ @mutex = Mutex.new
68
+ @buffers = {}
69
+ end
70
+
71
+ def info(message, redactor:)
72
+ write_message("INFO", redactor.redact_string(message))
73
+ end
74
+
75
+ def command_start(spec, redactor:)
76
+ id = SecureRandom.hex(4)
77
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
78
+ display = spec.display(redactor)
79
+
80
+ write_message("INFO", "Running #{colorize(display, :yellow, :bold)} #{local_target}", id)
81
+ write_message("DEBUG", "Command: #{colorize(display, :blue)}", id)
82
+
83
+ { id: id, started_at: started_at, redactor: redactor }
84
+ end
85
+
86
+ def command_output(context, _stream, data, redactor:)
87
+ raw = data.to_s
88
+ return if raw.empty?
89
+ return unless log_level?("DEBUG")
90
+
91
+ synchronize do
92
+ key = [context.fetch(:id), _stream]
93
+ @buffers[key] = "#{@buffers[key]}#{raw}"
94
+ flush_complete_output_lines(context, key, redactor: redactor)
95
+ end
96
+ end
97
+
98
+ def command_exit(context, status)
99
+ runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - context.fetch(:started_at)
100
+ result = status.to_i.zero? ? "successful" : "failed"
101
+ result_color = status.to_i.zero? ? :green : :red
102
+
103
+ synchronize do
104
+ flush_output_buffers(context)
105
+ write_message_unlocked("INFO", "Finished in #{format("%.3f seconds", runtime)} with exit status #{status} (#{colorize(result, result_color, :bold)}).", context.fetch(:id))
106
+ end
107
+ end
108
+
109
+ private
110
+ def write_message(level, message, id = nil)
111
+ return unless log_level?(level)
112
+
113
+ synchronize { write_message_unlocked(level, message, id) }
114
+ end
115
+
116
+ def write_message_unlocked(level, message, id = nil)
117
+ @io.puts(format_message(level, message, id)) if log_level?(level)
118
+ end
119
+
120
+ def synchronize(&block)
121
+ @mutex.synchronize(&block)
122
+ end
123
+
124
+ def flush_complete_output_lines(context, key, redactor:)
125
+ buffer = @buffers.fetch(key)
126
+ output = +""
127
+
128
+ while (index = buffer.index("\n"))
129
+ output << buffer.slice!(0..index)
130
+ end
131
+
132
+ @buffers[key] = buffer
133
+ write_output(context, output, redactor: redactor, stream: key.last) unless output.empty?
134
+ end
135
+
136
+ def flush_output_buffers(context)
137
+ id = context.fetch(:id)
138
+ keys = @buffers.keys.select { |key_id, _stream| key_id == id }
139
+
140
+ keys.each do |key|
141
+ output = @buffers.delete(key)
142
+ next if output.to_s.empty?
143
+
144
+ write_output(context, output, redactor: context.fetch(:redactor), stream: key.last)
145
+ end
146
+ end
147
+
148
+ def write_output(context, output, redactor:, stream: nil)
149
+ color = stream == :stderr ? :red : :green
150
+
151
+ redactor.redact_string(output).each_line do |line|
152
+ write_message_unlocked("DEBUG", colorize("\t#{line}".chomp, color), context.fetch(:id))
153
+ end
154
+ @io.flush if @io.respond_to?(:flush)
155
+ end
156
+
157
+ def format_message(level, message, id = nil)
158
+ message = "[#{colorize(id, :green)}] #{message}" if id
159
+ "#{colorize(level.rjust(6), LEVEL_COLORS.fetch(level))} #{message}"
160
+ end
161
+
162
+ def local_target
163
+ user = @env["USER"].to_s.empty? ? @env["USERNAME"].to_s : @env["USER"].to_s
164
+
165
+ if user.empty?
166
+ "on #{colorize("localhost", :blue)}"
167
+ else
168
+ "as #{colorize(user, :blue)}@#{colorize("localhost", :blue)}"
169
+ end
170
+ end
171
+
172
+ def log_level?(level)
173
+ LEVELS.fetch(level) >= @verbosity
174
+ end
175
+
176
+ def colorize(value, color, mode = nil)
177
+ string = value.to_s
178
+ return string unless colorize?
179
+ return string unless COLOR_CODES.key?(color)
180
+
181
+ prefix = mode == :bold ? "\e[1;" : "\e[0;"
182
+ "#{prefix}#{COLOR_CODES.fetch(color)};49m#{string}\e[0m"
183
+ end
184
+
185
+ def colorize?
186
+ @env["SSHKIT_COLOR"] || (@io.respond_to?(:tty?) && @io.tty?)
187
+ end
188
+ end
26
189
 
27
190
  class Command
28
191
  class << self
192
+ attr_accessor :output
193
+
194
+ def with_output(output)
195
+ previous_output = self.output
196
+ self.output = output
197
+ yield
198
+ ensure
199
+ self.output = previous_output
200
+ end
201
+
29
202
  def available?(name)
30
203
  ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
31
204
  path = File.join(dir, name)
@@ -33,9 +206,23 @@ module KamalBackup
33
206
  end
34
207
  end
35
208
 
36
- def capture(spec, input: nil, redactor:)
37
- stdout, stderr, status = Open3.capture3(spec.env, *spec.argv, stdin_data: input)
38
- result = CommandResult.new(stdout: stdout, stderr: stderr, status: status.exitstatus)
209
+ def capture(spec, input: nil, redactor:, log: true, log_output: true, tee_stdout: nil, tee_stderr: nil)
210
+ output = log ? self.output : nil
211
+ return capture_quietly(spec, input: input, redactor: redactor) unless output || tee_stdout || tee_stderr
212
+
213
+ context = output&.command_start(spec, redactor: redactor)
214
+ stdout, stderr, status = popen_capture(
215
+ spec,
216
+ input: input,
217
+ redactor: redactor,
218
+ output: output,
219
+ context: context,
220
+ log_output: log_output,
221
+ tee_stdout: tee_stdout,
222
+ tee_stderr: tee_stderr
223
+ )
224
+ output&.command_exit(context, status.exitstatus)
225
+ result = CommandResult.new(stdout: stdout, stderr: stderr, status: status.exitstatus, streamed: !!(tee_stdout || tee_stderr))
39
226
 
40
227
  if status.success?
41
228
  result
@@ -46,7 +233,72 @@ module KamalBackup
46
233
  raise command_not_found(spec, e)
47
234
  end
48
235
 
236
+ def collect_stream(io, command_output: self.output, context: nil, stream: :stdout, log_output: true, tee_io: nil, redactor: nil)
237
+ captured_output = +""
238
+ tee_buffer = +""
239
+
240
+ loop do
241
+ chunk = io.readpartial(16 * 1024)
242
+ captured_output << chunk
243
+ command_output&.command_output(context, stream, chunk, redactor: redactor) if log_output && context
244
+ tee_buffer = tee_stream(tee_io, redactor, tee_buffer, chunk) if tee_io
245
+ rescue EOFError
246
+ flush_tee_stream(tee_io, redactor, tee_buffer) if tee_io
247
+ break
248
+ end
249
+
250
+ captured_output
251
+ end
252
+
49
253
  private
254
+ def tee_stream(io, redactor, buffer, chunk)
255
+ buffer << chunk
256
+
257
+ while (index = buffer.index("\n"))
258
+ io.print(redactor.redact_string(buffer.slice!(0..index)))
259
+ end
260
+
261
+ io.flush if io.respond_to?(:flush)
262
+ buffer
263
+ end
264
+
265
+ def flush_tee_stream(io, redactor, buffer)
266
+ return if buffer.empty?
267
+
268
+ io.print(redactor.redact_string(buffer))
269
+ io.flush if io.respond_to?(:flush)
270
+ end
271
+
272
+ def capture_quietly(spec, input:, redactor:)
273
+ stdout, stderr, status = Open3.capture3(spec.env, *spec.argv, stdin_data: input)
274
+ result = CommandResult.new(stdout: stdout, stderr: stderr, status: status.exitstatus, streamed: false)
275
+
276
+ if status.success?
277
+ result
278
+ else
279
+ raise command_failure(spec, status.exitstatus, stdout, stderr, redactor)
280
+ end
281
+ end
282
+
283
+ def popen_capture(spec, input:, redactor:, output:, context:, log_output:, tee_stdout:, tee_stderr:)
284
+ Open3.popen3(spec.env, *spec.argv) do |stdin, stdout, stderr, wait_thread|
285
+ stdin_writer = Thread.new do
286
+ stdin.write(input) if input
287
+ ensure
288
+ stdin.close unless stdin.closed?
289
+ end
290
+ stdout_reader = Thread.new do
291
+ collect_stream(stdout, command_output: output, context: context, stream: :stdout, log_output: log_output, tee_io: tee_stdout, redactor: redactor)
292
+ end
293
+ stderr_reader = Thread.new do
294
+ collect_stream(stderr, command_output: output, context: context, stream: :stderr, log_output: log_output, tee_io: tee_stderr, redactor: redactor)
295
+ end
296
+
297
+ stdin_writer.join
298
+ [stdout_reader.value, stderr_reader.value, wait_thread.value]
299
+ end
300
+ end
301
+
50
302
  def command_failure(spec, status, stdout, stderr, redactor)
51
303
  CommandError.new(
52
304
  "command failed (#{status}): #{spec.display(redactor)}\n#{redactor.redact_string(stderr)}",
@@ -7,12 +7,14 @@ module KamalBackup
7
7
  DEFAULT_CONFIG_FILE = "config/deploy.yml"
8
8
  VERSION_LINE_PATTERN = /\A\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*\z/
9
9
 
10
- def initialize(redactor:, config_file: nil, destination: nil, env: ENV, cwd: Dir.pwd)
10
+ def initialize(redactor:, config_file: nil, destination: nil, env: ENV, cwd: Dir.pwd, stdout: $stdout, stderr: $stderr)
11
11
  @redactor = redactor
12
12
  @config_file = config_file
13
13
  @destination = destination
14
14
  @env = env
15
15
  @cwd = cwd
16
+ @stdout = stdout
17
+ @stderr = stderr
16
18
  end
17
19
 
18
20
  def accessory_name(preferred: nil)
@@ -50,8 +52,8 @@ module KamalBackup
50
52
  accessory_secret_placeholders(accessory_name).merge(accessory_clear_env(accessory_name))
51
53
  end
52
54
 
53
- def execute_on_accessory(accessory_name:, command:)
54
- capture_kamal(kamal_exec_argv(accessory_name, command))
55
+ def execute_on_accessory(accessory_name:, command:, stream: false)
56
+ capture_kamal(kamal_exec_argv(accessory_name, command), stream: stream)
55
57
  end
56
58
 
57
59
  def remote_version(accessory_name:)
@@ -212,13 +214,20 @@ module KamalBackup
212
214
  argv
213
215
  end
214
216
 
215
- def capture_kamal(argv)
217
+ def capture_kamal(argv, stream: false)
216
218
  spec = CommandSpec.new(argv: argv)
219
+ options = {
220
+ redactor: @redactor,
221
+ log: false,
222
+ log_output: false,
223
+ tee_stdout: stream ? @stdout : nil,
224
+ tee_stderr: stream ? @stderr : nil
225
+ }
217
226
 
218
227
  if defined?(Bundler)
219
- Bundler.with_unbundled_env { Command.capture(spec, redactor: @redactor) }
228
+ Bundler.with_unbundled_env { Command.capture(spec, **options) }
220
229
  else
221
- Command.capture(spec, redactor: @redactor)
230
+ Command.capture(spec, **options)
222
231
  end
223
232
  end
224
233
 
@@ -16,7 +16,7 @@ module KamalBackup
16
16
  end
17
17
 
18
18
  def ensure_repository
19
- run(%w[snapshots --json])
19
+ run(%w[snapshots --json], log_output: false)
20
20
  rescue CommandError => e
21
21
  if config.restic_init_if_missing?
22
22
  log("restic repository not ready, running restic init")
@@ -43,14 +43,17 @@ module KamalBackup
43
43
  log("backing up file content as #{filename}")
44
44
 
45
45
  File.open(path, "rb") do |file|
46
+ output = Command.output
47
+ context = output&.command_start(command, redactor: redactor)
46
48
  Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
47
- stdout_reader = Thread.new { stdout.read }
48
- stderr_reader = Thread.new { stderr.read }
49
+ stdout_reader = Thread.new { Command.collect_stream(stdout, command_output: output, context: context, stream: :stdout, redactor: redactor) }
50
+ stderr_reader = Thread.new { Command.collect_stream(stderr, command_output: output, context: context, stream: :stderr, redactor: redactor) }
49
51
  IO.copy_stream(file, stdin)
50
52
  stdin.close
51
53
  out = stdout_reader.value
52
54
  err = stderr_reader.value
53
55
  status = wait_thread.value
56
+ output&.command_exit(context, status.exitstatus)
54
57
  raise_command_error(command, status, out, err) unless status.success?
55
58
 
56
59
  CommandResult.new(stdout: out, stderr: err, status: status.exitstatus)
@@ -99,7 +102,7 @@ module KamalBackup
99
102
  end
100
103
 
101
104
  def snapshots_json(tags: common_tags)
102
- output = run(["snapshots", "--json"] + filter_tag_args(tags)).stdout
105
+ output = run(["snapshots", "--json"] + filter_tag_args(tags), log_output: false).stdout
103
106
  snapshots = JSON.parse(output)
104
107
  required_tags = tags.compact
105
108
  snapshots.select do |snapshot|
@@ -116,7 +119,7 @@ module KamalBackup
116
119
  end
117
120
 
118
121
  def ls_json(snapshot)
119
- output = run(["ls", "--json", snapshot]).stdout
122
+ output = run(["ls", "--json", snapshot], log_output: false).stdout
120
123
  output.lines.filter_map do |line|
121
124
  JSON.parse(line)
122
125
  rescue JSON::ParserError
@@ -153,12 +156,15 @@ module KamalBackup
153
156
  FileUtils.mkdir_p(File.dirname(target_path))
154
157
  temp_path = "#{target_path}.kamal-backup-#{$$}.tmp"
155
158
 
159
+ output = Command.output
160
+ context = output&.command_start(command, redactor: redactor)
156
161
  Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
157
162
  stdin.close
158
- stderr_reader = Thread.new { stderr.read }
163
+ stderr_reader = Thread.new { Command.collect_stream(stderr, command_output: output, context: context, stream: :stderr, redactor: redactor) }
159
164
  File.open(temp_path, "wb") { |file| IO.copy_stream(stdout, file) }
160
165
  err = stderr_reader.value
161
166
  status = wait_thread.value
167
+ output&.command_exit(context, status.exitstatus)
162
168
  raise_command_error(command, status, "", err) unless status.success?
163
169
  end
164
170
  File.rename(temp_path, target_path)
@@ -176,8 +182,12 @@ module KamalBackup
176
182
  run(["restore", snapshot, "--target", target])
177
183
  end
178
184
 
179
- def run(args)
180
- Command.capture(CommandSpec.new(argv: ["restic"] + args, env: restic_env), redactor: redactor)
185
+ def run(args, log_output: true)
186
+ Command.capture(
187
+ CommandSpec.new(argv: ["restic"] + args, env: restic_env),
188
+ redactor: redactor,
189
+ log_output: log_output
190
+ )
181
191
  end
182
192
 
183
193
  def common_tags
@@ -240,13 +250,16 @@ module KamalBackup
240
250
  end
241
251
 
242
252
  def pipe_commands(producer, consumer, producer_label:, consumer_label:)
253
+ output = Command.output
254
+ producer_context = output&.command_start(producer, redactor: redactor)
243
255
  Open3.popen3(producer.env, *producer.argv) do |producer_stdin, producer_stdout, producer_stderr, producer_wait|
244
256
  producer_stdin.close
245
257
 
258
+ consumer_context = output&.command_start(consumer, redactor: redactor)
246
259
  Open3.popen3(consumer.env, *consumer.argv) do |consumer_stdin, consumer_stdout, consumer_stderr, consumer_wait|
247
- producer_err_reader = Thread.new { producer_stderr.read }
248
- consumer_out_reader = Thread.new { consumer_stdout.read }
249
- consumer_err_reader = Thread.new { consumer_stderr.read }
260
+ producer_err_reader = Thread.new { Command.collect_stream(producer_stderr, command_output: output, context: producer_context, stream: :stderr, redactor: redactor) }
261
+ consumer_out_reader = Thread.new { Command.collect_stream(consumer_stdout, command_output: output, context: consumer_context, stream: :stdout, redactor: redactor) }
262
+ consumer_err_reader = Thread.new { Command.collect_stream(consumer_stderr, command_output: output, context: consumer_context, stream: :stderr, redactor: redactor) }
250
263
 
251
264
  copy_error = nil
252
265
  copy_thread = Thread.new do
@@ -260,6 +273,8 @@ module KamalBackup
260
273
  copy_thread.join
261
274
  producer_status = producer_wait.value
262
275
  consumer_status = consumer_wait.value
276
+ output&.command_exit(producer_context, producer_status.exitstatus)
277
+ output&.command_exit(consumer_context, consumer_status.exitstatus)
263
278
 
264
279
  producer_err = producer_err_reader.value
265
280
  consumer_out = consumer_out_reader.value
@@ -302,7 +317,11 @@ module KamalBackup
302
317
  end
303
318
 
304
319
  def log(message)
305
- $stdout.puts("[kamal-backup] #{redactor.redact_string(message)}")
320
+ if Command.output
321
+ Command.output.info(message, redactor: redactor)
322
+ else
323
+ $stdout.puts("[kamal-backup] #{redactor.redact_string(message)}")
324
+ end
306
325
  end
307
326
  end
308
327
  end
@@ -1,3 +1,3 @@
1
1
  module KamalBackup
2
- VERSION = "0.3.0.beta5"
2
+ VERSION = "0.3.0.beta7"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.beta5
4
+ version: 0.3.0.beta7
5
5
  platform: ruby
6
6
  authors:
7
7
  - crmne