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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b93c203599481320559eb006ccf07a07c010c5bc4d5dfcae3e11b7a9a832fa3
4
- data.tar.gz: 705fc119fcacdd54ee431e9cfb5b88220a0faae02540cba77e38c45b94044905
3
+ metadata.gz: 2402ed466c957ae450d1e3196018d7099da3e01c59e7cc27e0a8dd7db0dbec5b
4
+ data.tar.gz: 43d46f5648d0a2e410ea3fee13876a65c032b42a7260adb7a9210f8e8452ae6c
5
5
  SHA512:
6
- metadata.gz: fd7b30595fe843980d39979b8b7e241fbd54c7fd814c55bccb4bfcdf6b175b5767e84910929050f4e3aa40f150aec5b01e15015b196e522e3c48566f9527020f
7
- data.tar.gz: e81f5d21997f6f072b9556a73b7e751accbe99098adaebaec3435ca15600e8d13f204b9d343cc3ae7f6905a86efd3abea3c5dd0d64ea602bd2f2617d8bca8fef
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:ro"
71
+ - "chatwithwork_storage:/data/storage"
72
72
  - "chatwithwork_backup_state:/var/lib/kamal-backup"
73
73
  ```
74
74
 
75
- For SQLite databases stored on the mounted storage volume, omit `:ro` from that volume.
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`:
@@ -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).each do |source_path, target_path|
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
- FileUtils.rm_rf(target)
405
- FileUtils.mkdir_p(File.dirname(target))
406
- FileUtils.mv(source, target)
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
@@ -289,7 +289,7 @@ module KamalBackup
289
289
  - AWS_ACCESS_KEY_ID
290
290
  - AWS_SECRET_ACCESS_KEY
291
291
  volumes:
292
- - "your_app_storage:/data/storage:ro"
292
+ - "your_app_storage:/data/storage"
293
293
  - "your_app_backup_state:/var/lib/kamal-backup"
294
294
  YAML
295
295
  end
@@ -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
- pipe_commands(command, restic_command, producer_label: 'dump', consumer_label: 'restic backup')
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: restic_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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KamalBackup
4
- VERSION = '0.4.0'
4
+ VERSION = '0.4.1'
5
5
  end
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.0
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-06-21 00:00:00.000000000 Z
11
+ date: 2026-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor