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.
@@ -1,16 +1,18 @@
1
- require_relative "../command"
2
- require_relative "../errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../command'
4
+ require_relative '../errors'
3
5
 
4
6
  module KamalBackup
5
7
  module Databases
6
8
  class Base
7
9
  def self.build(config, redactor:)
8
10
  case config.database_adapter
9
- when "postgres"
11
+ when 'postgres'
10
12
  Postgres.new(config, redactor: redactor)
11
- when "mysql"
13
+ when 'mysql'
12
14
  Mysql.new(config, redactor: redactor)
13
- when "sqlite"
15
+ when 'sqlite'
14
16
  Sqlite.new(config, redactor: redactor)
15
17
  else
16
18
  raise ConfigurationError, "unsupported DATABASE_ADAPTER: #{config.database_adapter.inspect}"
@@ -42,13 +44,13 @@ module KamalBackup
42
44
  end
43
45
 
44
46
  def database_filename
45
- app = config.app_name.gsub(/[^A-Za-z0-9_.-]+/, "-")
46
- database = config.database_name.gsub(/[^A-Za-z0-9_.-]+/, "-")
47
+ app = config.app_name.gsub(/[^A-Za-z0-9_.-]+/, '-')
48
+ database = config.database_name.gsub(/[^A-Za-z0-9_.-]+/, '-')
47
49
  "databases/#{app}/#{database}/#{adapter_name}.#{dump_extension}"
48
50
  end
49
51
 
50
52
  def backup_tags
51
- ["type:database", "database:#{config.database_name}", "adapter:#{adapter_name}"]
53
+ ['type:database', "database:#{config.database_name}", "adapter:#{adapter_name}"]
52
54
  end
53
55
 
54
56
  def adapter_name
@@ -84,13 +86,14 @@ module KamalBackup
84
86
  end
85
87
 
86
88
  private
87
- def value(key)
88
- config.value(key)
89
- end
90
89
 
91
- def executable_available?(name)
92
- Command.available?(name)
93
- end
90
+ def value(key)
91
+ config.value(key)
92
+ end
93
+
94
+ def executable_available?(name)
95
+ Command.available?(name)
96
+ end
94
97
  end
95
98
  end
96
99
  end
@@ -1,26 +1,28 @@
1
- require "uri"
2
- require_relative "base"
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require_relative 'base'
3
5
 
4
6
  module KamalBackup
5
7
  module Databases
6
8
  class Mysql < Base
7
9
  def adapter_name
8
- "mysql"
10
+ 'mysql'
9
11
  end
10
12
 
11
13
  def dump_extension
12
- "sql"
14
+ 'sql'
13
15
  end
14
16
 
15
17
  def dump_command
16
18
  connection = current_connection
17
19
  argv = [
18
20
  dump_binary,
19
- "--single-transaction",
20
- "--quick",
21
- "--routines",
22
- "--triggers",
23
- "--events"
21
+ '--single-transaction',
22
+ '--quick',
23
+ '--routines',
24
+ '--triggers',
25
+ '--events'
24
26
  ] + connection_args(connection)
25
27
  argv << connection.fetch(:database)
26
28
  CommandSpec.new(argv: argv, env: password_env(connection))
@@ -42,80 +44,79 @@ module KamalBackup
42
44
 
43
45
  def current_target_identifier
44
46
  connection = current_connection
45
- [connection[:host], connection[:database]].compact.join("/")
47
+ [connection[:host], connection[:database]].compact.join('/')
46
48
  end
47
49
 
48
50
  def scratch_target_identifier(target)
49
- [current_connection[:host], target].compact.join("/")
51
+ [current_connection[:host], target].compact.join('/')
50
52
  end
51
53
 
52
54
  private
53
- def validate_scratch_restore_target(target)
54
- if current_connection.fetch(:database) == target
55
- raise ConfigurationError, "scratch database must differ from the current MySQL database"
56
- end
57
55
 
58
- super
59
- end
56
+ def validate_scratch_restore_target(target)
57
+ raise ConfigurationError, 'scratch database must differ from the current MySQL database' if current_connection.fetch(:database) == target
60
58
 
61
- def dump_binary
62
- value("MYSQL_DUMP_BIN") || (executable_available?("mariadb-dump") ? "mariadb-dump" : "mysqldump")
63
- end
59
+ super
60
+ end
64
61
 
