kamal-backup 0.2.7 → 0.2.8
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 +2 -0
- data/lib/kamal_backup/cli.rb +40 -2
- data/lib/kamal_backup/config.rb +6 -11
- data/lib/kamal_backup/databases/sqlite.rb +4 -36
- data/lib/kamal_backup/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3bd8ce5d3434c45246641943cdcd4eeeab4bda3caf5fec309f9b65efbd3a21a
|
|
4
|
+
data.tar.gz: 3bf191da068b6c64540d98d786c49772f451085b08dce8f5058c1923818a3a91
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a74bcf4abc214cbc22f4c9aed16c067b8e928c8d666ba3e73155c52ac8ef0b3ec3be0b5f68b01f258025b6cfada5629562de372e3a46f1f8321c395ef92d3aee
|
|
7
|
+
data.tar.gz: 0bc5fc2dd8e7e103cb93f0e4fa576ae9a7e643f4084edbec2b644ae9c8a47ba5917014f7b6ba0e1ae261a98c3b574b75f789985710eb27bcb912ea87d84d6d6f
|
data/README.md
CHANGED
data/lib/kamal_backup/cli.rb
CHANGED
|
@@ -147,6 +147,43 @@ module KamalBackup
|
|
|
147
147
|
end
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
def confirm_production_restore!(snapshot)
|
|
151
|
+
return if options[:"confirm-production-restore"]
|
|
152
|
+
|
|
153
|
+
if options[:yes]
|
|
154
|
+
raise ConfigurationError, "--yes does not bypass restore production; use --confirm-production-restore only for deliberate automation"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
unless $stdin.tty?
|
|
158
|
+
raise ConfigurationError, "production restore confirmation required; rerun interactively or pass --confirm-production-restore only for deliberate automation"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
app_name = production_restore_confirmation_config.required_app_name
|
|
162
|
+
say "This will overwrite the production database and file paths for #{app_name} from backup #{snapshot}.", :red
|
|
163
|
+
require_typed_confirmation("Type the app name to continue", app_name)
|
|
164
|
+
require_typed_confirmation("Type RESTORE PRODUCTION to continue", "RESTORE PRODUCTION")
|
|
165
|
+
confirm!("Restore #{snapshot} into production now? This will overwrite production data.")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def require_typed_confirmation(prompt, expected)
|
|
169
|
+
answer = ask("#{prompt}:").to_s.strip
|
|
170
|
+
return if answer == expected
|
|
171
|
+
|
|
172
|
+
raise ConfigurationError, "aborted"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def production_restore_confirmation_config
|
|
176
|
+
if deployment_mode?
|
|
177
|
+
Config.new(
|
|
178
|
+
env: bridge.accessory_environment(accessory_name: accessory_name),
|
|
179
|
+
config_paths: [Config::SHARED_CONFIG_PATH],
|
|
180
|
+
load_project_defaults: false
|
|
181
|
+
)
|
|
182
|
+
else
|
|
183
|
+
direct_app.config
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
150
187
|
def prompt_required(label)
|
|
151
188
|
unless $stdin.tty?
|
|
152
189
|
raise ConfigurationError, "#{label.downcase} is required; pass it on the command line"
|
|
@@ -234,12 +271,13 @@ module KamalBackup
|
|
|
234
271
|
puts(JSON.pretty_generate(local_app.restore_to_local_machine(snapshot)))
|
|
235
272
|
end
|
|
236
273
|
|
|
274
|
+
method_option :"confirm-production-restore", type: :boolean, default: false, desc: "Confirm production restore without interactive prompts"
|
|
237
275
|
desc "production [SNAPSHOT]", "Restore the backup into the production database and Active Storage path"
|
|
238
276
|
def production(snapshot = "latest")
|
|
239
|
-
|
|
277
|
+
confirm_production_restore!(snapshot)
|
|
240
278
|
|
|
241
279
|
if deployment_mode?
|
|
242
|
-
exec_remote(["kamal-backup", "restore", "production", snapshot, "--
|
|
280
|
+
exec_remote(["kamal-backup", "restore", "production", snapshot, "--confirm-production-restore"])
|
|
243
281
|
else
|
|
244
282
|
puts(JSON.pretty_generate(direct_app.restore_to_production(snapshot)))
|
|
245
283
|
end
|
data/lib/kamal_backup/config.rb
CHANGED
|
@@ -42,7 +42,6 @@ module KamalBackup
|
|
|
42
42
|
"backup_schedule_seconds" => "BACKUP_SCHEDULE_SECONDS",
|
|
43
43
|
"backup_start_delay_seconds" => "BACKUP_START_DELAY_SECONDS",
|
|
44
44
|
"state_dir" => "KAMAL_BACKUP_STATE_DIR",
|
|
45
|
-
"allow_production_restore" => "KAMAL_BACKUP_ALLOW_PRODUCTION_RESTORE",
|
|
46
45
|
"allow_suspicious_paths" => "KAMAL_BACKUP_ALLOW_SUSPICIOUS_PATHS",
|
|
47
46
|
"pgpassword" => "PGPASSWORD",
|
|
48
47
|
"mysql_pwd" => "MYSQL_PWD"
|
|
@@ -104,10 +103,6 @@ module KamalBackup
|
|
|
104
103
|
value("RESTIC_CHECK_READ_DATA_SUBSET")
|
|
105
104
|
end
|
|
106
105
|
|
|
107
|
-
def allow_production_restore?
|
|
108
|
-
truthy?("KAMAL_BACKUP_ALLOW_PRODUCTION_RESTORE")
|
|
109
|
-
end
|
|
110
|
-
|
|
111
106
|
def allow_in_place_file_restore?
|
|
112
107
|
truthy?("KAMAL_BACKUP_ALLOW_IN_PLACE_FILE_RESTORE")
|
|
113
108
|
end
|
|
@@ -245,8 +240,8 @@ module KamalBackup
|
|
|
245
240
|
def validate_local_database_restore_target(target)
|
|
246
241
|
raise ConfigurationError, "local restore database target is required" if target.to_s.strip.empty?
|
|
247
242
|
|
|
248
|
-
if production_named_target?(target)
|
|
249
|
-
raise ConfigurationError, "refusing production-looking local restore target #{target};
|
|
243
|
+
if production_named_target?(target)
|
|
244
|
+
raise ConfigurationError, "refusing production-looking local restore target #{target}; use restore production for production restores"
|
|
250
245
|
end
|
|
251
246
|
end
|
|
252
247
|
|
|
@@ -266,8 +261,8 @@ module KamalBackup
|
|
|
266
261
|
def validate_database_restore_target(target)
|
|
267
262
|
raise ConfigurationError, "restore database target is required" if target.to_s.strip.empty?
|
|
268
263
|
|
|
269
|
-
if production_like_target?(target)
|
|
270
|
-
raise ConfigurationError, "refusing production-looking restore target #{target};
|
|
264
|
+
if production_like_target?(target)
|
|
265
|
+
raise ConfigurationError, "refusing production-looking restore target #{target}; choose a scratch target that does not look like production"
|
|
271
266
|
end
|
|
272
267
|
end
|
|
273
268
|
|
|
@@ -384,8 +379,8 @@ module KamalBackup
|
|
|
384
379
|
if environment = local_restore_environment
|
|
385
380
|
key, value = environment
|
|
386
381
|
|
|
387
|
-
if production_environment?(value)
|
|
388
|
-
raise ConfigurationError, "restore local refuses to run with #{key}=#{value};
|
|
382
|
+
if production_environment?(value)
|
|
383
|
+
raise ConfigurationError, "restore local refuses to run with #{key}=#{value}; unset #{key} or use restore production"
|
|
389
384
|
end
|
|
390
385
|
end
|
|
391
386
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
require "tempfile"
|
|
3
|
-
require "uri"
|
|
4
3
|
require_relative "base"
|
|
5
4
|
|
|
6
5
|
module KamalBackup
|
|
@@ -18,7 +17,10 @@ module KamalBackup
|
|
|
18
17
|
source = sqlite_source
|
|
19
18
|
Tempfile.create(["kamal-backup-", ".sqlite3"]) do |tempfile|
|
|
20
19
|
tempfile.close
|
|
21
|
-
|
|
20
|
+
Command.capture(
|
|
21
|
+
CommandSpec.new(argv: ["sqlite3", source, ".backup #{sqlite_literal(tempfile.path)}"]),
|
|
22
|
+
redactor: redactor
|
|
23
|
+
)
|
|
22
24
|
restic.backup_file(
|
|
23
25
|
tempfile.path,
|
|
24
26
|
filename: database_filename(timestamp),
|
|
@@ -53,40 +55,6 @@ module KamalBackup
|
|
|
53
55
|
config.required_value("SQLITE_DATABASE_PATH")
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
def backup_to_file(source, target)
|
|
57
|
-
run_backup(source, target)
|
|
58
|
-
rescue CommandError => e
|
|
59
|
-
raise unless immutable_retry_safe?(source, e)
|
|
60
|
-
|
|
61
|
-
# Immutable mode skips WAL change detection, so only use it when no WAL sidecar exists.
|
|
62
|
-
run_backup(sqlite_immutable_uri(source), target)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def run_backup(source, target)
|
|
66
|
-
Command.capture(
|
|
67
|
-
CommandSpec.new(argv: ["sqlite3", source, ".backup #{sqlite_literal(target)}"]),
|
|
68
|
-
redactor: redactor
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def immutable_retry_safe?(source, error)
|
|
73
|
-
readonly_database_error?(error) &&
|
|
74
|
-
!File.exist?("#{source}-wal") &&
|
|
75
|
-
!File.exist?("#{source}-shm")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def readonly_database_error?(error)
|
|
79
|
-
error.stderr.include?("readonly database") || error.message.include?("readonly database")
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def sqlite_immutable_uri(source)
|
|
83
|
-
path = File.expand_path(source).split("/").map do |part|
|
|
84
|
-
URI.encode_www_form_component(part).gsub("+", "%20")
|
|
85
|
-
end.join("/")
|
|
86
|
-
|
|
87
|
-
"file:#{path}?immutable=1"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
58
|
def validate_scratch_restore_target(target)
|
|
91
59
|
if File.expand_path(sqlite_source) == File.expand_path(target)
|
|
92
60
|
raise ConfigurationError, "scratch SQLite path must differ from SQLITE_DATABASE_PATH"
|
data/lib/kamal_backup/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kamal-backup
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crmne
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|