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