65
- def client_binary
66
- value("MYSQL_CLIENT_BIN") || (executable_available?("mariadb") ? "mariadb" : "mysql")
67
- end
62
+ def dump_binary
63
+ value('MYSQL_DUMP_BIN') || (executable_available?('mariadb-dump') ? 'mariadb-dump' : 'mysqldump')
64
+ end
65
+
66
+ def client_binary
67
+ value('MYSQL_CLIENT_BIN') || (executable_available?('mariadb') ? 'mariadb' : 'mysql')
68
+ end
68
69
 
69
- def current_connection
70
- if value("DATABASE_URL")
71
- parse_url(value("DATABASE_URL")).tap do |connection|
72
- connection[:password] ||= value("MYSQL_PWD") || value("MYSQL_PASSWORD") || value("MARIADB_PASSWORD")
73
- end
74
- else
75
- connection_from_env("")
70
+ def current_connection
71
+ if value('DATABASE_URL')
72
+ parse_url(value('DATABASE_URL')).tap do |connection|
73
+ connection[:password] ||= value('MYSQL_PWD') || value('MYSQL_PASSWORD') || value('MARIADB_PASSWORD')
76
74
  end
75
+ else
76
+ connection_from_env('')
77
77
  end
78
+ end
78
79
 
79
- def connection_from_env(prefix)
80
- database = value("#{prefix}MYSQL_DATABASE") || value("#{prefix}MARIADB_DATABASE")
81
- raise ConfigurationError, "#{prefix}MYSQL_DATABASE or #{prefix}MARIADB_DATABASE is required" unless database
82
-
83
- {
84
- host: value("#{prefix}MYSQL_HOST") || value("#{prefix}MARIADB_HOST"),
85
- port: value("#{prefix}MYSQL_PORT") || value("#{prefix}MARIADB_PORT"),
86
- user: value("#{prefix}MYSQL_USER") || value("#{prefix}MARIADB_USER"),
87
- password: value("#{prefix}MYSQL_PWD") || value("#{prefix}MYSQL_PASSWORD") || value("#{prefix}MARIADB_PASSWORD"),
88
- database: database
89
- }
90
- end
80
+ def connection_from_env(prefix)
81
+ database = value("#{prefix}MYSQL_DATABASE") || value("#{prefix}MARIADB_DATABASE")
82
+ raise ConfigurationError, "#{prefix}MYSQL_DATABASE or #{prefix}MARIADB_DATABASE is required" unless database
83
+
84
+ {
85
+ host: value("#{prefix}MYSQL_HOST") || value("#{prefix}MARIADB_HOST"),
86
+ port: value("#{prefix}MYSQL_PORT") || value("#{prefix}MARIADB_PORT"),
87
+ user: value("#{prefix}MYSQL_USER") || value("#{prefix}MARIADB_USER"),
88
+ password: value("#{prefix}MYSQL_PWD") || value("#{prefix}MYSQL_PASSWORD") || value("#{prefix}MARIADB_PASSWORD"),
89
+ database: database
90
+ }
91
+ end
91
92
 
92
- def parse_url(url)
93
- uri = URI.parse(url)
94
- database = uri.path.to_s.sub(%r{\A/}, "")
95
- raise ConfigurationError, "database name is missing in #{uri.scheme} DATABASE_URL" if database.empty?
96
-
97
- {
98
- host: uri.host,
99
- port: uri.port,
100
- user: uri.user ? URI.decode_www_form_component(uri.user) : nil,
101
- password: uri.password ? URI.decode_www_form_component(uri.password) : nil,
102
- database: URI.decode_www_form_component(database)
103
- }
104
- rescue URI::InvalidURIError => e
105
- raise ConfigurationError, "invalid database URL: #{e.message}"
106
- end
93
+ def parse_url(url)
94
+ uri = URI.parse(url)
95
+ database = uri.path.to_s.sub(%r{\A/}, '')
96
+ raise ConfigurationError, "database name is missing in #{uri.scheme} DATABASE_URL" if database.empty?
97
+
98
+ {
99
+ host: uri.host,
100
+ port: uri.port,
101
+ user: uri.user ? URI.decode_www_form_component(uri.user) : nil,
102
+ password: uri.password ? URI.decode_www_form_component(uri.password) : nil,
103
+ database: URI.decode_www_form_component(database)
104
+ }
105
+ rescue URI::InvalidURIError => e
106
+ raise ConfigurationError, "invalid database URL: #{e.message}"
107
+ end
107
108
 
