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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Reads contents of a directory and returns a corresponding BackupList
5
+ class GenerateBackupListService
6
+ def self.call(path)
7
+ new(path).call
8
+ end
9
+
10
+ def initialize(path)
11
+ @path = path
12
+ end
13
+
14
+ attr_reader :path
15
+
16
+ def call
17
+ if path == '-'
18
+ generate_backup_list_from($stdin)
19
+ else
20
+ pathname = Pathname.new(path)
21
+ raise InvalidPathError, "#{pathname} does not exit" unless pathname.exist?
22
+
23
+ generate_backup_list_from(pathname.children)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def generate_backup_list_from(source)
30
+ backup_list = BackupList.new
31
+
32
+ source.each do |backup_path|
33
+ backup_list << BackupFromPathService.call(path: purify_backup_path(backup_path))
34
+ end
35
+
36
+ backup_list
37
+ end
38
+
39
+ # backup_path may be a String or a Pathname so we call #to_s to
40
+ # ensure that chomp works as expected
41
+ def purify_backup_path(backup_path)
42
+ backup_path.to_s.chomp.strip
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Thrown if a path is invalid
5
+ class InvalidPathError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Hold backups for a period
5
+ class KeepAdjectiveForRuleBase < FromNowKeepAdjectiveForRuleBase
6
+ ADJECTIVE_FOR = {
7
+ 'week' => 'weekly',
8
+ 'month' => 'monthly',
9
+ 'year' => 'yearly'
10
+ }.freeze
11
+
12
+ PRIMARY_RANK = 30
13
+
14
+ def self.primary_rank
15
+ PRIMARY_RANK
16
+ end
17
+
18
+ def self.rank
19
+ primary_rank + secondary_rank
20
+ end
21
+
22
+ def apply(backups, _)
23
+ super(backups, backups.newest)
24
+ end
25
+
26
+ def primary_rank
27
+ self.class.primary_rank
28
+ end
29
+
30
+ def reason_to_keep
31
+ "keep #{amount} #{ADJECTIVE_FOR[spacing]} #{numerus_backup}"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Base class for rules with an adjective in their name
5
+ class KeepAdjectiveRuleBase < RuleBase
6
+ using RefineAllAndNone
7
+
8
+ NOUN_FOR = {
9
+ 'hourly' => 'hour',
10
+ 'daily' => 'day',
11
+ 'weekly' => 'week',
12
+ 'monthly' => 'month',
13
+ 'yearly' => 'year'
14
+ }.freeze
15
+
16
+ PRIMARY_RANK = 20
17
+ SECONDARY_RANK_FOR = {
18
+ 'hourly' => 1,
19
+ 'daily' => 2,
20
+ 'weekly' => 3,
21
+ 'monthly' => 4,
22
+ 'yearly' => 5
23
+ }.freeze
24
+
25
+ def self.from_value(value)
26
+ value = -1 if value.all?
27
+ value = 0 if value.none?
28
+
29
+ integer_value = Integer(value)
30
+ raise ArgumentError, 'must be at least -1' if integer_value < -1
31
+
32
+ new(amount: integer_value)
33
+ end
34
+
35
+ def self.primary_rank
36
+ PRIMARY_RANK
37
+ end
38
+
39
+ def self.rank
40
+ primary_rank + secondary_rank
41
+ end
42
+
43
+ def self.secondary_rank
44
+ match = name.downcase.match(/(hourly|daily|weekly|monthly|yearly)/)
45
+ return unless match
46
+
47
+ SECONDARY_RANK_FOR[match[1]]
48
+ end
49
+
50
+ def adjective
51
+ @adjective ||= infer_adjective
52
+ end
53
+
54
+ def apply(backups, _)
55
+ per_spacing = backups.one_per(spacing)
56
+ kept = amount == -1 ? per_spacing : per_spacing.most_recent(amount)
57
+ kept.each { |backup| backup.add_reason_to_keep(reason_to_keep) }
58
+ end
59
+
60
+ def rank
61
+ @rank ||= primary_rank + secondary_rank
62
+ end
63
+
64
+ def primary_rank
65
+ self.class.primary_rank
66
+ end
67
+
68
+ def secondary_rank
69
+ self.class.secondary_rank
70
+ end
71
+
72
+ def spacing
73
+ NOUN_FOR[adjective]
74
+ end
75
+
76
+ private
77
+
78
+ def class_name
79
+ self.class.to_s
80
+ end
81
+
82
+ def infer_adjective
83
+ match = class_name.downcase.match(/(hourly|daily|weekly|monthly|yearly)/)
84
+ return unless match
85
+
86
+ match[1]
87
+ end
88
+
89
+ def reason_to_keep
90
+ "keep #{pretty_amount} #{adjective} #{numerus_backup}"
91
+ end
92
+
93
+ def pretty_amount
94
+ amount == -1 ? 'all' : amount.to_s
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep a daily backup for a certain period of time
5
+ class KeepDailyForRule < KeepAdjectiveForRuleBase
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per day
5
+ class KeepDailyRule < KeepAdjectiveRuleBase
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per hour for a certain period of time.
5
+ class KeepHourlyForRule < KeepAdjectiveForRuleBase
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per hour
5
+ class KeepHourlyRule < KeepAdjectiveRuleBase
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per mounth for a certain period of time
5
+ class KeepMonthlyForRule < KeepAdjectiveForRuleBase
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per month
5
+ class KeepMonthlyRule < KeepAdjectiveRuleBase
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep the most recent backups for a
5
+ # certain period of time.
6
+ class KeepMostRecentForRule < FromNowKeepMostRecentForRule
7
+ extend FromRangeValue
8
+ include NumerusUnit
9
+
10
+ RULE_RANK = 11
11
+
12
+ attr_reader :unit
13
+
14
+ def self.rank
15
+ RULE_RANK
16
+ end
17
+
18
+ def apply(backups, _)
19
+ reference_datetime = backups.newest
20
+ super(backups, reference_datetime)
21
+ end
22
+
23
+ def rank
24
+ self.class.rank
25
+ end
26
+
27
+ def reason_to_keep
28
+ "keep most recent backups for #{amount} #{numerus_unit}"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep the most recent Backups
5
+ class KeepMostRecentRule < RuleBase
6
+ using RefineAllAndNone
7
+
8
+ RULE_RANK = 10
9
+
10
+ def self.from_value(value)
11
+ return new(amount: 0) if value.none?
12
+
13
+ new(amount: Integer(value))
14
+ end
15
+
16
+ def self.rank
17
+ RULE_RANK
18
+ end
19
+
20
+ def rank
21
+ self.class.rank
22
+ end
23
+
24
+ def apply(backups, _)
25
+ backups.most_recent(amount).each do |backup|
26
+ backup.add_reason_to_keep(reason_to_keep)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def reason_to_keep
33
+ return 'keep the most recent backup' if amount == 1
34
+
35
+ "keep the #{amount} most recent backups"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per week for a certain
5
+ # period of time.
6
+ class KeepWeeklyForRule < KeepAdjectiveForRuleBase
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per week
5
+ class KeepWeeklyRule < KeepAdjectiveRuleBase
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per year
5
+ class KeepYearlyForRule < KeepAdjectiveForRuleBase
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Keep one backup per year
5
+ class KeepYearlyRule < KeepAdjectiveRuleBase
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Thrown if no backups can be found
5
+ class NoBackupsError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Thrown if a rule-name is not known
5
+ class NoRulesError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # A mixin to get the right numerus of #unit
5
+ module NumerusUnit
6
+ def numerus_unit
7
+ unit.pluralize(amount)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Thrown if a file or directory exists when it should not
5
+ class PathAlreadyExistsError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Create playground with example data
5
+ class Playground
6
+ STEP_WIDTHS = {
7
+ 'hourly' => 'hour',
8
+ 'daily' => 'day',
9
+ 'weekly' => 'week',
10
+ 'monthly' => 'month',
11
+ 'yearly' => 'year'
12
+ }.freeze
13
+
14
+ def self.create(base)
15
+ new(base).create
16
+ end
17
+
18
+ def initialize(base)
19
+ @base = base
20
+ @backups_dir = Pathname.new("#{base}/backups")
21
+
22
+ @options = {
23
+ hourly: 42,
24
+ daily: 15,
25
+ weekly: 15,
26
+ monthly: 25,
27
+ yearly: 5
28
+ }
29
+ end
30
+
31
+ attr_reader :backups_dir, :base, :options
32
+
33
+ def create
34
+ raise_if_backups_dir_exists
35
+
36
+ oldest_backup = DateTime.now
37
+
38
+ STEP_WIDTHS.each do |adjective, noun|
39
+ options[adjective.to_sym].times do
40
+ oldest_backup = oldest_backup.ago(1.send(noun))
41
+ mkbackup(oldest_backup)
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def mkbackup(datetime)
49
+ backup_name = datetime.to_s.sub(/:\d\d[+-]\d\d:\d\d\z/, '')
50
+ FileUtils.mkdir_p("#{backups_dir}/#{backup_name}")
51
+ end
52
+
53
+ def raise_if_backups_dir_exists
54
+ return unless FileTest.exist?(backups_dir)
55
+
56
+ raise(
57
+ PathAlreadyExistsError,
58
+ "Will not create playground in existing path #{backups_dir}"
59
+ )
60
+ end
61
+ end
62
+ end