kamal-backup 0.3.0 → 0.3.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.
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'uri'
4
- require 'yaml'
5
4
  require_relative 'errors'
5
+ require_relative 'config_file'
6
+ require_relative 'databases/base'
6
7
  require_relative 'rails_app'
7
8
 
8
9
  module KamalBackup
@@ -19,42 +20,6 @@ module KamalBackup
19
20
  SHARED_CONFIG_PATH = 'config/kamal-backup.yml'
20
21
  LOCAL_CONFIG_PATH = 'config/kamal-backup.local.yml'
21
22
  DEFAULT_CONFIG_PATHS = [SHARED_CONFIG_PATH, LOCAL_CONFIG_PATH].freeze
22
- TOP_LEVEL_YAML_KEYS = %w[app accessory databases paths restore_from restic backup state].freeze
23
- LEGACY_YAML_KEYS = %w[
24
- app_name
25
- database_adapter
26
- database_url
27
- sqlite_database_path
28
- backup_paths
29
- local_restore_source_paths
30
- restic_repository
31
- restic_repository_file
32
- restic_password
33
- restic_password_file
34
- restic_password_command
35
- restic_init_if_missing
36
- restic_check_after_backup
37
- restic_check_read_data_subset
38
- restic_forget_after_backup
39
- restic_keep_last
40
- restic_keep_daily
41
- restic_keep_weekly
42
- restic_keep_monthly
43
- restic_keep_yearly
44
- backup_schedule_seconds
45
- backup_start_delay_seconds
46
- state_dir
47
- allow_suspicious_paths
48
- pgpassword
49
- mysql_pwd
50
- ].freeze
51
- ConfigData = Struct.new(:env, :database_definitions, :path_definitions, :restore_from_definitions,
52
- keyword_init: true) do
53
- def self.empty
54
- new(env: {}, database_definitions: nil, path_definitions: nil, restore_from_definitions: nil)
55
- end
56
- end
57
- PathDefinition = Struct.new(:path, :exclude, keyword_init: true)
58
23
 
59
24
  class DatabaseSource
