kamal-backup 0.1.0.pre.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +345 -0
- data/exe/kamal-backup +7 -0
- data/lib/kamal_backup/cli.rb +156 -0
- data/lib/kamal_backup/command.rb +49 -0
- data/lib/kamal_backup/config.rb +273 -0
- data/lib/kamal_backup/databases/base.rb +74 -0
- data/lib/kamal_backup/databases/mysql.rb +109 -0
- data/lib/kamal_backup/databases/postgres.rb +125 -0
- data/lib/kamal_backup/databases/sqlite.rb +73 -0
- data/lib/kamal_backup/errors.rb +16 -0
- data/lib/kamal_backup/evidence.rb +80 -0
- data/lib/kamal_backup/redactor.rb +53 -0
- data/lib/kamal_backup/restic.rb +239 -0
- data/lib/kamal_backup/scheduler.rb +50 -0
- data/lib/kamal_backup/version.rb +3 -0
- data/lib/kamal_backup.rb +13 -0
- metadata +68 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "time"
|
|
3
|
+
require_relative "command"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
|
|
6
|
+
module KamalBackup
|
|
7
|
+
class Evidence
|
|
8
|
+
def initialize(config, restic:, redactor:)
|
|
9
|
+
@config = config
|
|
10
|
+
@restic = restic
|
|
11
|
+
@redactor = redactor
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
{
|
|
16
|
+
app_name: @config.app_name,
|
|
17
|
+
generated_at: Time.now.utc.iso8601,
|
|
18
|
+
database_adapter: @config.database_adapter,
|
|
19
|
+
restic_repository: @redactor.redact_string(@config.restic_repository.to_s),
|
|
20
|
+
backup_paths: @config.backup_paths,
|
|
21
|
+
forget_after_backup: @config.forget_after_backup?,
|
|
22
|
+
retention: @config.retention,
|
|
23
|
+
latest_database_backup: latest_snapshot_summary(["type:database"]),
|
|
24
|
+
latest_file_backup: latest_snapshot_summary(["type:files"]),
|
|
25
|
+
last_restic_check: last_check,
|
|
26
|
+
image_version: ENV.fetch("KAMAL_BACKUP_IMAGE_VERSION", VERSION),
|
|
27
|
+
tool_versions: tool_versions
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_json(*args)
|
|
32
|
+
JSON.pretty_generate(to_h, *args)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def latest_snapshot_summary(tags)
|
|
38
|
+
snapshot = @restic.latest_snapshot(tags: tags)
|
|
39
|
+
return nil unless snapshot
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
id: snapshot["short_id"] || snapshot["id"],
|
|
43
|
+
time: snapshot["time"],
|
|
44
|
+
tags: snapshot["tags"]
|
|
45
|
+
}
|
|
46
|
+
rescue Error => e
|
|
47
|
+
{ error: @redactor.redact_string(e.message) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def last_check
|
|
51
|
+
return nil unless File.file?(@config.last_check_path)
|
|
52
|
+
|
|
53
|
+
JSON.parse(File.read(@config.last_check_path))
|
|
54
|
+
rescue JSON::ParserError, SystemCallError => e
|
|
55
|
+
{ error: @redactor.redact_string(e.message) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def tool_versions
|
|
59
|
+
{
|
|
60
|
+
pg_dump: version_for(["pg_dump", "--version"]),
|
|
61
|
+
pg_restore: version_for(["pg_restore", "--version"]),
|
|
62
|
+
mysql_dump: version_for(["mariadb-dump", "--version"], ["mysqldump", "--version"]),
|
|
63
|
+
mysql_client: version_for(["mariadb", "--version"], ["mysql", "--version"]),
|
|
64
|
+
sqlite3: version_for(["sqlite3", "--version"]),
|
|
65
|
+
restic: version_for(["restic", "version"])
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def version_for(*commands)
|
|
70
|
+
commands.each do |argv|
|
|
71
|
+
result = Command.capture(CommandSpec.new(argv: argv), redactor: @redactor)
|
|
72
|
+
output = result.stdout.empty? ? result.stderr : result.stdout
|
|
73
|
+
return @redactor.redact_string(output.strip)
|
|
74
|
+
rescue CommandError
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
"unavailable"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module KamalBackup
|
|
4
|
+
class Redactor
|
|
5
|
+
SECRET_KEY_PATTERN = /(pass|password|secret|token|key|credential|authorization)/i
|
|
6
|
+
REDACTED = "[REDACTED]"
|
|
7
|
+
|
|
8
|
+
def initialize(secret_values: [], env: ENV)
|
|
9
|
+
@secret_values = Array(secret_values).compact.map(&:to_s).reject { |value| value.empty? || value.length < 4 }
|
|
10
|
+
@env = env
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def redact_hash(hash)
|
|
14
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
15
|
+
result[key] = redact_value(key, value)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def redact_value(key, value)
|
|
20
|
+
return nil if value.nil?
|
|
21
|
+
return REDACTED if key.to_s.match?(SECRET_KEY_PATTERN)
|
|
22
|
+
|
|
23
|
+
redact_string(value.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def redact_string(value)
|
|
27
|
+
redacted = redact_url_credentials(value.to_s)
|
|
28
|
+
known_secret_values.each do |secret|
|
|
29
|
+
redacted = redacted.gsub(secret, REDACTED)
|
|
30
|
+
end
|
|
31
|
+
redacted
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def known_secret_values
|
|
37
|
+
@known_secret_values ||= begin
|
|
38
|
+
env_secrets = @env.each_with_object([]) do |(key, value), values|
|
|
39
|
+
values << value.to_s if key.to_s.match?(SECRET_KEY_PATTERN)
|
|
40
|
+
end
|
|
41
|
+
(@secret_values + env_secrets).compact.uniq.reject { |value| value.empty? || value.length < 4 }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def redact_url_credentials(value)
|
|
46
|
+
value.gsub(%r{(://)([^/\s:@]+)(?::([^/\s@]*))?@}) do
|
|
47
|
+
"#{$1}#{REDACTED}@"
|
|
48
|
+
end.gsub(/([?&](?:password|token|secret|key|access_key_id|secret_access_key)=)[^&\s]+/i) do
|
|
49
|
+
"#{$1}#{REDACTED}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "json"
|
|
3
|
+
require "open3"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "command"
|
|
6
|
+
|
|
7
|
+
module KamalBackup
|
|
8
|
+
class Restic
|
|
9
|
+
attr_reader :config, :redactor
|
|
10
|
+
|
|
11
|
+
def initialize(config, redactor:)
|
|
12
|
+
@config = config
|
|
13
|
+
@redactor = redactor
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ensure_repository!
|
|
17
|
+
run(%w[snapshots --json])
|
|
18
|
+
rescue CommandError => e
|
|
19
|
+
raise e unless config.restic_init_if_missing?
|
|
20
|
+
|
|
21
|
+
log("restic repository not ready, running restic init")
|
|
22
|
+
run(%w[init])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def backup_command_output(command, filename:, tags:)
|
|
26
|
+
restic_command = CommandSpec.new(argv: ["restic", "backup", "--stdin", "--stdin-filename", filename] + tag_args(common_tags + tags))
|
|
27
|
+
log("backing up stream as #{filename}")
|
|
28
|
+
pipe_commands(command, restic_command, producer_label: "dump", consumer_label: "restic backup")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def backup_file_content(path, filename:, tags:)
|
|
32
|
+
command = CommandSpec.new(argv: ["restic", "backup", "--stdin", "--stdin-filename", filename] + tag_args(common_tags + tags))
|
|
33
|
+
log("backing up file content as #{filename}")
|
|
34
|
+
|
|
35
|
+
File.open(path, "rb") do |file|
|
|
36
|
+
Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
|
|
37
|
+
stdout_reader = Thread.new { stdout.read }
|
|
38
|
+
stderr_reader = Thread.new { stderr.read }
|
|
39
|
+
IO.copy_stream(file, stdin)
|
|
40
|
+
stdin.close
|
|
41
|
+
out = stdout_reader.value
|
|
42
|
+
err = stderr_reader.value
|
|
43
|
+
status = wait_thread.value
|
|
44
|
+
raise_command_error(command, status, out, err) unless status.success?
|
|
45
|
+
|
|
46
|
+
CommandResult.new(stdout: out, stderr: err, status: status.exitstatus)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
rescue Errno::ENOENT => e
|
|
50
|
+
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127, stderr: e.message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def backup_paths(paths, tags:)
|
|
54
|
+
paths = Array(paths).compact.map(&:to_s).reject(&:empty?)
|
|
55
|
+
return if paths.empty?
|
|
56
|
+
|
|
57
|
+
path_tags = paths.map { |path| "path:#{config.backup_path_label(path)}" }
|
|
58
|
+
log("backing up #{paths.size} file path(s): #{paths.join(", ")}")
|
|
59
|
+
run(["backup"] + paths + tag_args(common_tags + tags + path_tags))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def backup_path(path, tags:)
|
|
63
|
+
backup_paths([path], tags: tags)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def forget_after_success!
|
|
67
|
+
args = ["forget", "--prune"] + config.retention_args + tag_args(common_tags)
|
|
68
|
+
log("running restic forget/prune with retention policy")
|
|
69
|
+
run(args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def check!
|
|
73
|
+
args = %w[check]
|
|
74
|
+
args.concat(["--read-data-subset", config.check_read_data_subset]) if config.check_read_data_subset
|
|
75
|
+
started_at = Time.now.utc
|
|
76
|
+
result = run(args)
|
|
77
|
+
write_last_check(status: "ok", started_at: started_at, finished_at: Time.now.utc, output: result.stdout)
|
|
78
|
+
result
|
|
79
|
+
rescue CommandError => e
|
|
80
|
+
write_last_check(status: "failed", started_at: started_at || Time.now.utc, finished_at: Time.now.utc, error: e.message)
|
|
81
|
+
raise
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def snapshots(tags: common_tags)
|
|
85
|
+
run(["snapshots"] + tag_args(tags))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def snapshots_json(tags: common_tags)
|
|
89
|
+
output = run(["snapshots", "--json"] + tag_args(tags)).stdout
|
|
90
|
+
snapshots = JSON.parse(output)
|
|
91
|
+
required_tags = tags.compact
|
|
92
|
+
snapshots.select do |snapshot|
|
|
93
|
+
snapshot_tags = Array(snapshot["tags"])
|
|
94
|
+
required_tags.all? { |tag| snapshot_tags.include?(tag) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def latest_snapshot(tags:)
|
|
99
|
+
snapshots = snapshots_json(tags: common_tags + tags)
|
|
100
|
+
snapshots.max_by { |snapshot| Time.parse(snapshot.fetch("time")) }
|
|
101
|
+
rescue JSON::ParserError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ls_json(snapshot)
|
|
106
|
+
output = run(["ls", "--json", snapshot]).stdout
|
|
107
|
+
output.lines.filter_map do |line|
|
|
108
|
+
JSON.parse(line)
|
|
109
|
+
rescue JSON::ParserError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def database_file(snapshot, adapter)
|
|
115
|
+
legacy_prefix = "databases/#{config.app_name}/#{adapter}/"
|
|
116
|
+
flat_prefix = "databases-#{config.app_name.gsub(/[^A-Za-z0-9_.-]+/, "-")}-#{adapter}-"
|
|
117
|
+
ls_json(snapshot).find do |entry|
|
|
118
|
+
next false unless entry["type"] == "file"
|
|
119
|
+
|
|
120
|
+
normalized = entry["path"].to_s.sub(%r{\A/+}, "")
|
|
121
|
+
normalized.start_with?(legacy_prefix) || File.basename(normalized).start_with?(flat_prefix)
|
|
122
|
+
end&.fetch("path")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def dump_file_to_command(snapshot, filename, command)
|
|
126
|
+
restic_command = CommandSpec.new(argv: ["restic", "dump", snapshot, filename])
|
|
127
|
+
pipe_commands(restic_command, command, producer_label: "restic dump", consumer_label: command.argv.first)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def dump_file_to_path(snapshot, filename, target_path)
|
|
131
|
+
command = CommandSpec.new(argv: ["restic", "dump", snapshot, filename])
|
|
132
|
+
target_path = File.expand_path(target_path)
|
|
133
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
|
134
|
+
temp_path = "#{target_path}.kamal-backup-#{$$}.tmp"
|
|
135
|
+
|
|
136
|
+
Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
|
|
137
|
+
stdin.close
|
|
138
|
+
stderr_reader = Thread.new { stderr.read }
|
|
139
|
+
File.open(temp_path, "wb") { |file| IO.copy_stream(stdout, file) }
|
|
140
|
+
err = stderr_reader.value
|
|
141
|
+
status = wait_thread.value
|
|
142
|
+
raise_command_error(command, status, "", err) unless status.success?
|
|
143
|
+
end
|
|
144
|
+
File.rename(temp_path, target_path)
|
|
145
|
+
target_path
|
|
146
|
+
rescue Errno::ENOENT => e
|
|
147
|
+
FileUtils.rm_f(temp_path) if temp_path
|
|
148
|
+
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127, stderr: e.message)
|
|
149
|
+
rescue StandardError
|
|
150
|
+
FileUtils.rm_f(temp_path) if temp_path
|
|
151
|
+
raise
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def restore_snapshot(snapshot, target)
|
|
155
|
+
log("restoring file snapshot #{snapshot} to #{target}")
|
|
156
|
+
run(["restore", snapshot, "--target", target])
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def run(args)
|
|
160
|
+
Command.capture(CommandSpec.new(argv: ["restic"] + args), redactor: redactor)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def common_tags
|
|
164
|
+
["kamal-backup", "app:#{config.app_name}"]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def tag_args(tags)
|
|
170
|
+
tags.compact.each_with_object([]) { |tag, args| args.concat(["--tag", tag]) }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def pipe_commands(producer, consumer, producer_label:, consumer_label:)
|
|
174
|
+
Open3.popen3(producer.env, *producer.argv) do |producer_stdin, producer_stdout, producer_stderr, producer_wait|
|
|
175
|
+
producer_stdin.close
|
|
176
|
+
|
|
177
|
+
Open3.popen3(consumer.env, *consumer.argv) do |consumer_stdin, consumer_stdout, consumer_stderr, consumer_wait|
|
|
178
|
+
producer_err_reader = Thread.new { producer_stderr.read }
|
|
179
|
+
consumer_out_reader = Thread.new { consumer_stdout.read }
|
|
180
|
+
consumer_err_reader = Thread.new { consumer_stderr.read }
|
|
181
|
+
|
|
182
|
+
copy_error = nil
|
|
183
|
+
copy_thread = Thread.new do
|
|
184
|
+
IO.copy_stream(producer_stdout, consumer_stdin)
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
copy_error = e
|
|
187
|
+
ensure
|
|
188
|
+
consumer_stdin.close unless consumer_stdin.closed?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
copy_thread.join
|
|
192
|
+
producer_status = producer_wait.value
|
|
193
|
+
consumer_status = consumer_wait.value
|
|
194
|
+
|
|
195
|
+
producer_err = producer_err_reader.value
|
|
196
|
+
consumer_out = consumer_out_reader.value
|
|
197
|
+
consumer_err = consumer_err_reader.value
|
|
198
|
+
|
|
199
|
+
if copy_error
|
|
200
|
+
raise CommandError.new(
|
|
201
|
+
"failed to pipe #{producer_label} to #{consumer_label}: #{copy_error.message}",
|
|
202
|
+
command: consumer,
|
|
203
|
+
stderr: copy_error.message
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
raise_command_error(producer, producer_status, "", producer_err) unless producer_status.success?
|
|
208
|
+
raise_command_error(consumer, consumer_status, consumer_out, consumer_err) unless consumer_status.success?
|
|
209
|
+
|
|
210
|
+
CommandResult.new(stdout: consumer_out, stderr: consumer_err, status: consumer_status.exitstatus)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
rescue Errno::ENOENT => e
|
|
214
|
+
command = e.message.include?(producer.argv.first) ? producer : consumer
|
|
215
|
+
raise CommandError.new("command not found: #{command.argv.first}", command: command, status: 127, stderr: e.message)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def raise_command_error(command, status, stdout, stderr)
|
|
219
|
+
raise CommandError.new(
|
|
220
|
+
"command failed (#{status.exitstatus}): #{command.display(redactor)}\n#{redactor.redact_string(stderr)}",
|
|
221
|
+
command: command,
|
|
222
|
+
status: status.exitstatus,
|
|
223
|
+
stdout: redactor.redact_string(stdout),
|
|
224
|
+
stderr: redactor.redact_string(stderr)
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def write_last_check(payload)
|
|
229
|
+
FileUtils.mkdir_p(config.state_dir)
|
|
230
|
+
File.write(config.last_check_path, JSON.pretty_generate(payload.transform_values { |value| value.respond_to?(:iso8601) ? value.iso8601 : redactor.redact_string(value.to_s) }))
|
|
231
|
+
rescue SystemCallError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def log(message)
|
|
236
|
+
$stdout.puts("[kamal-backup] #{redactor.redact_string(message)}")
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module KamalBackup
|
|
4
|
+
class Scheduler
|
|
5
|
+
def initialize(config, &backup_block)
|
|
6
|
+
@config = config
|
|
7
|
+
@backup_block = backup_block
|
|
8
|
+
@stop = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
install_signal_handlers
|
|
13
|
+
sleep_interruptibly(@config.backup_start_delay_seconds)
|
|
14
|
+
|
|
15
|
+
until @stop
|
|
16
|
+
started_at = Time.now.utc
|
|
17
|
+
log("backup started at #{started_at.iso8601}")
|
|
18
|
+
begin
|
|
19
|
+
@backup_block.call
|
|
20
|
+
log("backup completed at #{Time.now.utc.iso8601}")
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
warn("[kamal-backup] backup failed at #{Time.now.utc.iso8601}: #{e.class}: #{e.message}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sleep_interruptibly(@config.backup_schedule_seconds)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
log("scheduler stopped at #{Time.now.utc.iso8601}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def install_signal_handlers
|
|
34
|
+
%w[TERM INT].each do |signal|
|
|
35
|
+
Signal.trap(signal) { @stop = true }
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def sleep_interruptibly(seconds)
|
|
42
|
+
deadline = Time.now + seconds
|
|
43
|
+
sleep([deadline - Time.now, 1].min) while !@stop && Time.now < deadline
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def log(message)
|
|
47
|
+
$stdout.puts("[kamal-backup] #{message}")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/kamal_backup.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative "kamal_backup/version"
|
|
2
|
+
require_relative "kamal_backup/errors"
|
|
3
|
+
require_relative "kamal_backup/command"
|
|
4
|
+
require_relative "kamal_backup/redactor"
|
|
5
|
+
require_relative "kamal_backup/config"
|
|
6
|
+
require_relative "kamal_backup/restic"
|
|
7
|
+
require_relative "kamal_backup/evidence"
|
|
8
|
+
require_relative "kamal_backup/scheduler"
|
|
9
|
+
require_relative "kamal_backup/databases/base"
|
|
10
|
+
require_relative "kamal_backup/databases/postgres"
|
|
11
|
+
require_relative "kamal_backup/databases/mysql"
|
|
12
|
+
require_relative "kamal_backup/databases/sqlite"
|
|
13
|
+
require_relative "kamal_backup/cli"
|
metadata
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: kamal-backup
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.pre.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Carmine Paolino
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A small CLI and Docker image for encrypted, verifiable Kamal accessory
|
|
14
|
+
backups using restic.
|
|
15
|
+
email:
|
|
16
|
+
- carmine@paolino.me
|
|
17
|
+
executables:
|
|
18
|
+
- kamal-backup
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- exe/kamal-backup
|
|
25
|
+
- lib/kamal_backup.rb
|
|
26
|
+
- lib/kamal_backup/cli.rb
|
|
27
|
+
- lib/kamal_backup/command.rb
|
|
28
|
+
- lib/kamal_backup/config.rb
|
|
29
|
+
- lib/kamal_backup/databases/base.rb
|
|
30
|
+
- lib/kamal_backup/databases/mysql.rb
|
|
31
|
+
- lib/kamal_backup/databases/postgres.rb
|
|
32
|
+
- lib/kamal_backup/databases/sqlite.rb
|
|
33
|
+
- lib/kamal_backup/errors.rb
|
|
34
|
+
- lib/kamal_backup/evidence.rb
|
|
35
|
+
- lib/kamal_backup/redactor.rb
|
|
36
|
+
- lib/kamal_backup/restic.rb
|
|
37
|
+
- lib/kamal_backup/scheduler.rb
|
|
38
|
+
- lib/kamal_backup/version.rb
|
|
39
|
+
homepage: https://kamal-backup.dev
|
|
40
|
+
licenses:
|
|
41
|
+
- MIT
|
|
42
|
+
metadata:
|
|
43
|
+
homepage_uri: https://kamal-backup.dev
|
|
44
|
+
source_code_uri: https://github.com/crmne/kamal-backup
|
|
45
|
+
changelog_uri: https://github.com/crmne/kamal-backup/releases
|
|
46
|
+
bug_tracker_uri: https://github.com/crmne/kamal-backup/issues
|
|
47
|
+
funding_uri: https://github.com/sponsors/crmne
|
|
48
|
+
rubygems_mfa_required: 'true'
|
|
49
|
+
post_install_message:
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '3.1'
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
requirements: []
|
|
64
|
+
rubygems_version: 3.5.22
|
|
65
|
+
signing_key:
|
|
66
|
+
specification_version: 4
|
|
67
|
+
summary: Kamal-first restic backups for databases and mounted application files.
|
|
68
|
+
test_files: []
|