108
- def connection_args(connection)
109
- args = []
110
- args.concat(["--host", connection[:host]]) if connection[:host]
111
- args.concat(["--port", connection[:port].to_s]) if connection[:port]
112
- args.concat(["--user", connection[:user]]) if connection[:user]
113
- args
114
- end
109
+ def connection_args(connection)
110
+ args = []
111
+ args.concat(['--host', connection[:host]]) if connection[:host]
112
+ args.concat(['--port', connection[:port].to_s]) if connection[:port]
113
+ args.concat(['--user', connection[:user]]) if connection[:user]
114
+ args
115
+ end
115
116
 
116
- def password_env(connection)
117
- connection[:password] ? { "MYSQL_PWD" => connection[:password] } : {}
118
- end
117
+ def password_env(connection)
118
+ connection[:password] ? { 'MYSQL_PWD' => connection[:password] } : {}
119
+ end
119
120
  end
120
121
  end
121
122
  end
@@ -1,5 +1,7 @@
1
- require "uri"
2
- require_relative "base"
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require_relative 'base'
3
5
 
4
6
  module KamalBackup
5
7
  module Databases
@@ -20,11 +22,11 @@ module KamalBackup
20
22
  ].freeze
21
23
 
22
24
  def adapter_name
23
- "postgres"
25
+ 'postgres'
24
26
  end
25
27
 
26
28
  def dump_extension
27
- "pgdump"
29
+ 'pgdump'
28
30
  end
29
31
 
30
32
  def dump_command
@@ -34,7 +36,7 @@ module KamalBackup
34
36
 
35
37
  def current_restore_command
36
38
  connection = current_connection
37
- database = connection.fetch("PGDATABASE")
39
+ database = connection.fetch('PGDATABASE')
38
40
 
39
41
  argv = %w[pg_restore --clean --if-exists --no-owner --no-privileges --dbname]
40
42
  argv << database
@@ -42,7 +44,7 @@ module KamalBackup
42
44
  end
43
45
 
44
46
  def scratch_restore_command(target)
45
- connection = current_connection.merge("PGDATABASE" => target)
47
+ connection = current_connection.merge('PGDATABASE' => target)
46
48
 
47
49
  argv = %w[pg_restore --clean --if-exists --no-owner --no-privileges --dbname]
48
50
  argv << target
@@ -50,74 +52,73 @@ module KamalBackup
50
52
  end
51
53
 
52
54
  def current_target_identifier
53
- value("DATABASE_URL") || value("PGDATABASE")
55
+ value('DATABASE_URL') || value('PGDATABASE')
54
56
  end
55
57
 
56
58
  def scratch_target_identifier(target)
57
- [current_connection["PGHOST"], target].compact.join("/")
59
+ [current_connection['PGHOST'], target].compact.join('/')
58
60
  end
59
61
 
60
62
  private
61
- def validate_scratch_restore_target(target)
62
- if current_connection.fetch("PGDATABASE") == target
63
- raise ConfigurationError, "scratch database must differ from the current PostgreSQL database"
64
- end
65
63
 
66
- super
67
- end
64
+ def validate_scratch_restore_target(target)
65
+ raise ConfigurationError, 'scratch database must differ from the current PostgreSQL database' if current_connection.fetch('PGDATABASE') == target
68
66
 
69
- def current_connection
70
- if value("DATABASE_URL")
71
- connection_from_url(value("DATABASE_URL"), "DATABASE_URL").tap do |connection|
72
- connection["PGPASSWORD"] ||= value("PGPASSWORD") if value("PGPASSWORD")
73
- end
74
- else
75
- connection = prefixed_env("", SOURCE_ENV_KEYS)
76
- raise ConfigurationError, "DATABASE_URL or PGDATABASE is required for PostgreSQL restore" unless connection["PGDATABASE"]
67
+ super
68
+ end
77
69
 
78
- connection
70
+ def current_connection
71
+ if value('DATABASE_URL')
72
+ connection_from_url(value('DATABASE_URL'), 'DATABASE_URL').tap do |connection|
73
+ connection['PGPASSWORD'] ||= value('PGPASSWORD') if value('PGPASSWORD')
79
74
  end
