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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.reek.yml +23 -0
- data/.rspec +3 -0
- data/.rubocop.yml +45 -0
- data/.simplecov +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +152 -0
- data/LICENSE.txt +21 -0
- data/README.md +551 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/example_rules/bad_rules.yml +2 -0
- data/example_rules/good_rules.yml +1 -0
- data/exe/expire +7 -0
- data/expire.gemspec +54 -0
- data/lib/expire.rb +51 -0
- data/lib/expire/all_backups_expired_error.rb +7 -0
- data/lib/expire/backup.rb +74 -0
- data/lib/expire/backup_from_path_service.rb +56 -0
- data/lib/expire/backup_list.rb +69 -0
- data/lib/expire/cli.rb +221 -0
- data/lib/expire/command.rb +122 -0
- data/lib/expire/commands/newest.rb +21 -0
- data/lib/expire/commands/oldest.rb +21 -0
- data/lib/expire/commands/purge.rb +23 -0
- data/lib/expire/commands/remove.rb +26 -0
- data/lib/expire/commands/rule_classes.rb +18 -0
- data/lib/expire/commands/rule_names.rb +20 -0
- data/lib/expire/commands/rule_option_names.rb +20 -0
- data/lib/expire/from_now_keep_adjective_for_rule_base.rb +38 -0
- data/lib/expire/from_now_keep_daily_for_rule.rb +7 -0
- data/lib/expire/from_now_keep_hourly_for_rule.rb +7 -0
- data/lib/expire/from_now_keep_monthly_for_rule.rb +7 -0
- data/lib/expire/from_now_keep_most_recent_for_rule.rb +41 -0
- data/lib/expire/from_now_keep_weekly_for_rule.rb +8 -0
- data/lib/expire/from_now_keep_yearly_for_rule.rb +8 -0
- data/lib/expire/from_range_value.rb +29 -0
- data/lib/expire/generate_backup_list_service.rb +45 -0
- data/lib/expire/invalid_path_error.rb +7 -0
- data/lib/expire/keep_adjective_for_rule_base.rb +34 -0
- data/lib/expire/keep_adjective_rule_base.rb +97 -0
- data/lib/expire/keep_daily_for_rule.rb +7 -0
- data/lib/expire/keep_daily_rule.rb +7 -0
- data/lib/expire/keep_hourly_for_rule.rb +7 -0
- data/lib/expire/keep_hourly_rule.rb +7 -0
- data/lib/expire/keep_monthly_for_rule.rb +7 -0
- data/lib/expire/keep_monthly_rule.rb +7 -0
- data/lib/expire/keep_most_recent_for_rule.rb +31 -0
- data/lib/expire/keep_most_recent_rule.rb +38 -0
- data/lib/expire/keep_weekly_for_rule.rb +8 -0
- data/lib/expire/keep_weekly_rule.rb +7 -0
- data/lib/expire/keep_yearly_for_rule.rb +7 -0
- data/lib/expire/keep_yearly_rule.rb +7 -0
- data/lib/expire/no_backups_error.rb +7 -0
- data/lib/expire/no_rules_error.rb +7 -0
- data/lib/expire/numerus_unit.rb +10 -0
- data/lib/expire/path_already_exists_error.rb +7 -0
- data/lib/expire/playground.rb +62 -0
- data/lib/expire/purge_service.rb +91 -0
- data/lib/expire/refine_all_and_none.rb +29 -0
- data/lib/expire/report_base.rb +23 -0
- data/lib/expire/report_enhanced.rb +14 -0
- data/lib/expire/report_expired.rb +10 -0
- data/lib/expire/report_kept.rb +10 -0
- data/lib/expire/report_null.rb +21 -0
- data/lib/expire/report_simple.rb +14 -0
- data/lib/expire/rule_base.rb +56 -0
- data/lib/expire/rule_list.rb +52 -0
- data/lib/expire/rules.rb +66 -0
- data/lib/expire/templates/newest/.gitkeep +1 -0
- data/lib/expire/templates/oldest/.gitkeep +1 -0
- data/lib/expire/templates/purge/.gitkeep +1 -0
- data/lib/expire/templates/remove/.gitkeep +1 -0
- data/lib/expire/templates/rule_classes/.gitkeep +1 -0
- data/lib/expire/templates/rule_names/.gitkeep +1 -0
- data/lib/expire/templates/rule_option_names/.gitkeep +1 -0
- data/lib/expire/unknown_rule_error.rb +10 -0
- data/lib/expire/version.rb +9 -0
- 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,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,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,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
|