60
25
  CONNECTION_KEYS = %w[
@@ -105,7 +70,7 @@ module KamalBackup
105
70
  end
106
71
 
107
72
  def database_adapter
108
- @adapter || parent.send(:legacy_database_adapter)
73
+ @adapter
109
74
  end
110
75
 
111
76
  def value(key)
@@ -242,11 +207,7 @@ module KamalBackup
242
207
  end
243
208
 
244
209
  def local_restore_source_paths
245
- if path_definitions?
246
- @restore_from_definitions || legacy_local_restore_source_paths || backup_paths
247
- else
248
- legacy_local_restore_source_paths || backup_paths
249
- end
210
+ @restore_from_definitions || legacy_local_restore_source_paths || backup_paths
250
211
  end
251
212
 
252
213
  def local_restore_path_pairs
@@ -271,10 +232,6 @@ module KamalBackup
271
232
  end
272
233
  end
273
234
 
274
- def database_name
275
- 'app'
276
- end
277
-
278
235
  def databases
279
236
  @databases ||= if database_definitions?
280
237
  @database_definitions.map do |definition|
@@ -291,7 +248,7 @@ module KamalBackup
291
248
  [
292
249
  DatabaseSource.new(
293
250
  parent: self,
294
- name: database_name,
251
+ name: 'app',
295
252
  adapter: legacy_database_adapter,
296
253
  env: {},
297
254
  structured: false
@@ -451,7 +408,7 @@ module KamalBackup
451
408
  config_paths(raw_env, cwd: cwd, paths: paths).each_with_object(ConfigData.empty) do |path, merged|
452
409
  next unless File.file?(path)
453
410
 
454
- data = normalize_config_file(path, raw_env: raw_env)
411
+ data = ConfigFile.new(path, env: raw_env).data
455
412
  merged.env.merge!(data.env)
456
413
  merged.database_definitions = data.database_definitions if data.database_definitions
457
414
  merged.path_definitions = data.path_definitions if data.path_definitions
@@ -469,378 +426,108 @@ module KamalBackup
469
426
  end
470
427
  end
471
428
 
472
- def validate_restic_repository(check_files:)
473
- return if restic_repository
474
-
475
- if (path = restic_repository_file)
476
- raise ConfigurationError, "RESTIC_REPOSITORY_FILE does not exist: #{path}" if check_files && !File.file?(path)
477
-
478
- return
479
- end
480
-
481
- raise ConfigurationError, 'RESTIC_REPOSITORY or RESTIC_REPOSITORY_FILE is required'
482
- end
483
-
484
- def validate_restic_password(check_files:)
485
- return if restic_password || restic_password_command
486
-
487
- if (path = restic_password_file)
488
- raise ConfigurationError, "RESTIC_PASSWORD_FILE does not exist: #{path}" if check_files && !File.file?(path)
489
-
490
- return
491
- end
492
-
493
- raise ConfigurationError, 'RESTIC_PASSWORD, RESTIC_PASSWORD_FILE, or RESTIC_PASSWORD_COMMAND is required'
494
- end
429
+ def integer(key, default, minimum:)
430
+ raw = value(key)
431
+ number = raw ? Integer(raw) : default
432
+ raise ConfigurationError, "#{key} must be >= #{minimum}" if number < minimum
495
433
 
496
- def normalize_config_file(path, raw_env:)
497
- data = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
498
- return ConfigData.empty if data.nil?
499
-
500
- raise ConfigurationError, "#{path} must contain a YAML mapping" unless data.is_a?(Hash)
501
-
502
- result = ConfigData.empty
503
- data.each do |raw_key, raw_value|
504
- key = raw_key.to_s
505
- validate_top_level_yaml_key!(path, key)
506
-
507
- case key
508
- when 'app'
509
- result.env['APP_NAME'] = normalize_yaml_value(raw_value)
510
- when 'accessory'
511
- result.env['KAMAL_BACKUP_ACCESSORY'] = normalize_yaml_value(raw_value)
512
- when 'databases'
513
- result.database_definitions = normalize_yaml_databases(raw_value, raw_env: raw_env, path: path)
514
- when 'paths'
515
- result.path_definitions = normalize_yaml_backup_paths(raw_value, "#{path} paths")
516
- when 'restore_from'
517
- result.restore_from_definitions = normalize_yaml_paths(raw_value, "#{path} restore_from")
518
- when 'restic'
519
- result.env.merge!(normalize_yaml_restic(raw_value, raw_env: raw_env, path: path))
520
- when 'backup'
521
- result.env.merge!(normalize_yaml_backup(raw_value, path: path))
522
- when 'state'
523
- result.env.merge!(normalize_yaml_state(raw_value, path: path))
524
- end
525
- end
526
- result
527
- rescue Psych::SyntaxError => e
528
- raise ConfigurationError, "invalid YAML in #{path}: #{e.message}"
434
+ number
435
+ rescue ArgumentError
436
+ raise ConfigurationError, "#{key} must be an integer"
529
437
  end
530
438
 
531
- def validate_top_level_yaml_key!(path, key)
532
- if LEGACY_YAML_KEYS.include?(key)
533
- raise ConfigurationError,
534
- "#{path} uses legacy key #{key}; use databases, paths, restic, and backup instead. See the upgrading guide for the 0.3 config migration."
535
- end
536
-
537
- return if TOP_LEVEL_YAML_KEYS.include?(key)
538
-
539
- raise ConfigurationError,
540
- "#{path} contains unknown key #{key.inspect}; expected #{TOP_LEVEL_YAML_KEYS.join(', ')}"
439
+ def legacy_backup_paths
440
+ split_paths(value('BACKUP_PATHS'))
541
441
  end
542
442
 
543
- def normalize_yaml_value(raw_value)
544
- case raw_value
545
- when Array
546
- raw_value.map(&:to_s).join("\n")
547
- when NilClass
548
- nil
549
- else
550
- raw_value.to_s
443
+ def legacy_local_restore_source_paths
444
+ if (raw = value('LOCAL_RESTORE_SOURCE_PATHS'))
445
+ split_paths(raw)
551
446
  end
552
447
  end
553
448
 
554
- def normalize_yaml_databases(raw_value, raw_env:, path:)
555
- entries = require_array(raw_value, "#{path} databases")
556
- entries.map.with_index(1) do |entry, index|
557
- hash = require_mapping(entry, "#{path} databases[#{index}]")
558
- name = required_yaml_scalar(hash, 'name', "#{path} databases[#{index}]")
559
- adapter = normalize_adapter(required_yaml_scalar(hash, 'adapter', "#{path} databases[#{index}]"))
560
- unless adapter
561
- raise ConfigurationError,
562
- "#{path} databases[#{index}] adapter must be postgres, mysql, or sqlite"
563
- end
564
-
565
- env = {}
566
- missing_secrets = []
567
- case adapter
568
- when 'postgres'
569
- if hash.key?('url')
570
- env['DATABASE_URL'] =
571
- resolve_yaml_value(hash['url'], raw_env: raw_env, context: "#{path} databases[#{index}].url")
572
- missing_secrets.concat(missing_yaml_secrets(hash['url'], raw_env: raw_env))
573
- end
574
- if hash.key?('password')
575
- env['PGPASSWORD'] =
576
- resolve_yaml_value(hash['password'], raw_env: raw_env, context: "#{path} databases[#{index}].password")
577
- missing_secrets.concat(missing_yaml_secrets(hash['password'], raw_env: raw_env))
578
- end
579
- when 'mysql'
580
- if hash.key?('url')
581
- env['DATABASE_URL'] =
582
- resolve_yaml_value(hash['url'], raw_env: raw_env, context: "#{path} databases[#{index}].url")
583
- missing_secrets.concat(missing_yaml_secrets(hash['url'], raw_env: raw_env))
584
- end
585
- if hash.key?('password')
586
- env['MYSQL_PWD'] =
587
- resolve_yaml_value(hash['password'], raw_env: raw_env, context: "#{path} databases[#{index}].password")
588
- missing_secrets.concat(missing_yaml_secrets(hash['password'], raw_env: raw_env))
589
- end
590
- when 'sqlite'
591
- sqlite_path = hash.key?('path') ? hash['path'] : hash['database']
592
- if sqlite_path
593
- env['SQLITE_DATABASE_PATH'] =
594
- resolve_yaml_value(sqlite_path, raw_env: raw_env, context: "#{path} databases[#{index}].path")
595
- missing_secrets.concat(missing_yaml_secrets(sqlite_path, raw_env: raw_env))
596
- end
597
- end
598
-
599
- {
600
- name: name,
601
- adapter: adapter,
602
- env: env.compact,
603
- missing_secrets: missing_secrets
604
- }
605
- end
449
+ def split_paths(raw)
450
+ raw.to_s.split(/[\n:]+/).map(&:strip).reject(&:empty?)
606
451
  end
607
452
 
608
- def normalize_yaml_restic(raw_value, raw_env:, path:)
609
- hash = require_mapping(raw_value, "#{path} restic")
610
- env = {}
611
-
612
- if hash.key?('repository')
613
- env['RESTIC_REPOSITORY'] =
614
- resolve_yaml_value(hash['repository'], raw_env: raw_env,
615
- context: "#{path} restic.repository")
616
- end
617
- env['RESTIC_REPOSITORY_FILE'] = normalize_yaml_value(hash['repository_file']) if hash.key?('repository_file')
618
-
619
- if hash.key?('password')
620
- normalize_yaml_restic_password(hash['password'], raw_env: raw_env, path: path).each do |key, value|
621
- env[key] = value
622
- end
623
- end
624
-
625
- env.merge!(normalize_yaml_restic_rest(hash['rest'], raw_env: raw_env, path: path)) if hash.key?('rest')
626
-
627
- {
628
- 'init_if_missing' => 'RESTIC_INIT_IF_MISSING',
629
- 'check_after_backup' => 'RESTIC_CHECK_AFTER_BACKUP',
630
- 'check_read_data_subset' => 'RESTIC_CHECK_READ_DATA_SUBSET',
631
- 'forget_after_backup' => 'RESTIC_FORGET_AFTER_BACKUP'
632
- }.each do |source, target|
633
- env[target] = normalize_yaml_value(hash[source]) if hash.key?(source)
634
- end
635
-
636
- if hash.key?('retention')
637
- retention = require_mapping(hash['retention'], "#{path} restic.retention")
638
- {
639
- 'keep_last' => 'RESTIC_KEEP_LAST',
640
- 'keep_daily' => 'RESTIC_KEEP_DAILY',
641
- 'keep_weekly' => 'RESTIC_KEEP_WEEKLY',
642
- 'keep_monthly' => 'RESTIC_KEEP_MONTHLY',
643
- 'keep_yearly' => 'RESTIC_KEEP_YEARLY'
644
- }.each do |source, target|
645
- env[target] = normalize_yaml_value(retention[source]) if retention.key?(source)
646
- end
647
- end
648
-
649
- env.compact
453
+ def path_definitions?
454
+ !@path_definitions.nil?
650
455
  end
651
456
 
652
- def normalize_yaml_restic_password(raw_value, raw_env:, path:)
653
- case raw_value
654
- when Hash
655
- hash = stringify_keys(raw_value)
656
- if hash.key?('secret')
657
- { 'RESTIC_PASSWORD' => resolve_yaml_value(hash, raw_env: raw_env, context: "#{path} restic.password") }
658
- elsif hash.key?('file')
659
- { 'RESTIC_PASSWORD_FILE' => normalize_yaml_value(hash['file']) }
660
- elsif hash.key?('command')
661
- { 'RESTIC_PASSWORD_COMMAND' => normalize_yaml_value(hash['command']) }
662
- else
663
- raise ConfigurationError, "#{path} restic.password must use secret, file, or command"
664
- end
457
+ def backup_path_definitions
458
+ if path_definitions?
459
+ @path_definitions
665
460
  else
666
- { 'RESTIC_PASSWORD' => normalize_yaml_value(raw_value) }
461
+ legacy_backup_paths.map { |path| PathDefinition.new(path: path, exclude: []) }
667
462
  end
668
463
  end
669
464
 
670
- def normalize_yaml_restic_rest(raw_value, raw_env:, path:)
671
- hash = require_mapping(raw_value, "#{path} restic.rest")
672
- env = {}
673
- username = hash.key?('username') ? hash['username'] : hash['user']
674
-
675
- if username
676
- env['RESTIC_REST_USERNAME'] =
677
- resolve_yaml_value(username, raw_env: raw_env, context: "#{path} restic.rest.username")
678
- end
679
- if hash.key?('password')
680
- env['RESTIC_REST_PASSWORD'] =
681
- resolve_yaml_value(hash['password'], raw_env: raw_env,
682
- context: "#{path} restic.rest.password")
465
+ def configured_backup_path_excludes(paths)
466
+ backup_path_definitions.each_with_object([]) do |definition, excludes|
467
+ excludes.concat(definition.exclude) if paths.include?(definition.path)
683
468
  end
684
- env.compact
685
469
  end
686
470
 
687
- def normalize_yaml_backup(raw_value, path:)
688
- hash = require_mapping(raw_value, "#{path} backup")
689
- env = {}
690
- if hash.key?('schedule')
691
- env['BACKUP_SCHEDULE_SECONDS'] =
692
- normalize_duration(hash['schedule'], "#{path} backup.schedule")
693
- end
694
- env.compact
695
- end
471
+ def sqlite_backup_path_excludes(paths)
472
+ databases.select { |database| database.database_adapter == 'sqlite' }.flat_map do |database|
473
+ sqlite_database_path = database.value('SQLITE_DATABASE_PATH')
474
+ next [] if sqlite_database_path.to_s.empty?
475
+ next [] unless paths.any? { |path| path_contains?(path, sqlite_database_path) }
696
476
 
697
- def normalize_yaml_state(raw_value, path:)
698
- hash = require_mapping(raw_value, "#{path} state")
699
- env = {}
700
- env['KAMAL_BACKUP_STATE_DIR'] = normalize_yaml_value(hash['path']) if hash.key?('path')
701
- env.compact
477
+ [sqlite_database_path, "#{sqlite_database_path}-wal", "#{sqlite_database_path}-shm"]
478
+ end.uniq
702
479
  end
703
480
 
704
- def normalize_yaml_paths(raw_value, context)
705
- case raw_value
706
- when Array
707
- raw_value.map { |path| normalize_yaml_path(path, context) }.reject(&:empty?)
708
- when NilClass
709
- []
710
- else
711
- [normalize_yaml_path(raw_value, context)].reject(&:empty?)
712
- end
481
+ def path_contains?(parent, child)
482
+ expanded_parent = File.expand_path(parent)
483
+ expanded_child = File.expand_path(child)
484
+ expanded_child == expanded_parent || expanded_child.start_with?("#{expanded_parent}/")
713
485
  end
714
486
 
715
- def normalize_yaml_path(raw_value, context)
716
- raise ConfigurationError, "#{context} entries must be path strings" if raw_value.is_a?(Hash) || raw_value.is_a?(Array)
717
-
718
- normalize_yaml_value(raw_value)
487
+ def database_definitions?
488
+ !@database_definitions.nil?
719
489
  end
720
490
 
721
- def normalize_yaml_backup_paths(raw_value, context)
722
- case raw_value
723
- when Array
724
- definitions = raw_value.map.with_index(1) do |entry, index|
725
- normalize_yaml_backup_path(entry, "#{context}[#{index}]")
726
- end
727
- definitions.reject { |definition| definition.path.to_s.empty? }
728
- when NilClass
729
- []
730
- else
731
- [normalize_yaml_backup_path(raw_value, "#{context}[1]")].reject { |definition| definition.path.to_s.empty? }
491
+ def legacy_database_adapter
492
+ if (explicit = value('DATABASE_ADAPTER'))
493
+ Databases.normalize_adapter(explicit)
494
+ elsif (adapter = adapter_from_database_url)
495
+ adapter
496
+ elsif value('SQLITE_DATABASE_PATH')
497
+ 'sqlite'
732
498
  end
733
499
  end
734
500
 
735
- def normalize_yaml_backup_path(raw_value, context)
736
- case raw_value
737
- when Hash
738
- hash = require_mapping(raw_value, context)
739
- unknown_keys = hash.keys - %w[path exclude]
740
- unless unknown_keys.empty?
741
- raise ConfigurationError,
742
- "#{context} contains unknown key #{unknown_keys.first.inspect}; expected path and exclude"
743
- end
744
-
745
- path = required_yaml_string(hash, 'path', context)
746
- exclude = hash.key?('exclude') ? normalize_yaml_excludes(hash['exclude'], "#{context}.exclude") : []
747
- PathDefinition.new(path: path, exclude: exclude)
748
- when Array
749
- raise ConfigurationError, "#{context} must be a path string or a mapping with path and optional exclude"
750
- else
751
- PathDefinition.new(path: normalize_yaml_value(raw_value), exclude: [])
501
+ def adapter_from_database_url
502
+ if (url = value('DATABASE_URL'))
503
+ Databases.normalize_adapter(URI.parse(url).scheme)
752
504
  end
505
+ rescue URI::InvalidURIError
506
+ nil
753
507
  end
754
508
 
755
- def normalize_yaml_excludes(raw_value, context)
756
- entries = require_array(raw_value, context)
757
- entries.map.with_index(1) do |entry, index|
758
- pattern = optional_yaml_string(entry, "#{context}[#{index}]")
759
- raise ConfigurationError, "#{context}[#{index}] must not be empty" if pattern.to_s.strip.empty?
760
-
761
- pattern
762
- end
763
- end
509
+ def validate_restic_repository(check_files:)
510
+ return if restic_repository
764
511
 
765
- def resolve_yaml_value(raw_value, raw_env:, context:)
766
- case raw_value
767
- when Hash
768
- hash = stringify_keys(raw_value)
769
- raise ConfigurationError, "#{context} must be a scalar value or { secret: NAME }" unless hash.keys == ['secret']
512
+ if (path = restic_repository_file)
513
+ raise ConfigurationError, "RESTIC_REPOSITORY_FILE does not exist: #{path}" if check_files && !File.file?(path)
770
514
 
771
- secret_name = normalize_yaml_value(hash.fetch('secret'))
772
- raw_env[secret_name]
773
- else
774
- normalize_yaml_value(raw_value)
515
+ return
775
516
  end
776
- end
777
-
778
- def missing_yaml_secrets(raw_value, raw_env:)
779
- return [] unless raw_value.is_a?(Hash)
780
-
781
- hash = stringify_keys(raw_value)
782
- return [] unless hash.key?('secret')
783
-
784
- secret_name = normalize_yaml_value(hash.fetch('secret'))
785
- raw_env[secret_name].to_s.strip.empty? ? [secret_name] : []
786
- end
787
-
788
- def normalize_duration(raw_value, context)
789
- value = normalize_yaml_value(raw_value)
790
- raise ConfigurationError, "#{context} is required" if value.to_s.empty?
791
-
792
- return value if value.match?(/\A\d+\z/)
793
-
794
- match = value.match(/\A(\d+)\s*([smhdw])\z/i)
795
- raise ConfigurationError, "#{context} must be seconds or a duration like 30m, 6h, or 1d" unless match
796
-
797
- amount = match[1].to_i
798
- multiplier = {
799
- 's' => 1,
800
- 'm' => 60,
801
- 'h' => 3600,
802
- 'd' => 86_400,
803
- 'w' => 604_800
804
- }.fetch(match[2].downcase)
805
- (amount * multiplier).to_s
806
- end
807
-
808
- def require_array(value, context)
809
- return value if value.is_a?(Array)
810
-
811
- raise ConfigurationError, "#{context} must be a YAML sequence"
812
- end
813
-
814
- def require_mapping(value, context)
815
- raise ConfigurationError, "#{context} must be a YAML mapping" unless value.is_a?(Hash)
816
-
817
- stringify_keys(value)
818
- end
819
517
 
820
- def optional_yaml_string(value, context)
821
- raise ConfigurationError, "#{context} must be a string" if value.is_a?(Hash) || value.is_a?(Array)
822
-
823
- normalize_yaml_value(value)
518
+ raise ConfigurationError, 'RESTIC_REPOSITORY or RESTIC_REPOSITORY_FILE is required'
824
519
  end
825
520
 
826
- def required_yaml_string(hash, key, context)
827
- raise ConfigurationError, "#{context} #{key} is required" unless hash.key?(key)
828
-
829
- value = optional_yaml_string(hash[key], "#{context}.#{key}")
830
- raise ConfigurationError, "#{context} #{key} is required" if value.to_s.strip.empty?
831
-
832
- value
833
- end
521
+ def validate_restic_password(check_files:)
522
+ return if restic_password || restic_password_command
834
523
 
835
- def required_yaml_scalar(hash, key, context)
836
- value = normalize_yaml_value(hash[key])
837
- raise ConfigurationError, "#{context} #{key} is required" if value.to_s.empty?
524
+ if (path = restic_password_file)
525
+ raise ConfigurationError, "RESTIC_PASSWORD_FILE does not exist: #{path}" if check_files && !File.file?(path)
838
526
 
839
- value
840
- end
527
+ return
528
+ end
841
529
 
842
- def stringify_keys(hash)
843
- hash.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
530
+ raise ConfigurationError, 'RESTIC_PASSWORD, RESTIC_PASSWORD_FILE, or RESTIC_PASSWORD_COMMAND is required'
844
531
  end
845
532
 
846
533
  def validate_local_machine_environment
@@ -854,6 +541,16 @@ module KamalBackup
854
541
  end
855
542
  end
856
543
 
544
+ def local_restore_environment
545
+ %w[RAILS_ENV RACK_ENV APP_ENV KAMAL_ENVIRONMENT].each do |key|
546
+ return [key, value(key)] if value(key)
547
+ end
548
+ end
549
+
550
+ def production_environment?(value)
551
+ %w[production prod live].include?(value.to_s.downcase)
552
+ end
553
+
857
554
  def validate_local_machine_paths
858
555
  path_pairs = local_restore_path_pairs
859
556
 
@@ -867,83 +564,6 @@ module KamalBackup
867
564
  end
868
565
  end
869
566
 
870
- def integer(key, default, minimum:)
871
- raw = value(key)
872
- number = raw ? Integer(raw) : default
873
- raise ConfigurationError, "#{key} must be >= #{minimum}" if number < minimum
874
-
875
- number
876
- rescue ArgumentError
877
- raise ConfigurationError, "#{key} must be an integer"
878
- end
879
-
880
- def normalize_adapter(value)
881
- case value.to_s.downcase
882
- when 'postgres', 'postgresql'
883
- 'postgres'
884
- when 'mysql', 'mysql2', 'mariadb'
885
- 'mysql'
886
- when 'sqlite', 'sqlite3'
887
- 'sqlite'
888
- end
889
- end
890
-
891
- def database_definitions?
892
- !@database_definitions.nil?
893
- end
894
-
895
- def path_definitions?
896
- !@path_definitions.nil?
897
- end
898
-
899
- def backup_path_definitions
900
- if path_definitions?
901
- @path_definitions
902
- else
903
- legacy_backup_paths.map { |path| PathDefinition.new(path: path, exclude: []) }
904
- end
905
- end
906
-
907
- def configured_backup_path_excludes(paths)
908
- backup_path_definitions.each_with_object([]) do |definition, excludes|
909
- excludes.concat(definition.exclude) if paths.include?(definition.path)
910
- end
911
- end
912
-
913
- def sqlite_backup_path_excludes(paths)
914
- databases.select { |database| database.database_adapter == 'sqlite' }.flat_map do |database|
915
- sqlite_database_path = database.value('SQLITE_DATABASE_PATH')
916
- next [] if sqlite_database_path.to_s.empty?
917
- next [] unless paths.any? { |path| path_contains?(path, sqlite_database_path) }
918
-
919
- [sqlite_database_path, "#{sqlite_database_path}-wal", "#{sqlite_database_path}-shm"]
920
- end.uniq
921
- end
922
-
923
- def path_contains?(parent, child)
924
- expanded_parent = File.expand_path(parent)
925
- expanded_child = File.expand_path(child)
926
- expanded_child == expanded_parent || expanded_child.start_with?("#{expanded_parent}/")
927
- end
928
-
929
- def legacy_database_adapter
930
- if (explicit = value('DATABASE_ADAPTER'))
931
- normalize_adapter(explicit)
932
- elsif (adapter = adapter_from_database_url)
933
- adapter
934
- elsif value('SQLITE_DATABASE_PATH')
935
- 'sqlite'
936
- end
937
- end
938
-
939
- def adapter_from_database_url
940
- if (url = value('DATABASE_URL'))
941
- normalize_adapter(URI.parse(url).scheme)
942
- end
943
- rescue URI::InvalidURIError
944
- nil
945
- end
946
-
947
567
  def in_place_file_restore?(expanded_target)
948
568
  backup_paths.any? do |path|
949
569
  expanded_path = File.expand_path(path)
@@ -963,30 +583,6 @@ module KamalBackup
963
583
  end.compact
964
584
  end
965
585
 
966
- def legacy_backup_paths
967
- split_paths(value('BACKUP_PATHS'))
968
- end
969
-
970
- def legacy_local_restore_source_paths
971
- if (raw = value('LOCAL_RESTORE_SOURCE_PATHS'))
972
- split_paths(raw)
973
- end
974
- end
975
-
976
- def split_paths(raw)
977
- raw.to_s.split(/[\n:]+/).map(&:strip).reject(&:empty?)
978
- end
979
-
980
- def local_restore_environment
981
- %w[RAILS_ENV RACK_ENV APP_ENV KAMAL_ENVIRONMENT].each do |key|
982
- return [key, value(key)] if value(key)
983
- end
984
- end
985
-
986
- def production_environment?(value)
987
- %w[production prod live].include?(value.to_s.downcase)
988
- end
989
-
990
586
  def production_named_target?(target)
991
587
  target.include?('production') ||
992
588
  target.match?(%r{(^|[/_.:-])prod([/_.:-]|$)}) ||