expire 0.2.0

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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.reek.yml +23 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +45 -0
  6. data/.simplecov +3 -0
  7. data/.travis.yml +7 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +152 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +551 -0
  12. data/Rakefile +11 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/example_rules/bad_rules.yml +2 -0
  16. data/example_rules/good_rules.yml +1 -0
  17. data/exe/expire +7 -0
  18. data/expire.gemspec +54 -0
  19. data/lib/expire.rb +51 -0
  20. data/lib/expire/all_backups_expired_error.rb +7 -0
  21. data/lib/expire/backup.rb +74 -0
  22. data/lib/expire/backup_from_path_service.rb +56 -0
  23. data/lib/expire/backup_list.rb +69 -0
  24. data/lib/expire/cli.rb +221 -0
  25. data/lib/expire/command.rb +122 -0
  26. data/lib/expire/commands/newest.rb +21 -0
  27. data/lib/expire/commands/oldest.rb +21 -0
  28. data/lib/expire/commands/purge.rb +23 -0
  29. data/lib/expire/commands/remove.rb +26 -0
  30. data/lib/expire/commands/rule_classes.rb +18 -0
  31. data/lib/expire/commands/rule_names.rb +20 -0
  32. data/lib/expire/commands/rule_option_names.rb +20 -0
  33. data/lib/expire/from_now_keep_adjective_for_rule_base.rb +38 -0
  34. data/lib/expire/from_now_keep_daily_for_rule.rb +7 -0
  35. data/lib/expire/from_now_keep_hourly_for_rule.rb +7 -0
  36. data/lib/expire/from_now_keep_monthly_for_rule.rb +7 -0
  37. data/lib/expire/from_now_keep_most_recent_for_rule.rb +41 -0
  38. data/lib/expire/from_now_keep_weekly_for_rule.rb +8 -0
  39. data/lib/expire/from_now_keep_yearly_for_rule.rb +8 -0
  40. data/lib/expire/from_range_value.rb +29 -0
  41. data/lib/expire/generate_backup_list_service.rb +45 -0
  42. data/lib/expire/invalid_path_error.rb +7 -0
  43. data/lib/expire/keep_adjective_for_rule_base.rb +34 -0
  44. data/lib/expire/keep_adjective_rule_base.rb +97 -0
  45. data/lib/expire/keep_daily_for_rule.rb +7 -0
  46. data/lib/expire/keep_daily_rule.rb +7 -0
  47. data/lib/expire/keep_hourly_for_rule.rb +7 -0
  48. data/lib/expire/keep_hourly_rule.rb +7 -0
  49. data/lib/expire/keep_monthly_for_rule.rb +7 -0
  50. data/lib/expire/keep_monthly_rule.rb +7 -0
  51. data/lib/expire/keep_most_recent_for_rule.rb +31 -0
  52. data/lib/expire/keep_most_recent_rule.rb +38 -0
  53. data/lib/expire/keep_weekly_for_rule.rb +8 -0
  54. data/lib/expire/keep_weekly_rule.rb +7 -0
  55. data/lib/expire/keep_yearly_for_rule.rb +7 -0
  56. data/lib/expire/keep_yearly_rule.rb +7 -0
  57. data/lib/expire/no_backups_error.rb +7 -0
  58. data/lib/expire/no_rules_error.rb +7 -0
  59. data/lib/expire/numerus_unit.rb +10 -0
  60. data/lib/expire/path_already_exists_error.rb +7 -0
  61. data/lib/expire/playground.rb +62 -0
  62. data/lib/expire/purge_service.rb +91 -0
  63. data/lib/expire/refine_all_and_none.rb +29 -0
  64. data/lib/expire/report_base.rb +23 -0
  65. data/lib/expire/report_enhanced.rb +14 -0
  66. data/lib/expire/report_expired.rb +10 -0
  67. data/lib/expire/report_kept.rb +10 -0
  68. data/lib/expire/report_null.rb +21 -0
  69. data/lib/expire/report_simple.rb +14 -0
  70. data/lib/expire/rule_base.rb +56 -0
  71. data/lib/expire/rule_list.rb +52 -0
  72. data/lib/expire/rules.rb +66 -0
  73. data/lib/expire/templates/newest/.gitkeep +1 -0
  74. data/lib/expire/templates/oldest/.gitkeep +1 -0
  75. data/lib/expire/templates/purge/.gitkeep +1 -0
  76. data/lib/expire/templates/remove/.gitkeep +1 -0
  77. data/lib/expire/templates/rule_classes/.gitkeep +1 -0
  78. data/lib/expire/templates/rule_names/.gitkeep +1 -0
  79. data/lib/expire/templates/rule_option_names/.gitkeep +1 -0
  80. data/lib/expire/unknown_rule_error.rb +10 -0
  81. data/lib/expire/version.rb +9 -0
  82. metadata +321 -0
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Purge expired backups
5
+ class PurgeService
6
+ def self.call(path, options)
7
+ new(path, options).call
8
+ end
9
+
10
+ def initialize(path, options)
11
+ @options = options
12
+ @path = path
13
+ end
14
+
15
+ attr_reader :options, :path
16
+
17
+ def call
18
+ check_preconditions
19
+ purge_expired_backups
20
+ rescue StandardError => e
21
+ report.error(e.message)
22
+ raise
23
+ end
24
+
25
+ private
26
+
27
+ def annotated_backup_list
28
+ @annotated_backup_list ||= rules.apply(backup_list, DateTime.now)
29
+ end
30
+
31
+ def backup_list
32
+ @backup_list ||= GenerateBackupListService.call(path)
33
+ end
34
+
35
+ def check_preconditions
36
+ raise NoBackupsError, "Can't find any backups" unless backup_list.any?
37
+ raise NoRulesError, 'Will not purge without rules' unless rules.any?
38
+ raise AllBackupsExpiredError, 'Will not delete all backups' if annotated_backup_list.keep_count < 1
39
+ end
40
+
41
+ def report
42
+ @report ||= report_class.new
43
+ end
44
+
45
+ def report_class
46
+ wanted_format = options[:format]
47
+
48
+ return ReportNull unless wanted_format
49
+ return ReportNull if wanted_format == 'none'
50
+
51
+ class_name = "::Expire::Report#{wanted_format.titleize}"
52
+ class_name.safe_constantize or raise ArgumentError, "unknown format \"#{wanted_format}\""
53
+ end
54
+
55
+ def merge_rules
56
+ rules_file = options[:rules_file]
57
+ file_rules = rules_file ? Rules.from_yaml(rules_file) : Rules.new
58
+
59
+ option_rules = Rules.from_options(options.transform_keys(&:to_sym))
60
+ file_rules.merge(option_rules)
61
+ end
62
+
63
+ def purge_expired_backups
64
+ annotated_backup_list.sort.each do |backup|
65
+ if backup.expired?
66
+ report.before_purge(backup)
67
+ purge_pathname(backup.pathname)
68
+ report.after_purge(backup)
69
+ else
70
+ report.on_keep(backup)
71
+ end
72
+ end
73
+ end
74
+
75
+ def purge_pathname(pathname)
76
+ return if options[:simulate]
77
+
78
+ purge_command = options[:purge_command]
79
+
80
+ if purge_command
81
+ system("#{purge_command} #{pathname}")
82
+ else
83
+ pathname.unlink
84
+ end
85
+ end
86
+
87
+ def rules
88
+ @rules ||= merge_rules
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Enhance Integer and String with the methods #all? and #none?
5
+ module RefineAllAndNone
6
+ refine String do
7
+ def all?
8
+ return true if ['-1', 'all'].include?(strip.downcase)
9
+ end
10
+
11
+ # %w does not work here, I assume there is a problem with "0"
12
+ # rubocop:disable Style/RedundantPercentQ
13
+ def none?
14
+ return true if %q(0 none).include?(strip.downcase)
15
+ end
16
+ # rubocop:enable Style/RedundantPercentQ
17
+ end
18
+
19
+ refine Integer do
20
+ def all?
21
+ return true if self == -1
22
+ end
23
+
24
+ def none?
25
+ return true if zero?
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # For some unknown reason Pastel is not autoloaded by Zeitwerk
4
+ require 'pastel'
5
+
6
+ module Expire
7
+ # Base class for Reporters
8
+ class ReportBase < ReportNull
9
+ def initialize(receiver: $stdout)
10
+ @receiver = receiver
11
+ end
12
+
13
+ attr_reader :receiver
14
+
15
+ def error(message)
16
+ receiver.puts(pastel.red(message))
17
+ end
18
+
19
+ def pastel
20
+ @pastel ||= ::Pastel.new
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Detailed information about what is being kept and why
5
+ class ReportEnhanced < ReportSimple
6
+ def on_keep(backup)
7
+ receiver.puts(pastel.green("keeping #{backup.pathname}"))
8
+ receiver.puts ' reasons:'
9
+ backup.reasons_to_keep.each do |reason|
10
+ receiver.puts " - #{reason}"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Print the paths of expired backups
5
+ class ReportExpired < ReportBase
6
+ def before_purge(backup)
7
+ receiver.puts backup.pathname.to_s
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Print the backups that are kept
5
+ class ReportKept < ReportBase
6
+ def on_keep(backup)
7
+ receiver.puts backup.pathname.to_s
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Ignores all messages expect #error and report nothing.
5
+ # When error is received the message is printed to STDOUT.
6
+ class ReportNull
7
+ def error(message)
8
+ puts message
9
+ end
10
+
11
+ def before_all(_); end
12
+
13
+ def after_all(_); end
14
+
15
+ def on_keep(_); end
16
+
17
+ def before_purge(_); end
18
+
19
+ def after_purge(_); end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Sends "keeping" and "purged" to it's receiver
5
+ class ReportSimple < ReportBase
6
+ def on_keep(backup)
7
+ receiver.puts(pastel.green("keeping #{backup.pathname}"))
8
+ end
9
+
10
+ def after_purge(backup)
11
+ receiver.puts(pastel.yellow("purged #{backup.pathname}"))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Base class of all rules
5
+ class RuleBase
6
+ include Comparable
7
+
8
+ def self.<=>(other)
9
+ rank <=> other.rank
10
+ end
11
+
12
+ def self.camelized_name
13
+ match = to_s.match(/\A.*::(.+)Rule\z/) || return
14
+ match[1]
15
+ end
16
+
17
+ def self.name
18
+ camelized_name&.underscore
19
+ end
20
+
21
+ def self.option_name
22
+ rule_name = name || return
23
+ "--#{rule_name.dasherize}"
24
+ end
25
+
26
+ def initialize(amount:)
27
+ @amount = amount
28
+ end
29
+
30
+ attr_reader :amount
31
+
32
+ def name
33
+ camelized_name&.underscore
34
+ end
35
+
36
+ def numerus_backup
37
+ 'backup'.pluralize(amount)
38
+ end
39
+
40
+ def option_name
41
+ rule_name = name || return
42
+ "--#{rule_name.dasherize}"
43
+ end
44
+
45
+ def <=>(other)
46
+ rank <=> other.rank
47
+ end
48
+
49
+ private
50
+
51
+ def camelized_name
52
+ match = self.class.to_s.match(/\A.*::(.+)Rule\z/) || return
53
+ match[1]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # List rule classes, names and option-names
5
+ class RuleList
6
+ include Singleton
7
+
8
+ def self.class_names
9
+ instance.class_names
10
+ end
11
+
12
+ def self.names
13
+ instance.names
14
+ end
15
+
16
+ def self.name_symbols
17
+ instance.name_symbols
18
+ end
19
+
20
+ def self.option_names
21
+ instance.option_names
22
+ end
23
+
24
+ def class_names
25
+ @class_names ||= rule_classes.map(&:to_s).freeze
26
+ end
27
+
28
+ def names
29
+ rule_classes.map(&:name)
30
+ end
31
+
32
+ def name_symbols
33
+ names.map(&:to_sym)
34
+ end
35
+
36
+ def option_names
37
+ rule_classes.map(&:option_name)
38
+ end
39
+
40
+ private
41
+
42
+ def rule_classes
43
+ @rule_classes ||= rule_class_names.map(&:constantize).sort.freeze
44
+ end
45
+
46
+ def rule_class_names
47
+ class_symbols = Expire.constants.select { |klass| Expire.const_get(klass).to_s =~ /Rule\z/ }
48
+
49
+ class_symbols.map { |c_sym| "Expire::#{c_sym}" }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # How backups are expired
5
+ class Rules
6
+ def self.from_options(options)
7
+ known_rules = RuleList.name_symbols
8
+
9
+ rule_options = options.select { |opt, _| known_rules.include?(opt) }
10
+
11
+ new(rule_options)
12
+ end
13
+
14
+ def self.from_yaml(file_name)
15
+ pathname = Pathname.new(file_name)
16
+ yaml_text = pathname.read
17
+ yaml_rules = YAML.safe_load(yaml_text, symbolize_names: true)
18
+ new(yaml_rules)
19
+ end
20
+
21
+ def initialize(given = {})
22
+ @rules = given.map do |rule_name, value|
23
+ if value.respond_to? :rank
24
+ value
25
+ else
26
+ rule_class = rule_class_for(rule_name)
27
+ rule_class.from_value(value)
28
+ end
29
+ end
30
+ end
31
+
32
+ attr_reader :rules
33
+
34
+ def any?
35
+ rules.any?
36
+ end
37
+
38
+ def apply(backups, reference_datetime)
39
+ rules.sort.each { |rule| rule.apply(backups, reference_datetime) }
40
+
41
+ backups
42
+ end
43
+
44
+ def count
45
+ @rules.length
46
+ end
47
+
48
+ def merge(prior_rules)
49
+ self.class.new(to_h.merge(prior_rules.to_h))
50
+ end
51
+
52
+ def to_h
53
+ rules.map { |rule| [rule.name.to_sym, rule] }.to_h
54
+ end
55
+
56
+ private
57
+
58
+ def rule_class_for(key)
59
+ rule_class_name_for(key).safe_constantize || raise(UnknownRuleError, key)
60
+ end
61
+
62
+ def rule_class_name_for(key)
63
+ "::Expire::#{key.to_s.camelize}Rule"
64
+ end
65
+ end
66
+ end
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1 @@
1
+ #
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Thrown if a rule-name is not known
5
+ class UnknownRuleError < StandardError
6
+ def initialize(rule_name)
7
+ super("unknown rule name \"#{rule_name}\"")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ MAJOR = 0
5
+ MINOR = 2
6
+ TINY = 0
7
+
8
+ VERSION = [MAJOR, MINOR, TINY].compact * '.'
9
+ end