kamal-backup 0.4.0 → 0.4.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 +4 -4
- data/README.md +4 -2
- data/lib/kamal_backup/app.rb +45 -9
- data/lib/kamal_backup/cli/helpers.rb +1 -1
- data/lib/kamal_backup/restic.rb +7 -8
- data/lib/kamal_backup/scheduler.rb +2 -2
- 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: 2402ed466c957ae450d1e3196018d7099da3e01c59e7cc27e0a8dd7db0dbec5b
|
|
4
|
+
data.tar.gz: 43d46f5648d0a2e410ea3fee13876a65c032b42a7260adb7a9210f8e8452ae6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 645f1b5d3a0da81237a3391efde6f22747447b93504580693ebe2dd0e7fb2d81024a00d54d427890992234687034948b25053d741e8a974ee379a385b9e6a9d3
|
|
7
|
+
data.tar.gz: 83577eddd32f3cd423408fea71c75769138d5b67041d555b369478129f20045eea2d51fd031b45067e6862a53dc6b4a2f7218a374e57a879d9ff606a41258c2d
|
data/README.md
CHANGED
|
@@ -68,11 +68,13 @@ accessories:
|
|
|
68
68
|
- AWS_ACCESS_KEY_ID
|
|
69
69
|
- AWS_SECRET_ACCESS_KEY
|
|
70
70
|
volumes:
|
|
71
|
-
- "chatwithwork_storage:/data/storage
|
|
71
|
+
- "chatwithwork_storage:/data/storage"
|
|
72
72
|
- "chatwithwork_backup_state:/var/lib/kamal-backup"
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
The storage volume is writable so `restore production` can replace its contents safely without replacing the mount
|
|
76
|
+
point. Writable access is also required for SQLite databases stored on that volume. For a backup-only accessory,
|
|
77
|
+
you can append `:ro`; remove it and reboot the accessory before a production file restore.
|
|
76
78
|
When the configured SQLite database file lives under a configured file backup path, `kamal-backup` excludes the raw database, WAL, and shared-memory files from the restic file snapshot automatically.
|
|
77
79
|
|
|
78
80
|
Put the backup settings in `config/kamal-backup.yml`:
|
data/lib/kamal_backup/app.rb
CHANGED
|
@@ -58,8 +58,8 @@ module KamalBackup
|
|
|
58
58
|
|
|
59
59
|
build_restore_result('local', snapshot) do |result|
|
|
60
60
|
databases.each { |adapter| validate_local_machine_database_target(adapter) }
|
|
61
|
-
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
62
61
|
result[:files] = perform_replacement_file_restore(snapshot, production_source: false)
|
|
62
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -67,8 +67,8 @@ module KamalBackup
|
|
|
67
67
|
validate_production_restore
|
|
68
68
|
|
|
69
69
|
build_restore_result('production', snapshot) do |result|
|
|
70
|
-
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
71
70
|
result[:files] = perform_replacement_file_restore(snapshot, production_source: true)
|
|
71
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -77,8 +77,8 @@ module KamalBackup
|
|
|
77
77
|
|
|
78
78
|
run_drill('local', snapshot, check_command: check_command) do |result|
|
|
79
79
|
databases.each { |adapter| validate_local_machine_database_target(adapter) }
|
|
80
|
-
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
81
80
|
result[:files] = perform_replacement_file_restore(snapshot, production_source: false)
|
|
81
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -87,6 +87,7 @@ module KamalBackup
|
|
|
87
87
|
validate_production_drill(file_target, database_name, sqlite_path)
|
|
88
88
|
|
|
89
89
|
run_drill('production', snapshot, check_command: check_command) do |result|
|
|
90
|
+
result[:files] = perform_file_restore(snapshot, target: file_target)
|
|
90
91
|
result[:databases] = databases.map do |adapter|
|
|
91
92
|
perform_database_restore_to_scratch(
|
|
92
93
|
snapshot,
|
|
@@ -95,7 +96,6 @@ module KamalBackup
|
|
|
95
96
|
sqlite_path: sqlite_path
|
|
96
97
|
)
|
|
97
98
|
end
|
|
98
|
-
result[:files] = perform_file_restore(snapshot, target: file_target)
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
@@ -374,6 +374,8 @@ module KamalBackup
|
|
|
374
374
|
end
|
|
375
375
|
|
|
376
376
|
def perform_replacement_file_restore(snapshot, production_source:)
|
|
377
|
+
return nil if config.backup_paths.empty?
|
|
378
|
+
|
|
377
379
|
source_paths = production_source ? config.backup_paths : config.local_restore_source_paths
|
|
378
380
|
target_paths = config.backup_paths
|
|
379
381
|
resolved_snapshot = resolve_snapshot(snapshot, tags: ['type:files'])
|
|
@@ -390,20 +392,52 @@ module KamalBackup
|
|
|
390
392
|
end
|
|
391
393
|
|
|
392
394
|
def replace_target_paths(stage_dir, source_paths:, target_paths:)
|
|
393
|
-
source_paths.zip(target_paths)
|
|
395
|
+
path_pairs = source_paths.zip(target_paths)
|
|
396
|
+
validate_replacement_paths(stage_dir, path_pairs)
|
|
397
|
+
|
|
398
|
+
path_pairs.each do |source_path, target_path|
|
|
394
399
|
replace_target_path(stage_dir, source_path, target_path)
|
|
395
400
|
end
|
|
396
401
|
end
|
|
397
402
|
|
|
403
|
+
def validate_replacement_paths(stage_dir, path_pairs)
|
|
404
|
+
path_pairs.each do |source_path, target_path|
|
|
405
|
+
source = staged_backup_path(stage_dir, source_path)
|
|
406
|
+
raise ConfigurationError, "restored file snapshot is missing #{source_path}" unless File.exist?(source)
|
|
407
|
+
|
|
408
|
+
ensure_writable_restore_target(target_path)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
398
412
|
def replace_target_path(stage_dir, source_path, target_path)
|
|
399
413
|
source = staged_backup_path(stage_dir, source_path)
|
|
400
414
|
target = File.expand_path(target_path)
|
|
401
415
|
|
|
402
416
|
raise ConfigurationError, "restored file snapshot is missing #{source_path}" unless File.exist?(source)
|
|
403
417
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
418
|
+
if File.directory?(source) && File.directory?(target) && !File.symlink?(target)
|
|
419
|
+
replace_directory_contents(source, target)
|
|
420
|
+
else
|
|
421
|
+
FileUtils.remove_entry(target) if File.exist?(target) || File.symlink?(target)
|
|
422
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
423
|
+
FileUtils.mv(source, target)
|
|
424
|
+
end
|
|
425
|
+
rescue Errno::EACCES, Errno::EPERM, Errno::EROFS => e
|
|
426
|
+
raise ConfigurationError, "cannot restore files to #{target}: target must be writable (#{e.message})"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def ensure_writable_restore_target(target_path)
|
|
430
|
+
target = File.expand_path(target_path)
|
|
431
|
+
return unless File.directory?(target) && !File.symlink?(target)
|
|
432
|
+
|
|
433
|
+
Dir.mktmpdir('.kamal-backup-write-test-', target) {}
|
|
434
|
+
rescue Errno::EACCES, Errno::EPERM, Errno::EROFS => e
|
|
435
|
+
raise ConfigurationError, "cannot restore files to #{target}: target must be writable (#{e.message})"
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def replace_directory_contents(source, target)
|
|
439
|
+
Dir.children(target).each { |entry| FileUtils.remove_entry(File.join(target, entry)) }
|
|
440
|
+
Dir.children(source).each { |entry| FileUtils.mv(File.join(source, entry), target) }
|
|
407
441
|
end
|
|
408
442
|
|
|
409
443
|
def staged_backup_path(stage_dir, path)
|
|
@@ -411,6 +445,8 @@ module KamalBackup
|
|
|
411
445
|
end
|
|
412
446
|
|
|
413
447
|
def perform_file_restore(snapshot, target:)
|
|
448
|
+
return nil if config.backup_paths.empty?
|
|
449
|
+
|
|
414
450
|
resolved_snapshot = resolve_snapshot(snapshot, tags: ['type:files'])
|
|
415
451
|
validated_target = config.validate_file_restore_target(target)
|
|
416
452
|
restic.restore_snapshot(resolved_snapshot, validated_target)
|
|
@@ -436,7 +472,7 @@ module KamalBackup
|
|
|
436
472
|
def validate_production_drill(file_target, database_name, sqlite_path)
|
|
437
473
|
config.validate_restic
|
|
438
474
|
config.validate_database_backup
|
|
439
|
-
config.validate_file_restore_target(file_target)
|
|
475
|
+
config.validate_file_restore_target(file_target) if config.backup_paths.any?
|
|
440
476
|
|
|
441
477
|
databases.each do |adapter|
|
|
442
478
|
case adapter.adapter_name
|
data/lib/kamal_backup/restic.rb
CHANGED
|
@@ -28,13 +28,12 @@ module KamalBackup
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def backup_stream(command, filename:, tags:)
|
|
31
|
-
restic_command = CommandSpec.new(
|
|
32
|
-
argv: %w[restic
|
|
33
|
-
backup] + host_args + ['--stdin', '--stdin-filename', filename] + tag_args(common_tags + tags),
|
|
34
|
-
env: restic_env
|
|
35
|
-
)
|
|
36
31
|
log("backing up stream as #{filename}")
|
|
37
|
-
|
|
32
|
+
run(
|
|
33
|
+
['backup'] + host_args + ['--stdin-filename', filename] + tag_args(common_tags + tags) +
|
|
34
|
+
['--stdin-from-command', '--'] + command.argv,
|
|
35
|
+
env: restic_env.merge(command.env)
|
|
36
|
+
)
|
|
38
37
|
end
|
|
39
38
|
|
|
40
39
|
def backup_file(path, filename:, tags:)
|
|
@@ -211,9 +210,9 @@ module KamalBackup
|
|
|
211
210
|
|
|
212
211
|
private
|
|
213
212
|
|
|
214
|
-
def run(args, log_output: true)
|
|
213
|
+
def run(args, log_output: true, env: restic_env)
|
|
215
214
|
Command.capture(
|
|
216
|
-
CommandSpec.new(argv: ['restic'] + args, env:
|
|
215
|
+
CommandSpec.new(argv: ['restic'] + args, env: env),
|
|
217
216
|
redactor: redactor,
|
|
218
217
|
log_output: log_output
|
|
219
218
|
)
|
|
@@ -24,7 +24,7 @@ module KamalBackup
|
|
|
24
24
|
@backup_block.call
|
|
25
25
|
log("backup completed at #{Time.now.utc.iso8601}")
|
|
26
26
|
rescue StandardError => e
|
|
27
|
-
warn("[kamal-backup] backup failed at #{Time.now.utc.iso8601}: #{e.class}: #{e.message}")
|
|
27
|
+
warn("ERROR [kamal-backup] backup failed at #{Time.now.utc.iso8601}: #{e.class}: #{e.message}")
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
sleep_interruptibly(@config.backup_schedule_seconds)
|
|
@@ -49,7 +49,7 @@ module KamalBackup
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def log(message)
|
|
52
|
-
$stdout.puts("[kamal-backup] #{message}")
|
|
52
|
+
$stdout.puts("INFO [kamal-backup] #{message}")
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
end
|
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.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crmne
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|