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,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
|
+
#
|