80
- end
81
-
82
- def prefixed_env(prefix, keys)
83
- keys.each_with_object({}) do |key, env|
84
- env[key] = value("#{prefix}#{key}") if value("#{prefix}#{key}")
75
+ else
76
+ connection = prefixed_env('', SOURCE_ENV_KEYS)
77
+ unless connection['PGDATABASE']
78
+ raise ConfigurationError,
79
+ 'DATABASE_URL or PGDATABASE is required for PostgreSQL restore'
85
80
  end
86
- end
87
81
 
88
- def connection_from_url(url, name)
89
- uri = URI.parse(url)
90
- unless %w[postgres postgresql].include?(uri.scheme)
91
- raise ConfigurationError, "#{name} must use postgres:// or postgresql://"
92
- end
82
+ connection
83
+ end
84
+ end
93
85
 
94
- database = URI.decode_www_form_component(uri.path.to_s.sub(%r{\A/}, ""))
95
- raise ConfigurationError, "database name is missing in #{name}" if database.empty?
96
-
97
- env = {
98
- "PGHOST" => uri.host,
99
- "PGPORT" => uri.port&.to_s,
100
- "PGUSER" => uri.user ? URI.decode_www_form_component(uri.user) : nil,
101
- "PGPASSWORD" => uri.password ? URI.decode_www_form_component(uri.password) : nil,
102
- "PGDATABASE" => database
103
- }.compact
104
-
105
- query = URI.decode_www_form(uri.query.to_s).to_h
106
- {
107
- "sslmode" => "PGSSLMODE",
108
- "sslrootcert" => "PGSSLROOTCERT",
109
- "sslcert" => "PGSSLCERT",
110
- "sslkey" => "PGSSLKEY",
111
- "connect_timeout" => "PGCONNECT_TIMEOUT"
112
- }.each do |source, target|
113
- env[target] = query[source] if query[source]
114
- end
86
+ def prefixed_env(prefix, keys)
87
+ keys.each_with_object({}) do |key, env|
88
+ env[key] = value("#{prefix}#{key}") if value("#{prefix}#{key}")
89
+ end
90
+ end
115
91
 
116
- env
117
- rescue URI::InvalidURIError => e
118
- raise ConfigurationError, "invalid #{name}: #{e.message}"
92
+ def connection_from_url(url, name)
93
+ uri = URI.parse(url)
94
+ raise ConfigurationError, "#{name} must use postgres:// or postgresql://" unless %w[postgres postgresql].include?(uri.scheme)
95
+
96
+ database = URI.decode_www_form_component(uri.path.to_s.sub(%r{\A/}, ''))
97
+ raise ConfigurationError, "database name is missing in #{name}" if database.empty?
98
+
99
+ env = {
100
+ 'PGHOST' => uri.host,
101
+ 'PGPORT' => uri.port&.to_s,
102
+ 'PGUSER' => uri.user ? URI.decode_www_form_component(uri.user) : nil,
103
+ 'PGPASSWORD' => uri.password ? URI.decode_www_form_component(uri.password) : nil,
104
+ 'PGDATABASE' => database
105
+ }.compact
106
+
107
+ query = URI.decode_www_form(uri.query.to_s).to_h
108
+ {
109
+ 'sslmode' => 'PGSSLMODE',
110
+ 'sslrootcert' => 'PGSSLROOTCERT',
111
+ 'sslcert' => 'PGSSLCERT',
112
+ 'sslkey' => 'PGSSLKEY',
113
+ 'connect_timeout' => 'PGCONNECT_TIMEOUT'
114
+ }.each do |source, target|
115
+ env[target] = query[source] if query[source]
119
116
  end
120
117
 
118
+ env
119
+ rescue URI::InvalidURIError => e
120
+ raise ConfigurationError, "invalid #{name}: #{e.message}"
121
+ end
121
122
  end
122
123
  end
123
124
  end
@@ -1,24 +1,26 @@
1
- require "fileutils"
2
- require "tempfile"
3
- require_relative "base"
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+ require_relative 'base'
4
6
 
5
7
  module KamalBackup
6
8
  module Databases
7
9
  class Sqlite < Base
8
10
  def adapter_name
9
- "sqlite"
11
+ 'sqlite'
10
12
  end
11
13
 
12
14
  def dump_extension
13
- "sqlite3"
15
+ 'sqlite3'
14
16
  end
15
17
 
16
18
  def backup(restic)
17
19
  source = sqlite_source
18
- Tempfile.create(["kamal-backup-", ".sqlite3"]) do |tempfile|
20
+ Tempfile.create(['kamal-backup-', '.sqlite3']) do |tempfile|
19
21
  tempfile.close
