expire 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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