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,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,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
|
data/lib/expire/rules.rb
ADDED
@@ -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 @@
|
|
1
|
+
#
|
@@ -0,0 +1 @@
|
|
1
|
+
#
|