20
22
  Command.capture(
21
- CommandSpec.new(argv: ["sqlite3", source, ".backup #{sqlite_literal(tempfile.path)}"]),
23
+ CommandSpec.new(argv: ['sqlite3', source, ".backup #{sqlite_literal(tempfile.path)}"]),
22
24
  redactor: redactor
23
25
  )
24
26
  restic.backup_file(
@@ -39,7 +41,7 @@ module KamalBackup
39
41
  end
40
42
 
41
43
  def dump_command
42
- raise NotImplementedError, "SQLite backup uses .backup into a temporary file"
44
+ raise NotImplementedError, 'SQLite backup uses .backup into a temporary file'
43
45
  end
44
46
 
45
47
  def current_target_identifier
@@ -51,21 +53,20 @@ module KamalBackup
51
53
  end
52
54
 
53
55
  private
54
- def sqlite_source
55
- config.required_value("SQLITE_DATABASE_PATH")
56
- end
57
56
 
58
- def validate_scratch_restore_target(target)
59
- if File.expand_path(sqlite_source) == File.expand_path(target)
60
- raise ConfigurationError, "scratch SQLite path must differ from SQLITE_DATABASE_PATH"
61
- end
57
+ def sqlite_source
58
+ config.required_value('SQLITE_DATABASE_PATH')
59
+ end
62
60
 
63
- super
64
- end
61
+ def validate_scratch_restore_target(target)
62
+ raise ConfigurationError, 'scratch SQLite path must differ from SQLITE_DATABASE_PATH' if File.expand_path(sqlite_source) == File.expand_path(target)
65
63
 
66
- def sqlite_literal(value)
67
- "'#{value.to_s.gsub("'", "''")}'"
68
- end
64
+ super
65
+ end
66
+
67
+ def sqlite_literal(value)
68
+ "'#{value.to_s.gsub("'", "''")}'"
69
+ end
69
70
  end
70
71
  end
71
72
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KamalBackup
2
4
  class Error < StandardError; end
3
5
  class ConfigurationError < Error; end
@@ -5,7 +7,7 @@ module KamalBackup
5
7
  class CommandError < Error
6
8
  attr_reader :command, :status, :stdout, :stderr
7
9
 
8
- def initialize(message, command:, status: nil, stdout: "", stderr: "")
10
+ def initialize(message, command:, status: nil, stdout: '', stderr: '')
9
11
  super(message)
10
12
  @command = command
11
13
  @status = status
@@ -1,8 +1,10 @@
1
- require "json"
2
- require "time"
3
- require_relative "command"
4
- require_relative "schema"
5
- require_relative "version"
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+ require_relative 'command'
6
+ require_relative 'schema'
7
+ require_relative 'version'
6
8
 
7
9
  module KamalBackup
8
10
  class Evidence
@@ -14,19 +16,21 @@ module KamalBackup
14
16
 
15
17
  def to_h
16
18
  Schema.record(
17
- kind: "evidence",
19
+ kind: 'evidence',
18
20
  app_name: @config.app_name,
19
21
  generated_at: Time.now.utc.iso8601,
20
22
  database_adapter: @config.database_adapter,
21
- databases: @config.databases.map { |database| { name: database.database_name, adapter: database.database_adapter } },
23
+ databases: @config.databases.map do |database|
24
+ { name: database.database_name, adapter: database.database_adapter }
25
+ end,
22
26
  restic_repository: @redactor.redact_string(@config.restic_repository.to_s),
23
27
  backup_paths: @config.backup_paths,
24
28
  paths: @config.backup_paths,
25
29
  forget_after_backup: @config.forget_after_backup?,
26
30
  retention: @config.retention,
27
- latest_database_backup: latest_snapshot_summary(["type:database"]),
31
+ latest_database_backup: latest_snapshot_summary(['type:database']),
28
32
  latest_database_backups: latest_database_backups,
29
- latest_file_backup: latest_snapshot_summary(["type:files"]),
33
+ latest_file_backup: latest_snapshot_summary(['type:files']),
30
34
  last_restic_check: last_check,
31
35
  last_restore_drill: last_restore_drill,
32
36
  image_version: VERSION,
@@ -39,67 +43,64 @@ module KamalBackup
39
43
  end
40
44
 
41
45
  private
42
- def latest_snapshot_summary(tags)
43
- snapshot = @restic.latest_snapshot(tags: tags)
44
46
 
45
- if snapshot
46
- {
47
- id: snapshot["short_id"] || snapshot["id"],
48
- time: snapshot["time"],
49
- tags: snapshot["tags"]
50
- }
51
- end
52
- rescue Error => e
53
- { error: @redactor.redact_string(e.message) }
54
- end
47
+ def latest_snapshot_summary(tags)
48
+ snapshot = @restic.latest_snapshot(tags: tags)
55
49
 
56
- def latest_database_backups
57
- @config.databases.each_with_object({}) do |database, backups|
58
- backups[database.database_name] = latest_snapshot_summary([
59
- "type:database",
60
- "database:#{database.database_name}",
61
- "adapter:#{database.database_adapter}"
62
- ])
63
- end
50
+ if snapshot
51
+ {
52
+ id: snapshot['short_id'] || snapshot['id'],
53
+ time: snapshot['time'],
54
+ tags: snapshot['tags']
55
+ }
64
56
  end
57
+ rescue Error => e
58
+ { error: @redactor.redact_string(e.message) }
59
+ end
65
60
 
66
- def last_check
67
- if File.file?(@config.last_check_path)
68
- JSON.parse(File.read(@config.last_check_path))
69
- end
70
- rescue JSON::ParserError, SystemCallError => e
71
- { error: @redactor.redact_string(e.message) }
61
+ def latest_database_backups
62
+ @config.databases.each_with_object({}) do |database, backups|
63
+ backups[database.database_name] = latest_snapshot_summary([
64
+ 'type:database',
65
+ "database:#{database.database_name}",
66
+ "adapter:#{database.database_adapter}"
67
+ ])
72
68
  end
69
+ end
73
70
 
74
- def last_restore_drill
75
- if File.file?(@config.last_restore_drill_path)
76
- JSON.parse(File.read(@config.last_restore_drill_path))
77
- end
78
- rescue JSON::ParserError, SystemCallError => e
79
- { error: @redactor.redact_string(e.message) }
80
- end
71
+ def last_check
72
+ JSON.parse(File.read(@config.last_check_path)) if File.file?(@config.last_check_path)
73
+ rescue JSON::ParserError, SystemCallError => e
74
+ { error: @redactor.redact_string(e.message) }
75
+ end
81
76
 
82
- def tool_versions
83
- {
84
- pg_dump: version_for(["pg_dump", "--version"]),
85
- pg_restore: version_for(["pg_restore", "--version"]),
86
- mysql_dump: version_for(["mariadb-dump", "--version"], ["mysqldump", "--version"]),
87
- mysql_client: version_for(["mariadb", "--version"], ["mysql", "--version"]),
88
- sqlite3: version_for(["sqlite3", "--version"]),
89
- restic: version_for(["restic", "version"])
90
- }
91
- end
77
+ def last_restore_drill
78
+ JSON.parse(File.read(@config.last_restore_drill_path)) if File.file?(@config.last_restore_drill_path)
79
+ rescue JSON::ParserError, SystemCallError => e
80
+ { error: @redactor.redact_string(e.message) }
81
+ end
92
82
 
93
- def version_for(*commands)
94
- commands.each do |argv|
95
- result = Command.capture(CommandSpec.new(argv: argv), redactor: @redactor)
96
- output = result.stdout.empty? ? result.stderr : result.stdout
97
- return @redactor.redact_string(output.strip)
98
- rescue CommandError
99
- next
100
- end
83
+ def tool_versions
84
+ {
85
+ pg_dump: version_for(['pg_dump', '--version']),
86
+ pg_restore: version_for(['pg_restore', '--version']),
87
+ mysql_dump: version_for(['mariadb-dump', '--version'], ['mysqldump', '--version']),
88
+ mysql_client: version_for(['mariadb', '--version'], ['mysql', '--version']),
89
+ sqlite3: version_for(['sqlite3', '--version']),
90
+ restic: version_for(%w[restic version])
91
+ }
92
+ end
101
93
 
102
- "unavailable"
94
+ def version_for(*commands)
95
+ commands.each do |argv|
96
+ result = Command.capture(CommandSpec.new(argv: argv), redactor: @redactor)
97
+ output = result.stdout.empty? ? result.stderr : result.stdout
98
+ return @redactor.redact_string(output.strip)
99
+ rescue CommandError
100
+ next
103
101
  end
102
+
103
+ 'unavailable'
104
+ end
104
105
  end
105
106
  end